mirror of
https://github.com/goharbor/harbor.git
synced 2025-01-22 23:51:27 +01:00
sync from upstream and merge the conflic
Signed-off-by: Yuan Lei <371304458@qq.com>
This commit is contained in:
commit
253600ad39
@ -34,8 +34,9 @@ env:
|
||||
- CORE_SECRET: tempString
|
||||
- KEY_PATH: "/data/secretkey"
|
||||
- REDIS_HOST: localhost
|
||||
- REG_VERSION: v2.6.2
|
||||
- REG_VERSION: v2.7.1
|
||||
- UI_BUILDER_VERSION: 1.6.0
|
||||
- TOKEN_PRIVATE_KEY_PATH: "/home/travis/gopath/src/github.com/goharbor/harbor/tests/private_key.pem"
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
|
4
Makefile
4
Makefile
@ -95,7 +95,7 @@ VERSIONFILEPATH=$(CURDIR)
|
||||
VERSIONFILENAME=UIVERSION
|
||||
|
||||
#versions
|
||||
REGISTRYVERSION=v2.6.2
|
||||
REGISTRYVERSION=v2.7.1
|
||||
NGINXVERSION=$(VERSIONTAG)
|
||||
NOTARYVERSION=v0.6.1
|
||||
CLAIRVERSION=v2.0.7
|
||||
@ -104,7 +104,7 @@ MIGRATORVERSION=$(VERSIONTAG)
|
||||
REDISVERSION=$(VERSIONTAG)
|
||||
|
||||
# version of chartmuseum
|
||||
CHARTMUSEUMVERSION=v0.7.1
|
||||
CHARTMUSEUMVERSION=v0.8.1
|
||||
|
||||
# docker parameters
|
||||
DOCKERCMD=$(shell which docker)
|
||||
|
@ -2,13 +2,13 @@
|
||||
|
||||
Harbor has integrated with Clair to scan vulnerabilities in images. When Harbor is installed in an environment without internet connection, Clair cannot fetch data from the public vulnerability database. Under this circumstance, Harbor administrator needs to manually update the Clair database.
|
||||
|
||||
This document provides step-by-step instructions on updating Clair vulnerability database in Harbor v1.2.
|
||||
This document provides step-by-step instructions on updating Clair vulnerability database in Harbor.
|
||||
|
||||
**NOTE:** Harbor does not ship with any vulnerability data. For this reason, if Harbor cannot connect to Internet, the administrator must manually import vulnerability data to Harbor by using instructions given in this document.
|
||||
|
||||
### Preparation
|
||||
|
||||
A. You need to install an instance of Clair 2.0.1 with internet connection. If you have another instance of Harbor v1.2 with internet access, it also works.
|
||||
A. You need to install an instance of Clair with internet connection. If you have another instance of Harbor with internet access, it also works.
|
||||
|
||||
B. Check whether your Clair instance has already updated the vulnerability database to the latest version. If it has not, wait for Clair to get the data from public endpoints.
|
||||
|
||||
@ -29,28 +29,39 @@ B. Check whether your Clair instance has already updated the vulnerability datab
|
||||
```
|
||||
- The phrase "finished fetching" indicates that Clair has finished a round of vulnerability update from an endpoint. Please make sure all five endpoints (rhel, alpine, oracle, debian, ubuntu) are updated correctly.
|
||||
|
||||
## Harbor version < 1.6
|
||||
|
||||
If you're using a version of Harbor prior to 1.6, you can access the correct instructions for your version using the following URL.
|
||||
https://github.com/goharbor/harbor/blob/v\<VERSION NUMBER>/docs/import_vulnerability_data.md
|
||||
|
||||
## Harbor version >= 1.6
|
||||
|
||||
Databased were consolidated in version 1.6 which moved the clair database to the harbor-db container and removed the clair-db container.
|
||||
|
||||
### Dumping vulnerability data
|
||||
|
||||
- Log in to the host (that is connected to Internet) where Clair database (Postgres) is running.
|
||||
- Dump Clair's vulnerability database by the following commands, two files (`vulnerability.sql` and `clear.sql`) are generated:
|
||||
|
||||
_NOTE: The container name 'clair-db' is a placeholder for the db container used by the internet connected instance of clair_
|
||||
|
||||
```
|
||||
$ docker exec clair-db /bin/bash -c "pg_dump -U postgres -a -t feature -t keyvalue -t namespace -t schema_migrations -t vulnerability -t vulnerability_fixedin_feature" > vulnerability.sql
|
||||
$ docker exec clair-db /bin/bash -c "pg_dump -U postgres -c -s" > clear.sql
|
||||
$ docker exec clair-db /bin/sh -c "pg_dump -U postgres -a -t feature -t keyvalue -t namespace -t schema_migrations -t vulnerability -t vulnerability_fixedin_feature" > vulnerability.sql
|
||||
$ docker exec clair-db /bin/sh -c "pg_dump -U postgres -c -s" > clear.sql
|
||||
```
|
||||
|
||||
### Back up Harbor's Clair database
|
||||
Before importing the data, it is strongly recommended to back up the Clair database in Harbor.
|
||||
```
|
||||
$ docker exec clair-db /bin/bash -c "pg_dump -U postgres -c" > all.sql
|
||||
$ docker exec harbor-db /bin/sh -c "pg_dump -U postgres -c" > all.sql
|
||||
```
|
||||
|
||||
### Update Harbor's Clair database
|
||||
Copy the `vulnerability.sql` and `clear.sql` to the host where Harbor is running on. Run the below commands to import the data to Harbor's Clair database:
|
||||
|
||||
```
|
||||
$ docker exec -i clair-db psql -U postgres < clear.sql
|
||||
$ docker exec -i clair-db psql -U postgres < vulnerability.sql
|
||||
$ docker exec -i harbor-db psql -U postgres < clear.sql
|
||||
$ docker exec -i harbor-db psql -U postgres < vulnerability.sql
|
||||
```
|
||||
|
||||
### Rescanning images
|
||||
|
45
docs/permissions.md
Normal file
45
docs/permissions.md
Normal file
@ -0,0 +1,45 @@
|
||||
# Permissions
|
||||
|
||||
Users have different abilities depending on the role they in a project.
|
||||
|
||||
On public projects all users will be able to see the list of repositories, images, image vulnerabilities, helm charts and helm chart versions, pull images, retag images (need push permission for destination image), download helm charts, download helm chart versions.
|
||||
|
||||
System admin have all permissions for the project.
|
||||
|
||||
## Project members permissions
|
||||
|
||||
The following table depicts the various user permission levels in a project.
|
||||
|
||||
| Action | Guest | Developer | Master | Project Admin |
|
||||
| --------------------------------------- | ----- | --------- | ------ | ------------- |
|
||||
| See the porject configurations | ✓ | ✓ | ✓ | ✓ |
|
||||
| Edit the project configurations | | | | ✓ |
|
||||
| See a list of project members | ✓ | ✓ | ✓ | ✓ |
|
||||
| Create/edit/delete project members | | | | ✓ |
|
||||
| See a list of project logs | ✓ | ✓ | ✓ | ✓ |
|
||||
| See a list of project replications | | | ✓ | ✓ |
|
||||
| See a list of project replication jobs | | | | ✓ |
|
||||
| See a list of project labels | | | ✓ | ✓ |
|
||||
| Create/edit/delete project lables | | | ✓ | ✓ |
|
||||
| See a list of repositories | ✓ | ✓ | ✓ | ✓ |
|
||||
| Create repositories | | ✓ | ✓ | ✓ |
|
||||
| Edit/delete repositories | | | ✓ | ✓ |
|
||||
| See a list of images | ✓ | ✓ | ✓ | ✓ |
|
||||
| Retag image | ✓ | ✓ | ✓ | ✓ |
|
||||
| Pull image | ✓ | ✓ | ✓ | ✓ |
|
||||
| Push image | | ✓ | ✓ | ✓ |
|
||||
| Scan/delete image | | | ✓ | ✓ |
|
||||
| See a list of image vulnerabilities | ✓ | ✓ | ✓ | ✓ |
|
||||
| See image build history | ✓ | ✓ | ✓ | ✓ |
|
||||
| Add/Remove labels of image | | ✓ | ✓ | ✓ |
|
||||
| See a list of helm charts | ✓ | ✓ | ✓ | ✓ |
|
||||
| Download helm charts | ✓ | ✓ | ✓ | ✓ |
|
||||
| Upload helm charts | | ✓ | ✓ | ✓ |
|
||||
| Delete helm charts | | | ✓ | ✓ |
|
||||
| See a list of helm chart versions | ✓ | ✓ | ✓ | ✓ |
|
||||
| Download helm chart versions | ✓ | ✓ | ✓ | ✓ |
|
||||
| Upload helm chart versions | | ✓ | ✓ | ✓ |
|
||||
| Delete helm chart versions | | | ✓ | ✓ |
|
||||
| Add/Remove labels of helm chart version | | ✓ | ✓ | ✓ |
|
||||
| See a list of project robots | | | ✓ | ✓ |
|
||||
| Create/edit/delete project robots | | | | ✓ |
|
@ -19,6 +19,18 @@ securityDefinitions:
|
||||
security:
|
||||
- basicAuth: []
|
||||
paths:
|
||||
/health:
|
||||
get:
|
||||
summary: 'Health check API'
|
||||
description: |
|
||||
The endpoint returns the health stauts of the system.
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: The system health status.
|
||||
schema:
|
||||
$ref: '#/definitions/OverallHealthStatus'
|
||||
/search:
|
||||
get:
|
||||
summary: 'Search for projects, repositories and helm charts'
|
||||
@ -705,6 +717,37 @@ paths:
|
||||
$ref: '#/definitions/User'
|
||||
'401':
|
||||
description: User need to log in first.
|
||||
/users/current/permissions:
|
||||
get:
|
||||
summary: Get current user permissions.
|
||||
description: |
|
||||
This endpoint is to get the current user permissions.
|
||||
parameters:
|
||||
- name: scope
|
||||
in: query
|
||||
type: string
|
||||
required: false
|
||||
description: Get permissions of the scope
|
||||
- name: relative
|
||||
in: query
|
||||
type: boolean
|
||||
required: false
|
||||
description: |
|
||||
If true, the resources in the response are relative to the scope,
|
||||
eg for resource '/project/1/repository' if relative is 'true' then the resource in response will be 'repository'.
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Get current user permission successfully.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Permission'
|
||||
'401':
|
||||
description: User need to log in first.
|
||||
'500':
|
||||
description: Internal errors.
|
||||
'/users/{user_id}':
|
||||
get:
|
||||
summary: Get a user's profile.
|
||||
@ -2706,24 +2749,6 @@ paths:
|
||||
description: User does not have permission of admin role.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
/configurations/reset:
|
||||
post:
|
||||
summary: Reset system configurations.
|
||||
description: |
|
||||
Reset system configurations from environment variables. Can only be accessed by admin user.
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Reset system configurations successfully.
|
||||
'401':
|
||||
description: User need to log in first.
|
||||
'403':
|
||||
description: User does not have permission of admin role.
|
||||
'415':
|
||||
$ref: '#/responses/UnsupportedMediaType'
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
/email/ping:
|
||||
post:
|
||||
summary: Test connection and authentication with email server.
|
||||
@ -3136,6 +3161,164 @@ paths:
|
||||
$ref: '#/definitions/NotFoundChartAPIError'
|
||||
'500':
|
||||
$ref: '#/definitions/InternalChartAPIError'
|
||||
'/projects/{project_id}/robots':
|
||||
get:
|
||||
summary: Get all robot accounts of specified project
|
||||
description: Get all robot accounts of specified project
|
||||
parameters:
|
||||
- name: project_id
|
||||
in: path
|
||||
type: integer
|
||||
format: int64
|
||||
required: true
|
||||
description: Relevant project ID.
|
||||
tags:
|
||||
- Products
|
||||
- Robot Account
|
||||
responses:
|
||||
'200':
|
||||
description: Get project robot accounts successfully.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/RobotAccount'
|
||||
'400':
|
||||
description: The project id is invalid.
|
||||
'401':
|
||||
description: User need to log in first.
|
||||
'403':
|
||||
description: User in session does not have permission to the project.
|
||||
'404':
|
||||
description: Project ID does not exist.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
post:
|
||||
summary: Create a robot account for project
|
||||
description: Create a robot account for project
|
||||
tags:
|
||||
- Products
|
||||
- Robot Account
|
||||
parameters:
|
||||
- name: project_id
|
||||
in: path
|
||||
type: integer
|
||||
format: int64
|
||||
required: true
|
||||
description: Relevant project ID.
|
||||
- name: robot
|
||||
in: body
|
||||
description: Request body of creating a robot account.
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/RobotAccountCreate'
|
||||
responses:
|
||||
'201':
|
||||
description: Project member created successfully.
|
||||
schema:
|
||||
$ref: '#/definitions/RobotAccountPostRep'
|
||||
'400':
|
||||
description: Project id is not valid.
|
||||
'401':
|
||||
description: User need to log in first.
|
||||
'403':
|
||||
description: User in session does not have permission to the project.
|
||||
'409':
|
||||
description: An robot account with same name already exist in the project.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
'/projects/{project_id}/robots/{robot_id}':
|
||||
get:
|
||||
summary: Return the infor of the specified robot account.
|
||||
description: Return the infor of the specified robot account.
|
||||
tags:
|
||||
- Products
|
||||
- Robot Account
|
||||
parameters:
|
||||
- name: project_id
|
||||
in: path
|
||||
type: integer
|
||||
format: int64
|
||||
required: true
|
||||
description: Relevant project ID.
|
||||
- name: robot_id
|
||||
in: path
|
||||
type: integer
|
||||
format: int64
|
||||
required: true
|
||||
description: The ID of robot account.
|
||||
responses:
|
||||
'200':
|
||||
description: Robot account information.
|
||||
schema:
|
||||
$ref: '#/definitions/RobotAccount'
|
||||
'401':
|
||||
description: User need to log in first.
|
||||
'403':
|
||||
description: User in session does not have permission to the project.
|
||||
'404':
|
||||
description: The robot account is not found.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
put:
|
||||
summary: Update status of robot account.
|
||||
description: Used to disable/enable a specified robot account.
|
||||
tags:
|
||||
- Products
|
||||
- Robot Account
|
||||
parameters:
|
||||
- name: project_id
|
||||
in: path
|
||||
type: integer
|
||||
format: int64
|
||||
required: true
|
||||
description: Relevant project ID.
|
||||
- name: robot_id
|
||||
in: path
|
||||
type: integer
|
||||
format: int64
|
||||
required: true
|
||||
description: The ID of robot account.
|
||||
- name: robot
|
||||
in: body
|
||||
description: Request body of enable/disable a robot account.
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/RobotAccountUpdate'
|
||||
responses:
|
||||
'200':
|
||||
description: Robot account has been modified success.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
delete:
|
||||
summary: Delete the specified robot account
|
||||
description: Delete the specified robot account
|
||||
tags:
|
||||
- Products
|
||||
- Robot Account
|
||||
parameters:
|
||||
- name: project_id
|
||||
in: path
|
||||
type: integer
|
||||
format: int64
|
||||
required: true
|
||||
description: Relevant project ID.
|
||||
- name: robot_id
|
||||
in: path
|
||||
type: integer
|
||||
format: int64
|
||||
required: true
|
||||
description: The ID of robot account.
|
||||
responses:
|
||||
'200':
|
||||
description: The specified robot account is successfully deleted.
|
||||
'401':
|
||||
description: User need to log in first.
|
||||
'403':
|
||||
description: User in session does not have permission to the project.
|
||||
'404':
|
||||
description: The robot account is not found.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
responses:
|
||||
UnsupportedMediaType:
|
||||
description: 'The Media Type of the request is not supported, it has to be "application/json"'
|
||||
@ -4170,7 +4353,7 @@ definitions:
|
||||
properties:
|
||||
role_id:
|
||||
type: integer
|
||||
description: 'The role id 1 for projectAdmin, 2 for developer, 3 for guest'
|
||||
description: 'The role id 1 for projectAdmin, 2 for developer, 3 for guest, 4 for master'
|
||||
member_user:
|
||||
$ref: '#/definitions/UserEntity'
|
||||
member_group:
|
||||
@ -4180,7 +4363,7 @@ definitions:
|
||||
properties:
|
||||
role_id:
|
||||
type: integer
|
||||
description: 'The role id 1 for projectAdmin, 2 for developer, 3 for guest'
|
||||
description: 'The role id 1 for projectAdmin, 2 for developer, 3 for guest, 4 for master'
|
||||
UserEntity:
|
||||
type: object
|
||||
properties:
|
||||
@ -4514,3 +4697,100 @@ definitions:
|
||||
description: A list of label
|
||||
items:
|
||||
$ref: '#/definitions/Label'
|
||||
OverallHealthStatus:
|
||||
type: object
|
||||
description: The system health status
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
description: The overall health status. It is "healthy" only when all the components' status are "healthy"
|
||||
components:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/ComponentHealthStatus'
|
||||
ComponentHealthStatus:
|
||||
type: object
|
||||
description: The health status of component
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: The component name
|
||||
status:
|
||||
type: string
|
||||
description: The health status of component
|
||||
error:
|
||||
type: string
|
||||
description: (optional) The error message when the status is "unhealthy"
|
||||
RobotAccount:
|
||||
type: object
|
||||
description: The object of robot account
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
description: The id of robot account
|
||||
name:
|
||||
type: string
|
||||
description: The name of robot account
|
||||
description:
|
||||
type: string
|
||||
description: The description of robot account
|
||||
project_id:
|
||||
type: integer
|
||||
description: The project id of robot account
|
||||
disabled:
|
||||
type: boolean
|
||||
description: The robot account is disable or enable
|
||||
creation_time:
|
||||
type: string
|
||||
description: The creation time of the robot account
|
||||
update_time:
|
||||
type: string
|
||||
description: The update time of the robot account
|
||||
RobotAccountCreate:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: The name of robot account
|
||||
description:
|
||||
type: string
|
||||
description: The description of robot account
|
||||
access:
|
||||
type: array
|
||||
description: The permission of robot account
|
||||
items:
|
||||
$ref: '#/definitions/RobotAccountAccess'
|
||||
RobotAccountPostRep:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: the name of robot account
|
||||
token:
|
||||
type: string
|
||||
description: the token of robot account
|
||||
RobotAccountAccess:
|
||||
type: object
|
||||
properties:
|
||||
resource:
|
||||
type: string
|
||||
description: the resource of harbor
|
||||
action:
|
||||
type: string
|
||||
description: the action to resource that perdefined in harbor rbac
|
||||
RobotAccountUpdate:
|
||||
type: object
|
||||
properties:
|
||||
disable:
|
||||
type: boolean
|
||||
description: The robot account is disable or enable
|
||||
Permission:
|
||||
type: object
|
||||
description: The permission
|
||||
properties:
|
||||
resource:
|
||||
type: string
|
||||
description: The permission resoruce
|
||||
action:
|
||||
type: string
|
||||
description: The permission action
|
||||
|
@ -94,37 +94,12 @@ services:
|
||||
options:
|
||||
syslog-address: "tcp://127.0.0.1:1514"
|
||||
tag: "postgresql"
|
||||
adminserver:
|
||||
image: goharbor/harbor-adminserver:__version__
|
||||
container_name: harbor-adminserver
|
||||
env_file:
|
||||
- ./common/config/adminserver/env
|
||||
restart: always
|
||||
cap_drop:
|
||||
- ALL
|
||||
cap_add:
|
||||
- CHOWN
|
||||
- SETGID
|
||||
- SETUID
|
||||
volumes:
|
||||
- /data/config/:/etc/adminserver/config/:z
|
||||
- /data/secretkey:/etc/adminserver/key:z
|
||||
- /data/:/data/:z
|
||||
networks:
|
||||
- harbor
|
||||
dns_search: .
|
||||
depends_on:
|
||||
- log
|
||||
logging:
|
||||
driver: "syslog"
|
||||
options:
|
||||
syslog-address: "tcp://127.0.0.1:1514"
|
||||
tag: "adminserver"
|
||||
core:
|
||||
image: goharbor/harbor-core:__version__
|
||||
container_name: harbor-core
|
||||
env_file:
|
||||
- ./common/config/core/env
|
||||
- ./common/config/adminserver/env
|
||||
restart: always
|
||||
cap_drop:
|
||||
- ALL
|
||||
@ -144,7 +119,6 @@ services:
|
||||
dns_search: .
|
||||
depends_on:
|
||||
- log
|
||||
- adminserver
|
||||
- registry
|
||||
logging:
|
||||
driver: "syslog"
|
||||
@ -195,7 +169,6 @@ services:
|
||||
depends_on:
|
||||
- redis
|
||||
- core
|
||||
- adminserver
|
||||
logging:
|
||||
driver: "syslog"
|
||||
options:
|
||||
|
@ -1,10 +1,6 @@
|
||||
CREATE TABLE robot (
|
||||
id SERIAL PRIMARY KEY NOT NULL,
|
||||
name varchar(255),
|
||||
/*
|
||||
The maximum length of token is 7k
|
||||
*/
|
||||
token varchar(7168),
|
||||
description varchar(1024),
|
||||
project_id int,
|
||||
disabled boolean DEFAULT false NOT NULL,
|
||||
|
1
make/migrations/postgresql/0005_add_master_role.up.sql
Normal file
1
make/migrations/postgresql/0005_add_master_role.up.sql
Normal file
@ -0,0 +1 @@
|
||||
INSERT INTO role (role_code, name) VALUES ('DRWS', 'master');
|
@ -148,7 +148,7 @@ _build_chart_server:
|
||||
@if [ "$(CHARTFLAG)" = "true" ] ; then \
|
||||
if [ "$(BUILDBIN)" != "true" ] ; then \
|
||||
rm -rf $(DOCKERFILEPATH_CHART_SERVER)/binary && mkdir -p $(DOCKERFILEPATH_CHART_SERVER)/binary && \
|
||||
$(call _get_binary, https://storage.googleapis.com/harbor-builds/bin/chartm, $(DOCKERFILEPATH_CHART_SERVER)/binary/chartm); \
|
||||
$(call _get_binary, https://storage.googleapis.com/harbor-builds/bin/chartmuseum/release-$(CHARTMUSEUMVERSION)/chartm, $(DOCKERFILEPATH_CHART_SERVER)/binary/chartm); \
|
||||
else \
|
||||
cd $(DOCKERFILEPATH_CHART_SERVER) && $(DOCKERFILEPATH_CHART_SERVER)/builder $(GOBUILDIMAGE) $(CHART_SERVER_CODE_BASE) $(CHARTMUSEUMVERSION) $(CHART_SERVER_MAIN_PATH) $(CHART_SERVER_BIN_NAME) && cd - ; \
|
||||
fi ; \
|
||||
@ -184,7 +184,7 @@ _build_notary:
|
||||
_build_registry:
|
||||
@if [ "$(BUILDBIN)" != "true" ] ; then \
|
||||
rm -rf $(DOCKERFILEPATH_REG)/binary && mkdir -p $(DOCKERFILEPATH_REG)/binary && \
|
||||
$(call _get_binary, https://storage.googleapis.com/harbor-builds/bin/registry, $(DOCKERFILEPATH_REG)/binary/registry); \
|
||||
$(call _get_binary, https://storage.googleapis.com/harbor-builds/bin/registry/release-$(REGISTRYVERSION)/registry, $(DOCKERFILEPATH_REG)/binary/registry); \
|
||||
else \
|
||||
cd $(DOCKERFILEPATH_REG) && $(DOCKERFILEPATH_REG)/builder $(REGISTRYVERSION) && cd - ; \
|
||||
fi
|
||||
@ -204,9 +204,11 @@ _build_redis:
|
||||
@echo "Done."
|
||||
|
||||
_build_migrator:
|
||||
@echo "building db migrator container for photon..."
|
||||
@cd $(DOCKERFILEPATH_MIGRATOR) && $(DOCKERBUILD) -f $(DOCKERFILEPATH_MIGRATOR)/$(DOCKERFILENAME_MIGRATOR) -t $(DOCKERIMAGENAME_MIGRATOR):$(MIGRATORVERSION) .
|
||||
@echo "Done."
|
||||
@if [ "$(MIGRATORFLAG)" = "true" ] ; then \
|
||||
echo "building db migrator container for photon..."; \
|
||||
cd $(DOCKERFILEPATH_MIGRATOR) && $(DOCKERBUILD) -f $(DOCKERFILEPATH_MIGRATOR)/$(DOCKERFILENAME_MIGRATOR) -t $(DOCKERIMAGENAME_MIGRATOR):$(MIGRATORVERSION) . ; \
|
||||
echo "Done."; \
|
||||
fi
|
||||
|
||||
define _get_binary
|
||||
$(WGET) --timeout 30 --no-check-certificate $1 -O $2
|
||||
|
@ -8,6 +8,7 @@ RUN tdnf install sudo -y >> /dev/null\
|
||||
HEALTHCHECK CMD curl --fail -s http://127.0.0.1:8080/api/ping || exit 1
|
||||
COPY ./make/photon/core/harbor_core ./make/photon/core/start.sh ./UIVERSION /harbor/
|
||||
COPY ./src/core/views /harbor/views
|
||||
COPY ./make/migrations /harbor/migrations
|
||||
|
||||
RUN chmod u+x /harbor/start.sh /harbor/harbor_core
|
||||
WORKDIR /harbor/
|
||||
|
@ -10,12 +10,15 @@ COPY ./make/photon/log/rsyslog.conf /etc/rsyslog.conf
|
||||
# rsyslog configuration file for docker
|
||||
COPY ./make/photon/log/rsyslog_docker.conf /etc/rsyslog.d/
|
||||
|
||||
# run logrotate hourly
|
||||
RUN mv /etc/cron.daily/logrotate /etc/cron.hourly/logrotate
|
||||
# remove the original "logrotate" in directory "/etc/cron.daily/"
|
||||
# and copy the customized one to directory "/etc/cron.hourly/"
|
||||
# to run logrotate hourly
|
||||
RUN rm /etc/cron.daily/logrotate
|
||||
COPY ./make/photon/log/logrotate /etc/cron.hourly/
|
||||
|
||||
COPY ./make/photon/log/start.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/start.sh /etc/rsyslog.d/ && \
|
||||
chown -R 10000:10000 /etc/rsyslog.conf /etc/rsyslog.d/ /run
|
||||
chown -R 10000:10000 /etc/rsyslog.conf /etc/rsyslog.d/ /run /var/lib/logrotate/
|
||||
|
||||
HEALTHCHECK CMD netstat -ltun|grep 10514
|
||||
|
||||
|
6
make/photon/log/logrotate
Executable file
6
make/photon/log/logrotate
Executable file
@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
|
||||
# run the logrotate with user 10000, the state file "/var/lib/logrotate/logrotate.status"
|
||||
# is specified to avoid the permission error
|
||||
sudo -u \#10000 -E /usr/sbin/logrotate -s /var/lib/logrotate/logrotate.status /etc/logrotate.conf
|
||||
exit 0
|
@ -1,10 +1,9 @@
|
||||
FROM golang:1.7.3
|
||||
FROM golang:1.11
|
||||
|
||||
ENV DISTRIBUTION_DIR /go/src/github.com/docker/distribution
|
||||
ENV DOCKER_BUILDTAGS include_oss include_gcs
|
||||
|
||||
WORKDIR $DISTRIBUTION_DIR
|
||||
COPY . $DISTRIBUTION_DIR
|
||||
COPY cmd/registry/config-dev.yml /etc/docker/registry/config.yml
|
||||
|
||||
RUN make PREFIX=/go clean binaries
|
||||
RUN CGO_ENABLED=0 make PREFIX=/go clean binaries
|
||||
|
@ -22,13 +22,13 @@ cur=$PWD
|
||||
TEMP=`mktemp -d /$TMPDIR/distribution.XXXXXX`
|
||||
git clone -b $VERSION https://github.com/docker/distribution.git $TEMP
|
||||
|
||||
echo 'build the registry binary bases on the golang:1.11.2...'
|
||||
echo 'build the registry binary bases on the golang:1.11...'
|
||||
cp Dockerfile.binary $TEMP
|
||||
docker build -f $TEMP/Dockerfile.binary -t registry-golang $TEMP
|
||||
|
||||
echo 'copy the registry binary to local...'
|
||||
ID=$(docker create registry-golang)
|
||||
docker cp $ID:/go/bin/registry binary
|
||||
docker cp $ID:/go/src/github.com/docker/distribution/bin binary
|
||||
|
||||
docker rm -f $ID
|
||||
docker rmi -f registry-golang
|
||||
|
@ -27,5 +27,52 @@ func TestHandleInternalServerError(t *testing.T) {
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("unexpected status code: %d != %d", w.Code, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestHandleBadRequestError(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
err := "error message"
|
||||
handleBadRequestError(w, err)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("unexpected status code: %d != %d", w.Code, http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleUnauthorized(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
handleUnauthorized(w)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("unexpected status code: %d != %d", w.Code, http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteJSONNilInterface(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
if err := writeJSON(w, nil); err != nil {
|
||||
t.Errorf("Expected nil error, received: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteJSONMarshallErr(t *testing.T) {
|
||||
// Tests capture json.Marshall error
|
||||
x := map[string]interface{}{
|
||||
"foo": make(chan int),
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
if err := writeJSON(w, x); err == nil {
|
||||
t.Errorf("Expected %v error received: no no error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteJSON(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
if err := writeJSON(w, "Pong"); err != nil {
|
||||
t.Errorf("Expected nil error, received: %v", err)
|
||||
}
|
||||
}
|
||||
|
@ -16,14 +16,16 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/config/metadata"
|
||||
"github.com/goharbor/harbor/src/common/config/store"
|
||||
"github.com/goharbor/harbor/src/common/config/store/driver"
|
||||
"github.com/goharbor/harbor/src/common/http/modifier/auth"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"os"
|
||||
)
|
||||
|
||||
// CfgManager ... Configure Manager
|
||||
@ -48,18 +50,20 @@ func NewRESTCfgManager(configURL, secret string) *CfgManager {
|
||||
return manager
|
||||
}
|
||||
|
||||
// InmemoryDriver driver for unit testing
|
||||
type InmemoryDriver struct {
|
||||
// InMemoryDriver driver for unit testing
|
||||
type InMemoryDriver struct {
|
||||
cfgMap map[string]interface{}
|
||||
}
|
||||
|
||||
// Load ...
|
||||
func (d *InmemoryDriver) Load() (map[string]interface{}, error) {
|
||||
// Load load data from driver, for example load from database,
|
||||
// it should be invoked before get any user scope config
|
||||
// for system scope config, because it is immutable, no need to call this method
|
||||
func (d *InMemoryDriver) Load() (map[string]interface{}, error) {
|
||||
return d.cfgMap, nil
|
||||
}
|
||||
|
||||
// Save ...
|
||||
func (d *InmemoryDriver) Save(cfg map[string]interface{}) error {
|
||||
// Save only save user config setting to driver, for example: database, REST
|
||||
func (d *InMemoryDriver) Save(cfg map[string]interface{}) error {
|
||||
for k, v := range cfg {
|
||||
d.cfgMap[k] = v
|
||||
}
|
||||
@ -68,7 +72,12 @@ func (d *InmemoryDriver) Save(cfg map[string]interface{}) error {
|
||||
|
||||
// NewInMemoryManager create a manager for unit testing, doesn't involve database or REST
|
||||
func NewInMemoryManager() *CfgManager {
|
||||
return &CfgManager{store: store.NewConfigStore(&InmemoryDriver{cfgMap: map[string]interface{}{}})}
|
||||
manager := &CfgManager{store: store.NewConfigStore(&InMemoryDriver{cfgMap: map[string]interface{}{}})}
|
||||
// load default value
|
||||
manager.loadDefault()
|
||||
// load system config from env
|
||||
manager.loadSystemConfigFromEnv()
|
||||
return manager
|
||||
}
|
||||
|
||||
// loadDefault ...
|
||||
@ -106,23 +115,47 @@ func (c *CfgManager) loadSystemConfigFromEnv() {
|
||||
}
|
||||
}
|
||||
|
||||
// GetAll ... Get all settings
|
||||
func (c *CfgManager) GetAll() []metadata.ConfigureValue {
|
||||
results := make([]metadata.ConfigureValue, 0)
|
||||
// GetAll get all settings.
|
||||
func (c *CfgManager) GetAll() map[string]interface{} {
|
||||
resultMap := map[string]interface{}{}
|
||||
if err := c.store.Load(); err != nil {
|
||||
log.Errorf("GetAll failed, error %v", err)
|
||||
return results
|
||||
return resultMap
|
||||
}
|
||||
metaDataList := metadata.Instance().GetAll()
|
||||
for _, item := range metaDataList {
|
||||
if cfgValue, err := c.store.Get(item.Name); err == nil {
|
||||
results = append(results, *cfgValue)
|
||||
cfgValue, err := c.store.GetAnyType(item.Name)
|
||||
if err != metadata.ErrValueNotSet && err != nil {
|
||||
log.Errorf("Failed to get value of key %v, error %v", item.Name, err)
|
||||
continue
|
||||
}
|
||||
resultMap[item.Name] = cfgValue
|
||||
}
|
||||
return results
|
||||
return resultMap
|
||||
}
|
||||
|
||||
// Load - Load configuration from storage, like database or redis
|
||||
// GetUserCfgs retrieve all user configs
|
||||
func (c *CfgManager) GetUserCfgs() map[string]interface{} {
|
||||
resultMap := map[string]interface{}{}
|
||||
if err := c.store.Load(); err != nil {
|
||||
log.Errorf("GetUserCfgs failed, error %v", err)
|
||||
return resultMap
|
||||
}
|
||||
metaDataList := metadata.Instance().GetAll()
|
||||
for _, item := range metaDataList {
|
||||
if item.Scope == metadata.UserScope {
|
||||
cfgValue, err := c.store.GetAnyType(item.Name)
|
||||
if err != metadata.ErrValueNotSet && err != nil {
|
||||
log.Errorf("Failed to get value of key %v, error %v", item.Name, err)
|
||||
continue
|
||||
}
|
||||
resultMap[item.Name] = cfgValue
|
||||
}
|
||||
}
|
||||
return resultMap
|
||||
}
|
||||
|
||||
// Load load configuration from storage, like database or redis
|
||||
func (c *CfgManager) Load() error {
|
||||
return c.store.Load()
|
||||
}
|
||||
@ -144,7 +177,7 @@ func (c *CfgManager) Get(key string) *metadata.ConfigureValue {
|
||||
|
||||
// Set ...
|
||||
func (c *CfgManager) Set(key string, value interface{}) {
|
||||
configValue, err := metadata.NewCfgValue(key, fmt.Sprintf("%v", value))
|
||||
configValue, err := metadata.NewCfgValue(key, utils.GetStrValueOfAnyType(value))
|
||||
if err != nil {
|
||||
log.Errorf("error when setting key: %v, error %v", key, err)
|
||||
return
|
||||
@ -153,14 +186,6 @@ func (c *CfgManager) Set(key string, value interface{}) {
|
||||
}
|
||||
|
||||
// GetDatabaseCfg - Get database configurations
|
||||
/*
|
||||
In database related testing, call it in the TestMain to initialize database schema and set testing configures
|
||||
|
||||
cfgMgr := config.NewDBCfgManager()
|
||||
dao.InitDatabase(cfgMgr.GetDatabaseCfg())
|
||||
cfgMgr.Load()
|
||||
cfgMrg.UpdateConfig(testingConfigs)
|
||||
*/
|
||||
func (c *CfgManager) GetDatabaseCfg() *models.Database {
|
||||
return &models.Database{
|
||||
Type: c.Get(common.DatabaseType).GetString(),
|
||||
@ -175,7 +200,27 @@ func (c *CfgManager) GetDatabaseCfg() *models.Database {
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateConfig - Update config store with a specified configuration and also save updated configure
|
||||
// UpdateConfig - Update config store with a specified configuration and also save updated configure.
|
||||
func (c *CfgManager) UpdateConfig(cfgs map[string]interface{}) error {
|
||||
return c.store.Update(cfgs)
|
||||
}
|
||||
|
||||
// ValidateCfg validate config by metadata. return the first error if exist.
|
||||
func (c *CfgManager) ValidateCfg(cfgs map[string]interface{}) error {
|
||||
for key, value := range cfgs {
|
||||
strVal := utils.GetStrValueOfAnyType(value)
|
||||
_, err := metadata.NewCfgValue(key, strVal)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%v, item name: %v", err, key)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DumpTrace dump all configurations
|
||||
func (c *CfgManager) DumpTrace() {
|
||||
cfgs := c.GetAll()
|
||||
for k, v := range cfgs {
|
||||
log.Info(k, ":=", v)
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/utils/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"os"
|
||||
"testing"
|
||||
@ -16,32 +16,30 @@ var TestDBConfig = map[string]interface{}{
|
||||
"postgresql_sslmode": "disable",
|
||||
"email_host": "127.0.0.1",
|
||||
"clair_url": "http://clair:6060",
|
||||
"scan_all_policy": `{"parameter":{"daily_time":0},"type":"daily"}`,
|
||||
}
|
||||
|
||||
var configManager *CfgManager
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
configManager = NewDBCfgManager()
|
||||
dao.InitDatabase(configManager.GetDatabaseCfg())
|
||||
test.InitDatabaseFromEnv()
|
||||
configManager.UpdateConfig(TestDBConfig)
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestLoadFromDatabase(t *testing.T) {
|
||||
|
||||
dao.InitDatabase(configManager.GetDatabaseCfg())
|
||||
configManager.Load()
|
||||
configManager.UpdateConfig(TestDBConfig)
|
||||
configManager.Load()
|
||||
assert.Equal(t, "127.0.0.1", configManager.Get("email_host").GetString())
|
||||
assert.Equal(t, "http://clair:6060", configManager.Get("clair_url").GetString())
|
||||
assert.Equal(t, `{"parameter":{"daily_time":0},"type":"daily"}`, configManager.Get("scan_all_policy").GetString())
|
||||
}
|
||||
|
||||
func TestSaveToDatabase(t *testing.T) {
|
||||
dao.InitDatabase(configManager.GetDatabaseCfg())
|
||||
fmt.Printf("database config %#v\n", configManager.GetDatabaseCfg())
|
||||
configManager.Load()
|
||||
configManager.Set("read_only", "true")
|
||||
configManager.UpdateConfig(TestDBConfig)
|
||||
configManager.Save()
|
||||
configManager.Load()
|
||||
assert.Equal(t, true, configManager.Get("read_only").GetBool())
|
||||
@ -55,7 +53,6 @@ func TestUpdateCfg(t *testing.T) {
|
||||
"ldap_search_password": "admin",
|
||||
"ldap_base_dn": "dc=example,dc=com",
|
||||
}
|
||||
dao.InitDatabase(configManager.GetDatabaseCfg())
|
||||
configManager.Load()
|
||||
configManager.UpdateConfig(testConfig)
|
||||
|
||||
@ -111,3 +108,16 @@ func TestNewInMemoryManager(t *testing.T) {
|
||||
assert.Equal(t, 5, inMemoryManager.Get("ldap_timeout").GetInt())
|
||||
assert.Equal(t, true, inMemoryManager.Get("ldap_verify_cert").GetBool())
|
||||
}
|
||||
|
||||
/*
|
||||
func TestNewRESTCfgManager(t *testing.T) {
|
||||
restMgr := NewRESTCfgManager("http://10.161.47.13:8080"+common.CoreConfigPath, "0XtgSGFx1amMDTaH")
|
||||
err := restMgr.Load()
|
||||
if err != nil {
|
||||
t.Errorf("Failed with error %v", err)
|
||||
}
|
||||
fmt.Printf("db:%v", restMgr.GetDatabaseCfg().Type)
|
||||
fmt.Printf("host:%#v\n", restMgr.GetDatabaseCfg().PostGreSQL.Host)
|
||||
fmt.Printf("port:%#v\n", restMgr.GetDatabaseCfg().PostGreSQL.Port)
|
||||
|
||||
}*/
|
||||
|
@ -59,15 +59,15 @@ var (
|
||||
// 3. CfgManager.Load()/CfgManager.Save() to load/save from configure storage.
|
||||
ConfigList = []Item{
|
||||
{Name: "admin_initial_password", Scope: SystemScope, Group: BasicGroup, EnvKey: "HARBOR_ADMIN_PASSWORD", DefaultValue: "", ItemType: &PasswordType{}, Editable: true},
|
||||
{Name: "admiral_url", Scope: SystemScope, Group: BasicGroup, EnvKey: "ADMIRAL_URL", DefaultValue: "NA", ItemType: &StringType{}, Editable: false},
|
||||
{Name: "auth_mode", Scope: UserScope, Group: BasicGroup, EnvKey: "AUTH_MODE", DefaultValue: "db_auth", ItemType: &StringType{}, Editable: false},
|
||||
{Name: "admiral_url", Scope: SystemScope, Group: BasicGroup, EnvKey: "ADMIRAL_URL", DefaultValue: "", ItemType: &StringType{}, Editable: false},
|
||||
{Name: "auth_mode", Scope: UserScope, Group: BasicGroup, EnvKey: "AUTH_MODE", DefaultValue: "db_auth", ItemType: &AuthModeType{}, Editable: false},
|
||||
{Name: "cfg_expiration", Scope: SystemScope, Group: BasicGroup, EnvKey: "CFG_EXPIRATION", DefaultValue: "5", ItemType: &IntType{}, Editable: false},
|
||||
{Name: "chart_repository_url", Scope: SystemScope, Group: BasicGroup, EnvKey: "CHART_REPOSITORY_URL", DefaultValue: "http://chartmuseum:9999", ItemType: &StringType{}, Editable: false},
|
||||
|
||||
{Name: "clair_db", Scope: SystemScope, Group: ClairGroup, EnvKey: "CLAIR_DB", DefaultValue: "postgres", ItemType: &StringType{}, Editable: false},
|
||||
{Name: "clair_db_host", Scope: SystemScope, Group: ClairGroup, EnvKey: "CLAIR_DB_HOST", DefaultValue: "postgresql", ItemType: &StringType{}, Editable: false},
|
||||
{Name: "clair_db_password", Scope: SystemScope, Group: ClairGroup, EnvKey: "CLAIR_DB_PASSWORD", DefaultValue: "root123", ItemType: &PasswordType{}, Editable: false},
|
||||
{Name: "clair_db_port", Scope: SystemScope, Group: ClairGroup, EnvKey: "CLAIR_DB_PORT", DefaultValue: "5432", ItemType: &IntType{}, Editable: false},
|
||||
{Name: "clair_db_port", Scope: SystemScope, Group: ClairGroup, EnvKey: "CLAIR_DB_PORT", DefaultValue: "5432", ItemType: &PortType{}, Editable: false},
|
||||
{Name: "clair_db_sslmode", Scope: SystemScope, Group: ClairGroup, EnvKey: "CLAIR_DB_SSLMODE", DefaultValue: "disable", ItemType: &StringType{}, Editable: false},
|
||||
{Name: "clair_db_username", Scope: SystemScope, Group: ClairGroup, EnvKey: "CLAIR_DB_USERNAME", DefaultValue: "postgres", ItemType: &StringType{}, Editable: false},
|
||||
{Name: "clair_url", Scope: SystemScope, Group: ClairGroup, EnvKey: "CLAIR_URL", DefaultValue: "http://clair:6060", ItemType: &StringType{}, Editable: false},
|
||||
@ -80,39 +80,40 @@ var (
|
||||
{Name: "email_identity", Scope: UserScope, Group: EmailGroup, EnvKey: "EMAIL_IDENTITY", DefaultValue: "", ItemType: &StringType{}, Editable: false},
|
||||
{Name: "email_insecure", Scope: UserScope, Group: EmailGroup, EnvKey: "EMAIL_INSECURE", DefaultValue: "false", ItemType: &BoolType{}, Editable: false},
|
||||
{Name: "email_password", Scope: UserScope, Group: EmailGroup, EnvKey: "EMAIL_PWD", DefaultValue: "", ItemType: &PasswordType{}, Editable: false},
|
||||
{Name: "email_port", Scope: UserScope, Group: EmailGroup, EnvKey: "EMAIL_PORT", DefaultValue: "25", ItemType: &IntType{}, Editable: false},
|
||||
{Name: "email_port", Scope: UserScope, Group: EmailGroup, EnvKey: "EMAIL_PORT", DefaultValue: "25", ItemType: &PortType{}, Editable: false},
|
||||
{Name: "email_ssl", Scope: UserScope, Group: EmailGroup, EnvKey: "EMAIL_SSL", DefaultValue: "false", ItemType: &BoolType{}, Editable: false},
|
||||
{Name: "email_username", Scope: UserScope, Group: EmailGroup, EnvKey: "EMAIL_USR", DefaultValue: "sample_admin@mydomain.com", ItemType: &StringType{}, Editable: false},
|
||||
|
||||
{Name: "ext_endpoint", Scope: SystemScope, Group: BasicGroup, EnvKey: "EXT_ENDPOINT", DefaultValue: "https://host01.com", ItemType: &StringType{}, Editable: false},
|
||||
{Name: "jobservice_url", Scope: SystemScope, Group: BasicGroup, EnvKey: "JOBSERVICE_URL", DefaultValue: "http://jobservice:8080", ItemType: &StringType{}, Editable: false},
|
||||
|
||||
{Name: "ldap_base_dn", Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_BASE_DN", DefaultValue: "", ItemType: &StringType{}, Editable: false},
|
||||
{Name: "ldap_base_dn", Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_BASE_DN", DefaultValue: "", ItemType: &NonEmptyStringType{}, Editable: false},
|
||||
{Name: "ldap_filter", Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_FILTER", DefaultValue: "", ItemType: &StringType{}, Editable: false},
|
||||
{Name: "ldap_group_base_dn", Scope: UserScope, Group: LdapGroupGroup, EnvKey: "LDAP_GROUP_BASE_DN", DefaultValue: "", ItemType: &StringType{}, Editable: false},
|
||||
{Name: "ldap_group_admin_dn", Scope: UserScope, Group: LdapGroupGroup, EnvKey: "LDAP_GROUP_ADMIN_DN", DefaultValue: "", ItemType: &StringType{}, Editable: false},
|
||||
{Name: "ldap_group_attribute_name", Scope: UserScope, Group: LdapGroupGroup, EnvKey: "LDAP_GROUP_GID", DefaultValue: "", ItemType: &StringType{}, Editable: false},
|
||||
{Name: "ldap_group_search_filter", Scope: UserScope, Group: LdapGroupGroup, EnvKey: "LDAP_GROUP_FILTER", DefaultValue: "", ItemType: &StringType{}, Editable: false},
|
||||
{Name: "ldap_group_search_scope", Scope: UserScope, Group: LdapGroupGroup, EnvKey: "LDAP_GROUP_SCOPE", DefaultValue: "2", ItemType: &IntType{}, Editable: false},
|
||||
{Name: "ldap_scope", Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_SCOPE", DefaultValue: "2", ItemType: &IntType{}, Editable: true},
|
||||
{Name: "ldap_group_search_scope", Scope: UserScope, Group: LdapGroupGroup, EnvKey: "LDAP_GROUP_SCOPE", DefaultValue: "2", ItemType: &LdapScopeType{}, Editable: false},
|
||||
{Name: "ldap_scope", Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_SCOPE", DefaultValue: "2", ItemType: &LdapScopeType{}, Editable: false},
|
||||
{Name: "ldap_search_dn", Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_SEARCH_DN", DefaultValue: "", ItemType: &StringType{}, Editable: false},
|
||||
{Name: "ldap_search_password", Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_SEARCH_PWD", DefaultValue: "", ItemType: &PasswordType{}, Editable: false},
|
||||
{Name: "ldap_timeout", Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_TIMEOUT", DefaultValue: "5", ItemType: &IntType{}, Editable: false},
|
||||
{Name: "ldap_uid", Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_UID", DefaultValue: "cn", ItemType: &StringType{}, Editable: true},
|
||||
{Name: "ldap_url", Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_URL", DefaultValue: "", ItemType: &StringType{}, Editable: true},
|
||||
{Name: "ldap_uid", Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_UID", DefaultValue: "cn", ItemType: &NonEmptyStringType{}, Editable: false},
|
||||
{Name: "ldap_url", Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_URL", DefaultValue: "", ItemType: &NonEmptyStringType{}, Editable: false},
|
||||
{Name: "ldap_verify_cert", Scope: UserScope, Group: LdapBasicGroup, EnvKey: "LDAP_VERIFY_CERT", DefaultValue: "true", ItemType: &BoolType{}, Editable: false},
|
||||
|
||||
{Name: "max_job_workers", Scope: SystemScope, Group: BasicGroup, EnvKey: "MAX_JOB_WORKERS", DefaultValue: "10", ItemType: &IntType{}, Editable: false},
|
||||
{Name: "notary_url", Scope: SystemScope, Group: BasicGroup, EnvKey: "NOTARY_URL", DefaultValue: "http://notary-server:4443", ItemType: &StringType{}, Editable: false},
|
||||
{Name: "scan_all_policy", Scope: UserScope, Group: BasicGroup, EnvKey: "", DefaultValue: "", ItemType: &MapType{}, Editable: false},
|
||||
|
||||
{Name: "postgresql_database", Scope: SystemScope, Group: DatabaseGroup, EnvKey: "POSTGRESQL_DATABASE", DefaultValue: "registry", ItemType: &StringType{}, Editable: false},
|
||||
{Name: "postgresql_host", Scope: SystemScope, Group: DatabaseGroup, EnvKey: "POSTGRESQL_HOST", DefaultValue: "postgresql", ItemType: &StringType{}, Editable: false},
|
||||
{Name: "postgresql_password", Scope: SystemScope, Group: DatabaseGroup, EnvKey: "POSTGRESQL_PASSWORD", DefaultValue: "root123", ItemType: &PasswordType{}, Editable: false},
|
||||
{Name: "postgresql_port", Scope: SystemScope, Group: DatabaseGroup, EnvKey: "POSTGRESQL_PORT", DefaultValue: "5432", ItemType: &IntType{}, Editable: false},
|
||||
{Name: "postgresql_port", Scope: SystemScope, Group: DatabaseGroup, EnvKey: "POSTGRESQL_PORT", DefaultValue: "5432", ItemType: &PortType{}, Editable: false},
|
||||
{Name: "postgresql_sslmode", Scope: SystemScope, Group: DatabaseGroup, EnvKey: "POSTGRESQL_SSLMODE", DefaultValue: "disable", ItemType: &StringType{}, Editable: false},
|
||||
{Name: "postgresql_username", Scope: SystemScope, Group: DatabaseGroup, EnvKey: "POSTGRESQL_USERNAME", DefaultValue: "postgres", ItemType: &StringType{}, Editable: false},
|
||||
|
||||
{Name: "project_creation_restriction", Scope: UserScope, Group: BasicGroup, EnvKey: "PROJECT_CREATION_RESTRICTION", DefaultValue: common.ProCrtRestrEveryone, ItemType: &StringType{}, Editable: false},
|
||||
{Name: "project_creation_restriction", Scope: UserScope, Group: BasicGroup, EnvKey: "PROJECT_CREATION_RESTRICTION", DefaultValue: common.ProCrtRestrEveryone, ItemType: &ProjectCreationRestrictionType{}, Editable: false},
|
||||
{Name: "read_only", Scope: UserScope, Group: BasicGroup, EnvKey: "READ_ONLY", DefaultValue: "false", ItemType: &BoolType{}, Editable: false},
|
||||
|
||||
{Name: "registry_storage_provider_name", Scope: SystemScope, Group: BasicGroup, EnvKey: "REGISTRY_STORAGE_PROVIDER_NAME", DefaultValue: "filesystem", ItemType: &StringType{}, Editable: false},
|
||||
@ -120,7 +121,7 @@ var (
|
||||
{Name: "registry_controller_url", Scope: SystemScope, Group: BasicGroup, EnvKey: "REGISTRY_CONTROLLER_URL", DefaultValue: "http://registryctl:8080", ItemType: &StringType{}, Editable: false},
|
||||
{Name: "self_registration", Scope: UserScope, Group: BasicGroup, EnvKey: "SELF_REGISTRATION", DefaultValue: "true", ItemType: &BoolType{}, Editable: false},
|
||||
{Name: "token_expiration", Scope: UserScope, Group: BasicGroup, EnvKey: "TOKEN_EXPIRATION", DefaultValue: "30", ItemType: &IntType{}, Editable: false},
|
||||
{Name: "token_service_url", Scope: SystemScope, Group: BasicGroup, EnvKey: "TOKEN_SERVICE_URL", DefaultValue: "", ItemType: &StringType{}, Editable: false},
|
||||
{Name: "token_service_url", Scope: SystemScope, Group: BasicGroup, EnvKey: "TOKEN_SERVICE_URL", DefaultValue: "http://core:8080/service/token", ItemType: &StringType{}, Editable: false},
|
||||
|
||||
{Name: "uaa_client_id", Scope: UserScope, Group: UAAGroup, EnvKey: "UAA_CLIENTID", DefaultValue: "", ItemType: &StringType{}, Editable: false},
|
||||
{Name: "uaa_client_secret", Scope: UserScope, Group: UAAGroup, EnvKey: "UAA_CLIENTSECRET", DefaultValue: "", ItemType: &StringType{}, Editable: false},
|
||||
@ -128,7 +129,7 @@ var (
|
||||
{Name: "uaa_verify_cert", Scope: UserScope, Group: UAAGroup, EnvKey: "UAA_VERIFY_CERT", DefaultValue: "false", ItemType: &BoolType{}, Editable: false},
|
||||
|
||||
{Name: "with_chartmuseum", Scope: SystemScope, Group: BasicGroup, EnvKey: "WITH_CHARTMUSEUM", DefaultValue: "false", ItemType: &BoolType{}, Editable: true},
|
||||
{Name: "with_clair", Scope: SystemScope, Group: BasicGroup, EnvKey: "WITH_CLAIR", DefaultValue: "true", ItemType: &BoolType{}, Editable: true},
|
||||
{Name: "with_clair", Scope: SystemScope, Group: BasicGroup, EnvKey: "WITH_CLAIR", DefaultValue: "false", ItemType: &BoolType{}, Editable: true},
|
||||
{Name: "with_notary", Scope: SystemScope, Group: BasicGroup, EnvKey: "WITH_NOTARY", DefaultValue: "false", ItemType: &BoolType{}, Editable: true},
|
||||
}
|
||||
)
|
||||
|
@ -12,11 +12,15 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package metadata define config related metadata
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Type - Use this interface to define and encapsulate the behavior of validation and transformation
|
||||
@ -39,6 +43,46 @@ func (t *StringType) get(str string) (interface{}, error) {
|
||||
return str, nil
|
||||
}
|
||||
|
||||
// NonEmptyStringType ...
|
||||
type NonEmptyStringType struct {
|
||||
StringType
|
||||
}
|
||||
|
||||
func (t *NonEmptyStringType) validate(str string) error {
|
||||
if len(strings.TrimSpace(str)) == 0 {
|
||||
return ErrStringValueIsEmpty
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AuthModeType ...
|
||||
type AuthModeType struct {
|
||||
StringType
|
||||
}
|
||||
|
||||
func (t *AuthModeType) validate(str string) error {
|
||||
if str == common.LDAPAuth || str == common.DBAuth || str == common.UAAAuth {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid %s, shoud be one of %s, %s, %s",
|
||||
common.AUTHMode, common.DBAuth, common.LDAPAuth, common.UAAAuth)
|
||||
}
|
||||
|
||||
// ProjectCreationRestrictionType ...
|
||||
type ProjectCreationRestrictionType struct {
|
||||
StringType
|
||||
}
|
||||
|
||||
func (t *ProjectCreationRestrictionType) validate(str string) error {
|
||||
if !(str == common.ProCrtRestrAdmOnly || str == common.ProCrtRestrEveryone) {
|
||||
return fmt.Errorf("invalid %s, should be %s or %s",
|
||||
common.ProjectCreationRestriction,
|
||||
common.ProCrtRestrAdmOnly,
|
||||
common.ProCrtRestrEveryone)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IntType ..
|
||||
type IntType struct {
|
||||
}
|
||||
@ -48,22 +92,45 @@ func (t *IntType) validate(str string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// GetInt ...
|
||||
func (t *IntType) get(str string) (interface{}, error) {
|
||||
return strconv.Atoi(str)
|
||||
}
|
||||
|
||||
// PortType ...
|
||||
type PortType struct {
|
||||
IntType
|
||||
}
|
||||
|
||||
func (t *PortType) validate(str string) error {
|
||||
val, err := strconv.Atoi(str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if val < 0 {
|
||||
return fmt.Errorf("network port should be greater than 0")
|
||||
}
|
||||
|
||||
if val > 65535 {
|
||||
return fmt.Errorf("network port should be less than 65535")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// LdapScopeType - The LDAP scope is a int type, but its is limit to 0, 1, 2
|
||||
type LdapScopeType struct {
|
||||
IntType
|
||||
}
|
||||
|
||||
// Validate - Verify the range is limited
|
||||
// validate - Verify the range is limited
|
||||
func (t *LdapScopeType) validate(str string) error {
|
||||
if str == "0" || str == "1" || str == "2" {
|
||||
return nil
|
||||
}
|
||||
return ErrInvalidData
|
||||
return fmt.Errorf("invalid scope, should be %d, %d or %d",
|
||||
common.LDAPScopeBase,
|
||||
common.LDAPScopeOnelevel,
|
||||
common.LDAPScopeSubtree)
|
||||
}
|
||||
|
||||
// Int64Type ...
|
||||
@ -75,7 +142,6 @@ func (t *Int64Type) validate(str string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// GetInt64 ...
|
||||
func (t *Int64Type) get(str string) (interface{}, error) {
|
||||
return strconv.ParseInt(str, 10, 64)
|
||||
}
|
||||
@ -116,7 +182,7 @@ func (t *MapType) validate(str string) error {
|
||||
}
|
||||
|
||||
func (t *MapType) get(str string) (interface{}, error) {
|
||||
result := map[string]string{}
|
||||
result := map[string]interface{}{}
|
||||
err := json.Unmarshal([]byte(str), &result)
|
||||
return result, err
|
||||
}
|
||||
|
@ -94,5 +94,5 @@ func TestMapType_validate(t *testing.T) {
|
||||
func TestMapType_get(t *testing.T) {
|
||||
test := &MapType{}
|
||||
result, _ := test.get(`{"sample":"abc", "another":"welcome"}`)
|
||||
assert.Equal(t, result, map[string]string{"sample": "abc", "another": "welcome"})
|
||||
assert.Equal(t, map[string]interface{}{"sample": "abc", "another": "welcome"}, result)
|
||||
}
|
||||
|
@ -29,6 +29,8 @@ var (
|
||||
ErrInvalidData = errors.New("the data provided is invalid")
|
||||
// ErrValueNotSet ...
|
||||
ErrValueNotSet = errors.New("the configure value is not set")
|
||||
// ErrStringValueIsEmpty ...
|
||||
ErrStringValueIsEmpty = errors.New("the configure value can not be empty")
|
||||
)
|
||||
|
||||
// ConfigureValue - struct to hold a actual value, also include the name of config metadata.
|
||||
@ -126,6 +128,14 @@ func (c *ConfigureValue) GetStringToStringMap() map[string]string {
|
||||
return result
|
||||
}
|
||||
|
||||
// GetAnyType get the interface{} of current value
|
||||
func (c *ConfigureValue) GetAnyType() (interface{}, error) {
|
||||
if item, ok := Instance().GetByName(c.Name); ok {
|
||||
return item.ItemType.get(c.Value)
|
||||
}
|
||||
return nil, ErrNotDefined
|
||||
}
|
||||
|
||||
// Validate - to validate configure items, if passed, return nil, else return error
|
||||
func (c *ConfigureValue) Validate() error {
|
||||
if item, ok := Instance().GetByName(c.Name); ok {
|
||||
|
@ -26,6 +26,7 @@ var testingMetaDataArray = []Item{
|
||||
{Name: "ulimit", ItemType: &Int64Type{}, Scope: "user", Group: "ldapbasic"},
|
||||
{Name: "ldap_verify_cert", ItemType: &BoolType{}, Scope: "user", Group: "ldapbasic"},
|
||||
{Name: "sample_map_setting", ItemType: &MapType{}, Scope: "user", Group: "ldapbasic"},
|
||||
{Name: "scan_all_policy", ItemType: &MapType{}, Scope: "user", Group: "basic"},
|
||||
}
|
||||
|
||||
// createCfgValue ... Create a ConfigureValue object, only used in test
|
||||
@ -50,7 +51,11 @@ func TestConfigureValue_GetString(t *testing.T) {
|
||||
|
||||
func TestConfigureValue_GetStringToStringMap(t *testing.T) {
|
||||
Instance().initFromArray(testingMetaDataArray)
|
||||
assert.Equal(t, createCfgValue("sample_map_setting", `{"sample":"abc"}`).GetStringToStringMap(), map[string]string{"sample": "abc"})
|
||||
val, err := createCfgValue("sample_map_setting", `{"sample":"abc"}`).GetAnyType()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.Equal(t, val, map[string]interface{}{"sample": "abc"})
|
||||
Instance().init()
|
||||
}
|
||||
func TestConfigureValue_GetInt(t *testing.T) {
|
||||
@ -61,3 +66,12 @@ func TestConfigureValue_GetInt64(t *testing.T) {
|
||||
Instance().initFromArray(testingMetaDataArray)
|
||||
assert.Equal(t, createCfgValue("ulimit", "99999").GetInt64(), int64(99999))
|
||||
}
|
||||
|
||||
func TestNewScanAllPolicy(t *testing.T) {
|
||||
Instance().initFromArray(testingMetaDataArray)
|
||||
value, err := NewCfgValue("scan_all_policy", `{"parameter":{"daily_time":0},"type":"daily"}`)
|
||||
if err != nil {
|
||||
t.Errorf("Can not create scan all policy err: %v", err)
|
||||
}
|
||||
fmt.Printf("value %v\n", value.GetString())
|
||||
}
|
||||
|
@ -15,12 +15,13 @@
|
||||
package driver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/common/config/encrypt"
|
||||
"github.com/goharbor/harbor/src/common/config/metadata"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Database - Used to load/save configuration in database
|
||||
@ -61,11 +62,11 @@ func (d *Database) Save(cfgs map[string]interface{}) error {
|
||||
var configEntries []models.ConfigEntry
|
||||
for key, value := range cfgs {
|
||||
if item, ok := metadata.Instance().GetByName(key); ok {
|
||||
if item.Scope == metadata.SystemScope {
|
||||
if os.Getenv("UTTEST") != "true" && item.Scope == metadata.SystemScope {
|
||||
log.Errorf("system setting can not updated, key %v", key)
|
||||
continue
|
||||
}
|
||||
strValue := fmt.Sprintf("%v", value)
|
||||
strValue := utils.GetStrValueOfAnyType(value)
|
||||
entry := &models.ConfigEntry{Key: key, Value: strValue}
|
||||
if _, ok := item.ItemType.(*metadata.PasswordType); ok {
|
||||
if encryptPassword, err := encrypt.Instance().Encrypt(strValue); err == nil {
|
||||
|
@ -14,6 +14,7 @@
|
||||
package driver
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"os"
|
||||
@ -26,18 +27,22 @@ func TestMain(m *testing.M) {
|
||||
}
|
||||
|
||||
func TestDatabase_Load(t *testing.T) {
|
||||
|
||||
cfgs := map[string]interface{}{
|
||||
common.AUTHMode: "db_auth",
|
||||
common.LDAPURL: "ldap://ldap.vmware.com",
|
||||
}
|
||||
driver := Database{}
|
||||
driver.Save(cfgs)
|
||||
cfgMap, err := driver.Load()
|
||||
if err != nil {
|
||||
t.Errorf("failed to load, error %v", err)
|
||||
}
|
||||
|
||||
assert.True(t, len(cfgMap) > 10)
|
||||
assert.True(t, len(cfgMap) >= 1)
|
||||
|
||||
if _, ok := cfgMap["ldap_url"]; !ok {
|
||||
t.Error("Can not find ldap_url")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestDatabase_Save(t *testing.T) {
|
||||
|
@ -1,29 +1,38 @@
|
||||
package driver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/goharbor/harbor/src/common/http"
|
||||
"github.com/goharbor/harbor/src/common/http/modifier"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
)
|
||||
|
||||
// RESTDriver - config store driver based on REST API
|
||||
type RESTDriver struct {
|
||||
coreURL string
|
||||
client *http.Client
|
||||
configRESTURL string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewRESTDriver - Create RESTDriver
|
||||
func NewRESTDriver(coreURL string, modifiers ...modifier.Modifier) *RESTDriver {
|
||||
return &RESTDriver{coreURL: coreURL, client: http.NewClient(nil, modifiers...)}
|
||||
func NewRESTDriver(configRESTURL string, modifiers ...modifier.Modifier) *RESTDriver {
|
||||
return &RESTDriver{configRESTURL: configRESTURL, client: http.NewClient(nil, modifiers...)}
|
||||
}
|
||||
|
||||
// Load - load config data from REST server
|
||||
func (h *RESTDriver) Load() (map[string]interface{}, error) {
|
||||
cfgMap := map[string]interface{}{}
|
||||
err := h.client.Get(h.coreURL, &cfgMap)
|
||||
log.Infof("get configuration from url: %+v", h.configRESTURL)
|
||||
err := h.client.Get(h.configRESTURL, &cfgMap)
|
||||
if err != nil {
|
||||
log.Errorf("Failed on load rest config err:%v, url:%v", err, h.configRESTURL)
|
||||
}
|
||||
if len(cfgMap) < 1 {
|
||||
return cfgMap, errors.New("failed to load rest config")
|
||||
}
|
||||
return cfgMap, err
|
||||
}
|
||||
|
||||
// Save - Save config data to REST server by PUT method
|
||||
func (h *RESTDriver) Save(cfgMap map[string]interface{}) error {
|
||||
return h.client.Put(h.coreURL, cfgMap)
|
||||
return h.client.Put(h.configRESTURL, cfgMap)
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/common/config/metadata"
|
||||
"github.com/goharbor/harbor/src/common/config/store/driver"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"sync"
|
||||
)
|
||||
@ -30,7 +31,17 @@ func (c *ConfigStore) Get(key string) (*metadata.ConfigureValue, error) {
|
||||
return nil, errors.New("data in config store is not a ConfigureValue type")
|
||||
}
|
||||
return nil, metadata.ErrValueNotSet
|
||||
}
|
||||
|
||||
// GetAnyType get interface{} type for config items
|
||||
func (c *ConfigStore) GetAnyType(key string) (interface{}, error) {
|
||||
if value, ok := c.cfgValues.Load(key); ok {
|
||||
if result, ok := value.(metadata.ConfigureValue); ok {
|
||||
return result.GetAnyType()
|
||||
}
|
||||
return nil, errors.New("data in config store is not a ConfigureValue type")
|
||||
}
|
||||
return nil, metadata.ErrValueNotSet
|
||||
}
|
||||
|
||||
// Set - Set configure value in store, not saved to config driver
|
||||
@ -71,7 +82,6 @@ func (c *ConfigStore) Save() error {
|
||||
if _, ok := metadata.Instance().GetByName(keyStr); ok {
|
||||
cfgMap[keyStr] = valueStr
|
||||
} else {
|
||||
|
||||
log.Errorf("failed to get metadata for key %v", keyStr)
|
||||
}
|
||||
}
|
||||
@ -89,7 +99,7 @@ func (c *ConfigStore) Save() error {
|
||||
func (c *ConfigStore) Update(cfgMap map[string]interface{}) error {
|
||||
// Update to store
|
||||
for key, value := range cfgMap {
|
||||
configValue, err := metadata.NewCfgValue(key, fmt.Sprintf("%v", value))
|
||||
configValue, err := metadata.NewCfgValue(key, utils.GetStrValueOfAnyType(value))
|
||||
if err != nil {
|
||||
log.Warningf("error %v, skip to update configure item, key:%v ", err, key)
|
||||
delete(cfgMap, key)
|
||||
|
@ -19,6 +19,7 @@ const (
|
||||
DBAuth = "db_auth"
|
||||
LDAPAuth = "ldap_auth"
|
||||
UAAAuth = "uaa_auth"
|
||||
HTTPAuth = "http_auth"
|
||||
ProCrtRestrEveryone = "everyone"
|
||||
ProCrtRestrAdmOnly = "adminonly"
|
||||
LDAPScopeBase = 0
|
||||
@ -28,6 +29,7 @@ const (
|
||||
RoleProjectAdmin = 1
|
||||
RoleDeveloper = 2
|
||||
RoleGuest = 3
|
||||
RoleMaster = 4
|
||||
|
||||
LabelLevelSystem = "s"
|
||||
LabelLevelUser = "u"
|
||||
@ -105,7 +107,6 @@ const (
|
||||
ClairURL = "clair_url"
|
||||
NotaryURL = "notary_url"
|
||||
DefaultAdminserverEndpoint = "http://adminserver:8080"
|
||||
DefaultJobserviceEndpoint = "http://jobservice:8080"
|
||||
DefaultCoreEndpoint = "http://core:8080"
|
||||
DefaultNotaryEndpoint = "http://notary-server:4443"
|
||||
LdapGroupType = 1
|
||||
@ -115,46 +116,17 @@ const (
|
||||
WithChartMuseum = "with_chartmuseum"
|
||||
ChartRepoURL = "chart_repository_url"
|
||||
DefaultChartRepoURL = "http://chartmuseum:9999"
|
||||
DefaultPortalURL = "http://portal"
|
||||
DefaultRegistryCtlURL = "http://registryctl:8080"
|
||||
DefaultClairHealthCheckServerURL = "http://clair:6061"
|
||||
// Use this prefix to distinguish harbor user, the prefix contains a special character($), so it cannot be registered as a harbor user.
|
||||
RobotPrefix = "robot$"
|
||||
CoreConfigPath = "/api/internal/configurations"
|
||||
)
|
||||
|
||||
// TODO remove with adminserver
|
||||
// Shared variable, not allowed to modify
|
||||
var (
|
||||
// the keys of configurations which user can modify in PUT method and user can
|
||||
// get in GET method
|
||||
HarborValidKeys = []string{
|
||||
AUTHMode,
|
||||
SelfRegistration,
|
||||
LDAPURL,
|
||||
LDAPSearchDN,
|
||||
LDAPSearchPwd,
|
||||
LDAPBaseDN,
|
||||
LDAPUID,
|
||||
LDAPFilter,
|
||||
LDAPScope,
|
||||
LDAPTimeout,
|
||||
LDAPVerifyCert,
|
||||
LDAPGroupAttributeName,
|
||||
LDAPGroupBaseDN,
|
||||
LDAPGroupSearchFilter,
|
||||
LDAPGroupSearchScope,
|
||||
LdapGroupAdminDn,
|
||||
EmailHost,
|
||||
EmailPort,
|
||||
EmailUsername,
|
||||
EmailPassword,
|
||||
EmailFrom,
|
||||
EmailSSL,
|
||||
EmailIdentity,
|
||||
EmailInsecure,
|
||||
ProjectCreationRestriction,
|
||||
TokenExpiration,
|
||||
ScanAllPolicy,
|
||||
UAAClientID,
|
||||
UAAClientSecret,
|
||||
UAAEndpoint,
|
||||
UAAVerifyCert,
|
||||
ReadOnly,
|
||||
}
|
||||
|
||||
// value is default value
|
||||
HarborStringKeysMap = map[string]string{
|
||||
@ -195,10 +167,4 @@ var (
|
||||
UAAVerifyCert: true,
|
||||
ReadOnly: false,
|
||||
}
|
||||
|
||||
HarborPasswordKeys = []string{
|
||||
EmailPassword,
|
||||
LDAPSearchPwd,
|
||||
UAAClientSecret,
|
||||
}
|
||||
)
|
||||
|
@ -15,6 +15,7 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -32,6 +33,9 @@ const (
|
||||
ClairDBAlias = "clair-db"
|
||||
)
|
||||
|
||||
// ErrDupRows is returned by DAO when inserting failed with error "duplicate key value violates unique constraint"
|
||||
var ErrDupRows = errors.New("sql: duplicate row in DB")
|
||||
|
||||
// Database is an interface of different databases
|
||||
type Database interface {
|
||||
// Name returns the name of database
|
||||
@ -86,6 +90,20 @@ func InitDatabase(database *models.Database) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitAndUpgradeDatabase - init database and upgrade when required
|
||||
func InitAndUpgradeDatabase(database *models.Database) error {
|
||||
if err := InitDatabase(database); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := UpgradeSchema(database); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := CheckSchemaVersion(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckSchemaVersion checks that whether the schema version matches with the expected one
|
||||
func CheckSchemaVersion() error {
|
||||
version, err := GetSchemaVersion()
|
||||
|
@ -142,6 +142,7 @@ func TestMain(m *testing.M) {
|
||||
switch database {
|
||||
case "postgresql":
|
||||
PrepareTestForPostgresSQL()
|
||||
PrepareTestData([]string{"delete from admin_job"}, []string{})
|
||||
default:
|
||||
log.Fatalf("invalid database: %s", database)
|
||||
}
|
||||
|
@ -249,7 +249,8 @@ func projectQueryConditions(query *models.ProjectQueryParam) (string, []interfac
|
||||
roleID = 2
|
||||
case common.RoleGuest:
|
||||
roleID = 3
|
||||
|
||||
case common.RoleMaster:
|
||||
roleID = 4
|
||||
}
|
||||
params = append(params, roleID)
|
||||
}
|
||||
@ -299,7 +300,7 @@ func GetRolesByLDAPGroup(projectID int64, groupDNCondition string) ([]int, error
|
||||
}
|
||||
o := GetOrmer()
|
||||
// Because an LDAP user can be memberof multiple groups,
|
||||
// the role is in descent order (1-admin, 2-developer, 3-guest), use min to select the max privilege role.
|
||||
// the role is in descent order (1-admin, 2-developer, 3-guest, 4-master), use min to select the max privilege role.
|
||||
sql := fmt.Sprintf(
|
||||
`select min(pm.role) from project_member pm
|
||||
left join user_group ug on pm.entity_type = 'g' and pm.entity_id = ug.id
|
||||
|
@ -17,6 +17,7 @@ package dao
|
||||
import (
|
||||
"github.com/astaxie/beego/orm"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -25,7 +26,14 @@ func AddRobot(robot *models.Robot) (int64, error) {
|
||||
now := time.Now()
|
||||
robot.CreationTime = now
|
||||
robot.UpdateTime = now
|
||||
return GetOrmer().Insert(robot)
|
||||
id, err := GetOrmer().Insert(robot)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "duplicate key value violates unique constraint") {
|
||||
return 0, ErrDupRows
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// GetRobotByID ...
|
||||
@ -79,6 +87,11 @@ func getRobotQuerySetter(query *models.RobotQuery) orm.QuerySeter {
|
||||
return qs
|
||||
}
|
||||
|
||||
// CountRobot ...
|
||||
func CountRobot(query *models.RobotQuery) (int64, error) {
|
||||
return getRobotQuerySetter(query).Count()
|
||||
}
|
||||
|
||||
// UpdateRobot ...
|
||||
func UpdateRobot(robot *models.Robot) error {
|
||||
robot.UpdateTime = time.Now()
|
||||
|
@ -27,7 +27,6 @@ func TestAddRobot(t *testing.T) {
|
||||
robotName := "test1"
|
||||
robot := &models.Robot{
|
||||
Name: robotName,
|
||||
Token: "rKgjKEMpMEK23zqejkWn5GIVvgJps1vKACTa6tnGXXyOlOTsXFESccDvgaJx047q1",
|
||||
Description: "test1 description",
|
||||
ProjectID: 1,
|
||||
}
|
||||
@ -46,7 +45,6 @@ func TestGetRobot(t *testing.T) {
|
||||
robotName := "test2"
|
||||
robot := &models.Robot{
|
||||
Name: robotName,
|
||||
Token: "rKgjKEMpMEK23zqejkWn5GIVvgJps1vKACTa6tnGXXyOlOTsXFESccDvgaJx047q2",
|
||||
Description: "test2 description",
|
||||
ProjectID: 1,
|
||||
}
|
||||
@ -66,7 +64,6 @@ func TestListRobots(t *testing.T) {
|
||||
robotName := "test3"
|
||||
robot := &models.Robot{
|
||||
Name: robotName,
|
||||
Token: "rKgjKEMpMEK23zqejkWn5GIVvgJps1vKACTa6tnGXXyOlOTsXFESccDvgaJx047q3",
|
||||
Description: "test3 description",
|
||||
ProjectID: 1,
|
||||
}
|
||||
@ -86,7 +83,6 @@ func TestDisableRobot(t *testing.T) {
|
||||
robotName := "test4"
|
||||
robot := &models.Robot{
|
||||
Name: robotName,
|
||||
Token: "rKgjKEMpMEK23zqejkWn5GIVvgJps1vKACTa6tnGXXyOlOTsXFESccDvgaJx047q4",
|
||||
Description: "test4 description",
|
||||
ProjectID: 1,
|
||||
}
|
||||
@ -111,7 +107,6 @@ func TestEnableRobot(t *testing.T) {
|
||||
robotName := "test5"
|
||||
robot := &models.Robot{
|
||||
Name: robotName,
|
||||
Token: "rKgjKEMpMEK23zqejkWn5GIVvgJps1vKACTa6tnGXXyOlOTsXFESccDvgaJx047q5",
|
||||
Description: "test5 description",
|
||||
Disabled: true,
|
||||
ProjectID: 1,
|
||||
@ -137,7 +132,6 @@ func TestDeleteRobot(t *testing.T) {
|
||||
robotName := "test6"
|
||||
robot := &models.Robot{
|
||||
Name: robotName,
|
||||
Token: "rKgjKEMpMEK23zqejkWn5GIVvgJps1vKACTa6tnGXXyOlOTsXFESccDvgaJx047q6",
|
||||
Description: "test6 description",
|
||||
ProjectID: 1,
|
||||
}
|
||||
|
@ -15,6 +15,8 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/astaxie/beego/validation"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -25,7 +27,6 @@ const RobotTable = "robot"
|
||||
type Robot struct {
|
||||
ID int64 `orm:"pk;auto;column(id)" json:"id"`
|
||||
Name string `orm:"column(name)" json:"name"`
|
||||
Token string `orm:"column(token)" json:"token"`
|
||||
Description string `orm:"column(description)" json:"description"`
|
||||
ProjectID int64 `orm:"column(project_id)" json:"project_id"`
|
||||
Disabled bool `orm:"column(disabled)" json:"disabled"`
|
||||
@ -42,7 +43,26 @@ type RobotQuery struct {
|
||||
Pagination
|
||||
}
|
||||
|
||||
// RobotReq ...
|
||||
type RobotReq struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Disabled bool `json:"disabled"`
|
||||
Access []*rbac.Policy `json:"access"`
|
||||
}
|
||||
|
||||
// Valid put request validation
|
||||
func (rq *RobotReq) Valid(v *validation.Validation) {
|
||||
// ToDo: add validation for access info.
|
||||
}
|
||||
|
||||
// RobotRep ...
|
||||
type RobotRep struct {
|
||||
Name string `json:"name"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
// TableName ...
|
||||
func (u *Robot) TableName() string {
|
||||
func (r *Robot) TableName() string {
|
||||
return RobotTable
|
||||
}
|
||||
|
@ -20,3 +20,9 @@ type Token struct {
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
IssuedAt string `json:"issued_at"`
|
||||
}
|
||||
|
||||
// ResourceActions ...
|
||||
type ResourceActions struct {
|
||||
Name string `json:"name"`
|
||||
Actions []string `json:"actions"`
|
||||
}
|
||||
|
@ -12,15 +12,18 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ram
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/casbin/casbin"
|
||||
"github.com/casbin/casbin/model"
|
||||
"github.com/casbin/casbin/persist"
|
||||
"github.com/casbin/casbin/util"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -50,6 +53,30 @@ e = some(where (p.eft == allow)) && !some(where (p.eft == deny))
|
||||
m = g(r.sub, p.sub) && keyMatch2(r.obj, p.obj) && (r.act == p.act || p.act == '*')
|
||||
`
|
||||
|
||||
// keyMatch2 determines whether key1 matches the pattern of key2, its behavior most likely the builtin KeyMatch2
|
||||
// except that the match of ("/project/1/robot", "/project/1") will return false
|
||||
func keyMatch2(key1 string, key2 string) bool {
|
||||
key2 = strings.Replace(key2, "/*", "/.*", -1)
|
||||
|
||||
re := regexp.MustCompile(`(.*):[^/]+(.*)`)
|
||||
for {
|
||||
if !strings.Contains(key2, "/:") {
|
||||
break
|
||||
}
|
||||
|
||||
key2 = re.ReplaceAllString(key2, "$1[^/]+$2")
|
||||
}
|
||||
|
||||
return util.RegexMatch(key1, "^"+key2+"$")
|
||||
}
|
||||
|
||||
func keyMatch2Func(args ...interface{}) (interface{}, error) {
|
||||
name1 := args[0].(string)
|
||||
name2 := args[1].(string)
|
||||
|
||||
return bool(keyMatch2(name1, name2)), nil
|
||||
}
|
||||
|
||||
type userAdapter struct {
|
||||
User
|
||||
}
|
||||
@ -134,5 +161,8 @@ func (a *userAdapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex
|
||||
func enforcerForUser(user User) *casbin.Enforcer {
|
||||
m := model.Model{}
|
||||
m.LoadModelFromText(modelText)
|
||||
return casbin.NewEnforcer(m, &userAdapter{User: user})
|
||||
|
||||
e := casbin.NewEnforcer(m, &userAdapter{User: user})
|
||||
e.AddFunction("keyMatch2", keyMatch2Func)
|
||||
return e
|
||||
}
|
59
src/common/rbac/casbin_test.go
Normal file
59
src/common/rbac/casbin_test.go
Normal file
@ -0,0 +1,59 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_keyMatch2(t *testing.T) {
|
||||
type args struct {
|
||||
key1 string
|
||||
key2 string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "match /project/1/robot, /project/1",
|
||||
args: args{"/project/1/robot", "/project/1"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "match /project/1/robot, /project/:pid",
|
||||
args: args{"/project/1/robot", "/project/:pid"},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "match /project/1/robot, /project/1/*",
|
||||
args: args{"/project/1/robot", "/project/1/*"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "match /project/1/robot, /project/:pid/robot",
|
||||
args: args{"/project/1/robot", "/project/:pid/robot"},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := keyMatch2(tt.args.key1, tt.args.key2); got != tt.want {
|
||||
t.Errorf("keyMatch2() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
56
src/common/rbac/const.go
Normal file
56
src/common/rbac/const.go
Normal file
@ -0,0 +1,56 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rbac
|
||||
|
||||
// const action variables
|
||||
const (
|
||||
ActionAll = Action("*") // action match any other actions
|
||||
|
||||
ActionPull = Action("pull") // pull repository tag
|
||||
ActionPush = Action("push") // push repository tag
|
||||
ActionPushPull = Action("push+pull") // compatible with security all perm of project
|
||||
|
||||
// create, read, update, delete, list actions compatible with restful api methods
|
||||
ActionCreate = Action("create")
|
||||
ActionRead = Action("read")
|
||||
ActionUpdate = Action("update")
|
||||
ActionDelete = Action("delete")
|
||||
ActionList = Action("list")
|
||||
)
|
||||
|
||||
// const resource variables
|
||||
const (
|
||||
ResourceAll = Resource("*") // resource match any other resources
|
||||
ResourceConfiguration = Resource("configuration") // project configuration compatible for portal only
|
||||
ResourceHelmChart = Resource("helm-chart")
|
||||
ResourceHelmChartVersion = Resource("helm-chart-version")
|
||||
ResourceHelmChartVersionLabel = Resource("helm-chart-version-label")
|
||||
ResourceLabel = Resource("label")
|
||||
ResourceLabelResource = Resource("label-resource")
|
||||
ResourceLog = Resource("log")
|
||||
ResourceMember = Resource("member")
|
||||
ResourceMetadata = Resource("metadata")
|
||||
ResourceReplication = Resource("replication")
|
||||
ResourceReplicationJob = Resource("replication-job")
|
||||
ResourceRepository = Resource("repository")
|
||||
ResourceRepositoryLabel = Resource("repository-label")
|
||||
ResourceRepositoryTag = Resource("repository-tag")
|
||||
ResourceRepositoryTagLabel = Resource("repository-tag-label")
|
||||
ResourceRepositoryTagManifest = Resource("repository-tag-manifest")
|
||||
ResourceRepositoryTagScanJob = Resource("repository-tag-scan-job")
|
||||
ResourceRepositoryTagVulnerability = Resource("repository-tag-vulnerability")
|
||||
ResourceRobot = Resource("robot")
|
||||
ResourceSelf = Resource("") // subresource for self
|
||||
)
|
61
src/common/rbac/namespace.go
Normal file
61
src/common/rbac/namespace.go
Normal file
@ -0,0 +1,61 @@
|
||||
// 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 rbac
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Namespace the namespace interface
|
||||
type Namespace interface {
|
||||
// Kind returns the kind of namespace
|
||||
Kind() string
|
||||
// Resource returns new resource for subresources with the namespace
|
||||
Resource(subresources ...Resource) Resource
|
||||
// Identity returns identity attached with namespace
|
||||
Identity() interface{}
|
||||
// IsPublic returns true if namespace is public
|
||||
IsPublic() bool
|
||||
}
|
||||
|
||||
type projectNamespace struct {
|
||||
projectIDOrName interface{}
|
||||
isPublic bool
|
||||
}
|
||||
|
||||
func (ns *projectNamespace) Kind() string {
|
||||
return "project"
|
||||
}
|
||||
|
||||
func (ns *projectNamespace) Resource(subresources ...Resource) Resource {
|
||||
return Resource(fmt.Sprintf("/project/%v", ns.projectIDOrName)).Subresource(subresources...)
|
||||
}
|
||||
|
||||
func (ns *projectNamespace) Identity() interface{} {
|
||||
return ns.projectIDOrName
|
||||
}
|
||||
|
||||
func (ns *projectNamespace) IsPublic() bool {
|
||||
return ns.isPublic
|
||||
}
|
||||
|
||||
// NewProjectNamespace returns namespace for project
|
||||
func NewProjectNamespace(projectIDOrName interface{}, isPublic ...bool) Namespace {
|
||||
isPublicNamespace := false
|
||||
if len(isPublic) > 0 {
|
||||
isPublicNamespace = isPublic[0]
|
||||
}
|
||||
return &projectNamespace{projectIDOrName: projectIDOrName, isPublic: isPublicNamespace}
|
||||
}
|
45
src/common/rbac/namespace_test.go
Normal file
45
src/common/rbac/namespace_test.go
Normal file
@ -0,0 +1,45 @@
|
||||
// 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 rbac
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type ProjectNamespaceTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *ProjectNamespaceTestSuite) TestResource() {
|
||||
var namespace Namespace
|
||||
|
||||
namespace = &projectNamespace{projectIDOrName: int64(1)}
|
||||
|
||||
suite.Equal(namespace.Resource(Resource("image")), Resource("/project/1/image"))
|
||||
}
|
||||
|
||||
func (suite *ProjectNamespaceTestSuite) TestIdentity() {
|
||||
namespace, _ := Resource("/project/1/image").GetNamespace()
|
||||
suite.Equal(namespace.Identity(), int64(1))
|
||||
|
||||
namespace, _ = Resource("/project/library/image").GetNamespace()
|
||||
suite.Equal(namespace.Identity(), "library")
|
||||
}
|
||||
|
||||
func TestProjectNamespaceTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(ProjectNamespaceTestSuite))
|
||||
}
|
50
src/common/rbac/parser.go
Normal file
50
src/common/rbac/parser.go
Normal file
@ -0,0 +1,50 @@
|
||||
// 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 rbac
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
namespaceParsers = map[string]namespaceParser{
|
||||
"project": projectNamespaceParser,
|
||||
}
|
||||
)
|
||||
|
||||
type namespaceParser func(resource Resource) (Namespace, error)
|
||||
|
||||
func projectNamespaceParser(resource Resource) (Namespace, error) {
|
||||
parserRe := regexp.MustCompile("^/project/([^/]*)/?")
|
||||
|
||||
matches := parserRe.FindStringSubmatch(resource.String())
|
||||
|
||||
if len(matches) <= 1 {
|
||||
return nil, errors.New("not support resource")
|
||||
}
|
||||
|
||||
var projectIDOrName interface{}
|
||||
|
||||
id, err := strconv.ParseInt(matches[1], 10, 64)
|
||||
if err == nil {
|
||||
projectIDOrName = id
|
||||
} else {
|
||||
projectIDOrName = matches[1]
|
||||
}
|
||||
|
||||
return &projectNamespace{projectIDOrName: projectIDOrName}, nil
|
||||
}
|
@ -11,11 +11,29 @@
|
||||
// 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 service
|
||||
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
type ProjectParserTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *ProjectParserTestSuite) TestParse() {
|
||||
namespace, err := projectNamespaceParser(Resource("/project/1/image"))
|
||||
suite.Equal(namespace, &projectNamespace{projectIDOrName: int64(1)})
|
||||
suite.Nil(err)
|
||||
|
||||
namespace, err = projectNamespaceParser(Resource("/fake/1/image"))
|
||||
suite.Nil(namespace)
|
||||
suite.Error(err)
|
||||
}
|
||||
|
||||
func TestProjectParserTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(ProjectParserTestSuite))
|
||||
}
|
166
src/common/rbac/project/util.go
Normal file
166
src/common/rbac/project/util.go
Normal file
@ -0,0 +1,166 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package project
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
)
|
||||
|
||||
var (
|
||||
// subresource policies for public project
|
||||
publicProjectPolicies = []*rbac.Policy{
|
||||
{Resource: rbac.ResourceSelf, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceLabel, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceLabel, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionList},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionPull},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryTagVulnerability, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryTagManifest, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList},
|
||||
}
|
||||
|
||||
// all policies for the projects
|
||||
allPolicies = []*rbac.Policy{
|
||||
{Resource: rbac.ResourceSelf, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceSelf, Action: rbac.ActionUpdate},
|
||||
{Resource: rbac.ResourceSelf, Action: rbac.ActionDelete},
|
||||
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionUpdate},
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceMetadata, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceMetadata, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceMetadata, Action: rbac.ActionUpdate},
|
||||
{Resource: rbac.ResourceMetadata, Action: rbac.ActionDelete},
|
||||
|
||||
{Resource: rbac.ResourceLog, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceReplication, Action: rbac.ActionList},
|
||||
{Resource: rbac.ResourceReplication, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceReplication, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceReplication, Action: rbac.ActionUpdate},
|
||||
{Resource: rbac.ResourceReplication, Action: rbac.ActionDelete},
|
||||
|
||||
{Resource: rbac.ResourceReplicationJob, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceReplicationJob, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceReplicationJob, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceLabel, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceLabel, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceLabel, Action: rbac.ActionUpdate},
|
||||
{Resource: rbac.ResourceLabel, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceLabel, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceLabelResource, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionUpdate},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionList},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionPull},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionPush},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionPushPull}, // compatible with security all perm of project
|
||||
|
||||
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryTagScanJob, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceRepositoryTagScanJob, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryTagVulnerability, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryTagManifest, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionDelete},
|
||||
|
||||
{Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceConfiguration, Action: rbac.ActionUpdate},
|
||||
|
||||
{Resource: rbac.ResourceRobot, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceRobot, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceRobot, Action: rbac.ActionUpdate},
|
||||
{Resource: rbac.ResourceRobot, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceRobot, Action: rbac.ActionList},
|
||||
}
|
||||
)
|
||||
|
||||
// PoliciesForPublicProject ...
|
||||
func PoliciesForPublicProject(namespace rbac.Namespace) []*rbac.Policy {
|
||||
policies := []*rbac.Policy{}
|
||||
|
||||
for _, policy := range publicProjectPolicies {
|
||||
policies = append(policies, &rbac.Policy{
|
||||
Resource: namespace.Resource(policy.Resource),
|
||||
Action: policy.Action,
|
||||
Effect: policy.Effect,
|
||||
})
|
||||
}
|
||||
|
||||
return policies
|
||||
}
|
||||
|
||||
// GetAllPolicies returns all policies for namespace of the project
|
||||
func GetAllPolicies(namespace rbac.Namespace) []*rbac.Policy {
|
||||
policies := []*rbac.Policy{}
|
||||
|
||||
for _, policy := range allPolicies {
|
||||
policies = append(policies, &rbac.Policy{
|
||||
Resource: namespace.Resource(policy.Resource),
|
||||
Action: policy.Action,
|
||||
Effect: policy.Effect,
|
||||
})
|
||||
}
|
||||
|
||||
return policies
|
||||
}
|
83
src/common/rbac/project/visitor.go
Normal file
83
src/common/rbac/project/visitor.go
Normal file
@ -0,0 +1,83 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package project
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
)
|
||||
|
||||
// visitorContext the context interface for the project visitor
|
||||
type visitorContext interface {
|
||||
IsAuthenticated() bool
|
||||
// GetUsername returns the username of user related to the context
|
||||
GetUsername() string
|
||||
// IsSysAdmin returns whether the user is system admin
|
||||
IsSysAdmin() bool
|
||||
}
|
||||
|
||||
// visitor implement the rbac.User interface for project visitor
|
||||
type visitor struct {
|
||||
ctx visitorContext
|
||||
namespace rbac.Namespace
|
||||
projectRoles []int
|
||||
}
|
||||
|
||||
// GetUserName returns username of the visitor
|
||||
func (v *visitor) GetUserName() string {
|
||||
// anonymous username for unauthenticated Visitor
|
||||
if !v.ctx.IsAuthenticated() {
|
||||
return "anonymous"
|
||||
}
|
||||
|
||||
return v.ctx.GetUsername()
|
||||
}
|
||||
|
||||
// GetPolicies returns policies of the visitor
|
||||
func (v *visitor) GetPolicies() []*rbac.Policy {
|
||||
if v.ctx.IsSysAdmin() {
|
||||
return GetAllPolicies(v.namespace)
|
||||
}
|
||||
|
||||
if v.namespace.IsPublic() {
|
||||
return PoliciesForPublicProject(v.namespace)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRoles returns roles of the visitor
|
||||
func (v *visitor) GetRoles() []rbac.Role {
|
||||
// Ignore roles when visitor is anonymous or system admin
|
||||
if !v.ctx.IsAuthenticated() || v.ctx.IsSysAdmin() {
|
||||
return nil
|
||||
}
|
||||
|
||||
roles := []rbac.Role{}
|
||||
|
||||
for _, roleID := range v.projectRoles {
|
||||
roles = append(roles, &visitorRole{roleID: roleID, namespace: v.namespace})
|
||||
}
|
||||
|
||||
return roles
|
||||
}
|
||||
|
||||
// NewUser returns rbac.User interface for the project visitor
|
||||
func NewUser(ctx visitorContext, namespace rbac.Namespace, projectRoles ...int) rbac.User {
|
||||
return &visitor{
|
||||
ctx: ctx,
|
||||
namespace: namespace,
|
||||
projectRoles: projectRoles,
|
||||
}
|
||||
}
|
301
src/common/rbac/project/visitor_role.go
Normal file
301
src/common/rbac/project/visitor_role.go
Normal file
@ -0,0 +1,301 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package project
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
)
|
||||
|
||||
var (
|
||||
rolePoliciesMap = map[string][]*rbac.Policy{
|
||||
"projectAdmin": {
|
||||
{Resource: rbac.ResourceSelf, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceSelf, Action: rbac.ActionUpdate},
|
||||
{Resource: rbac.ResourceSelf, Action: rbac.ActionDelete},
|
||||
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionUpdate},
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceMetadata, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceMetadata, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceMetadata, Action: rbac.ActionUpdate},
|
||||
{Resource: rbac.ResourceMetadata, Action: rbac.ActionDelete},
|
||||
|
||||
{Resource: rbac.ResourceLog, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceReplication, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceReplication, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceReplicationJob, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceReplicationJob, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceLabel, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceLabel, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceLabel, Action: rbac.ActionUpdate},
|
||||
{Resource: rbac.ResourceLabel, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceLabel, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceLabelResource, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionUpdate},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionList},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionPull},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionPush},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionPushPull}, // compatible with security all perm of project
|
||||
|
||||
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryTagScanJob, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceRepositoryTagScanJob, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryTagVulnerability, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryTagManifest, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionCreate}, // upload helm chart
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead}, // download helm chart
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionCreate}, // upload helm chart version
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead}, // read and download helm chart version
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionDelete},
|
||||
|
||||
{Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceConfiguration, Action: rbac.ActionUpdate},
|
||||
|
||||
{Resource: rbac.ResourceRobot, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceRobot, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceRobot, Action: rbac.ActionUpdate},
|
||||
{Resource: rbac.ResourceRobot, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceRobot, Action: rbac.ActionList},
|
||||
},
|
||||
|
||||
"master": {
|
||||
{Resource: rbac.ResourceSelf, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceMetadata, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceMetadata, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceMetadata, Action: rbac.ActionUpdate},
|
||||
{Resource: rbac.ResourceMetadata, Action: rbac.ActionDelete},
|
||||
|
||||
{Resource: rbac.ResourceLog, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceReplication, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceReplication, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceLabel, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceLabel, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceLabel, Action: rbac.ActionUpdate},
|
||||
{Resource: rbac.ResourceLabel, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceLabel, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionUpdate},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionList},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionPush},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionPull},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryTagScanJob, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceRepositoryTagScanJob, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryTagVulnerability, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryTagManifest, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionDelete},
|
||||
|
||||
{Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceRobot, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceRobot, Action: rbac.ActionList},
|
||||
},
|
||||
|
||||
"developer": {
|
||||
{Resource: rbac.ResourceSelf, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceLog, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceLabel, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceLabel, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionUpdate},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionList},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionPush},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionPull},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryTagVulnerability, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryTagManifest, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionDelete},
|
||||
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionCreate},
|
||||
{Resource: rbac.ResourceHelmChartVersionLabel, Action: rbac.ActionDelete},
|
||||
|
||||
{Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceRobot, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceRobot, Action: rbac.ActionList},
|
||||
},
|
||||
|
||||
"guest": {
|
||||
{Resource: rbac.ResourceSelf, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceMember, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceLog, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceLabel, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceLabel, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionList},
|
||||
{Resource: rbac.ResourceRepository, Action: rbac.ActionPull},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryLabel, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceRepositoryTag, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryTagLabel, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryTagVulnerability, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceRepositoryTagManifest, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceHelmChart, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceHelmChartVersion, Action: rbac.ActionList},
|
||||
|
||||
{Resource: rbac.ResourceConfiguration, Action: rbac.ActionRead},
|
||||
|
||||
{Resource: rbac.ResourceRobot, Action: rbac.ActionRead},
|
||||
{Resource: rbac.ResourceRobot, Action: rbac.ActionList},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// visitorRole implement the rbac.Role interface
|
||||
type visitorRole struct {
|
||||
namespace rbac.Namespace
|
||||
roleID int
|
||||
}
|
||||
|
||||
// GetRoleName returns role name for the visitor role
|
||||
func (role *visitorRole) GetRoleName() string {
|
||||
switch role.roleID {
|
||||
case common.RoleProjectAdmin:
|
||||
return "projectAdmin"
|
||||
case common.RoleMaster:
|
||||
return "master"
|
||||
case common.RoleDeveloper:
|
||||
return "developer"
|
||||
case common.RoleGuest:
|
||||
return "guest"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// GetPolicies returns policies for the visitor role
|
||||
func (role *visitorRole) GetPolicies() []*rbac.Policy {
|
||||
policies := []*rbac.Policy{}
|
||||
|
||||
roleName := role.GetRoleName()
|
||||
if roleName == "" {
|
||||
return policies
|
||||
}
|
||||
|
||||
for _, policy := range rolePoliciesMap[roleName] {
|
||||
policies = append(policies, &rbac.Policy{
|
||||
Resource: role.namespace.Resource(policy.Resource),
|
||||
Action: policy.Action,
|
||||
Effect: policy.Effect,
|
||||
})
|
||||
}
|
||||
|
||||
return policies
|
||||
}
|
44
src/common/rbac/project/visitor_role_test.go
Normal file
44
src/common/rbac/project/visitor_role_test.go
Normal file
@ -0,0 +1,44 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package project
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type VisitorRoleTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *VisitorRoleTestSuite) TestGetRoleName() {
|
||||
projectAdmin := visitorRole{roleID: common.RoleProjectAdmin}
|
||||
suite.Equal(projectAdmin.GetRoleName(), "projectAdmin")
|
||||
|
||||
developer := visitorRole{roleID: common.RoleDeveloper}
|
||||
suite.Equal(developer.GetRoleName(), "developer")
|
||||
|
||||
guest := visitorRole{roleID: common.RoleGuest}
|
||||
suite.Equal(guest.GetRoleName(), "guest")
|
||||
|
||||
unknow := visitorRole{roleID: 404}
|
||||
suite.Equal(unknow.GetRoleName(), "")
|
||||
}
|
||||
|
||||
func TestVisitorRoleTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(VisitorRoleTestSuite))
|
||||
}
|
93
src/common/rbac/project/visitor_test.go
Normal file
93
src/common/rbac/project/visitor_test.go
Normal file
@ -0,0 +1,93 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package project
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type fakeVisitorContext struct {
|
||||
username string
|
||||
isSysAdmin bool
|
||||
}
|
||||
|
||||
func (ctx *fakeVisitorContext) IsAuthenticated() bool {
|
||||
return ctx.username != ""
|
||||
}
|
||||
|
||||
func (ctx *fakeVisitorContext) GetUsername() string {
|
||||
return ctx.username
|
||||
}
|
||||
|
||||
func (ctx *fakeVisitorContext) IsSysAdmin() bool {
|
||||
return ctx.IsAuthenticated() && ctx.isSysAdmin
|
||||
}
|
||||
|
||||
var (
|
||||
anonymousCtx = &fakeVisitorContext{}
|
||||
authenticatedCtx = &fakeVisitorContext{username: "user"}
|
||||
sysAdminCtx = &fakeVisitorContext{username: "admin", isSysAdmin: true}
|
||||
)
|
||||
|
||||
type VisitorTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *VisitorTestSuite) TestGetPolicies() {
|
||||
namespace := rbac.NewProjectNamespace("library", false)
|
||||
publicNamespace := rbac.NewProjectNamespace("library", true)
|
||||
|
||||
anonymous := NewUser(anonymousCtx, namespace)
|
||||
suite.Nil(anonymous.GetPolicies())
|
||||
|
||||
anonymousForPublicProject := NewUser(anonymousCtx, publicNamespace)
|
||||
suite.Equal(anonymousForPublicProject.GetPolicies(), PoliciesForPublicProject(publicNamespace))
|
||||
|
||||
authenticated := NewUser(authenticatedCtx, namespace)
|
||||
suite.Nil(authenticated.GetPolicies())
|
||||
|
||||
authenticatedForPublicProject := NewUser(authenticatedCtx, publicNamespace)
|
||||
suite.Equal(authenticatedForPublicProject.GetPolicies(), PoliciesForPublicProject(publicNamespace))
|
||||
|
||||
systemAdmin := NewUser(sysAdminCtx, namespace)
|
||||
suite.Equal(systemAdmin.GetPolicies(), GetAllPolicies(namespace))
|
||||
|
||||
systemAdminForPublicProject := NewUser(sysAdminCtx, publicNamespace)
|
||||
suite.Equal(systemAdminForPublicProject.GetPolicies(), GetAllPolicies(publicNamespace))
|
||||
}
|
||||
|
||||
func (suite *VisitorTestSuite) TestGetRoles() {
|
||||
namespace := rbac.NewProjectNamespace("library", false)
|
||||
|
||||
anonymous := NewUser(anonymousCtx, namespace)
|
||||
suite.Nil(anonymous.GetRoles())
|
||||
|
||||
authenticated := NewUser(authenticatedCtx, namespace)
|
||||
suite.Empty(authenticated.GetRoles())
|
||||
|
||||
authenticated = NewUser(authenticatedCtx, namespace, common.RoleProjectAdmin)
|
||||
suite.Len(authenticated.GetRoles(), 1)
|
||||
|
||||
authenticated = NewUser(authenticatedCtx, namespace, common.RoleProjectAdmin, common.RoleDeveloper)
|
||||
suite.Len(authenticated.GetRoles(), 2)
|
||||
}
|
||||
|
||||
func TestVisitorTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(VisitorTestSuite))
|
||||
}
|
@ -12,10 +12,13 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ram
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -28,6 +31,27 @@ const (
|
||||
// Resource the type of resource
|
||||
type Resource string
|
||||
|
||||
// RelativeTo returns relative resource to other resource
|
||||
func (res Resource) RelativeTo(other Resource) (Resource, error) {
|
||||
prefix := other.String()
|
||||
str := res.String()
|
||||
|
||||
if !strings.HasPrefix(str, prefix) {
|
||||
return Resource(""), errors.New("value error")
|
||||
}
|
||||
|
||||
relative := strings.TrimPrefix(str, prefix)
|
||||
if strings.HasPrefix(relative, "/") {
|
||||
relative = relative[1:]
|
||||
}
|
||||
|
||||
if relative == "" {
|
||||
relative = "."
|
||||
}
|
||||
|
||||
return Resource(relative), nil
|
||||
}
|
||||
|
||||
func (res Resource) String() string {
|
||||
return string(res)
|
||||
}
|
||||
@ -43,6 +67,18 @@ func (res Resource) Subresource(resources ...Resource) Resource {
|
||||
return Resource(path.Join(elements...))
|
||||
}
|
||||
|
||||
// GetNamespace returns namespace from resource
|
||||
func (res Resource) GetNamespace() (Namespace, error) {
|
||||
for _, parser := range namespaceParsers {
|
||||
namespace, err := parser(res)
|
||||
if err == nil {
|
||||
return namespace, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no namespace found for %s", res)
|
||||
}
|
||||
|
||||
// Action the type of action
|
||||
type Action string
|
||||
|
||||
@ -74,14 +110,14 @@ func (p *Policy) GetEffect() string {
|
||||
return eft.String()
|
||||
}
|
||||
|
||||
// Role the interface of ram role
|
||||
// Role the interface of rbac role
|
||||
type Role interface {
|
||||
// GetRoleName returns the role identity, if empty string role's policies will be ignore
|
||||
GetRoleName() string
|
||||
GetPolicies() []*Policy
|
||||
}
|
||||
|
||||
// User the interface of ram user
|
||||
// User the interface of rbac user
|
||||
type User interface {
|
||||
// GetUserName returns the user identity, if empty string user's all policies will be ignore
|
||||
GetUserName() string
|
@ -12,9 +12,10 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ram
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@ -126,7 +127,7 @@ func TestHasPermissionUserWithoutRoles(t *testing.T) {
|
||||
{
|
||||
name: "project create for user without roles",
|
||||
args: args{
|
||||
&userWithoutRoles{Username: "user1", UserPolicies: []*Policy{{Resource: "project", Action: "create"}}},
|
||||
&userWithoutRoles{Username: "user1", UserPolicies: []*Policy{{Resource: "/project", Action: "create"}}},
|
||||
"/project",
|
||||
"create",
|
||||
},
|
||||
@ -135,7 +136,7 @@ func TestHasPermissionUserWithoutRoles(t *testing.T) {
|
||||
{
|
||||
name: "project delete test for user without roles",
|
||||
args: args{
|
||||
&userWithoutRoles{Username: "user1", UserPolicies: []*Policy{{Resource: "project", Action: "create"}}},
|
||||
&userWithoutRoles{Username: "user1", UserPolicies: []*Policy{{Resource: "/project", Action: "create"}}},
|
||||
"/project",
|
||||
"delete",
|
||||
},
|
||||
@ -167,7 +168,7 @@ func TestHasPermissionUsernameEmpty(t *testing.T) {
|
||||
{
|
||||
name: "project create for user without roles",
|
||||
args: args{
|
||||
&userWithoutRoles{Username: "", UserPolicies: []*Policy{{Resource: "project", Action: "create"}}},
|
||||
&userWithoutRoles{Username: "", UserPolicies: []*Policy{{Resource: "/project", Action: "create"}}},
|
||||
"/project",
|
||||
"create",
|
||||
},
|
||||
@ -355,3 +356,84 @@ func TestResource_Subresource(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResource_GetNamespace(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
res Resource
|
||||
want Namespace
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "project namespace",
|
||||
res: Resource("/project/1"),
|
||||
want: &projectNamespace{int64(1), false},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "unknow namespace",
|
||||
res: Resource("/unknow/1"),
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.res.GetNamespace()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Resource.GetNamespace() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Resource.GetNamespace() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResource_RelativeTo(t *testing.T) {
|
||||
type args struct {
|
||||
other Resource
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
res Resource
|
||||
args args
|
||||
want Resource
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "/project/1/image",
|
||||
res: Resource("/project/1/image"),
|
||||
args: args{other: Resource("/project/1")},
|
||||
want: Resource("image"),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "/project/1",
|
||||
res: Resource("/project/1"),
|
||||
args: args{other: Resource("/project/1")},
|
||||
want: Resource("."),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "/project/1",
|
||||
res: Resource("/project/1"),
|
||||
args: args{other: Resource("/system")},
|
||||
want: Resource(""),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.res.RelativeTo(tt.args.other)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Resource.RelativeTo() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("Resource.RelativeTo() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -15,10 +15,10 @@
|
||||
package admiral
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/rbac/project"
|
||||
"github.com/goharbor/harbor/src/common/security/admiral/authcontext"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/promgr"
|
||||
)
|
||||
|
||||
@ -69,72 +69,17 @@ func (s *SecurityContext) IsSolutionUser() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// HasReadPerm returns whether the user has read permission to the project
|
||||
func (s *SecurityContext) HasReadPerm(projectIDOrName interface{}) bool {
|
||||
public, err := s.pm.IsPublic(projectIDOrName)
|
||||
if err != nil {
|
||||
log.Errorf("failed to check the public of project %v: %v",
|
||||
projectIDOrName, err)
|
||||
return false
|
||||
}
|
||||
if public {
|
||||
return true
|
||||
}
|
||||
|
||||
// private project
|
||||
if !s.IsAuthenticated() {
|
||||
return false
|
||||
}
|
||||
|
||||
// system admin
|
||||
if s.IsSysAdmin() {
|
||||
return true
|
||||
}
|
||||
|
||||
roles := s.GetProjectRoles(projectIDOrName)
|
||||
|
||||
return len(roles) > 0
|
||||
}
|
||||
|
||||
// HasWritePerm returns whether the user has write permission to the project
|
||||
func (s *SecurityContext) HasWritePerm(projectIDOrName interface{}) bool {
|
||||
if !s.IsAuthenticated() {
|
||||
return false
|
||||
}
|
||||
|
||||
// system admin
|
||||
if s.IsSysAdmin() {
|
||||
return true
|
||||
}
|
||||
|
||||
roles := s.GetProjectRoles(projectIDOrName)
|
||||
for _, role := range roles {
|
||||
switch role {
|
||||
case common.RoleProjectAdmin,
|
||||
common.RoleDeveloper:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// HasAllPerm returns whether the user has all permissions to the project
|
||||
func (s *SecurityContext) HasAllPerm(projectIDOrName interface{}) bool {
|
||||
if !s.IsAuthenticated() {
|
||||
return false
|
||||
}
|
||||
|
||||
// system admin
|
||||
if s.IsSysAdmin() {
|
||||
return true
|
||||
}
|
||||
|
||||
roles := s.GetProjectRoles(projectIDOrName)
|
||||
for _, role := range roles {
|
||||
switch role {
|
||||
case common.RoleProjectAdmin:
|
||||
return true
|
||||
// Can returns whether the user can do action on resource
|
||||
func (s *SecurityContext) Can(action rbac.Action, resource rbac.Resource) bool {
|
||||
ns, err := resource.GetNamespace()
|
||||
if err == nil {
|
||||
switch ns.Kind() {
|
||||
case "project":
|
||||
projectIDOrName := ns.Identity()
|
||||
isPublicProject, _ := s.pm.IsPublic(projectIDOrName)
|
||||
projectNamespace := rbac.NewProjectNamespace(projectIDOrName, isPublicProject)
|
||||
user := project.NewUser(s, projectNamespace, s.GetProjectRoles(projectIDOrName)...)
|
||||
return rbac.HasPermission(user, resource, action)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@ package security
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
)
|
||||
|
||||
// Context abstracts the operations related with authN and authZ
|
||||
@ -28,14 +29,10 @@ type Context interface {
|
||||
IsSysAdmin() bool
|
||||
// IsSolutionUser returns whether the user is solution user
|
||||
IsSolutionUser() bool
|
||||
// HasReadPerm returns whether the user has read permission to the project
|
||||
HasReadPerm(projectIDOrName interface{}) bool
|
||||
// HasWritePerm returns whether the user has write permission to the project
|
||||
HasWritePerm(projectIDOrName interface{}) bool
|
||||
// HasAllPerm returns whether the user has all permissions to the project
|
||||
HasAllPerm(projectIDOrName interface{}) bool
|
||||
// Get current user's all project
|
||||
GetMyProjects() ([]*models.Project, error)
|
||||
// Get user's role in provided project
|
||||
GetProjectRoles(projectIDOrName interface{}) []int
|
||||
// Can returns whether the user can do action on resource
|
||||
Can(action rbac.Action, resource rbac.Resource) bool
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ import (
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/dao/group"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/rbac/project"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/promgr"
|
||||
)
|
||||
@ -65,69 +67,20 @@ func (s *SecurityContext) IsSolutionUser() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// HasReadPerm returns whether the user has read permission to the project
|
||||
func (s *SecurityContext) HasReadPerm(projectIDOrName interface{}) bool {
|
||||
// public project
|
||||
public, err := s.pm.IsPublic(projectIDOrName)
|
||||
if err != nil {
|
||||
log.Errorf("failed to check the public of project %v: %v",
|
||||
projectIDOrName, err)
|
||||
return false
|
||||
}
|
||||
if public {
|
||||
return true
|
||||
}
|
||||
|
||||
// private project
|
||||
if !s.IsAuthenticated() {
|
||||
return false
|
||||
}
|
||||
|
||||
// system admin
|
||||
if s.IsSysAdmin() {
|
||||
return true
|
||||
}
|
||||
|
||||
roles := s.GetProjectRoles(projectIDOrName)
|
||||
return len(roles) > 0
|
||||
}
|
||||
|
||||
// HasWritePerm returns whether the user has write permission to the project
|
||||
func (s *SecurityContext) HasWritePerm(projectIDOrName interface{}) bool {
|
||||
if !s.IsAuthenticated() {
|
||||
return false
|
||||
}
|
||||
// system admin
|
||||
if s.IsSysAdmin() {
|
||||
return true
|
||||
}
|
||||
roles := s.GetProjectRoles(projectIDOrName)
|
||||
for _, role := range roles {
|
||||
switch role {
|
||||
case common.RoleProjectAdmin,
|
||||
common.RoleDeveloper:
|
||||
return true
|
||||
// Can returns whether the user can do action on resource
|
||||
func (s *SecurityContext) Can(action rbac.Action, resource rbac.Resource) bool {
|
||||
ns, err := resource.GetNamespace()
|
||||
if err == nil {
|
||||
switch ns.Kind() {
|
||||
case "project":
|
||||
projectIDOrName := ns.Identity()
|
||||
isPublicProject, _ := s.pm.IsPublic(projectIDOrName)
|
||||
projectNamespace := rbac.NewProjectNamespace(projectIDOrName, isPublicProject)
|
||||
user := project.NewUser(s, projectNamespace, s.GetProjectRoles(projectIDOrName)...)
|
||||
return rbac.HasPermission(user, resource, action)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasAllPerm returns whether the user has all permissions to the project
|
||||
func (s *SecurityContext) HasAllPerm(projectIDOrName interface{}) bool {
|
||||
if !s.IsAuthenticated() {
|
||||
return false
|
||||
}
|
||||
// system admin
|
||||
if s.IsSysAdmin() {
|
||||
return true
|
||||
}
|
||||
roles := s.GetProjectRoles(projectIDOrName)
|
||||
for _, role := range roles {
|
||||
switch role {
|
||||
case common.RoleProjectAdmin:
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@ -167,6 +120,8 @@ func (s *SecurityContext) GetProjectRoles(projectIDOrName interface{}) []int {
|
||||
switch role.RoleCode {
|
||||
case "MDRWS":
|
||||
roles = append(roles, common.RoleProjectAdmin)
|
||||
case "DRWS":
|
||||
roles = append(roles, common.RoleMaster)
|
||||
case "RWS":
|
||||
roles = append(roles, common.RoleDeveloper)
|
||||
case "RS":
|
||||
|
@ -16,14 +16,15 @@ package local
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/dao/project"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/common/utils/test"
|
||||
"github.com/goharbor/harbor/src/core/promgr"
|
||||
"github.com/goharbor/harbor/src/core/promgr/pmsdriver/local"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -53,45 +54,8 @@ var (
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
dbHost := os.Getenv("POSTGRESQL_HOST")
|
||||
if len(dbHost) == 0 {
|
||||
log.Fatalf("environment variable POSTGRES_HOST is not set")
|
||||
}
|
||||
dbUser := os.Getenv("POSTGRESQL_USR")
|
||||
if len(dbUser) == 0 {
|
||||
log.Fatalf("environment variable POSTGRES_USR is not set")
|
||||
}
|
||||
dbPortStr := os.Getenv("POSTGRESQL_PORT")
|
||||
if len(dbPortStr) == 0 {
|
||||
log.Fatalf("environment variable POSTGRES_PORT is not set")
|
||||
}
|
||||
dbPort, err := strconv.Atoi(dbPortStr)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid POSTGRESQL_PORT: %v", err)
|
||||
}
|
||||
|
||||
dbPassword := os.Getenv("POSTGRESQL_PWD")
|
||||
dbDatabase := os.Getenv("POSTGRESQL_DATABASE")
|
||||
if len(dbDatabase) == 0 {
|
||||
log.Fatalf("environment variable POSTGRESQL_DATABASE is not set")
|
||||
}
|
||||
|
||||
database := &models.Database{
|
||||
Type: "postgresql",
|
||||
PostGreSQL: &models.PostGreSQL{
|
||||
Host: dbHost,
|
||||
Port: dbPort,
|
||||
Username: dbUser,
|
||||
Password: dbPassword,
|
||||
Database: dbDatabase,
|
||||
},
|
||||
}
|
||||
|
||||
log.Infof("POSTGRES_HOST: %s, POSTGRES_USR: %s, POSTGRES_PORT: %d, POSTGRES_PWD: %s\n", dbHost, dbUser, dbPort, dbPassword)
|
||||
|
||||
if err := dao.InitDatabase(database); err != nil {
|
||||
log.Fatalf("failed to initialize database: %v", err)
|
||||
}
|
||||
test.InitDatabaseFromEnv()
|
||||
|
||||
// regiser users
|
||||
id, err := dao.Register(*projectAdminUser)
|
||||
@ -210,66 +174,73 @@ func TestIsSolutionUser(t *testing.T) {
|
||||
func TestHasReadPerm(t *testing.T) {
|
||||
// public project
|
||||
ctx := NewSecurityContext(nil, pm)
|
||||
assert.True(t, ctx.HasReadPerm("library"))
|
||||
|
||||
resource := rbac.NewProjectNamespace("library").Resource(rbac.ResourceRepository)
|
||||
assert.True(t, ctx.Can(rbac.ActionPull, resource))
|
||||
|
||||
// private project, unauthenticated
|
||||
ctx = NewSecurityContext(nil, pm)
|
||||
assert.False(t, ctx.HasReadPerm(private.Name))
|
||||
resource = rbac.NewProjectNamespace(private.Name).Resource(rbac.ResourceRepository)
|
||||
assert.False(t, ctx.Can(rbac.ActionPull, resource))
|
||||
|
||||
// private project, authenticated, has no perm
|
||||
ctx = NewSecurityContext(&models.User{
|
||||
Username: "test",
|
||||
}, pm)
|
||||
assert.False(t, ctx.HasReadPerm(private.Name))
|
||||
assert.False(t, ctx.Can(rbac.ActionPull, resource))
|
||||
|
||||
// private project, authenticated, has read perm
|
||||
ctx = NewSecurityContext(guestUser, pm)
|
||||
assert.True(t, ctx.HasReadPerm(private.Name))
|
||||
assert.True(t, ctx.Can(rbac.ActionPull, resource))
|
||||
|
||||
// private project, authenticated, system admin
|
||||
ctx = NewSecurityContext(&models.User{
|
||||
Username: "admin",
|
||||
HasAdminRole: true,
|
||||
}, pm)
|
||||
assert.True(t, ctx.HasReadPerm(private.Name))
|
||||
assert.True(t, ctx.Can(rbac.ActionPull, resource))
|
||||
}
|
||||
|
||||
func TestHasWritePerm(t *testing.T) {
|
||||
resource := rbac.NewProjectNamespace(private.Name).Resource(rbac.ResourceRepository)
|
||||
|
||||
// unauthenticated
|
||||
ctx := NewSecurityContext(nil, pm)
|
||||
assert.False(t, ctx.HasWritePerm(private.Name))
|
||||
assert.False(t, ctx.Can(rbac.ActionPush, resource))
|
||||
|
||||
// authenticated, has read perm
|
||||
ctx = NewSecurityContext(guestUser, pm)
|
||||
assert.False(t, ctx.HasWritePerm(private.Name))
|
||||
assert.False(t, ctx.Can(rbac.ActionPush, resource))
|
||||
|
||||
// authenticated, has write perm
|
||||
ctx = NewSecurityContext(developerUser, pm)
|
||||
assert.True(t, ctx.HasWritePerm(private.Name))
|
||||
assert.True(t, ctx.Can(rbac.ActionPush, resource))
|
||||
|
||||
// authenticated, system admin
|
||||
ctx = NewSecurityContext(&models.User{
|
||||
Username: "admin",
|
||||
HasAdminRole: true,
|
||||
}, pm)
|
||||
assert.True(t, ctx.HasReadPerm(private.Name))
|
||||
assert.True(t, ctx.Can(rbac.ActionPush, resource))
|
||||
}
|
||||
|
||||
func TestHasAllPerm(t *testing.T) {
|
||||
resource := rbac.NewProjectNamespace(private.Name).Resource(rbac.ResourceRepository)
|
||||
|
||||
// unauthenticated
|
||||
ctx := NewSecurityContext(nil, pm)
|
||||
assert.False(t, ctx.HasAllPerm(private.Name))
|
||||
assert.False(t, ctx.Can(rbac.ActionPushPull, resource))
|
||||
|
||||
// authenticated, has all perms
|
||||
ctx = NewSecurityContext(projectAdminUser, pm)
|
||||
assert.True(t, ctx.HasAllPerm(private.Name))
|
||||
assert.True(t, ctx.Can(rbac.ActionPushPull, resource))
|
||||
|
||||
// authenticated, system admin
|
||||
ctx = NewSecurityContext(&models.User{
|
||||
Username: "admin",
|
||||
HasAdminRole: true,
|
||||
}, pm)
|
||||
assert.True(t, ctx.HasAllPerm(private.Name))
|
||||
assert.True(t, ctx.Can(rbac.ActionPushPull, resource))
|
||||
}
|
||||
|
||||
func TestHasAllPermWithGroup(t *testing.T) {
|
||||
@ -285,10 +256,13 @@ func TestHasAllPermWithGroup(t *testing.T) {
|
||||
developer.GroupList = []*models.UserGroup{
|
||||
{GroupName: "test_group", GroupType: 1, LdapGroupDN: "cn=harbor_user,dc=example,dc=com"},
|
||||
}
|
||||
|
||||
resource := rbac.NewProjectNamespace(project.Name).Resource(rbac.ResourceRepository)
|
||||
|
||||
ctx := NewSecurityContext(developer, pm)
|
||||
assert.False(t, ctx.HasAllPerm(project.Name))
|
||||
assert.True(t, ctx.HasWritePerm(project.Name))
|
||||
assert.True(t, ctx.HasReadPerm(project.Name))
|
||||
assert.False(t, ctx.Can(rbac.ActionPushPull, resource))
|
||||
assert.True(t, ctx.Can(rbac.ActionPush, resource))
|
||||
assert.True(t, ctx.Can(rbac.ActionPull, resource))
|
||||
}
|
||||
|
||||
func TestGetMyProjects(t *testing.T) {
|
||||
|
88
src/common/security/robot/context.go
Normal file
88
src/common/security/robot/context.go
Normal file
@ -0,0 +1,88 @@
|
||||
// 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 robot
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/core/promgr"
|
||||
)
|
||||
|
||||
// SecurityContext implements security.Context interface based on database
|
||||
type SecurityContext struct {
|
||||
robot *models.Robot
|
||||
pm promgr.ProjectManager
|
||||
policy []*rbac.Policy
|
||||
}
|
||||
|
||||
// NewSecurityContext ...
|
||||
func NewSecurityContext(robot *models.Robot, pm promgr.ProjectManager, policy []*rbac.Policy) *SecurityContext {
|
||||
return &SecurityContext{
|
||||
robot: robot,
|
||||
pm: pm,
|
||||
policy: policy,
|
||||
}
|
||||
}
|
||||
|
||||
// IsAuthenticated returns true if the user has been authenticated
|
||||
func (s *SecurityContext) IsAuthenticated() bool {
|
||||
return s.robot != nil
|
||||
}
|
||||
|
||||
// GetUsername returns the username of the authenticated user
|
||||
// It returns null if the user has not been authenticated
|
||||
func (s *SecurityContext) GetUsername() string {
|
||||
if !s.IsAuthenticated() {
|
||||
return ""
|
||||
}
|
||||
return s.robot.Name
|
||||
}
|
||||
|
||||
// IsSysAdmin robot cannot be a system admin
|
||||
func (s *SecurityContext) IsSysAdmin() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsSolutionUser robot cannot be a system admin
|
||||
func (s *SecurityContext) IsSolutionUser() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// GetMyProjects no implementation
|
||||
func (s *SecurityContext) GetMyProjects() ([]*models.Project, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetProjectRoles no implementation
|
||||
func (s *SecurityContext) GetProjectRoles(projectIDOrName interface{}) []int {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Can returns whether the robot can do action on resource
|
||||
func (s *SecurityContext) Can(action rbac.Action, resource rbac.Resource) bool {
|
||||
ns, err := resource.GetNamespace()
|
||||
if err == nil {
|
||||
switch ns.Kind() {
|
||||
case "project":
|
||||
projectIDOrName := ns.Identity()
|
||||
isPublicProject, _ := s.pm.IsPublic(projectIDOrName)
|
||||
projectNamespace := rbac.NewProjectNamespace(projectIDOrName, isPublicProject)
|
||||
robot := NewRobot(s.GetUsername(), projectNamespace, s.policy)
|
||||
return rbac.HasPermission(robot, resource, action)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
200
src/common/security/robot/context_test.go
Normal file
200
src/common/security/robot/context_test.go
Normal file
@ -0,0 +1,200 @@
|
||||
// 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 robot
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/promgr"
|
||||
"github.com/goharbor/harbor/src/core/promgr/pmsdriver/local"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
private = &models.Project{
|
||||
Name: "testrobot",
|
||||
OwnerID: 1,
|
||||
}
|
||||
pm = promgr.NewDefaultProjectManager(local.NewDriver(), true)
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
dbHost := os.Getenv("POSTGRESQL_HOST")
|
||||
if len(dbHost) == 0 {
|
||||
log.Fatalf("environment variable POSTGRES_HOST is not set")
|
||||
}
|
||||
dbUser := os.Getenv("POSTGRESQL_USR")
|
||||
if len(dbUser) == 0 {
|
||||
log.Fatalf("environment variable POSTGRES_USR is not set")
|
||||
}
|
||||
dbPortStr := os.Getenv("POSTGRESQL_PORT")
|
||||
if len(dbPortStr) == 0 {
|
||||
log.Fatalf("environment variable POSTGRES_PORT is not set")
|
||||
}
|
||||
dbPort, err := strconv.Atoi(dbPortStr)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid POSTGRESQL_PORT: %v", err)
|
||||
}
|
||||
|
||||
dbPassword := os.Getenv("POSTGRESQL_PWD")
|
||||
dbDatabase := os.Getenv("POSTGRESQL_DATABASE")
|
||||
if len(dbDatabase) == 0 {
|
||||
log.Fatalf("environment variable POSTGRESQL_DATABASE is not set")
|
||||
}
|
||||
|
||||
database := &models.Database{
|
||||
Type: "postgresql",
|
||||
PostGreSQL: &models.PostGreSQL{
|
||||
Host: dbHost,
|
||||
Port: dbPort,
|
||||
Username: dbUser,
|
||||
Password: dbPassword,
|
||||
Database: dbDatabase,
|
||||
},
|
||||
}
|
||||
|
||||
log.Infof("POSTGRES_HOST: %s, POSTGRES_USR: %s, POSTGRES_PORT: %d, POSTGRES_PWD: %s\n", dbHost, dbUser, dbPort, dbPassword)
|
||||
|
||||
if err := dao.InitDatabase(database); err != nil {
|
||||
log.Fatalf("failed to initialize database: %v", err)
|
||||
}
|
||||
|
||||
// add project
|
||||
id, err := dao.AddProject(*private)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to add project: %v", err)
|
||||
}
|
||||
private.ProjectID = id
|
||||
defer dao.DeleteProject(id)
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestIsAuthenticated(t *testing.T) {
|
||||
// unauthenticated
|
||||
ctx := NewSecurityContext(nil, nil, nil)
|
||||
assert.False(t, ctx.IsAuthenticated())
|
||||
|
||||
// authenticated
|
||||
ctx = NewSecurityContext(&models.Robot{
|
||||
Name: "test",
|
||||
Disabled: false,
|
||||
}, nil, nil)
|
||||
assert.True(t, ctx.IsAuthenticated())
|
||||
}
|
||||
|
||||
func TestGetUsername(t *testing.T) {
|
||||
// unauthenticated
|
||||
ctx := NewSecurityContext(nil, nil, nil)
|
||||
assert.Equal(t, "", ctx.GetUsername())
|
||||
|
||||
// authenticated
|
||||
ctx = NewSecurityContext(&models.Robot{
|
||||
Name: "test",
|
||||
Disabled: false,
|
||||
}, nil, nil)
|
||||
assert.Equal(t, "test", ctx.GetUsername())
|
||||
}
|
||||
|
||||
func TestIsSysAdmin(t *testing.T) {
|
||||
// unauthenticated
|
||||
ctx := NewSecurityContext(nil, nil, nil)
|
||||
assert.False(t, ctx.IsSysAdmin())
|
||||
|
||||
// authenticated, non admin
|
||||
ctx = NewSecurityContext(&models.Robot{
|
||||
Name: "test",
|
||||
Disabled: false,
|
||||
}, nil, nil)
|
||||
assert.False(t, ctx.IsSysAdmin())
|
||||
}
|
||||
|
||||
func TestIsSolutionUser(t *testing.T) {
|
||||
ctx := NewSecurityContext(nil, nil, nil)
|
||||
assert.False(t, ctx.IsSolutionUser())
|
||||
}
|
||||
|
||||
func TestHasReadPerm(t *testing.T) {
|
||||
|
||||
rbacPolicy := &rbac.Policy{
|
||||
Resource: "/project/testrobot/repository",
|
||||
Action: "pull",
|
||||
}
|
||||
policies := []*rbac.Policy{}
|
||||
policies = append(policies, rbacPolicy)
|
||||
robot := &models.Robot{
|
||||
Name: "test_robot_1",
|
||||
Description: "desc",
|
||||
}
|
||||
|
||||
ctx := NewSecurityContext(robot, pm, policies)
|
||||
resource := rbac.NewProjectNamespace(private.Name).Resource(rbac.ResourceRepository)
|
||||
assert.True(t, ctx.Can(rbac.ActionPull, resource))
|
||||
}
|
||||
|
||||
func TestHasWritePerm(t *testing.T) {
|
||||
|
||||
rbacPolicy := &rbac.Policy{
|
||||
Resource: "/project/testrobot/repository",
|
||||
Action: "push",
|
||||
}
|
||||
policies := []*rbac.Policy{}
|
||||
policies = append(policies, rbacPolicy)
|
||||
robot := &models.Robot{
|
||||
Name: "test_robot_2",
|
||||
Description: "desc",
|
||||
}
|
||||
|
||||
ctx := NewSecurityContext(robot, pm, policies)
|
||||
resource := rbac.NewProjectNamespace(private.Name).Resource(rbac.ResourceRepository)
|
||||
assert.True(t, ctx.Can(rbac.ActionPush, resource))
|
||||
}
|
||||
|
||||
func TestHasAllPerm(t *testing.T) {
|
||||
rbacPolicy := &rbac.Policy{
|
||||
Resource: "/project/testrobot/repository",
|
||||
Action: "push+pull",
|
||||
}
|
||||
policies := []*rbac.Policy{}
|
||||
policies = append(policies, rbacPolicy)
|
||||
robot := &models.Robot{
|
||||
Name: "test_robot_3",
|
||||
Description: "desc",
|
||||
}
|
||||
|
||||
ctx := NewSecurityContext(robot, pm, policies)
|
||||
resource := rbac.NewProjectNamespace(private.Name).Resource(rbac.ResourceRepository)
|
||||
assert.True(t, ctx.Can(rbac.ActionPushPull, resource))
|
||||
}
|
||||
|
||||
func TestGetMyProjects(t *testing.T) {
|
||||
ctx := NewSecurityContext(nil, nil, nil)
|
||||
projects, err := ctx.GetMyProjects()
|
||||
require.Nil(t, err)
|
||||
assert.Nil(t, projects)
|
||||
}
|
||||
|
||||
func TestGetProjectRoles(t *testing.T) {
|
||||
ctx := NewSecurityContext(nil, nil, nil)
|
||||
roles := ctx.GetProjectRoles("test")
|
||||
assert.Nil(t, roles)
|
||||
}
|
42
src/common/security/robot/robot.go
Normal file
42
src/common/security/robot/robot.go
Normal file
@ -0,0 +1,42 @@
|
||||
package robot
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/rbac/project"
|
||||
)
|
||||
|
||||
// robot implement the rbac.User interface for project robot account
|
||||
type robot struct {
|
||||
username string
|
||||
namespace rbac.Namespace
|
||||
policy []*rbac.Policy
|
||||
}
|
||||
|
||||
// GetUserName get the robot name.
|
||||
func (r *robot) GetUserName() string {
|
||||
return r.username
|
||||
}
|
||||
|
||||
// GetPolicies ...
|
||||
func (r *robot) GetPolicies() []*rbac.Policy {
|
||||
policies := []*rbac.Policy{}
|
||||
if r.namespace.IsPublic() {
|
||||
policies = append(policies, project.PoliciesForPublicProject(r.namespace)...)
|
||||
}
|
||||
policies = append(policies, r.policy...)
|
||||
return policies
|
||||
}
|
||||
|
||||
// GetRoles robot has no definition of role, always return nil here.
|
||||
func (r *robot) GetRoles() []rbac.Role {
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewRobot ...
|
||||
func NewRobot(username string, namespace rbac.Namespace, policy []*rbac.Policy) rbac.User {
|
||||
return &robot{
|
||||
username: username,
|
||||
namespace: namespace,
|
||||
policy: policy,
|
||||
}
|
||||
}
|
27
src/common/security/robot/robot_test.go
Normal file
27
src/common/security/robot/robot_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
package robot
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetPolicies(t *testing.T) {
|
||||
|
||||
rbacPolicy := &rbac.Policy{
|
||||
Resource: "/project/libray/repository",
|
||||
Action: "pull",
|
||||
}
|
||||
policies := []*rbac.Policy{}
|
||||
policies = append(policies, rbacPolicy)
|
||||
|
||||
robot := robot{
|
||||
username: "test",
|
||||
namespace: rbac.NewProjectNamespace("library", false),
|
||||
policy: policies,
|
||||
}
|
||||
|
||||
assert.Equal(t, robot.GetUserName(), "test")
|
||||
assert.NotNil(t, robot.GetPolicies())
|
||||
assert.Nil(t, robot.GetRoles())
|
||||
}
|
@ -19,6 +19,7 @@ import (
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/secret"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
)
|
||||
@ -70,27 +71,10 @@ func (s *SecurityContext) IsSolutionUser() bool {
|
||||
return s.IsAuthenticated()
|
||||
}
|
||||
|
||||
// HasReadPerm returns true if the corresponding user of the secret
|
||||
// Can returns whether the user can do action on resource
|
||||
// returns true if the corresponding user of the secret
|
||||
// is jobservice or core service, otherwise returns false
|
||||
func (s *SecurityContext) HasReadPerm(projectIDOrName interface{}) bool {
|
||||
if s.store == nil {
|
||||
return false
|
||||
}
|
||||
return s.store.GetUsername(s.secret) == secret.JobserviceUser || s.store.GetUsername(s.secret) == secret.CoreUser
|
||||
}
|
||||
|
||||
// HasWritePerm returns true if the corresponding user of the secret
|
||||
// is jobservice or core service, otherwise returns false
|
||||
func (s *SecurityContext) HasWritePerm(projectIDOrName interface{}) bool {
|
||||
if s.store == nil {
|
||||
return false
|
||||
}
|
||||
return s.store.GetUsername(s.secret) == secret.JobserviceUser || s.store.GetUsername(s.secret) == secret.CoreUser
|
||||
}
|
||||
|
||||
// HasAllPerm returns true if the corresponding user of the secret
|
||||
// is jobservice or core service, otherwise returns false
|
||||
func (s *SecurityContext) HasAllPerm(projectIDOrName interface{}) bool {
|
||||
func (s *SecurityContext) Can(action rbac.Action, resource rbac.Resource) bool {
|
||||
if s.store == nil {
|
||||
return false
|
||||
}
|
||||
@ -105,7 +89,9 @@ func (s *SecurityContext) GetMyProjects() ([]*models.Project, error) {
|
||||
// GetProjectRoles return guest role if has read permission, otherwise return nil
|
||||
func (s *SecurityContext) GetProjectRoles(projectIDOrName interface{}) []int {
|
||||
roles := []int{}
|
||||
if s.HasReadPerm(projectIDOrName) {
|
||||
if s.store != nil &&
|
||||
(s.store.GetUsername(s.secret) == secret.JobserviceUser ||
|
||||
s.store.GetUsername(s.secret) == secret.CoreUser) {
|
||||
roles = append(roles, common.RoleGuest)
|
||||
}
|
||||
return roles
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/secret"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@ -96,9 +97,11 @@ func TestIsSolutionUser(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHasReadPerm(t *testing.T) {
|
||||
readAction := rbac.Action("pull")
|
||||
resource := rbac.Resource("/project/project_name/repository")
|
||||
// secret store is null
|
||||
context := NewSecurityContext("", nil)
|
||||
hasReadPerm := context.HasReadPerm("project_name")
|
||||
hasReadPerm := context.Can(readAction, resource)
|
||||
assert.False(t, hasReadPerm)
|
||||
|
||||
// invalid secret
|
||||
@ -106,7 +109,7 @@ func TestHasReadPerm(t *testing.T) {
|
||||
secret.NewStore(map[string]string{
|
||||
"jobservice_secret": secret.JobserviceUser,
|
||||
}))
|
||||
hasReadPerm = context.HasReadPerm("project_name")
|
||||
hasReadPerm = context.Can(readAction, resource)
|
||||
assert.False(t, hasReadPerm)
|
||||
|
||||
// valid secret, project name
|
||||
@ -114,11 +117,12 @@ func TestHasReadPerm(t *testing.T) {
|
||||
secret.NewStore(map[string]string{
|
||||
"jobservice_secret": secret.JobserviceUser,
|
||||
}))
|
||||
hasReadPerm = context.HasReadPerm("project_name")
|
||||
hasReadPerm = context.Can(readAction, resource)
|
||||
assert.True(t, hasReadPerm)
|
||||
|
||||
// valid secret, project ID
|
||||
hasReadPerm = context.HasReadPerm(1)
|
||||
resource = rbac.Resource("/project/1/repository")
|
||||
hasReadPerm = context.Can(readAction, resource)
|
||||
assert.True(t, hasReadPerm)
|
||||
}
|
||||
|
||||
@ -128,12 +132,16 @@ func TestHasWritePerm(t *testing.T) {
|
||||
"secret": "username",
|
||||
}))
|
||||
|
||||
writeAction := rbac.Action("push")
|
||||
|
||||
// project name
|
||||
hasWritePerm := context.HasWritePerm("project_name")
|
||||
resource := rbac.Resource("/project/project_name/repository")
|
||||
hasWritePerm := context.Can(writeAction, resource)
|
||||
assert.False(t, hasWritePerm)
|
||||
|
||||
// project ID
|
||||
hasWritePerm = context.HasWritePerm(1)
|
||||
resource = rbac.Resource("/project/1/repository")
|
||||
hasWritePerm = context.Can(writeAction, resource)
|
||||
assert.False(t, hasWritePerm)
|
||||
}
|
||||
|
||||
@ -143,12 +151,16 @@ func TestHasAllPerm(t *testing.T) {
|
||||
"secret": "username",
|
||||
}))
|
||||
|
||||
allAction := rbac.Action("push+pull")
|
||||
|
||||
// project name
|
||||
hasAllPerm := context.HasAllPerm("project_name")
|
||||
resource := rbac.Resource("/project/project_name/repository")
|
||||
hasAllPerm := context.Can(allAction, resource)
|
||||
assert.False(t, hasAllPerm)
|
||||
|
||||
// project ID
|
||||
hasAllPerm = context.HasAllPerm(1)
|
||||
resource = rbac.Resource("/project/1/repository")
|
||||
hasAllPerm = context.Can(allAction, resource)
|
||||
assert.False(t, hasAllPerm)
|
||||
}
|
||||
|
||||
|
29
src/common/token/claims.go
Normal file
29
src/common/token/claims.go
Normal file
@ -0,0 +1,29 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// RobotClaims implements the interface of jwt.Claims
|
||||
type RobotClaims struct {
|
||||
jwt.StandardClaims
|
||||
TokenID int64 `json:"id"`
|
||||
ProjectID int64 `json:"pid"`
|
||||
Access []*rbac.Policy `json:"access"`
|
||||
}
|
||||
|
||||
// Valid valid the claims "tokenID, projectID and access".
|
||||
func (rc RobotClaims) Valid() error {
|
||||
if rc.TokenID < 0 {
|
||||
return errors.New("Token id must an valid INT")
|
||||
}
|
||||
if rc.ProjectID < 0 {
|
||||
return errors.New("Project id must an valid INT")
|
||||
}
|
||||
if rc.Access == nil {
|
||||
return errors.New("The access info cannot be nil")
|
||||
}
|
||||
return nil
|
||||
}
|
68
src/common/token/claims_test.go
Normal file
68
src/common/token/claims_test.go
Normal file
@ -0,0 +1,68 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValid(t *testing.T) {
|
||||
|
||||
rbacPolicy := &rbac.Policy{
|
||||
Resource: "/project/libray/repository",
|
||||
Action: "pull",
|
||||
}
|
||||
policies := []*rbac.Policy{}
|
||||
policies = append(policies, rbacPolicy)
|
||||
|
||||
rClaims := &RobotClaims{
|
||||
TokenID: 1,
|
||||
ProjectID: 2,
|
||||
Access: policies,
|
||||
}
|
||||
assert.Nil(t, rClaims.Valid())
|
||||
}
|
||||
|
||||
func TestUnValidTokenID(t *testing.T) {
|
||||
|
||||
rbacPolicy := &rbac.Policy{
|
||||
Resource: "/project/libray/repository",
|
||||
Action: "pull",
|
||||
}
|
||||
policies := []*rbac.Policy{}
|
||||
policies = append(policies, rbacPolicy)
|
||||
|
||||
rClaims := &RobotClaims{
|
||||
TokenID: -1,
|
||||
ProjectID: 2,
|
||||
Access: policies,
|
||||
}
|
||||
assert.NotNil(t, rClaims.Valid())
|
||||
}
|
||||
|
||||
func TestUnValidProjectID(t *testing.T) {
|
||||
|
||||
rbacPolicy := &rbac.Policy{
|
||||
Resource: "/project/libray/repository",
|
||||
Action: "pull",
|
||||
}
|
||||
policies := []*rbac.Policy{}
|
||||
policies = append(policies, rbacPolicy)
|
||||
|
||||
rClaims := &RobotClaims{
|
||||
TokenID: 1,
|
||||
ProjectID: -2,
|
||||
Access: policies,
|
||||
}
|
||||
assert.NotNil(t, rClaims.Valid())
|
||||
}
|
||||
|
||||
func TestUnValidPolicy(t *testing.T) {
|
||||
|
||||
rClaims := &RobotClaims{
|
||||
TokenID: 1,
|
||||
ProjectID: 2,
|
||||
Access: nil,
|
||||
}
|
||||
assert.NotNil(t, rClaims.Valid())
|
||||
}
|
85
src/common/token/htoken.go
Normal file
85
src/common/token/htoken.go
Normal file
@ -0,0 +1,85 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HToken htoken is a jwt token for harbor robot account,
|
||||
// which contains the robot ID, project ID and the access permission for the project.
|
||||
// It used for authn/authz for robot account in Harbor.
|
||||
type HToken struct {
|
||||
jwt.Token
|
||||
}
|
||||
|
||||
// New ...
|
||||
func New(tokenID, projectID int64, access []*rbac.Policy) (*HToken, error) {
|
||||
rClaims := &RobotClaims{
|
||||
TokenID: tokenID,
|
||||
ProjectID: projectID,
|
||||
Access: access,
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
ExpiresAt: time.Now().Add(DefaultOptions.TTL).Unix(),
|
||||
Issuer: DefaultOptions.Issuer,
|
||||
},
|
||||
}
|
||||
err := rClaims.Valid()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &HToken{
|
||||
Token: *jwt.NewWithClaims(DefaultOptions.SignMethod, rClaims),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Raw get the Raw string of token
|
||||
func (htk *HToken) Raw() (string, error) {
|
||||
key, err := DefaultOptions.GetKey()
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
raw, err := htk.Token.SignedString(key)
|
||||
if err != nil {
|
||||
log.Debugf(fmt.Sprintf("failed to issue token %v", err))
|
||||
return "", err
|
||||
}
|
||||
return raw, err
|
||||
}
|
||||
|
||||
// ParseWithClaims ...
|
||||
func ParseWithClaims(rawToken string, claims jwt.Claims) (*HToken, error) {
|
||||
key, err := DefaultOptions.GetKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token, err := jwt.ParseWithClaims(rawToken, claims, func(token *jwt.Token) (interface{}, error) {
|
||||
if token.Method.Alg() != DefaultOptions.SignMethod.Alg() {
|
||||
return nil, errors.New("invalid signing method")
|
||||
}
|
||||
switch k := key.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return &k.PublicKey, nil
|
||||
case *ecdsa.PrivateKey:
|
||||
return &k.PublicKey, nil
|
||||
default:
|
||||
return key, nil
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf(fmt.Sprintf("parse token error, %v", err))
|
||||
return nil, err
|
||||
}
|
||||
if !token.Valid {
|
||||
log.Errorf(fmt.Sprintf("invalid jwt token, %v", token))
|
||||
return nil, err
|
||||
}
|
||||
return &HToken{
|
||||
Token: *token,
|
||||
}, nil
|
||||
}
|
77
src/common/token/htoken_test.go
Normal file
77
src/common/token/htoken_test.go
Normal file
@ -0,0 +1,77 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/utils/test"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
server, err := test.NewAdminserver(nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer server.Close()
|
||||
|
||||
if err := os.Setenv("ADMINSERVER_URL", server.URL); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := config.Init(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
result := m.Run()
|
||||
if result != 0 {
|
||||
os.Exit(result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
rbacPolicy := &rbac.Policy{
|
||||
Resource: "/project/libray/repository",
|
||||
Action: "pull",
|
||||
}
|
||||
policies := []*rbac.Policy{}
|
||||
policies = append(policies, rbacPolicy)
|
||||
|
||||
tokenID := int64(123)
|
||||
projectID := int64(321)
|
||||
token, err := New(tokenID, projectID, policies)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, token.Header["alg"], "RS256")
|
||||
assert.Equal(t, token.Header["typ"], "JWT")
|
||||
|
||||
}
|
||||
|
||||
func TestRaw(t *testing.T) {
|
||||
rbacPolicy := &rbac.Policy{
|
||||
Resource: "/project/library/repository",
|
||||
Action: "pull",
|
||||
}
|
||||
policies := []*rbac.Policy{}
|
||||
policies = append(policies, rbacPolicy)
|
||||
|
||||
tokenID := int64(123)
|
||||
projectID := int64(321)
|
||||
|
||||
token, err := New(tokenID, projectID, policies)
|
||||
assert.Nil(t, err)
|
||||
|
||||
rawTk, err := token.Raw()
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, rawTk)
|
||||
}
|
||||
|
||||
func TestParseWithClaims(t *testing.T) {
|
||||
rawTk := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJJRCI6MTIzLCJQcm9qZWN0SUQiOjAsIkFjY2VzcyI6W3siUmVzb3VyY2UiOiIvcHJvamVjdC9saWJyYXkvcmVwb3NpdG9yeSIsIkFjdGlvbiI6InB1bGwiLCJFZmZlY3QiOiIifV0sIlN0YW5kYXJkQ2xhaW1zIjp7ImV4cCI6MTU0ODE0MDIyOSwiaXNzIjoiaGFyYm9yLXRva2VuLWlzc3VlciJ9fQ.Jc3qSKN4SJVUzAvBvemVpRcSOZaHlu0Avqms04qzPm4ru9-r9IRIl3mnSkI6m9XkzLUeJ7Kiwyw63ghngnVKw_PupeclOGC6s3TK5Cfmo4h-lflecXjZWwyy-dtH_e7Us_ItS-R3nXDJtzSLEpsGHCcAj-1X2s93RB2qD8LNSylvYeDezVkTzqRzzfawPJheKKh9JTrz-3eUxCwQard9-xjlwvfUYULoHTn9npNAUq4-jqhipW4uE8HL-ym33AGF57la8U0RO11hmDM5K8-PiYknbqJ_oONeS3HBNym2pEFeGjtTv2co213wl4T5lemlg4SGolMBuJ03L7_beVZ0o-MKTkKDqDwJalb6_PM-7u3RbxC9IzJMiwZKIPnD3FvV10iPxUUQHaH8Jz5UZ2pFIhi_8BNnlBfT0JOPFVYATtLjHMczZelj2YvAeR1UHBzq3E0jPpjjwlqIFgaHCaN_KMwEvadTo_Fi2sEH4pNGP7M3yehU_72oLJQgF4paJarsmEoij6ZtPs6xekBz1fccVitq_8WNIz9aeCUdkUBRwI5QKw1RdW4ua-w74ld5MZStWJA8veyoLkEb_Q9eq2oAj5KWFjJbW5-ltiIfM8gxKflsrkWAidYGcEIYcuXr7UdqEKXxtPiWM0xb3B91ovYvO5402bn3f9-UGtlcestxNHA"
|
||||
rClaims := &RobotClaims{}
|
||||
_, _ = ParseWithClaims(rawTk, rClaims)
|
||||
assert.Equal(t, int64(123), rClaims.TokenID)
|
||||
assert.Equal(t, int64(0), rClaims.ProjectID)
|
||||
assert.Equal(t, "/project/libray/repository", rClaims.Access[0].Resource.String())
|
||||
}
|
83
src/common/token/options.go
Normal file
83
src/common/token/options.go
Normal file
@ -0,0 +1,83 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"io/ioutil"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
ttl = 60 * time.Minute
|
||||
issuer = "harbor-token-issuer"
|
||||
signedMethod = "RS256"
|
||||
)
|
||||
|
||||
var (
|
||||
privateKey = config.TokenPrivateKeyPath()
|
||||
// DefaultOptions ...
|
||||
DefaultOptions = NewOptions()
|
||||
)
|
||||
|
||||
// Options ...
|
||||
type Options struct {
|
||||
SignMethod jwt.SigningMethod
|
||||
PublicKey []byte
|
||||
PrivateKey []byte
|
||||
TTL time.Duration
|
||||
Issuer string
|
||||
}
|
||||
|
||||
// NewOptions ...
|
||||
func NewOptions() *Options {
|
||||
privateKey, err := ioutil.ReadFile(privateKey)
|
||||
if err != nil {
|
||||
log.Errorf(fmt.Sprintf("failed to read private key %v", err))
|
||||
return nil
|
||||
}
|
||||
opt := &Options{
|
||||
SignMethod: jwt.GetSigningMethod(signedMethod),
|
||||
PrivateKey: privateKey,
|
||||
Issuer: issuer,
|
||||
TTL: ttl,
|
||||
}
|
||||
return opt
|
||||
}
|
||||
|
||||
// GetKey ...
|
||||
func (o *Options) GetKey() (interface{}, error) {
|
||||
var err error
|
||||
var privateKey *rsa.PrivateKey
|
||||
var publicKey *rsa.PublicKey
|
||||
|
||||
switch o.SignMethod.(type) {
|
||||
case *jwt.SigningMethodRSA, *jwt.SigningMethodRSAPSS:
|
||||
if len(o.PrivateKey) > 0 {
|
||||
privateKey, err = jwt.ParseRSAPrivateKeyFromPEM(o.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(o.PublicKey) > 0 {
|
||||
publicKey, err = jwt.ParseRSAPublicKeyFromPEM(o.PublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if privateKey == nil {
|
||||
if publicKey != nil {
|
||||
return publicKey, nil
|
||||
}
|
||||
return nil, fmt.Errorf("key is provided")
|
||||
}
|
||||
if publicKey != nil && publicKey.E != privateKey.E && publicKey.N.Cmp(privateKey.N) != 0 {
|
||||
return nil, fmt.Errorf("the public key and private key are not match")
|
||||
}
|
||||
return privateKey, nil
|
||||
default:
|
||||
return nil, fmt.Errorf(fmt.Sprintf("unsupported sign method, %s", o.SignMethod))
|
||||
}
|
||||
}
|
23
src/common/token/options_test.go
Normal file
23
src/common/token/options_test.go
Normal file
@ -0,0 +1,23 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewOptions(t *testing.T) {
|
||||
defaultOpt := DefaultOptions
|
||||
assert.NotNil(t, defaultOpt)
|
||||
assert.Equal(t, defaultOpt.SignMethod, jwt.GetSigningMethod("RS256"))
|
||||
assert.Equal(t, defaultOpt.Issuer, "harbor-token-issuer")
|
||||
assert.Equal(t, defaultOpt.TTL, 60*time.Minute)
|
||||
}
|
||||
|
||||
func TestGetKey(t *testing.T) {
|
||||
defaultOpt := DefaultOptions
|
||||
key, err := defaultOpt.GetKey()
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, key)
|
||||
}
|
@ -6,7 +6,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/common/utils/test"
|
||||
@ -75,18 +74,9 @@ var adminServerDefaultConfigWithVerifyCert = map[string]interface{}{
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
server, err := test.NewAdminserver(adminServerLdapTestConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create a mock admin server: %v", err)
|
||||
}
|
||||
defer server.Close()
|
||||
|
||||
if err := os.Setenv("ADMINSERVER_URL", server.URL); err != nil {
|
||||
log.Fatalf("failed to set env %s: %v", "ADMINSERVER_URL", err)
|
||||
}
|
||||
|
||||
test.InitDatabaseFromEnv()
|
||||
secretKeyPath := "/tmp/secretkey"
|
||||
_, err = test.GenerateKey(secretKeyPath)
|
||||
_, err := test.GenerateKey(secretKeyPath)
|
||||
if err != nil {
|
||||
log.Errorf("failed to generate secret key: %v", err)
|
||||
return
|
||||
@ -101,14 +91,7 @@ func TestMain(m *testing.M) {
|
||||
log.Fatalf("failed to initialize configurations: %v", err)
|
||||
}
|
||||
|
||||
database, err := uiConfig.Database()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get database configuration: %v", err)
|
||||
}
|
||||
|
||||
if err := dao.InitDatabase(database); err != nil {
|
||||
log.Fatalf("failed to initialize database: %v", err)
|
||||
}
|
||||
uiConfig.Upload(adminServerLdapTestConfig)
|
||||
|
||||
os.Exit(m.Run())
|
||||
|
||||
|
@ -17,9 +17,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
notarytest "github.com/goharbor/harbor/src/common/utils/notary/test"
|
||||
utilstest "github.com/goharbor/harbor/src/common/utils/test"
|
||||
"github.com/goharbor/harbor/src/common/utils/test"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
@ -27,11 +26,13 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
)
|
||||
|
||||
var endpoint = "10.117.4.142"
|
||||
var notaryServer *httptest.Server
|
||||
var adminServer *httptest.Server
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
notaryServer = notarytest.NewNotaryServer(endpoint)
|
||||
@ -42,17 +43,12 @@ func TestMain(m *testing.M) {
|
||||
common.CfgExpiration: 5,
|
||||
common.TokenExpiration: 30,
|
||||
}
|
||||
adminServer, err := utilstest.NewAdminserver(defaultConfig)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer adminServer.Close()
|
||||
if err := os.Setenv("ADMINSERVER_URL", adminServer.URL); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := config.Init(); err != nil {
|
||||
panic(err)
|
||||
log.Fatalf("failed to initialize config: %v", err)
|
||||
}
|
||||
test.InitDatabaseFromEnv()
|
||||
config.Upload(defaultConfig)
|
||||
notaryCachePath = "/tmp/notary"
|
||||
result := m.Run()
|
||||
if result != 0 {
|
||||
|
@ -15,11 +15,13 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
)
|
||||
|
||||
@ -44,6 +46,7 @@ func InitDatabaseFromEnv() {
|
||||
|
||||
dbPassword := os.Getenv("POSTGRESQL_PWD")
|
||||
dbDatabase := os.Getenv("POSTGRESQL_DATABASE")
|
||||
adminPwd := os.Getenv("HARBOR_ADMIN_PASSWD")
|
||||
if len(dbDatabase) == 0 {
|
||||
log.Fatalf("environment variable POSTGRESQL_DATABASE is not set")
|
||||
}
|
||||
@ -61,7 +64,32 @@ func InitDatabaseFromEnv() {
|
||||
|
||||
log.Infof("POSTGRES_HOST: %s, POSTGRES_USR: %s, POSTGRES_PORT: %d, POSTGRES_PWD: %s\n", dbHost, dbUser, dbPort, dbPassword)
|
||||
|
||||
if err := dao.InitDatabase(database); err != nil {
|
||||
log.Fatalf("failed to initialize database: %v", err)
|
||||
if err := dao.InitAndUpgradeDatabase(database); err != nil {
|
||||
log.Fatalf("failed to init and upgrade database : %v", err)
|
||||
}
|
||||
if err := updateUserInitialPassword(1, adminPwd); err != nil {
|
||||
log.Fatalf("failed to init password for admin: %v", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func updateUserInitialPassword(userID int, password string) error {
|
||||
queryUser := models.User{UserID: userID}
|
||||
user, err := dao.GetUser(queryUser)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to get user, userID: %d %v", userID, err)
|
||||
}
|
||||
if user == nil {
|
||||
return fmt.Errorf("user id: %d does not exist", userID)
|
||||
}
|
||||
if user.Salt == "" {
|
||||
user.Salt = utils.GenerateRandomString()
|
||||
user.Password = password
|
||||
err = dao.ChangeUserPassword(*user)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to update user encrypted password, userID: %d, err: %v", userID, err)
|
||||
}
|
||||
} else {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -21,7 +21,11 @@ import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/gorilla/mux"
|
||||
"os"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// RequestHandlerMapping is a mapping between request and its handler
|
||||
@ -87,3 +91,54 @@ func NewServer(mappings ...*RequestHandlerMapping) *httptest.Server {
|
||||
|
||||
return httptest.NewServer(r)
|
||||
}
|
||||
|
||||
// GetUnitTestConfig ...
|
||||
func GetUnitTestConfig() map[string]interface{} {
|
||||
ipAddress := os.Getenv("IP")
|
||||
return map[string]interface{}{
|
||||
common.ExtEndpoint: fmt.Sprintf("https://%s", ipAddress),
|
||||
common.AUTHMode: "db_auth",
|
||||
common.DatabaseType: "postgresql",
|
||||
common.PostGreSQLHOST: ipAddress,
|
||||
common.PostGreSQLPort: 5432,
|
||||
common.PostGreSQLUsername: "postgres",
|
||||
common.PostGreSQLPassword: "root123",
|
||||
common.PostGreSQLDatabase: "registry",
|
||||
common.LDAPURL: "ldap://ldap.vmware.com",
|
||||
common.LDAPSearchDN: "cn=admin,dc=example,dc=com",
|
||||
common.LDAPSearchPwd: "admin",
|
||||
common.LDAPBaseDN: "dc=example,dc=com",
|
||||
common.LDAPUID: "uid",
|
||||
common.LDAPFilter: "",
|
||||
common.LDAPScope: 2,
|
||||
common.LDAPTimeout: 30,
|
||||
common.LDAPVerifyCert: true,
|
||||
common.UAAVerifyCert: true,
|
||||
common.ClairDBHost: "postgresql",
|
||||
common.CfgExpiration: 5,
|
||||
common.AdminInitialPassword: "Harbor12345",
|
||||
common.LDAPGroupSearchFilter: "objectclass=groupOfNames",
|
||||
common.LDAPGroupBaseDN: "dc=example,dc=com",
|
||||
common.LDAPGroupAttributeName: "cn",
|
||||
common.LDAPGroupSearchScope: 2,
|
||||
common.LdapGroupAdminDn: "cn=harbor_users,ou=groups,dc=example,dc=com",
|
||||
common.WithNotary: "false",
|
||||
common.WithChartMuseum: "false",
|
||||
common.SelfRegistration: "true",
|
||||
common.WithClair: "false",
|
||||
common.TokenServiceURL: "http://core:8080/service/token",
|
||||
common.RegistryURL: fmt.Sprintf("http://%s:5000", ipAddress),
|
||||
}
|
||||
}
|
||||
|
||||
// TraceCfgMap ...
|
||||
func TraceCfgMap(cfgs map[string]interface{}) {
|
||||
var keys []string
|
||||
for k := range cfgs {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
fmt.Printf("%v=%v\n", k, cfgs[k])
|
||||
}
|
||||
}
|
||||
|
@ -218,3 +218,19 @@ func ParseOfftime(offtime int64) (hour, minite, second int) {
|
||||
func TrimLower(str string) string {
|
||||
return strings.TrimSpace(strings.ToLower(str))
|
||||
}
|
||||
|
||||
// GetStrValueOfAnyType return string format of any value, for map, need to convert to json
|
||||
func GetStrValueOfAnyType(value interface{}) string {
|
||||
var strVal string
|
||||
if _, ok := value.(map[string]interface{}); ok {
|
||||
b, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
log.Errorf("can not marshal json object, error %v", err)
|
||||
return ""
|
||||
}
|
||||
strVal = string(b)
|
||||
} else {
|
||||
strVal = fmt.Sprintf("%v", value)
|
||||
}
|
||||
return strVal
|
||||
}
|
||||
|
@ -40,8 +40,8 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
nonSysAdminID, projAdminID, projDeveloperID, projGuestID int64
|
||||
projAdminPMID, projDeveloperPMID, projGuestPMID int
|
||||
nonSysAdminID, projAdminID, projDeveloperID, projGuestID, projAdminRobotID int64
|
||||
projAdminPMID, projDeveloperPMID, projGuestPMID, projAdminRobotPMID int
|
||||
// The following users/credentials are registered and assigned roles at the beginning of
|
||||
// running testing and cleaned up at the end.
|
||||
// Do not try to change the system and project roles that the users have during
|
||||
@ -67,6 +67,10 @@ var (
|
||||
Name: "proj_guest",
|
||||
Passwd: "Harbor12345",
|
||||
}
|
||||
projAdmin4Robot = &usrInfo{
|
||||
Name: "proj_admin_robot",
|
||||
Passwd: "Harbor12345",
|
||||
}
|
||||
)
|
||||
|
||||
type testingRequest struct {
|
||||
@ -240,6 +244,25 @@ func prepare() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// register projAdminRobots and assign project admin role
|
||||
projAdminRobotID, err = dao.Register(models.User{
|
||||
Username: projAdmin4Robot.Name,
|
||||
Password: projAdmin4Robot.Passwd,
|
||||
Email: projAdmin4Robot.Name + "@test.com",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if projAdminRobotPMID, err = project.AddProjectMember(models.Member{
|
||||
ProjectID: 1,
|
||||
Role: models.PROJECTADMIN,
|
||||
EntityID: int(projAdminRobotID),
|
||||
EntityType: common.UserMember,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// register projDeveloper and assign project developer role
|
||||
projDeveloperID, err = dao.Register(models.User{
|
||||
Username: projDeveloper.Name,
|
||||
|
@ -125,6 +125,7 @@ func (b *BaseController) WriteYamlData(object interface{}) {
|
||||
|
||||
// Init related objects/configurations for the API controllers
|
||||
func Init() error {
|
||||
registerHealthCheckers()
|
||||
// If chart repository is not enabled then directly return
|
||||
if !config.WithChartMuseum() {
|
||||
return nil
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -45,12 +46,6 @@ func (cla *ChartLabelAPI) Prepare() {
|
||||
}
|
||||
cla.project = existingProject
|
||||
|
||||
// Check permission
|
||||
if !cla.checkPermissions(project) {
|
||||
cla.SendForbiddenError(errors.New(cla.SecurityCtx.GetUsername()))
|
||||
return
|
||||
}
|
||||
|
||||
// Check the existence of target chart
|
||||
chartName := cla.GetStringFromPath(nameParam)
|
||||
version := cla.GetStringFromPath(versionParam)
|
||||
@ -62,8 +57,23 @@ func (cla *ChartLabelAPI) Prepare() {
|
||||
cla.chartFullName = fmt.Sprintf("%s/%s:%s", project, chartName, version)
|
||||
}
|
||||
|
||||
func (cla *ChartLabelAPI) requireAccess(action rbac.Action) bool {
|
||||
resource := rbac.NewProjectNamespace(cla.project.ProjectID).Resource(rbac.ResourceHelmChartVersionLabel)
|
||||
|
||||
if !cla.SecurityCtx.Can(action, resource) {
|
||||
cla.HandleForbidden(cla.SecurityCtx.GetUsername())
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// MarkLabel handles the request of marking label to chart.
|
||||
func (cla *ChartLabelAPI) MarkLabel() {
|
||||
if !cla.requireAccess(rbac.ActionCreate) {
|
||||
return
|
||||
}
|
||||
|
||||
l := &models.Label{}
|
||||
cla.DecodeJSONReq(l)
|
||||
|
||||
@ -83,6 +93,10 @@ func (cla *ChartLabelAPI) MarkLabel() {
|
||||
|
||||
// RemoveLabel handles the request of removing label from chart.
|
||||
func (cla *ChartLabelAPI) RemoveLabel() {
|
||||
if !cla.requireAccess(rbac.ActionDelete) {
|
||||
return
|
||||
}
|
||||
|
||||
lID, err := cla.GetInt64FromPath(idParam)
|
||||
if err != nil {
|
||||
cla.SendInternalServerError(err)
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/core/label"
|
||||
|
||||
"github.com/goharbor/harbor/src/chartserver"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
hlog "github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
)
|
||||
@ -84,10 +85,35 @@ func (cra *ChartRepositoryAPI) Prepare() {
|
||||
cra.labelManager = &label.BaseManager{}
|
||||
}
|
||||
|
||||
func (cra *ChartRepositoryAPI) requireAccess(action rbac.Action, subresource ...rbac.Resource) bool {
|
||||
if len(subresource) == 0 {
|
||||
subresource = append(subresource, rbac.ResourceHelmChart)
|
||||
}
|
||||
resource := rbac.NewProjectNamespace(cra.namespace).Resource(subresource...)
|
||||
|
||||
if !cra.SecurityCtx.Can(action, resource) {
|
||||
if !cra.SecurityCtx.IsAuthenticated() {
|
||||
cra.SendUnAuthorizedError(errors.New("Unauthorized"))
|
||||
} else {
|
||||
cra.HandleForbidden(cra.SecurityCtx.GetUsername())
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// GetHealthStatus handles GET /api/chartrepo/health
|
||||
func (cra *ChartRepositoryAPI) GetHealthStatus() {
|
||||
// Check access
|
||||
if !cra.requireAccess(cra.namespace, accessLevelSystem) {
|
||||
if !cra.SecurityCtx.IsAuthenticated() {
|
||||
cra.SendUnAuthorizedError(errors.New("Unauthorized"))
|
||||
return
|
||||
}
|
||||
|
||||
if !cra.SecurityCtx.IsSysAdmin() {
|
||||
cra.HandleForbidden(cra.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
|
||||
@ -98,7 +124,7 @@ func (cra *ChartRepositoryAPI) GetHealthStatus() {
|
||||
// GetIndexByRepo handles GET /:repo/index.yaml
|
||||
func (cra *ChartRepositoryAPI) GetIndexByRepo() {
|
||||
// Check access
|
||||
if !cra.requireAccess(cra.namespace, accessLevelRead) {
|
||||
if !cra.requireAccess(rbac.ActionRead) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -109,7 +135,13 @@ func (cra *ChartRepositoryAPI) GetIndexByRepo() {
|
||||
// GetIndex handles GET /index.yaml
|
||||
func (cra *ChartRepositoryAPI) GetIndex() {
|
||||
// Check access
|
||||
if !cra.requireAccess(cra.namespace, accessLevelSystem) {
|
||||
if !cra.SecurityCtx.IsAuthenticated() {
|
||||
cra.SendUnAuthorizedError(errors.New("Unauthorized"))
|
||||
return
|
||||
}
|
||||
|
||||
if !cra.SecurityCtx.IsSysAdmin() {
|
||||
cra.HandleForbidden(cra.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
|
||||
@ -136,7 +168,7 @@ func (cra *ChartRepositoryAPI) GetIndex() {
|
||||
// DownloadChart handles GET /:repo/charts/:filename
|
||||
func (cra *ChartRepositoryAPI) DownloadChart() {
|
||||
// Check access
|
||||
if !cra.requireAccess(cra.namespace, accessLevelRead) {
|
||||
if !cra.requireAccess(rbac.ActionRead) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -147,7 +179,7 @@ func (cra *ChartRepositoryAPI) DownloadChart() {
|
||||
// ListCharts handles GET /api/:repo/charts
|
||||
func (cra *ChartRepositoryAPI) ListCharts() {
|
||||
// Check access
|
||||
if !cra.requireAccess(cra.namespace, accessLevelRead) {
|
||||
if !cra.requireAccess(rbac.ActionList) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -163,7 +195,7 @@ func (cra *ChartRepositoryAPI) ListCharts() {
|
||||
// ListChartVersions GET /api/:repo/charts/:name
|
||||
func (cra *ChartRepositoryAPI) ListChartVersions() {
|
||||
// Check access
|
||||
if !cra.requireAccess(cra.namespace, accessLevelRead) {
|
||||
if !cra.requireAccess(rbac.ActionList, rbac.ResourceHelmChartVersion) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -191,7 +223,7 @@ func (cra *ChartRepositoryAPI) ListChartVersions() {
|
||||
// GetChartVersion handles GET /api/:repo/charts/:name/:version
|
||||
func (cra *ChartRepositoryAPI) GetChartVersion() {
|
||||
// Check access
|
||||
if !cra.requireAccess(cra.namespace, accessLevelRead) {
|
||||
if !cra.requireAccess(rbac.ActionRead, rbac.ResourceHelmChartVersion) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -219,7 +251,7 @@ func (cra *ChartRepositoryAPI) GetChartVersion() {
|
||||
// DeleteChartVersion handles DELETE /api/:repo/charts/:name/:version
|
||||
func (cra *ChartRepositoryAPI) DeleteChartVersion() {
|
||||
// Check access
|
||||
if !cra.requireAccess(cra.namespace, accessLevelAll) {
|
||||
if !cra.requireAccess(rbac.ActionDelete, rbac.ResourceHelmChartVersion) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -244,7 +276,7 @@ func (cra *ChartRepositoryAPI) UploadChartVersion() {
|
||||
hlog.Debugf("Header of request of uploading chart: %#v, content-len=%d", cra.Ctx.Request.Header, cra.Ctx.Request.ContentLength)
|
||||
|
||||
// Check access
|
||||
if !cra.requireAccess(cra.namespace, accessLevelWrite) {
|
||||
if !cra.requireAccess(rbac.ActionCreate, rbac.ResourceHelmChartVersion) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -272,7 +304,7 @@ func (cra *ChartRepositoryAPI) UploadChartVersion() {
|
||||
// UploadChartProvFile handles POST /api/:repo/prov
|
||||
func (cra *ChartRepositoryAPI) UploadChartProvFile() {
|
||||
// Check access
|
||||
if !cra.requireAccess(cra.namespace, accessLevelWrite) {
|
||||
if !cra.requireAccess(rbac.ActionCreate) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -297,7 +329,7 @@ func (cra *ChartRepositoryAPI) UploadChartProvFile() {
|
||||
// DeleteChart deletes all the chart versions of the specified chart.
|
||||
func (cra *ChartRepositoryAPI) DeleteChart() {
|
||||
// Check access
|
||||
if !cra.requireAccess(cra.namespace, accessLevelWrite) {
|
||||
if !cra.requireAccess(rbac.ActionDelete) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -365,62 +397,6 @@ func (cra *ChartRepositoryAPI) requireNamespace(namespace string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if the related access match the expected requirement
|
||||
// If with right access, return true
|
||||
// If without right access, return false
|
||||
func (cra *ChartRepositoryAPI) requireAccess(namespace string, accessLevel uint) bool {
|
||||
if accessLevel == accessLevelPublic {
|
||||
return true // do nothing
|
||||
}
|
||||
|
||||
theLevel := accessLevel
|
||||
// If repo is empty, system admin role must be required
|
||||
if len(namespace) == 0 {
|
||||
theLevel = accessLevelSystem
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
switch theLevel {
|
||||
// Should be system admin role
|
||||
case accessLevelSystem:
|
||||
if !cra.SecurityCtx.IsSysAdmin() {
|
||||
err = errors.New("permission denied: system admin role is required")
|
||||
}
|
||||
case accessLevelAll:
|
||||
if !cra.SecurityCtx.HasAllPerm(namespace) {
|
||||
err = errors.New("permission denied: project admin or higher role is required")
|
||||
}
|
||||
case accessLevelWrite:
|
||||
if !cra.SecurityCtx.HasWritePerm(namespace) {
|
||||
err = errors.New("permission denied: developer or higher role is required")
|
||||
}
|
||||
case accessLevelRead:
|
||||
if !cra.SecurityCtx.HasReadPerm(namespace) {
|
||||
err = errors.New("permission denied: guest or higher role is required")
|
||||
}
|
||||
default:
|
||||
// access rejected for invalid scope
|
||||
cra.SendForbiddenError(errors.New("unrecognized access scope"))
|
||||
return false
|
||||
}
|
||||
|
||||
// Access is not granted, check if user has authenticated
|
||||
if err != nil {
|
||||
// Unauthenticated, return 401
|
||||
if !cra.SecurityCtx.IsAuthenticated() {
|
||||
cra.SendUnAuthorizedError(errors.New("Unauthorized"))
|
||||
return false
|
||||
}
|
||||
|
||||
// Authenticated, return 403
|
||||
cra.SendForbiddenError(err)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// formFile is used to represent the uploaded files in the form
|
||||
type formFile struct {
|
||||
// form field key contains the form file
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/goharbor/harbor/src/chartserver"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/core/promgr/metamgr"
|
||||
)
|
||||
|
||||
@ -16,29 +17,6 @@ var (
|
||||
crMockServer *httptest.Server
|
||||
)
|
||||
|
||||
// Test access checking
|
||||
func TestRequireAccess(t *testing.T) {
|
||||
chartAPI := &ChartRepositoryAPI{}
|
||||
chartAPI.SecurityCtx = &mockSecurityContext{}
|
||||
|
||||
ns := "library"
|
||||
if !chartAPI.requireAccess(ns, accessLevelPublic) {
|
||||
t.Fatal("expect true result (public access level is granted) but got false")
|
||||
}
|
||||
if !chartAPI.requireAccess(ns, accessLevelAll) {
|
||||
t.Fatal("expect true result (admin has all perm) but got false")
|
||||
}
|
||||
if !chartAPI.requireAccess(ns, accessLevelRead) {
|
||||
t.Fatal("expect true result (admin has read perm) but got false")
|
||||
}
|
||||
if !chartAPI.requireAccess(ns, accessLevelWrite) {
|
||||
t.Fatal("expect true result (admin has write perm) but got false")
|
||||
}
|
||||
if !chartAPI.requireAccess(ns, accessLevelSystem) {
|
||||
t.Fatal("expect true result (admin has system perm) but got false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsMultipartFormData(t *testing.T) {
|
||||
req, err := createRequest(http.MethodPost, "/api/chartrepo/charts")
|
||||
if err != nil {
|
||||
@ -204,7 +182,7 @@ func TestDeleteChart(t *testing.T) {
|
||||
request: &testingRequest{
|
||||
url: "/api/chartrepo/library/charts/harbor",
|
||||
method: http.MethodDelete,
|
||||
credential: projDeveloper,
|
||||
credential: projAdmin,
|
||||
},
|
||||
code: http.StatusOK,
|
||||
})
|
||||
@ -309,8 +287,15 @@ func (msc *mockSecurityContext) IsSolutionUser() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// HasReadPerm returns whether the user has read permission to the project
|
||||
func (msc *mockSecurityContext) HasReadPerm(projectIDOrName interface{}) bool {
|
||||
// Can returns whether the user can do action on resource
|
||||
func (msc *mockSecurityContext) Can(action rbac.Action, resource rbac.Resource) bool {
|
||||
namespace, err := resource.GetNamespace()
|
||||
if err != nil || namespace.Kind() != "project" {
|
||||
return false
|
||||
}
|
||||
|
||||
projectIDOrName := namespace.Identity()
|
||||
|
||||
if projectIDOrName == nil {
|
||||
return false
|
||||
}
|
||||
@ -324,26 +309,6 @@ func (msc *mockSecurityContext) HasReadPerm(projectIDOrName interface{}) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// HasWritePerm returns whether the user has write permission to the project
|
||||
func (msc *mockSecurityContext) HasWritePerm(projectIDOrName interface{}) bool {
|
||||
if projectIDOrName == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if ns, ok := projectIDOrName.(string); ok {
|
||||
if ns == "library" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// HasAllPerm returns whether the user has all permissions to the project
|
||||
func (msc *mockSecurityContext) HasAllPerm(projectIDOrName interface{}) bool {
|
||||
return msc.HasReadPerm(projectIDOrName) && msc.HasWritePerm(projectIDOrName)
|
||||
}
|
||||
|
||||
// Get current user's all project
|
||||
func (msc *mockSecurityContext) GetMyProjects() ([]*models.Project, error) {
|
||||
return []*models.Project{{ProjectID: 0, Name: "library"}}, nil
|
||||
|
@ -17,31 +17,47 @@ package api
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/config"
|
||||
"github.com/goharbor/harbor/src/common/config/metadata"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/security/secret"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
corecfg "github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/core/filter"
|
||||
)
|
||||
|
||||
// ConfigAPI ...
|
||||
type ConfigAPI struct {
|
||||
BaseController
|
||||
cfgManager *config.CfgManager
|
||||
}
|
||||
|
||||
// Prepare validates the user
|
||||
func (c *ConfigAPI) Prepare() {
|
||||
c.BaseController.Prepare()
|
||||
c.cfgManager = corecfg.GetCfgManager()
|
||||
if !c.SecurityCtx.IsAuthenticated() {
|
||||
c.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
|
||||
// Only internal container can access /api/internal/configurations
|
||||
if strings.EqualFold(c.Ctx.Request.RequestURI, "/api/internal/configurations") {
|
||||
if _, ok := c.Ctx.Request.Context().Value(filter.SecurCtxKey).(*secret.SecurityContext); !ok {
|
||||
c.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !c.SecurityCtx.IsSysAdmin() && !c.SecurityCtx.IsSolutionUser() {
|
||||
c.HandleForbidden(c.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type value struct {
|
||||
@ -51,20 +67,8 @@ type value struct {
|
||||
|
||||
// Get returns configurations
|
||||
func (c *ConfigAPI) Get() {
|
||||
configs, err := config.GetSystemCfg()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get configurations: %v", err)
|
||||
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
cfgs := map[string]interface{}{}
|
||||
for _, k := range common.HarborValidKeys {
|
||||
if v, ok := configs[k]; ok {
|
||||
cfgs[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
m, err := convertForGet(cfgs)
|
||||
configs := c.cfgManager.GetUserCfgs()
|
||||
m, err := convertForGet(configs)
|
||||
if err != nil {
|
||||
log.Errorf("failed to convert configurations: %v", err)
|
||||
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
@ -76,11 +80,8 @@ func (c *ConfigAPI) Get() {
|
||||
|
||||
// GetInternalConfig returns internal configurations
|
||||
func (c *ConfigAPI) GetInternalConfig() {
|
||||
configs, err := config.GetSystemCfg()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get configurations: %v", err)
|
||||
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
configs := c.cfgManager.GetAll()
|
||||
c.Data["json"] = configs
|
||||
c.ServeJSON()
|
||||
}
|
||||
@ -89,16 +90,12 @@ func (c *ConfigAPI) GetInternalConfig() {
|
||||
func (c *ConfigAPI) Put() {
|
||||
m := map[string]interface{}{}
|
||||
c.DecodeJSONReq(&m)
|
||||
|
||||
cfg := map[string]interface{}{}
|
||||
for _, k := range common.HarborValidKeys {
|
||||
if v, ok := m[k]; ok {
|
||||
cfg[k] = v
|
||||
}
|
||||
err := c.cfgManager.Load()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get configurations: %v", err)
|
||||
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
isSysErr, err := validateCfg(cfg)
|
||||
|
||||
isSysErr, err := c.validateCfg(m)
|
||||
if err != nil {
|
||||
if isSysErr {
|
||||
log.Errorf("failed to validate configurations: %v", err)
|
||||
@ -108,146 +105,26 @@ func (c *ConfigAPI) Put() {
|
||||
c.CustomAbort(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
if err := config.Upload(cfg); err != nil {
|
||||
if err := c.cfgManager.UpdateConfig(m); err != nil {
|
||||
log.Errorf("failed to upload configurations: %v", err)
|
||||
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
if err := config.Load(); err != nil {
|
||||
log.Errorf("failed to load configurations: %v", err)
|
||||
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
// Everything is ok, detect the configurations to confirm if the option we are caring is changed.
|
||||
if err := watchConfigChanges(cfg); err != nil {
|
||||
log.Errorf("Failed to watch configuration change with error: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset system configurations
|
||||
func (c *ConfigAPI) Reset() {
|
||||
if err := config.Reset(); err != nil {
|
||||
log.Errorf("failed to reset configurations: %v", err)
|
||||
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
}
|
||||
|
||||
func validateCfg(c map[string]interface{}) (bool, error) {
|
||||
strMap := map[string]string{}
|
||||
for k := range common.HarborStringKeysMap {
|
||||
if _, ok := c[k]; !ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := c[k].(string); !ok {
|
||||
return false, fmt.Errorf("Invalid value type, expected string, key: %s, value: %v, type: %v", k, c[k], reflect.TypeOf(c[k]))
|
||||
}
|
||||
strMap[k] = c[k].(string)
|
||||
}
|
||||
numMap := map[string]int{}
|
||||
for k := range common.HarborNumKeysMap {
|
||||
if _, ok := c[k]; !ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := c[k].(float64); !ok {
|
||||
return false, fmt.Errorf("Invalid value type, expected float64, key: %s, value: %v, type: %v", k, c[k], reflect.TypeOf(c[k]))
|
||||
}
|
||||
numMap[k] = int(c[k].(float64))
|
||||
}
|
||||
boolMap := map[string]bool{}
|
||||
for k := range common.HarborBoolKeysMap {
|
||||
if _, ok := c[k]; !ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := c[k].(bool); !ok {
|
||||
return false, fmt.Errorf("Invalid value type, expected bool, key: %s, value: %v, type: %v", k, c[k], reflect.TypeOf(c[k]))
|
||||
}
|
||||
boolMap[k] = c[k].(bool)
|
||||
}
|
||||
|
||||
mode, err := config.AuthMode()
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
if value, ok := strMap[common.AUTHMode]; ok {
|
||||
if value != common.DBAuth && value != common.LDAPAuth && value != common.UAAAuth {
|
||||
return false, fmt.Errorf("invalid %s, shoud be one of %s, %s, %s", common.AUTHMode, common.DBAuth, common.LDAPAuth, common.UAAAuth)
|
||||
}
|
||||
func (c *ConfigAPI) validateCfg(cfgs map[string]interface{}) (bool, error) {
|
||||
mode := c.cfgManager.Get("auth_mode").GetString()
|
||||
if value, ok := cfgs[common.AUTHMode]; ok {
|
||||
flag, err := authModeCanBeModified()
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
if mode != value && !flag {
|
||||
if mode != fmt.Sprintf("%v", value) && !flag {
|
||||
return false, fmt.Errorf("%s can not be modified as new users have been inserted into database", common.AUTHMode)
|
||||
}
|
||||
mode = value
|
||||
}
|
||||
|
||||
if mode == common.LDAPAuth {
|
||||
ldapConf, err := config.LDAPConf()
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
if len(ldapConf.LdapURL) == 0 {
|
||||
if _, ok := strMap[common.LDAPURL]; !ok {
|
||||
return false, fmt.Errorf("%s is missing", common.LDAPURL)
|
||||
}
|
||||
}
|
||||
|
||||
if len(ldapConf.LdapBaseDn) == 0 {
|
||||
if _, ok := strMap[common.LDAPBaseDN]; !ok {
|
||||
return false, fmt.Errorf("%s is missing", common.LDAPBaseDN)
|
||||
}
|
||||
}
|
||||
if len(ldapConf.LdapUID) == 0 {
|
||||
if _, ok := strMap[common.LDAPUID]; !ok {
|
||||
return false, fmt.Errorf("%s is missing", common.LDAPUID)
|
||||
}
|
||||
}
|
||||
if ldapConf.LdapScope == 0 {
|
||||
if _, ok := numMap[common.LDAPScope]; !ok {
|
||||
return false, fmt.Errorf("%s is missing", common.LDAPScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ldapURL, ok := strMap[common.LDAPURL]; ok && len(ldapURL) == 0 {
|
||||
return false, fmt.Errorf("%s is empty", common.LDAPURL)
|
||||
}
|
||||
if baseDN, ok := strMap[common.LDAPBaseDN]; ok && len(baseDN) == 0 {
|
||||
return false, fmt.Errorf("%s is empty", common.LDAPBaseDN)
|
||||
}
|
||||
if uID, ok := strMap[common.LDAPUID]; ok && len(uID) == 0 {
|
||||
return false, fmt.Errorf("%s is empty", common.LDAPUID)
|
||||
}
|
||||
if scope, ok := numMap[common.LDAPScope]; ok &&
|
||||
scope != common.LDAPScopeBase &&
|
||||
scope != common.LDAPScopeOnelevel &&
|
||||
scope != common.LDAPScopeSubtree {
|
||||
return false, fmt.Errorf("invalid %s, should be %d, %d or %d",
|
||||
common.LDAPScope,
|
||||
common.LDAPScopeBase,
|
||||
common.LDAPScopeOnelevel,
|
||||
common.LDAPScopeSubtree)
|
||||
}
|
||||
for k, n := range numMap {
|
||||
if n < 0 {
|
||||
return false, fmt.Errorf("invalid %s: %d", k, n)
|
||||
}
|
||||
if (k == common.EmailPort ||
|
||||
k == common.PostGreSQLPort) && n > 65535 {
|
||||
return false, fmt.Errorf("invalid %s: %d", k, n)
|
||||
}
|
||||
}
|
||||
|
||||
if crt, ok := strMap[common.ProjectCreationRestriction]; ok &&
|
||||
crt != common.ProCrtRestrEveryone &&
|
||||
crt != common.ProCrtRestrAdmOnly {
|
||||
return false, fmt.Errorf("invalid %s, should be %s or %s",
|
||||
common.ProjectCreationRestriction,
|
||||
common.ProCrtRestrAdmOnly,
|
||||
common.ProCrtRestrEveryone)
|
||||
err := c.cfgManager.ValidateCfg(cfgs)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
@ -256,9 +133,11 @@ func validateCfg(c map[string]interface{}) (bool, error) {
|
||||
func convertForGet(cfg map[string]interface{}) (map[string]*value, error) {
|
||||
result := map[string]*value{}
|
||||
|
||||
for _, k := range common.HarborPasswordKeys {
|
||||
if _, ok := cfg[k]; ok {
|
||||
delete(cfg, k)
|
||||
mList := metadata.Instance().GetAll()
|
||||
|
||||
for _, item := range mList {
|
||||
if _, ok := item.ItemType.(*metadata.PasswordType); ok {
|
||||
delete(cfg, item.Name)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,7 +45,7 @@ func TestGetConfig(t *testing.T) {
|
||||
if !assert.Equal(200, code, "the status code of getting configurations with admin user should be 200") {
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("cfg: %+v", cfg)
|
||||
mode := cfg[common.AUTHMode].Value.(string)
|
||||
assert.Equal(common.DBAuth, mode, fmt.Sprintf("the auth mode should be %s", common.DBAuth))
|
||||
ccc, err := config.GetSystemCfg()
|
||||
@ -103,46 +103,6 @@ func TestPutConfig(t *testing.T) {
|
||||
t.Logf("%v", ccc)
|
||||
}
|
||||
|
||||
func TestResetConfig(t *testing.T) {
|
||||
fmt.Println("Testing resetting configurations")
|
||||
assert := assert.New(t)
|
||||
apiTest := newHarborAPI()
|
||||
|
||||
code, err := apiTest.ResetConfig(*admin)
|
||||
if err != nil {
|
||||
t.Errorf("failed to get configurations: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !assert.Equal(200, code, "unexpected response code") {
|
||||
return
|
||||
}
|
||||
|
||||
code, cfgs, err := apiTest.GetConfig(*admin)
|
||||
if err != nil {
|
||||
t.Errorf("failed to get configurations: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !assert.Equal(200, code, "unexpected response code") {
|
||||
return
|
||||
}
|
||||
|
||||
value, ok := cfgs[common.TokenExpiration]
|
||||
if !ok {
|
||||
t.Errorf("%s not found", common.TokenExpiration)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(int(value.Value.(float64)), 30, "unexpected 30")
|
||||
|
||||
ccc, err := config.GetSystemCfg()
|
||||
if err != nil {
|
||||
t.Logf("failed to get system configurations: %v", err)
|
||||
}
|
||||
t.Logf("%v", ccc)
|
||||
}
|
||||
|
||||
func TestPutConfigMaxLength(t *testing.T) {
|
||||
fmt.Println("Testing modifying configurations with max length.")
|
||||
assert := assert.New(t)
|
||||
|
@ -106,7 +106,7 @@ func CommonDelTarget() {
|
||||
|
||||
func CommonAddRepository() {
|
||||
commonRepository := &models.RepoRecord{
|
||||
RepositoryID: 1,
|
||||
RepositoryID: 31,
|
||||
Name: TestRepoName,
|
||||
ProjectID: 1,
|
||||
PullCount: 1,
|
||||
|
@ -27,20 +27,20 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/job/test"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
testutils "github.com/goharbor/harbor/src/common/utils/test"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/core/filter"
|
||||
"github.com/goharbor/harbor/tests/apitests/apilib"
|
||||
|
||||
// "strconv"
|
||||
// "strings"
|
||||
|
||||
"github.com/astaxie/beego"
|
||||
"github.com/dghubble/sling"
|
||||
|
||||
// for test env prepare
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
_ "github.com/goharbor/harbor/src/core/auth/db"
|
||||
_ "github.com/goharbor/harbor/src/core/auth/ldap"
|
||||
"github.com/goharbor/harbor/src/replication/core"
|
||||
@ -78,14 +78,14 @@ type usrInfo struct {
|
||||
}
|
||||
|
||||
func init() {
|
||||
if err := config.Init(); err != nil {
|
||||
log.Fatalf("failed to initialize configurations: %v", err)
|
||||
}
|
||||
database, err := config.Database()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get database configurations: %v", err)
|
||||
}
|
||||
dao.InitDatabase(database)
|
||||
config.Init()
|
||||
testutils.InitDatabaseFromEnv()
|
||||
dao.PrepareTestData([]string{"delete from harbor_user where user_id >2", "delete from project where owner_id >2"}, []string{})
|
||||
config.Upload(testutils.GetUnitTestConfig())
|
||||
|
||||
allCfgs, _ := config.GetSystemCfg()
|
||||
testutils.TraceCfgMap(allCfgs)
|
||||
|
||||
_, file, _, _ := runtime.Caller(0)
|
||||
dir := filepath.Dir(file)
|
||||
dir = filepath.Join(dir, "..")
|
||||
@ -96,12 +96,14 @@ func init() {
|
||||
filter.Init()
|
||||
beego.InsertFilter("/*", beego.BeforeRouter, filter.SecurityFilter)
|
||||
|
||||
beego.Router("/api/health", &HealthAPI{}, "get:CheckHealth")
|
||||
beego.Router("/api/search/", &SearchAPI{})
|
||||
beego.Router("/api/projects/", &ProjectAPI{}, "get:List;post:Post;head:Head")
|
||||
beego.Router("/api/projects/:id", &ProjectAPI{}, "delete:Delete;get:Get;put:Put")
|
||||
beego.Router("/api/users/:id", &UserAPI{}, "get:Get")
|
||||
beego.Router("/api/users", &UserAPI{}, "get:List;post:Post;delete:Delete;put:Put")
|
||||
beego.Router("/api/users/:id([0-9]+)/password", &UserAPI{}, "put:ChangePassword")
|
||||
beego.Router("/api/users/:id/permissions", &UserAPI{}, "get:ListUserPermissions")
|
||||
beego.Router("/api/users/:id/sysadmin", &UserAPI{}, "put:ToggleUserAdminRole")
|
||||
beego.Router("/api/projects/:id([0-9]+)/logs", &ProjectAPI{}, "get:Logs")
|
||||
beego.Router("/api/projects/:id([0-9]+)/_deletable", &ProjectAPI{}, "get:Deletable")
|
||||
@ -140,7 +142,6 @@ func init() {
|
||||
beego.Router("/api/ldap/groups/search", &LdapAPI{}, "get:SearchGroup")
|
||||
beego.Router("/api/ldap/users/import", &LdapAPI{}, "post:ImportUser")
|
||||
beego.Router("/api/configurations", &ConfigAPI{})
|
||||
beego.Router("/api/configurations/reset", &ConfigAPI{}, "post:Reset")
|
||||
beego.Router("/api/configs", &ConfigAPI{}, "get:GetInternalConfig")
|
||||
beego.Router("/api/email/ping", &EmailAPI{}, "post:Ping")
|
||||
beego.Router("/api/replications", &ReplicationAPI{})
|
||||
@ -152,6 +153,9 @@ func init() {
|
||||
beego.Router("/api/system/gc/:id([0-9]+)/log", &GCAPI{}, "get:GetLog")
|
||||
beego.Router("/api/system/gc/schedule", &GCAPI{}, "get:Get;put:Put;post:Post")
|
||||
|
||||
beego.Router("/api/projects/:pid([0-9]+)/robots/", &RobotAPI{}, "post:Post;get:List")
|
||||
beego.Router("/api/projects/:pid([0-9]+)/robots/:id([0-9]+)", &RobotAPI{}, "get:Get;put:Put;delete:Delete")
|
||||
|
||||
// Charts are controlled under projects
|
||||
chartRepositoryAPIType := &ChartRepositoryAPI{}
|
||||
beego.Router("/api/chartrepo/health", chartRepositoryAPIType, "get:GetHealthStatus")
|
||||
@ -173,8 +177,6 @@ func init() {
|
||||
beego.Router("/api/chartrepo/:repo/charts/:name/:version/labels", chartLabelAPIType, "get:GetLabels;post:MarkLabel")
|
||||
beego.Router("/api/chartrepo/:repo/charts/:name/:version/labels/:id([0-9]+)", chartLabelAPIType, "delete:RemoveLabel")
|
||||
|
||||
_ = updateInitPassword(1, "Harbor12345")
|
||||
|
||||
if err := core.Init(); err != nil {
|
||||
log.Fatalf("failed to initialize GlobalController: %v", err)
|
||||
}
|
||||
@ -992,6 +994,23 @@ func (a testapi) UsersUpdatePassword(userID int, password apilib.Password, authI
|
||||
return httpStatusCode, err
|
||||
}
|
||||
|
||||
func (a testapi) UsersGetPermissions(userID interface{}, scope string, authInfo usrInfo) (int, []apilib.Permission, error) {
|
||||
_sling := sling.New().Get(a.basePath)
|
||||
// create path and map variables
|
||||
path := fmt.Sprintf("/api/users/%v/permissions", userID)
|
||||
_sling = _sling.Path(path)
|
||||
type QueryParams struct {
|
||||
Scope string `url:"scope,omitempty"`
|
||||
}
|
||||
_sling = _sling.QueryStruct(&QueryParams{Scope: scope})
|
||||
httpStatusCode, body, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||
var successPayLoad []apilib.Permission
|
||||
if 200 == httpStatusCode && nil == err {
|
||||
err = json.Unmarshal(body, &successPayLoad)
|
||||
}
|
||||
return httpStatusCode, successPayLoad, err
|
||||
}
|
||||
|
||||
// Mark a registered user as be removed.
|
||||
func (a testapi) UsersDelete(userID int, authInfo usrInfo) (int, error) {
|
||||
_sling := sling.New().Delete(a.basePath)
|
||||
@ -1001,27 +1020,6 @@ func (a testapi) UsersDelete(userID int, authInfo usrInfo) (int, error) {
|
||||
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||
return httpStatusCode, err
|
||||
}
|
||||
func updateInitPassword(userID int, password string) error {
|
||||
queryUser := models.User{UserID: userID}
|
||||
user, err := dao.GetUser(queryUser)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to get user, userID: %d %v", userID, err)
|
||||
}
|
||||
if user == nil {
|
||||
return fmt.Errorf("user id: %d does not exist", userID)
|
||||
}
|
||||
if user.Salt == "" {
|
||||
user.Salt = utils.GenerateRandomString()
|
||||
user.Password = password
|
||||
err = dao.ChangeUserPassword(*user)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to update user encrypted password, userID: %d, err: %v", userID, err)
|
||||
}
|
||||
|
||||
} else {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get system volume info
|
||||
func (a testapi) VolumeInfoGet(authInfo usrInfo) (int, apilib.SystemInfo, error) {
|
||||
|
323
src/core/api/health.go
Normal file
323
src/core/api/health.go
Normal file
@ -0,0 +1,323 @@
|
||||
// Copyright 2019 Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
httputil "github.com/goharbor/harbor/src/common/http"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
|
||||
"github.com/docker/distribution/health"
|
||||
"github.com/gomodule/redigo/redis"
|
||||
)
|
||||
|
||||
var (
|
||||
timeout = 60 * time.Second
|
||||
healthCheckerRegistry = map[string]health.Checker{}
|
||||
)
|
||||
|
||||
type overallHealthStatus struct {
|
||||
Status string `json:"status"`
|
||||
Components []*componentHealthStatus `json:"components"`
|
||||
}
|
||||
|
||||
type componentHealthStatus struct {
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type healthy bool
|
||||
|
||||
func (h healthy) String() string {
|
||||
if h {
|
||||
return "healthy"
|
||||
}
|
||||
return "unhealthy"
|
||||
}
|
||||
|
||||
// HealthAPI handles the request for "/api/health"
|
||||
type HealthAPI struct {
|
||||
BaseController
|
||||
}
|
||||
|
||||
// CheckHealth checks the health of system
|
||||
func (h *HealthAPI) CheckHealth() {
|
||||
var isHealthy healthy = true
|
||||
components := []*componentHealthStatus{}
|
||||
c := make(chan *componentHealthStatus, len(healthCheckerRegistry))
|
||||
for name, checker := range healthCheckerRegistry {
|
||||
go check(name, checker, timeout, c)
|
||||
}
|
||||
for i := 0; i < len(healthCheckerRegistry); i++ {
|
||||
componentStatus := <-c
|
||||
if len(componentStatus.Error) != 0 {
|
||||
isHealthy = false
|
||||
}
|
||||
components = append(components, componentStatus)
|
||||
}
|
||||
status := &overallHealthStatus{}
|
||||
status.Status = isHealthy.String()
|
||||
status.Components = components
|
||||
if !isHealthy {
|
||||
log.Debugf("unhealthy system status: %v", status)
|
||||
}
|
||||
h.WriteJSONData(status)
|
||||
}
|
||||
|
||||
func check(name string, checker health.Checker,
|
||||
timeout time.Duration, c chan *componentHealthStatus) {
|
||||
statusChan := make(chan *componentHealthStatus)
|
||||
go func() {
|
||||
err := checker.Check()
|
||||
var healthy healthy = err == nil
|
||||
status := &componentHealthStatus{
|
||||
Name: name,
|
||||
Status: healthy.String(),
|
||||
}
|
||||
if !healthy {
|
||||
status.Error = err.Error()
|
||||
}
|
||||
statusChan <- status
|
||||
}()
|
||||
|
||||
select {
|
||||
case status := <-statusChan:
|
||||
c <- status
|
||||
case <-time.After(timeout):
|
||||
var healthy healthy = false
|
||||
c <- &componentHealthStatus{
|
||||
Name: name,
|
||||
Status: healthy.String(),
|
||||
Error: "failed to check the health status: timeout",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPStatusCodeHealthChecker implements a Checker to check that the HTTP status code
|
||||
// returned matches the expected one
|
||||
func HTTPStatusCodeHealthChecker(method string, url string, header http.Header,
|
||||
timeout time.Duration, statusCode int) health.Checker {
|
||||
return health.CheckFunc(func() error {
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %v", err)
|
||||
}
|
||||
for key, values := range header {
|
||||
for _, value := range values {
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
client := httputil.NewClient(&http.Client{
|
||||
Timeout: timeout,
|
||||
})
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check health: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != statusCode {
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Debugf("failed to read response body: %v", err)
|
||||
}
|
||||
return fmt.Errorf("received unexpected status code: %d %s", resp.StatusCode, string(data))
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
type updater struct {
|
||||
sync.Mutex
|
||||
status error
|
||||
}
|
||||
|
||||
func (u *updater) Check() error {
|
||||
u.Lock()
|
||||
defer u.Unlock()
|
||||
|
||||
return u.status
|
||||
}
|
||||
|
||||
func (u *updater) update(status error) {
|
||||
u.Lock()
|
||||
defer u.Unlock()
|
||||
|
||||
u.status = status
|
||||
}
|
||||
|
||||
// PeriodicHealthChecker implements a Checker to check status periodically
|
||||
func PeriodicHealthChecker(checker health.Checker, period time.Duration) health.Checker {
|
||||
u := &updater{
|
||||
// init the "status" as "unknown status" error to avoid returning nil error(which means healthy)
|
||||
// before the first health check request finished
|
||||
status: errors.New("unknown status"),
|
||||
}
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(period)
|
||||
for {
|
||||
u.update(checker.Check())
|
||||
<-ticker.C
|
||||
}
|
||||
}()
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
func coreHealthChecker() health.Checker {
|
||||
return health.CheckFunc(func() error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func portalHealthChecker() health.Checker {
|
||||
url := config.GetPortalURL()
|
||||
timeout := 60 * time.Second
|
||||
period := 10 * time.Second
|
||||
checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusOK)
|
||||
return PeriodicHealthChecker(checker, period)
|
||||
}
|
||||
|
||||
func jobserviceHealthChecker() health.Checker {
|
||||
url := config.InternalJobServiceURL() + "/api/v1/stats"
|
||||
timeout := 60 * time.Second
|
||||
period := 10 * time.Second
|
||||
checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusOK)
|
||||
return PeriodicHealthChecker(checker, period)
|
||||
}
|
||||
|
||||
func registryHealthChecker() health.Checker {
|
||||
url := getRegistryURL() + "/v2"
|
||||
timeout := 60 * time.Second
|
||||
period := 10 * time.Second
|
||||
checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusUnauthorized)
|
||||
return PeriodicHealthChecker(checker, period)
|
||||
}
|
||||
|
||||
func registryCtlHealthChecker() health.Checker {
|
||||
url := config.GetRegistryCtlURL() + "/api/health"
|
||||
timeout := 60 * time.Second
|
||||
period := 10 * time.Second
|
||||
checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusOK)
|
||||
return PeriodicHealthChecker(checker, period)
|
||||
}
|
||||
|
||||
func chartmuseumHealthChecker() health.Checker {
|
||||
url, err := config.GetChartMuseumEndpoint()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get the URL of chartmuseum: %v", err)
|
||||
}
|
||||
url = url + "/health"
|
||||
timeout := 60 * time.Second
|
||||
period := 10 * time.Second
|
||||
checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusOK)
|
||||
return PeriodicHealthChecker(checker, period)
|
||||
}
|
||||
|
||||
func clairHealthChecker() health.Checker {
|
||||
url := config.GetClairHealthCheckServerURL() + "/health"
|
||||
timeout := 60 * time.Second
|
||||
period := 10 * time.Second
|
||||
checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusOK)
|
||||
return PeriodicHealthChecker(checker, period)
|
||||
}
|
||||
|
||||
func notaryHealthChecker() health.Checker {
|
||||
url := config.InternalNotaryEndpoint() + "/_notary_server/health"
|
||||
timeout := 60 * time.Second
|
||||
period := 10 * time.Second
|
||||
checker := HTTPStatusCodeHealthChecker(http.MethodGet, url, nil, timeout, http.StatusOK)
|
||||
return PeriodicHealthChecker(checker, period)
|
||||
}
|
||||
|
||||
func databaseHealthChecker() health.Checker {
|
||||
period := 10 * time.Second
|
||||
checker := health.CheckFunc(func() error {
|
||||
_, err := dao.GetOrmer().Raw("SELECT 1").Exec()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run SQL \"SELECT 1\": %v", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return PeriodicHealthChecker(checker, period)
|
||||
}
|
||||
|
||||
func redisHealthChecker() health.Checker {
|
||||
url := config.GetRedisOfRegURL()
|
||||
timeout := 60 * time.Second
|
||||
period := 10 * time.Second
|
||||
checker := health.CheckFunc(func() error {
|
||||
conn, err := redis.DialURL(url,
|
||||
redis.DialConnectTimeout(timeout*time.Second),
|
||||
redis.DialReadTimeout(timeout*time.Second),
|
||||
redis.DialWriteTimeout(timeout*time.Second))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to establish connection with Redis: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
_, err = conn.Do("PING")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run \"PING\": %v", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return PeriodicHealthChecker(checker, period)
|
||||
}
|
||||
|
||||
func registerHealthCheckers() {
|
||||
healthCheckerRegistry["core"] = coreHealthChecker()
|
||||
healthCheckerRegistry["portal"] = portalHealthChecker()
|
||||
healthCheckerRegistry["jobservice"] = jobserviceHealthChecker()
|
||||
healthCheckerRegistry["registry"] = registryHealthChecker()
|
||||
healthCheckerRegistry["registryctl"] = registryCtlHealthChecker()
|
||||
healthCheckerRegistry["database"] = databaseHealthChecker()
|
||||
healthCheckerRegistry["redis"] = redisHealthChecker()
|
||||
if config.WithChartMuseum() {
|
||||
healthCheckerRegistry["chartmuseum"] = chartmuseumHealthChecker()
|
||||
}
|
||||
if config.WithClair() {
|
||||
healthCheckerRegistry["clair"] = clairHealthChecker()
|
||||
}
|
||||
if config.WithNotary() {
|
||||
healthCheckerRegistry["notary"] = notaryHealthChecker()
|
||||
}
|
||||
}
|
||||
|
||||
func getRegistryURL() string {
|
||||
endpoint, err := config.RegistryURL()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get the URL of registry: %v", err)
|
||||
return ""
|
||||
}
|
||||
url, err := utils.ParseEndpoint(endpoint)
|
||||
if err != nil {
|
||||
log.Errorf("failed to parse the URL of registry: %v", err)
|
||||
return ""
|
||||
}
|
||||
return url.String()
|
||||
}
|
134
src/core/api/health_test.go
Normal file
134
src/core/api/health_test.go
Normal file
@ -0,0 +1,134 @@
|
||||
// Copyright 2019 Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution/health"
|
||||
"github.com/goharbor/harbor/src/common/utils/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestStringOfHealthy(t *testing.T) {
|
||||
var isHealthy healthy = true
|
||||
assert.Equal(t, "healthy", isHealthy.String())
|
||||
isHealthy = false
|
||||
assert.Equal(t, "unhealthy", isHealthy.String())
|
||||
}
|
||||
|
||||
func TestUpdater(t *testing.T) {
|
||||
updater := &updater{}
|
||||
assert.Equal(t, nil, updater.Check())
|
||||
updater.status = errors.New("unhealthy")
|
||||
assert.Equal(t, "unhealthy", updater.Check().Error())
|
||||
}
|
||||
|
||||
func TestHTTPStatusCodeHealthChecker(t *testing.T) {
|
||||
handler := &test.RequestHandlerMapping{
|
||||
Method: http.MethodGet,
|
||||
Pattern: "/health",
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
},
|
||||
}
|
||||
server := test.NewServer(handler)
|
||||
defer server.Close()
|
||||
|
||||
url := server.URL + "/health"
|
||||
checker := HTTPStatusCodeHealthChecker(
|
||||
http.MethodGet, url, map[string][]string{
|
||||
"key": {"value"},
|
||||
}, 5*time.Second, http.StatusOK)
|
||||
assert.Equal(t, nil, checker.Check())
|
||||
|
||||
checker = HTTPStatusCodeHealthChecker(
|
||||
http.MethodGet, url, nil, 5*time.Second, http.StatusUnauthorized)
|
||||
assert.NotEqual(t, nil, checker.Check())
|
||||
}
|
||||
|
||||
func TestPeriodicHealthChecker(t *testing.T) {
|
||||
firstCheck := true
|
||||
checkFunc := func() error {
|
||||
time.Sleep(2 * time.Second)
|
||||
if firstCheck {
|
||||
firstCheck = false
|
||||
return nil
|
||||
}
|
||||
return errors.New("unhealthy")
|
||||
}
|
||||
|
||||
checker := PeriodicHealthChecker(health.CheckFunc(checkFunc), 1*time.Second)
|
||||
assert.Equal(t, "unknown status", checker.Check().Error())
|
||||
time.Sleep(3 * time.Second)
|
||||
assert.Equal(t, nil, checker.Check())
|
||||
time.Sleep(3 * time.Second)
|
||||
assert.Equal(t, "unhealthy", checker.Check().Error())
|
||||
}
|
||||
|
||||
func fakeHealthChecker(healthy bool) health.Checker {
|
||||
return health.CheckFunc(func() error {
|
||||
if healthy {
|
||||
return nil
|
||||
}
|
||||
return errors.New("unhealthy")
|
||||
})
|
||||
}
|
||||
func TestCheckHealth(t *testing.T) {
|
||||
// component01: healthy, component02: healthy => status: healthy
|
||||
healthCheckerRegistry = map[string]health.Checker{}
|
||||
healthCheckerRegistry["component01"] = fakeHealthChecker(true)
|
||||
healthCheckerRegistry["component02"] = fakeHealthChecker(true)
|
||||
status := map[string]interface{}{}
|
||||
err := handleAndParse(&testingRequest{
|
||||
method: http.MethodGet,
|
||||
url: "/api/health",
|
||||
}, &status)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, "healthy", status["status"].(string))
|
||||
|
||||
// component01: healthy, component02: unhealthy => status: unhealthy
|
||||
healthCheckerRegistry = map[string]health.Checker{}
|
||||
healthCheckerRegistry["component01"] = fakeHealthChecker(true)
|
||||
healthCheckerRegistry["component02"] = fakeHealthChecker(false)
|
||||
status = map[string]interface{}{}
|
||||
err = handleAndParse(&testingRequest{
|
||||
method: http.MethodGet,
|
||||
url: "/api/health",
|
||||
}, &status)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, "unhealthy", status["status"].(string))
|
||||
}
|
||||
|
||||
func TestCoreHealthChecker(t *testing.T) {
|
||||
checker := coreHealthChecker()
|
||||
assert.Equal(t, nil, checker.Check())
|
||||
}
|
||||
|
||||
func TestDatabaseHealthChecker(t *testing.T) {
|
||||
checker := databaseHealthChecker()
|
||||
time.Sleep(1 * time.Second)
|
||||
assert.Equal(t, nil, checker.Check())
|
||||
}
|
||||
|
||||
func TestRegisterHealthCheckers(t *testing.T) {
|
||||
healthCheckerRegistry = map[string]health.Checker{}
|
||||
registerHealthCheckers()
|
||||
assert.NotNil(t, healthCheckerRegistry["core"])
|
||||
}
|
@ -22,6 +22,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/replication"
|
||||
"github.com/goharbor/harbor/src/replication/core"
|
||||
rep_models "github.com/goharbor/harbor/src/replication/models"
|
||||
@ -65,15 +66,36 @@ func (l *LabelAPI) Prepare() {
|
||||
return
|
||||
}
|
||||
|
||||
if label.Scope == common.LabelScopeGlobal && !l.SecurityCtx.IsSysAdmin() ||
|
||||
label.Scope == common.LabelScopeProject && !l.SecurityCtx.HasAllPerm(label.ProjectID) {
|
||||
l.HandleForbidden(l.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
l.label = label
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LabelAPI) requireAccess(label *models.Label, action rbac.Action, subresources ...rbac.Resource) bool {
|
||||
var hasPermission bool
|
||||
|
||||
switch label.Scope {
|
||||
case common.LabelScopeGlobal:
|
||||
hasPermission = l.SecurityCtx.IsSysAdmin()
|
||||
case common.LabelScopeProject:
|
||||
if len(subresources) == 0 {
|
||||
subresources = append(subresources, rbac.ResourceLabel)
|
||||
}
|
||||
resource := rbac.NewProjectNamespace(label.ProjectID).Resource(subresources...)
|
||||
hasPermission = l.SecurityCtx.Can(action, resource)
|
||||
}
|
||||
|
||||
if !hasPermission {
|
||||
if !l.SecurityCtx.IsAuthenticated() {
|
||||
l.HandleUnauthorized()
|
||||
} else {
|
||||
l.HandleForbidden(l.SecurityCtx.GetUsername())
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Post creates a label
|
||||
func (l *LabelAPI) Post() {
|
||||
label := &models.Label{}
|
||||
@ -82,10 +104,6 @@ func (l *LabelAPI) Post() {
|
||||
|
||||
switch label.Scope {
|
||||
case common.LabelScopeGlobal:
|
||||
if !l.SecurityCtx.IsSysAdmin() {
|
||||
l.HandleForbidden(l.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
label.ProjectID = 0
|
||||
case common.LabelScopeProject:
|
||||
exist, err := l.ProjectMgr.Exists(label.ProjectID)
|
||||
@ -98,10 +116,10 @@ func (l *LabelAPI) Post() {
|
||||
l.HandleNotFound(fmt.Sprintf("project %d not found", label.ProjectID))
|
||||
return
|
||||
}
|
||||
if !l.SecurityCtx.HasAllPerm(label.ProjectID) {
|
||||
l.HandleForbidden(l.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !l.requireAccess(label, rbac.ActionCreate) {
|
||||
return
|
||||
}
|
||||
|
||||
labels, err := dao.ListLabels(&models.LabelQuery{
|
||||
@ -147,15 +165,8 @@ func (l *LabelAPI) Get() {
|
||||
return
|
||||
}
|
||||
|
||||
if label.Scope == common.LabelScopeProject {
|
||||
if !l.SecurityCtx.HasReadPerm(label.ProjectID) {
|
||||
if !l.SecurityCtx.IsAuthenticated() {
|
||||
l.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
l.HandleForbidden(l.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
if !l.requireAccess(label, rbac.ActionRead) {
|
||||
return
|
||||
}
|
||||
|
||||
l.Data["json"] = label
|
||||
@ -189,7 +200,8 @@ func (l *LabelAPI) List() {
|
||||
return
|
||||
}
|
||||
|
||||
if !l.SecurityCtx.HasReadPerm(projectID) {
|
||||
resource := rbac.NewProjectNamespace(projectID).Resource(rbac.ResourceLabel)
|
||||
if !l.SecurityCtx.Can(rbac.ActionList, resource) {
|
||||
if !l.SecurityCtx.IsAuthenticated() {
|
||||
l.HandleUnauthorized()
|
||||
return
|
||||
@ -221,6 +233,10 @@ func (l *LabelAPI) List() {
|
||||
|
||||
// Put updates the label
|
||||
func (l *LabelAPI) Put() {
|
||||
if !l.requireAccess(l.label, rbac.ActionUpdate) {
|
||||
return
|
||||
}
|
||||
|
||||
label := &models.Label{}
|
||||
l.DecodeJSONReq(label)
|
||||
|
||||
@ -259,6 +275,10 @@ func (l *LabelAPI) Put() {
|
||||
|
||||
// Delete the label
|
||||
func (l *LabelAPI) Delete() {
|
||||
if !l.requireAccess(l.label, rbac.ActionDelete) {
|
||||
return
|
||||
}
|
||||
|
||||
id := l.label.ID
|
||||
if err := dao.DeleteResourceLabelByLabel(id); err != nil {
|
||||
l.HandleInternalServerError(fmt.Sprintf("failed to delete resource label mappings of label %d: %v", id, err))
|
||||
@ -272,11 +292,6 @@ func (l *LabelAPI) Delete() {
|
||||
|
||||
// ListResources lists the resources that the label is referenced by
|
||||
func (l *LabelAPI) ListResources() {
|
||||
if !l.SecurityCtx.IsAuthenticated() {
|
||||
l.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
|
||||
id, err := l.GetInt64FromPath(":id")
|
||||
if err != nil || id <= 0 {
|
||||
l.HandleBadRequest("invalid label ID")
|
||||
@ -294,9 +309,7 @@ func (l *LabelAPI) ListResources() {
|
||||
return
|
||||
}
|
||||
|
||||
if label.Scope == common.LabelScopeGlobal && !l.SecurityCtx.IsSysAdmin() ||
|
||||
label.Scope == common.LabelScopeProject && !l.SecurityCtx.HasAllPerm(label.ProjectID) {
|
||||
l.HandleForbidden(l.SecurityCtx.GetUsername())
|
||||
if !l.requireAccess(label, rbac.ActionList, rbac.ResourceLabelResource) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -22,23 +22,6 @@ func (lra *LabelResourceAPI) Prepare() {
|
||||
lra.labelManager = &label.BaseManager{}
|
||||
}
|
||||
|
||||
func (lra *LabelResourceAPI) checkPermissions(project string) bool {
|
||||
if lra.Ctx.Request.Method == http.MethodPost ||
|
||||
lra.Ctx.Request.Method == http.MethodDelete {
|
||||
if lra.SecurityCtx.HasWritePerm(project) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if lra.Ctx.Request.Method == http.MethodGet {
|
||||
if lra.SecurityCtx.HasReadPerm(project) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (lra *LabelResourceAPI) getLabelsOfResource(rType string, rIDOrName interface{}) {
|
||||
labels, err := lra.labelManager.GetLabelsOfResource(rType, rIDOrName)
|
||||
if err != nil {
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/promgr/metamgr"
|
||||
)
|
||||
@ -72,24 +73,6 @@ func (m *MetadataAPI) Prepare() {
|
||||
|
||||
m.project = project
|
||||
|
||||
switch m.Ctx.Request.Method {
|
||||
case http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete:
|
||||
if !(m.Ctx.Request.Method == http.MethodGet && project.IsPublic()) {
|
||||
if !m.SecurityCtx.IsAuthenticated() {
|
||||
m.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
if !m.SecurityCtx.HasReadPerm(project.ProjectID) {
|
||||
m.HandleForbidden(m.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
}
|
||||
default:
|
||||
log.Debugf("%s method not allowed", m.Ctx.Request.Method)
|
||||
m.RenderError(http.StatusMethodNotAllowed, "")
|
||||
return
|
||||
}
|
||||
|
||||
name := m.GetStringFromPath(":name")
|
||||
if len(name) > 0 {
|
||||
m.name = name
|
||||
@ -105,8 +88,27 @@ func (m *MetadataAPI) Prepare() {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MetadataAPI) requireAccess(action rbac.Action) bool {
|
||||
resource := rbac.NewProjectNamespace(m.project.ProjectID).Resource(rbac.ResourceMetadata)
|
||||
|
||||
if !m.SecurityCtx.Can(action, resource) {
|
||||
if !m.SecurityCtx.IsAuthenticated() {
|
||||
m.HandleUnauthorized()
|
||||
} else {
|
||||
m.HandleForbidden(m.SecurityCtx.GetUsername())
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Get ...
|
||||
func (m *MetadataAPI) Get() {
|
||||
if !m.requireAccess(rbac.ActionRead) {
|
||||
return
|
||||
}
|
||||
|
||||
var metas map[string]string
|
||||
var err error
|
||||
if len(m.name) > 0 {
|
||||
@ -125,6 +127,10 @@ func (m *MetadataAPI) Get() {
|
||||
|
||||
// Post ...
|
||||
func (m *MetadataAPI) Post() {
|
||||
if !m.requireAccess(rbac.ActionCreate) {
|
||||
return
|
||||
}
|
||||
|
||||
var metas map[string]string
|
||||
m.DecodeJSONReq(&metas)
|
||||
|
||||
@ -161,6 +167,10 @@ func (m *MetadataAPI) Post() {
|
||||
|
||||
// Put ...
|
||||
func (m *MetadataAPI) Put() {
|
||||
if !m.requireAccess(rbac.ActionUpdate) {
|
||||
return
|
||||
}
|
||||
|
||||
var metas map[string]string
|
||||
m.DecodeJSONReq(&metas)
|
||||
|
||||
@ -188,6 +198,10 @@ func (m *MetadataAPI) Put() {
|
||||
|
||||
// Delete ...
|
||||
func (m *MetadataAPI) Delete() {
|
||||
if !m.requireAccess(rbac.ActionDelete) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.metaMgr.Delete(m.project.ProjectID, m.name); err != nil {
|
||||
m.HandleInternalServerError(fmt.Sprintf("failed to delete metadata %s of project %d: %v", m.name, m.project.ProjectID, err))
|
||||
return
|
||||
|
@ -71,6 +71,9 @@ type GCRep struct {
|
||||
|
||||
// Valid validates the gc request
|
||||
func (gr *GCReq) Valid(v *validation.Validation) {
|
||||
if gr.Schedule == nil {
|
||||
return
|
||||
}
|
||||
switch gr.Schedule.Type {
|
||||
case ScheduleDaily, ScheduleWeekly:
|
||||
if gr.Schedule.Offtime < 0 || gr.Schedule.Offtime > 3600*24 {
|
||||
|
@ -14,7 +14,6 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -22,6 +21,9 @@ import (
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
common_job "github.com/goharbor/harbor/src/common/job"
|
||||
"github.com/goharbor/harbor/src/common/utils/test"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var adminServerTestConfig = map[string]interface{}{
|
||||
@ -29,11 +31,11 @@ var adminServerTestConfig = map[string]interface{}{
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
server, err := test.NewAdminserver(adminServerTestConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create a mock admin server: %v", err)
|
||||
}
|
||||
defer server.Close()
|
||||
|
||||
test.InitDatabaseFromEnv()
|
||||
config.Init()
|
||||
config.Upload(adminServerTestConfig)
|
||||
os.Exit(m.Run())
|
||||
|
||||
}
|
||||
|
||||
@ -126,5 +128,5 @@ func TestCronString(t *testing.T) {
|
||||
Schedule: schedule,
|
||||
}
|
||||
cronStr := adminjob.CronString()
|
||||
assert.Equal(t, cronStr, "{\"type\":\"Daily\",\"Weekday\":0,\"Offtime\":102}")
|
||||
assert.True(t, strings.EqualFold(cronStr, "{\"type\":\"Daily\",\"Weekday\":0,\"Offtime\":102}"))
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
errutil "github.com/goharbor/harbor/src/common/utils/error"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
@ -77,6 +78,25 @@ func (p *ProjectAPI) Prepare() {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ProjectAPI) requireAccess(action rbac.Action, subresource ...rbac.Resource) bool {
|
||||
if len(subresource) == 0 {
|
||||
subresource = append(subresource, rbac.ResourceSelf)
|
||||
}
|
||||
resource := rbac.NewProjectNamespace(p.project.ProjectID).Resource(subresource...)
|
||||
|
||||
if !p.SecurityCtx.Can(action, resource) {
|
||||
if !p.SecurityCtx.IsAuthenticated() {
|
||||
p.HandleUnauthorized()
|
||||
} else {
|
||||
p.HandleForbidden(p.SecurityCtx.GetUsername())
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Post ...
|
||||
func (p *ProjectAPI) Post() {
|
||||
if !p.SecurityCtx.IsAuthenticated() {
|
||||
@ -187,16 +207,8 @@ func (p *ProjectAPI) Head() {
|
||||
|
||||
// Get ...
|
||||
func (p *ProjectAPI) Get() {
|
||||
if !p.project.IsPublic() {
|
||||
if !p.SecurityCtx.IsAuthenticated() {
|
||||
p.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
|
||||
if !p.SecurityCtx.HasReadPerm(p.project.ProjectID) {
|
||||
p.HandleForbidden(p.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
if !p.requireAccess(rbac.ActionRead) {
|
||||
return
|
||||
}
|
||||
|
||||
p.populateProperties(p.project)
|
||||
@ -207,13 +219,7 @@ func (p *ProjectAPI) Get() {
|
||||
|
||||
// Delete ...
|
||||
func (p *ProjectAPI) Delete() {
|
||||
if !p.SecurityCtx.IsAuthenticated() {
|
||||
p.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
|
||||
if !p.SecurityCtx.HasAllPerm(p.project.ProjectID) {
|
||||
p.HandleForbidden(p.SecurityCtx.GetUsername())
|
||||
if !p.requireAccess(rbac.ActionDelete) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -248,13 +254,7 @@ func (p *ProjectAPI) Delete() {
|
||||
|
||||
// Deletable ...
|
||||
func (p *ProjectAPI) Deletable() {
|
||||
if !p.SecurityCtx.IsAuthenticated() {
|
||||
p.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
|
||||
if !p.SecurityCtx.HasAllPerm(p.project.ProjectID) {
|
||||
p.HandleForbidden(p.SecurityCtx.GetUsername())
|
||||
if !p.requireAccess(rbac.ActionDelete) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -433,13 +433,7 @@ func (p *ProjectAPI) populateProperties(project *models.Project) {
|
||||
|
||||
// Put ...
|
||||
func (p *ProjectAPI) Put() {
|
||||
if !p.SecurityCtx.IsAuthenticated() {
|
||||
p.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
|
||||
if !p.SecurityCtx.HasAllPerm(p.project.ProjectID) {
|
||||
p.HandleForbidden(p.SecurityCtx.GetUsername())
|
||||
if !p.requireAccess(rbac.ActionUpdate) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -458,13 +452,7 @@ func (p *ProjectAPI) Put() {
|
||||
|
||||
// Logs ...
|
||||
func (p *ProjectAPI) Logs() {
|
||||
if !p.SecurityCtx.IsAuthenticated() {
|
||||
p.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
|
||||
if !p.SecurityCtx.HasReadPerm(p.project.ProjectID) {
|
||||
p.HandleForbidden(p.SecurityCtx.GetUsername())
|
||||
if !p.requireAccess(rbac.ActionList, rbac.ResourceLog) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/dao/project"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/auth"
|
||||
)
|
||||
@ -73,12 +74,6 @@ func (pma *ProjectMemberAPI) Prepare() {
|
||||
}
|
||||
pma.project = project
|
||||
|
||||
if !(pma.Ctx.Input.IsGet() && pma.SecurityCtx.HasReadPerm(pid) ||
|
||||
pma.SecurityCtx.HasAllPerm(pid)) {
|
||||
pma.HandleForbidden(pma.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
|
||||
pmid, err := pma.GetInt64FromPath(":pmid")
|
||||
if err != nil {
|
||||
log.Warningf("Failed to get pmid from path, error %v", err)
|
||||
@ -90,6 +85,22 @@ func (pma *ProjectMemberAPI) Prepare() {
|
||||
pma.id = int(pmid)
|
||||
}
|
||||
|
||||
func (pma *ProjectMemberAPI) requireAccess(action rbac.Action) bool {
|
||||
resource := rbac.NewProjectNamespace(pma.project.ProjectID).Resource(rbac.ResourceMember)
|
||||
|
||||
if !pma.SecurityCtx.Can(action, resource) {
|
||||
if !pma.SecurityCtx.IsAuthenticated() {
|
||||
pma.HandleUnauthorized()
|
||||
} else {
|
||||
pma.HandleForbidden(pma.SecurityCtx.GetUsername())
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Get ...
|
||||
func (pma *ProjectMemberAPI) Get() {
|
||||
projectID := pma.project.ProjectID
|
||||
@ -97,6 +108,9 @@ func (pma *ProjectMemberAPI) Get() {
|
||||
queryMember.ProjectID = projectID
|
||||
pma.Data["json"] = make([]models.Member, 0)
|
||||
if pma.id == 0 {
|
||||
if !pma.requireAccess(rbac.ActionList) {
|
||||
return
|
||||
}
|
||||
entityname := pma.GetString("entityname")
|
||||
memberList, err := project.SearchMemberByName(projectID, entityname)
|
||||
if err != nil {
|
||||
@ -119,6 +133,10 @@ func (pma *ProjectMemberAPI) Get() {
|
||||
pma.HandleNotFound(fmt.Sprintf("The project member does not exit, pmid:%v", pma.id))
|
||||
return
|
||||
}
|
||||
|
||||
if !pma.requireAccess(rbac.ActionRead) {
|
||||
return
|
||||
}
|
||||
pma.Data["json"] = memberList[0]
|
||||
}
|
||||
pma.ServeJSON()
|
||||
@ -126,6 +144,9 @@ func (pma *ProjectMemberAPI) Get() {
|
||||
|
||||
// Post ... Add a project member
|
||||
func (pma *ProjectMemberAPI) Post() {
|
||||
if !pma.requireAccess(rbac.ActionCreate) {
|
||||
return
|
||||
}
|
||||
projectID := pma.project.ProjectID
|
||||
var request models.MemberReq
|
||||
pma.DecodeJSONReq(&request)
|
||||
@ -156,11 +177,14 @@ func (pma *ProjectMemberAPI) Post() {
|
||||
|
||||
// Put ... Update an exist project member
|
||||
func (pma *ProjectMemberAPI) Put() {
|
||||
if !pma.requireAccess(rbac.ActionUpdate) {
|
||||
return
|
||||
}
|
||||
pid := pma.project.ProjectID
|
||||
pmID := pma.id
|
||||
var req models.Member
|
||||
pma.DecodeJSONReq(&req)
|
||||
if req.Role < 1 || req.Role > 3 {
|
||||
if req.Role < 1 || req.Role > 4 {
|
||||
pma.HandleBadRequest(fmt.Sprintf("Invalid role id %v", req.Role))
|
||||
return
|
||||
}
|
||||
@ -173,6 +197,9 @@ func (pma *ProjectMemberAPI) Put() {
|
||||
|
||||
// Delete ...
|
||||
func (pma *ProjectMemberAPI) Delete() {
|
||||
if !pma.requireAccess(rbac.ActionDelete) {
|
||||
return
|
||||
}
|
||||
pmid := pma.id
|
||||
err := project.DeleteProjectMemberByID(pmid)
|
||||
if err != nil {
|
||||
@ -226,7 +253,7 @@ func AddProjectMember(projectID int64, request models.MemberReq) (int, error) {
|
||||
return 0, ErrDuplicateProjectMember
|
||||
}
|
||||
|
||||
if member.Role < 1 || member.Role > 3 {
|
||||
if member.Role < 1 || member.Role > 4 {
|
||||
// Return invalid role error
|
||||
return 0, ErrInvalidRole
|
||||
}
|
||||
|
@ -209,6 +209,18 @@ func TestProjectMemberAPI_PutAndDelete(t *testing.T) {
|
||||
},
|
||||
code: http.StatusOK,
|
||||
},
|
||||
// 200
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: URL,
|
||||
bodyJSON: &models.Member{
|
||||
Role: 4,
|
||||
},
|
||||
credential: admin,
|
||||
},
|
||||
code: http.StatusOK,
|
||||
},
|
||||
// 400
|
||||
{
|
||||
request: &testingRequest{
|
||||
|
@ -24,6 +24,7 @@ import (
|
||||
common_http "github.com/goharbor/harbor/src/common/http"
|
||||
common_job "github.com/goharbor/harbor/src/common/job"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
api_models "github.com/goharbor/harbor/src/core/api/models"
|
||||
"github.com/goharbor/harbor/src/core/utils"
|
||||
@ -80,7 +81,8 @@ func (ra *RepJobAPI) List() {
|
||||
return
|
||||
}
|
||||
|
||||
if !ra.SecurityCtx.HasAllPerm(policy.ProjectIDs[0]) {
|
||||
resource := rbac.NewProjectNamespace(policy.ProjectIDs[0]).Resource(rbac.ResourceReplicationJob)
|
||||
if !ra.SecurityCtx.Can(rbac.ActionList, resource) {
|
||||
ra.HandleForbidden(ra.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
@ -190,7 +192,8 @@ func (ra *RepJobAPI) GetLog() {
|
||||
return
|
||||
}
|
||||
|
||||
if !ra.SecurityCtx.HasAllPerm(policy.ProjectIDs[0]) {
|
||||
resource := rbac.NewProjectNamespace(policy.ProjectIDs[0]).Resource(rbac.ResourceReplicationJob)
|
||||
if !ra.SecurityCtx.Can(rbac.ActionRead, resource) {
|
||||
ra.HandleForbidden(ra.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
api_models "github.com/goharbor/harbor/src/core/api/models"
|
||||
"github.com/goharbor/harbor/src/core/promgr"
|
||||
@ -63,7 +64,8 @@ func (pa *RepPolicyAPI) Get() {
|
||||
return
|
||||
}
|
||||
|
||||
if !pa.SecurityCtx.HasAllPerm(policy.ProjectIDs[0]) {
|
||||
resource := rbac.NewProjectNamespace(policy.ProjectIDs[0]).Resource(rbac.ResourceReplication)
|
||||
if !pa.SecurityCtx.Can(rbac.ActionRead, resource) {
|
||||
pa.HandleForbidden(pa.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
@ -105,7 +107,8 @@ func (pa *RepPolicyAPI) List() {
|
||||
if result != nil {
|
||||
total = result.Total
|
||||
for _, policy := range result.Policies {
|
||||
if !pa.SecurityCtx.HasAllPerm(policy.ProjectIDs[0]) {
|
||||
resource := rbac.NewProjectNamespace(policy.ProjectIDs[0]).Resource(rbac.ResourceReplication)
|
||||
if !pa.SecurityCtx.Can(rbac.ActionRead, resource) {
|
||||
continue
|
||||
}
|
||||
ply, err := convertFromRepPolicy(pa.ProjectMgr, *policy)
|
||||
|
@ -30,6 +30,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
commonhttp "github.com/goharbor/harbor/src/common/http"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/common/utils/clair"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
@ -131,7 +132,8 @@ func (ra *RepositoryAPI) Get() {
|
||||
return
|
||||
}
|
||||
|
||||
if !ra.SecurityCtx.HasReadPerm(projectID) {
|
||||
resource := rbac.NewProjectNamespace(projectID).Resource(rbac.ResourceRepository)
|
||||
if !ra.SecurityCtx.Can(rbac.ActionList, resource) {
|
||||
if !ra.SecurityCtx.IsAuthenticated() {
|
||||
ra.HandleUnauthorized()
|
||||
return
|
||||
@ -247,7 +249,8 @@ func (ra *RepositoryAPI) Delete() {
|
||||
return
|
||||
}
|
||||
|
||||
if !ra.SecurityCtx.HasAllPerm(projectName) {
|
||||
resource := rbac.NewProjectNamespace(project.ProjectID).Resource(rbac.ResourceRepository)
|
||||
if !ra.SecurityCtx.Can(rbac.ActionDelete, resource) {
|
||||
ra.HandleForbidden(ra.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
@ -393,7 +396,8 @@ func (ra *RepositoryAPI) GetTag() {
|
||||
return
|
||||
}
|
||||
project, _ := utils.ParseRepository(repository)
|
||||
if !ra.SecurityCtx.HasReadPerm(project) {
|
||||
resource := rbac.NewProjectNamespace(project).Resource(rbac.ResourceRepositoryTag)
|
||||
if !ra.SecurityCtx.Can(rbac.ActionRead, resource) {
|
||||
if !ra.SecurityCtx.IsAuthenticated() {
|
||||
ra.HandleUnauthorized()
|
||||
return
|
||||
@ -488,14 +492,16 @@ func (ra *RepositoryAPI) Retag() {
|
||||
}
|
||||
|
||||
// Check whether user has read permission to source project
|
||||
if !ra.SecurityCtx.HasReadPerm(srcImage.Project) {
|
||||
srcResource := rbac.NewProjectNamespace(srcImage.Project).Resource(rbac.ResourceRepository)
|
||||
if !ra.SecurityCtx.Can(rbac.ActionPull, srcResource) {
|
||||
log.Errorf("user has no read permission to project '%s'", srcImage.Project)
|
||||
ra.HandleForbidden(fmt.Sprintf("%s has no read permission to project %s", ra.SecurityCtx.GetUsername(), srcImage.Project))
|
||||
return
|
||||
}
|
||||
|
||||
// Check whether user has write permission to target project
|
||||
if !ra.SecurityCtx.HasWritePerm(project) {
|
||||
destResource := rbac.NewProjectNamespace(project).Resource(rbac.ResourceRepository)
|
||||
if !ra.SecurityCtx.Can(rbac.ActionPush, destResource) {
|
||||
log.Errorf("user has no write permission to project '%s'", project)
|
||||
ra.HandleForbidden(fmt.Sprintf("%s has no write permission to project %s", ra.SecurityCtx.GetUsername(), project))
|
||||
return
|
||||
@ -533,7 +539,8 @@ func (ra *RepositoryAPI) GetTags() {
|
||||
return
|
||||
}
|
||||
|
||||
if !ra.SecurityCtx.HasReadPerm(projectName) {
|
||||
resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepositoryTag)
|
||||
if !ra.SecurityCtx.Can(rbac.ActionList, resource) {
|
||||
if !ra.SecurityCtx.IsAuthenticated() {
|
||||
ra.HandleUnauthorized()
|
||||
return
|
||||
@ -741,7 +748,8 @@ func (ra *RepositoryAPI) GetManifests() {
|
||||
return
|
||||
}
|
||||
|
||||
if !ra.SecurityCtx.HasReadPerm(projectName) {
|
||||
resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepositoryTagManifest)
|
||||
if !ra.SecurityCtx.Can(rbac.ActionRead, resource) {
|
||||
if !ra.SecurityCtx.IsAuthenticated() {
|
||||
ra.HandleUnauthorized()
|
||||
return
|
||||
@ -872,7 +880,8 @@ func (ra *RepositoryAPI) Put() {
|
||||
}
|
||||
|
||||
project, _ := utils.ParseRepository(name)
|
||||
if !ra.SecurityCtx.HasWritePerm(project) {
|
||||
resource := rbac.NewProjectNamespace(project).Resource(rbac.ResourceRepository)
|
||||
if !ra.SecurityCtx.Can(rbac.ActionUpdate, resource) {
|
||||
ra.HandleForbidden(ra.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
@ -906,7 +915,8 @@ func (ra *RepositoryAPI) GetSignatures() {
|
||||
return
|
||||
}
|
||||
|
||||
if !ra.SecurityCtx.HasReadPerm(projectName) {
|
||||
resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepository)
|
||||
if !ra.SecurityCtx.Can(rbac.ActionRead, resource) {
|
||||
if !ra.SecurityCtx.IsAuthenticated() {
|
||||
ra.HandleUnauthorized()
|
||||
return
|
||||
@ -949,7 +959,9 @@ func (ra *RepositoryAPI) ScanImage() {
|
||||
ra.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
if !ra.SecurityCtx.HasAllPerm(projectName) {
|
||||
|
||||
resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepositoryTagScanJob)
|
||||
if !ra.SecurityCtx.Can(rbac.ActionCreate, resource) {
|
||||
ra.HandleForbidden(ra.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
@ -980,7 +992,9 @@ func (ra *RepositoryAPI) VulnerabilityDetails() {
|
||||
return
|
||||
}
|
||||
project, _ := utils.ParseRepository(repository)
|
||||
if !ra.SecurityCtx.HasReadPerm(project) {
|
||||
|
||||
resource := rbac.NewProjectNamespace(project).Resource(rbac.ResourceRepositoryTagVulnerability)
|
||||
if !ra.SecurityCtx.Can(rbac.ActionList, resource) {
|
||||
if !ra.SecurityCtx.IsAuthenticated() {
|
||||
ra.HandleUnauthorized()
|
||||
return
|
||||
|
@ -22,7 +22,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
coreutils "github.com/goharbor/harbor/src/core/utils"
|
||||
)
|
||||
|
||||
@ -45,12 +45,6 @@ func (r *RepositoryLabelAPI) Prepare() {
|
||||
}
|
||||
|
||||
repository := r.GetString(":splat")
|
||||
project, _ := utils.ParseRepository(repository)
|
||||
if !r.checkPermissions(project) {
|
||||
r.SendForbiddenError(errors.New(r.SecurityCtx.GetUsername()))
|
||||
return
|
||||
}
|
||||
|
||||
repo, err := dao.GetRepositoryByName(repository)
|
||||
if err != nil {
|
||||
r.SendInternalServerError(fmt.Errorf("failed to get repository %s: %v", repository, err))
|
||||
@ -77,25 +71,6 @@ func (r *RepositoryLabelAPI) Prepare() {
|
||||
r.tag = tag
|
||||
}
|
||||
|
||||
if r.Ctx.Request.Method == http.MethodPost {
|
||||
p, err := r.ProjectMgr.Get(project)
|
||||
if err != nil {
|
||||
r.SendInternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
l := &models.Label{}
|
||||
r.DecodeJSONReq(l)
|
||||
|
||||
label, ok := r.validate(l.ID, p.ProjectID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
r.label = label
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if r.Ctx.Request.Method == http.MethodDelete {
|
||||
labelID, err := r.GetInt64FromPath(":id")
|
||||
if err != nil {
|
||||
@ -112,13 +87,59 @@ func (r *RepositoryLabelAPI) Prepare() {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RepositoryLabelAPI) requireAccess(action rbac.Action, subresource ...rbac.Resource) bool {
|
||||
if len(subresource) == 0 {
|
||||
subresource = append(subresource, rbac.ResourceRepositoryLabel)
|
||||
}
|
||||
resource := rbac.NewProjectNamespace(r.repository.ProjectID).Resource(rbac.ResourceRepositoryLabel)
|
||||
|
||||
if !r.SecurityCtx.Can(action, resource) {
|
||||
if !r.SecurityCtx.IsAuthenticated() {
|
||||
r.SendUnAuthorizedError(errors.New("UnAuthorized"))
|
||||
} else {
|
||||
r.SendForbiddenError(errors.New(r.SecurityCtx.GetUsername()))
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *RepositoryLabelAPI) isValidLabelReq() bool {
|
||||
p, err := r.ProjectMgr.Get(r.repository.ProjectID)
|
||||
if err != nil {
|
||||
r.SendInternalServerError(err)
|
||||
return false
|
||||
}
|
||||
|
||||
l := &models.Label{}
|
||||
r.DecodeJSONReq(l)
|
||||
|
||||
label, ok := r.validate(l.ID, p.ProjectID)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
r.label = label
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// GetOfImage returns labels of an image
|
||||
func (r *RepositoryLabelAPI) GetOfImage() {
|
||||
if !r.requireAccess(rbac.ActionList, rbac.ResourceRepositoryTagLabel) {
|
||||
return
|
||||
}
|
||||
|
||||
r.getLabelsOfResource(common.ResourceTypeImage, fmt.Sprintf("%s:%s", r.repository.Name, r.tag))
|
||||
}
|
||||
|
||||
// AddToImage adds the label to an image
|
||||
func (r *RepositoryLabelAPI) AddToImage() {
|
||||
if !r.requireAccess(rbac.ActionCreate, rbac.ResourceRepositoryTagLabel) || !r.isValidLabelReq() {
|
||||
return
|
||||
}
|
||||
|
||||
rl := &models.ResourceLabel{
|
||||
LabelID: r.label.ID,
|
||||
ResourceType: common.ResourceTypeImage,
|
||||
@ -129,17 +150,29 @@ func (r *RepositoryLabelAPI) AddToImage() {
|
||||
|
||||
// RemoveFromImage removes the label from an image
|
||||
func (r *RepositoryLabelAPI) RemoveFromImage() {
|
||||
if !r.requireAccess(rbac.ActionDelete, rbac.ResourceRepositoryTagLabel) {
|
||||
return
|
||||
}
|
||||
|
||||
r.removeLabelFromResource(common.ResourceTypeImage,
|
||||
fmt.Sprintf("%s:%s", r.repository.Name, r.tag), r.label.ID)
|
||||
}
|
||||
|
||||
// GetOfRepository returns labels of a repository
|
||||
func (r *RepositoryLabelAPI) GetOfRepository() {
|
||||
if !r.requireAccess(rbac.ActionList) {
|
||||
return
|
||||
}
|
||||
|
||||
r.getLabelsOfResource(common.ResourceTypeRepository, r.repository.RepositoryID)
|
||||
}
|
||||
|
||||
// AddToRepository adds the label to a repository
|
||||
func (r *RepositoryLabelAPI) AddToRepository() {
|
||||
if !r.requireAccess(rbac.ActionCreate) || !r.isValidLabelReq() {
|
||||
return
|
||||
}
|
||||
|
||||
rl := &models.ResourceLabel{
|
||||
LabelID: r.label.ID,
|
||||
ResourceType: common.ResourceTypeRepository,
|
||||
@ -150,6 +183,10 @@ func (r *RepositoryLabelAPI) AddToRepository() {
|
||||
|
||||
// RemoveFromRepository removes the label from a repository
|
||||
func (r *RepositoryLabelAPI) RemoveFromRepository() {
|
||||
if !r.requireAccess(rbac.ActionDelete) {
|
||||
return
|
||||
}
|
||||
|
||||
r.removeLabelFromResource(common.ResourceTypeRepository, r.repository.RepositoryID, r.label.ID)
|
||||
}
|
||||
|
||||
|
238
src/core/api/robot.go
Normal file
238
src/core/api/robot.go
Normal file
@ -0,0 +1,238 @@
|
||||
// Copyright 2018 Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/token"
|
||||
)
|
||||
|
||||
// RobotAPI ...
|
||||
type RobotAPI struct {
|
||||
BaseController
|
||||
project *models.Project
|
||||
robot *models.Robot
|
||||
}
|
||||
|
||||
// Prepare ...
|
||||
func (r *RobotAPI) Prepare() {
|
||||
r.BaseController.Prepare()
|
||||
method := r.Ctx.Request.Method
|
||||
|
||||
if !r.SecurityCtx.IsAuthenticated() {
|
||||
r.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
|
||||
pid, err := r.GetInt64FromPath(":pid")
|
||||
if err != nil || pid <= 0 {
|
||||
var errMsg string
|
||||
if err != nil {
|
||||
errMsg = "failed to get project ID " + err.Error()
|
||||
} else {
|
||||
errMsg = "invalid project ID: " + fmt.Sprintf("%d", pid)
|
||||
}
|
||||
r.HandleBadRequest(errMsg)
|
||||
return
|
||||
}
|
||||
project, err := r.ProjectMgr.Get(pid)
|
||||
if err != nil {
|
||||
r.ParseAndHandleError(fmt.Sprintf("failed to get project %d", pid), err)
|
||||
return
|
||||
}
|
||||
if project == nil {
|
||||
r.HandleNotFound(fmt.Sprintf("project %d not found", pid))
|
||||
return
|
||||
}
|
||||
r.project = project
|
||||
|
||||
if method == http.MethodPut || method == http.MethodDelete {
|
||||
id, err := r.GetInt64FromPath(":id")
|
||||
if err != nil || id <= 0 {
|
||||
r.HandleBadRequest("invalid robot ID")
|
||||
return
|
||||
}
|
||||
|
||||
robot, err := dao.GetRobotByID(id)
|
||||
if err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to get robot %d: %v", id, err))
|
||||
return
|
||||
}
|
||||
|
||||
if robot == nil {
|
||||
r.HandleNotFound(fmt.Sprintf("robot %d not found", id))
|
||||
return
|
||||
}
|
||||
|
||||
r.robot = robot
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RobotAPI) requireAccess(action rbac.Action) bool {
|
||||
resource := rbac.NewProjectNamespace(r.project.ProjectID).Resource(rbac.ResourceRobot)
|
||||
if !r.SecurityCtx.Can(action, resource) {
|
||||
r.HandleForbidden(r.SecurityCtx.GetUsername())
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Post ...
|
||||
func (r *RobotAPI) Post() {
|
||||
if !r.requireAccess(rbac.ActionCreate) {
|
||||
return
|
||||
}
|
||||
|
||||
var robotReq models.RobotReq
|
||||
r.DecodeJSONReq(&robotReq)
|
||||
createdName := common.RobotPrefix + robotReq.Name
|
||||
|
||||
// first to add a robot account, and get its id.
|
||||
robot := models.Robot{
|
||||
Name: createdName,
|
||||
Description: robotReq.Description,
|
||||
ProjectID: r.project.ProjectID,
|
||||
}
|
||||
id, err := dao.AddRobot(&robot)
|
||||
if err != nil {
|
||||
if err == dao.ErrDupRows {
|
||||
r.HandleConflict()
|
||||
return
|
||||
}
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to create robot account: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// generate the token, and return it with response data.
|
||||
// token is not stored in the database.
|
||||
jwtToken, err := token.New(id, r.project.ProjectID, robotReq.Access)
|
||||
if err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to valid parameters to generate token for robot account, %v", err))
|
||||
err := dao.DeleteRobot(id)
|
||||
if err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to delete the robot account: %d, %v", id, err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
rawTk, err := jwtToken.Raw()
|
||||
if err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to sign token for robot account, %v", err))
|
||||
err := dao.DeleteRobot(id)
|
||||
if err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to delete the robot account: %d, %v", id, err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
robotRep := models.RobotRep{
|
||||
Name: robot.Name,
|
||||
Token: rawTk,
|
||||
}
|
||||
r.Redirect(http.StatusCreated, strconv.FormatInt(id, 10))
|
||||
r.Data["json"] = robotRep
|
||||
r.ServeJSON()
|
||||
}
|
||||
|
||||
// List list all the robots of a project
|
||||
func (r *RobotAPI) List() {
|
||||
if !r.requireAccess(rbac.ActionList) {
|
||||
return
|
||||
}
|
||||
|
||||
query := models.RobotQuery{
|
||||
ProjectID: r.project.ProjectID,
|
||||
}
|
||||
|
||||
count, err := dao.CountRobot(&query)
|
||||
if err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to list robots on project: %d, %v", r.project.ProjectID, err))
|
||||
return
|
||||
}
|
||||
query.Page, query.Size = r.GetPaginationParams()
|
||||
|
||||
robots, err := dao.ListRobots(&query)
|
||||
if err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to get robots %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
r.SetPaginationHeader(count, query.Page, query.Size)
|
||||
r.Data["json"] = robots
|
||||
r.ServeJSON()
|
||||
}
|
||||
|
||||
// Get get robot by id
|
||||
func (r *RobotAPI) Get() {
|
||||
if !r.requireAccess(rbac.ActionRead) {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := r.GetInt64FromPath(":id")
|
||||
if err != nil || id <= 0 {
|
||||
r.HandleBadRequest(fmt.Sprintf("invalid robot ID: %s", r.GetStringFromPath(":id")))
|
||||
return
|
||||
}
|
||||
|
||||
robot, err := dao.GetRobotByID(id)
|
||||
if err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to get robot %d: %v", id, err))
|
||||
return
|
||||
}
|
||||
if robot == nil {
|
||||
r.HandleNotFound(fmt.Sprintf("robot %d not found", id))
|
||||
return
|
||||
}
|
||||
|
||||
r.Data["json"] = robot
|
||||
r.ServeJSON()
|
||||
}
|
||||
|
||||
// Put disable or enable a robot account
|
||||
func (r *RobotAPI) Put() {
|
||||
if !r.requireAccess(rbac.ActionUpdate) {
|
||||
return
|
||||
}
|
||||
|
||||
var robotReq models.RobotReq
|
||||
r.DecodeJSONReqAndValidate(&robotReq)
|
||||
r.robot.Disabled = robotReq.Disabled
|
||||
|
||||
if err := dao.UpdateRobot(r.robot); err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to update robot %d: %v", r.robot.ID, err))
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Delete delete robot by id
|
||||
func (r *RobotAPI) Delete() {
|
||||
if !r.requireAccess(rbac.ActionDelete) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := dao.DeleteRobot(r.robot.ID); err != nil {
|
||||
r.HandleInternalServerError(fmt.Sprintf("failed to delete robot %d: %v", r.robot.ID, err))
|
||||
return
|
||||
}
|
||||
}
|
324
src/core/api/robot_test.go
Normal file
324
src/core/api/robot_test.go
Normal file
@ -0,0 +1,324 @@
|
||||
// Copyright 2018 Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
robotPath = "/api/projects/1/robots"
|
||||
robotID int64
|
||||
)
|
||||
|
||||
func TestRobotAPIPost(t *testing.T) {
|
||||
|
||||
rbacPolicy := &rbac.Policy{
|
||||
Resource: "/project/libray/repository",
|
||||
Action: "pull",
|
||||
}
|
||||
policies := []*rbac.Policy{}
|
||||
policies = append(policies, rbacPolicy)
|
||||
|
||||
cases := []*codeCheckingCase{
|
||||
// 401
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPost,
|
||||
url: robotPath,
|
||||
},
|
||||
code: http.StatusUnauthorized,
|
||||
},
|
||||
|
||||
// 403
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPost,
|
||||
url: robotPath,
|
||||
bodyJSON: &models.RobotReq{},
|
||||
credential: nonSysAdmin,
|
||||
},
|
||||
code: http.StatusForbidden,
|
||||
},
|
||||
// 201
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPost,
|
||||
url: robotPath,
|
||||
bodyJSON: &models.RobotReq{
|
||||
Name: "test",
|
||||
Description: "test desc",
|
||||
Access: policies,
|
||||
},
|
||||
credential: projAdmin4Robot,
|
||||
},
|
||||
code: http.StatusCreated,
|
||||
},
|
||||
// 403 -- developer
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPost,
|
||||
url: robotPath,
|
||||
bodyJSON: &models.RobotReq{
|
||||
Name: "test2",
|
||||
Description: "test2 desc",
|
||||
},
|
||||
credential: projDeveloper,
|
||||
},
|
||||
code: http.StatusForbidden,
|
||||
},
|
||||
|
||||
// 409
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPost,
|
||||
url: robotPath,
|
||||
bodyJSON: &models.RobotReq{
|
||||
Name: "test",
|
||||
Description: "test desc",
|
||||
Access: policies,
|
||||
},
|
||||
credential: projAdmin4Robot,
|
||||
},
|
||||
code: http.StatusConflict,
|
||||
},
|
||||
}
|
||||
runCodeCheckingCases(t, cases...)
|
||||
}
|
||||
|
||||
func TestRobotAPIGet(t *testing.T) {
|
||||
cases := []*codeCheckingCase{
|
||||
// 400
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/%d", robotPath, 0),
|
||||
},
|
||||
code: http.StatusUnauthorized,
|
||||
},
|
||||
|
||||
// 404
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/%d", robotPath, 1000),
|
||||
credential: projDeveloper,
|
||||
},
|
||||
code: http.StatusNotFound,
|
||||
},
|
||||
|
||||
// 200
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/%d", robotPath, 1),
|
||||
credential: projDeveloper,
|
||||
},
|
||||
code: http.StatusOK,
|
||||
},
|
||||
|
||||
// 200
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodGet,
|
||||
url: fmt.Sprintf("%s/%d", robotPath, 1),
|
||||
credential: projAdmin4Robot,
|
||||
},
|
||||
code: http.StatusOK,
|
||||
},
|
||||
}
|
||||
runCodeCheckingCases(t, cases...)
|
||||
}
|
||||
|
||||
func TestRobotAPIList(t *testing.T) {
|
||||
cases := []*codeCheckingCase{
|
||||
// 401
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodGet,
|
||||
url: robotPath,
|
||||
},
|
||||
code: http.StatusUnauthorized,
|
||||
},
|
||||
|
||||
// 400
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodGet,
|
||||
url: "/api/projects/0/robots",
|
||||
credential: projAdmin4Robot,
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
|
||||
// 200
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodGet,
|
||||
url: robotPath,
|
||||
credential: projDeveloper,
|
||||
},
|
||||
code: http.StatusOK,
|
||||
},
|
||||
|
||||
// 200
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodGet,
|
||||
url: robotPath,
|
||||
credential: projAdmin4Robot,
|
||||
},
|
||||
code: http.StatusOK,
|
||||
},
|
||||
}
|
||||
runCodeCheckingCases(t, cases...)
|
||||
}
|
||||
|
||||
func TestRobotAPIPut(t *testing.T) {
|
||||
cases := []*codeCheckingCase{
|
||||
// 401
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: fmt.Sprintf("%s/%d", robotPath, 1),
|
||||
},
|
||||
code: http.StatusUnauthorized,
|
||||
},
|
||||
|
||||
// 400
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: fmt.Sprintf("%s/%d", robotPath, 0),
|
||||
credential: projAdmin4Robot,
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
|
||||
// 404
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: fmt.Sprintf("%s/%d", robotPath, 10000),
|
||||
credential: projAdmin4Robot,
|
||||
},
|
||||
code: http.StatusNotFound,
|
||||
},
|
||||
|
||||
// 403 non-member user
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: fmt.Sprintf("%s/%d", robotPath, 1),
|
||||
credential: nonSysAdmin,
|
||||
},
|
||||
code: http.StatusForbidden,
|
||||
},
|
||||
|
||||
// 403 developer
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: fmt.Sprintf("%s/%d", robotPath, 1),
|
||||
credential: projDeveloper,
|
||||
},
|
||||
code: http.StatusForbidden,
|
||||
},
|
||||
|
||||
// 200
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodPut,
|
||||
url: fmt.Sprintf("%s/%d", robotPath, 1),
|
||||
bodyJSON: &models.Robot{
|
||||
Disabled: true,
|
||||
},
|
||||
credential: projAdmin4Robot,
|
||||
},
|
||||
code: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
runCodeCheckingCases(t, cases...)
|
||||
}
|
||||
|
||||
func TestRobotAPIDelete(t *testing.T) {
|
||||
cases := []*codeCheckingCase{
|
||||
// 401
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodDelete,
|
||||
url: fmt.Sprintf("%s/%d", robotPath, 1),
|
||||
},
|
||||
code: http.StatusUnauthorized,
|
||||
},
|
||||
|
||||
// 400
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodDelete,
|
||||
url: fmt.Sprintf("%s/%d", robotPath, 0),
|
||||
credential: projAdmin4Robot,
|
||||
},
|
||||
code: http.StatusBadRequest,
|
||||
},
|
||||
|
||||
// 404
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodDelete,
|
||||
url: fmt.Sprintf("%s/%d", robotPath, 10000),
|
||||
credential: projAdmin4Robot,
|
||||
},
|
||||
code: http.StatusNotFound,
|
||||
},
|
||||
|
||||
// 403 non-member user
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodDelete,
|
||||
url: fmt.Sprintf("%s/%d", robotPath, 1),
|
||||
credential: nonSysAdmin,
|
||||
},
|
||||
code: http.StatusForbidden,
|
||||
},
|
||||
|
||||
// 403 developer
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodDelete,
|
||||
url: fmt.Sprintf("%s/%d", robotPath, 1),
|
||||
credential: projDeveloper,
|
||||
},
|
||||
code: http.StatusForbidden,
|
||||
},
|
||||
|
||||
// 200
|
||||
{
|
||||
request: &testingRequest{
|
||||
method: http.MethodDelete,
|
||||
url: fmt.Sprintf("%s/%d", robotPath, 1),
|
||||
credential: projAdmin4Robot,
|
||||
},
|
||||
code: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
runCodeCheckingCases(t, cases...)
|
||||
}
|
@ -17,6 +17,7 @@ package api
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
common_http "github.com/goharbor/harbor/src/common/http"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/utils"
|
||||
|
||||
@ -54,7 +55,9 @@ func (sj *ScanJobAPI) Prepare() {
|
||||
sj.CustomAbort(http.StatusInternalServerError, "Failed to get Job data")
|
||||
}
|
||||
projectName := strings.SplitN(data.Repository, "/", 2)[0]
|
||||
if !sj.SecurityCtx.HasReadPerm(projectName) {
|
||||
|
||||
resource := rbac.NewProjectNamespace(projectName).Resource(rbac.ResourceRepositoryTagScanJob)
|
||||
if !sj.SecurityCtx.Can(rbac.ActionRead, resource) {
|
||||
log.Errorf("User does not have read permission for project: %s", projectName)
|
||||
sj.HandleForbidden(sj.SecurityCtx.GetUsername())
|
||||
}
|
||||
|
@ -16,14 +16,12 @@ package api
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils/test"
|
||||
"k8s.io/helm/cmd/helm/search"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
@ -182,43 +180,30 @@ func TestSearch(t *testing.T) {
|
||||
_, exist = repositories["search-2/hello-world"]
|
||||
assert.True(t, exist)
|
||||
|
||||
currentAdminServerURL, ok := os.LookupEnv("ADMINSERVER_URL")
|
||||
if ok {
|
||||
chartSettings := map[string]interface{}{
|
||||
common.WithChartMuseum: true,
|
||||
}
|
||||
adminServer, err := test.NewAdminserver(chartSettings)
|
||||
if err != nil {
|
||||
t.Fatal(nil)
|
||||
}
|
||||
defer adminServer.Close()
|
||||
|
||||
if err := config.InitByURL(adminServer.URL); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
// reset config
|
||||
if err := config.InitByURL(currentAdminServerURL); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Search chart
|
||||
err = handleAndParse(&testingRequest{
|
||||
method: http.MethodGet,
|
||||
url: "/api/search",
|
||||
queryStruct: struct {
|
||||
Keyword string `url:"q"`
|
||||
}{
|
||||
Keyword: "harbor",
|
||||
},
|
||||
credential: sysAdmin,
|
||||
}, result)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, len(result.Chart))
|
||||
require.Equal(t, "library/harbor", result.Chart[0].Name)
|
||||
|
||||
// Restore chart search handler
|
||||
searchHandler = nil
|
||||
chartSettings := map[string]interface{}{
|
||||
common.WithChartMuseum: true,
|
||||
}
|
||||
config.InitWithSettings(chartSettings)
|
||||
defer func() {
|
||||
// reset config
|
||||
config.Init()
|
||||
}()
|
||||
|
||||
// Search chart
|
||||
err = handleAndParse(&testingRequest{
|
||||
method: http.MethodGet,
|
||||
url: "/api/search",
|
||||
queryStruct: struct {
|
||||
Keyword string `url:"q"`
|
||||
}{
|
||||
Keyword: "harbor",
|
||||
},
|
||||
credential: sysAdmin,
|
||||
}, result)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, len(result.Chart))
|
||||
require.Equal(t, "library/harbor", result.Chart[0].Name)
|
||||
|
||||
// Restore chart search handler
|
||||
searchHandler = nil
|
||||
}
|
||||
|
@ -16,9 +16,10 @@ package api
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetVolumeInfo(t *testing.T) {
|
||||
@ -53,6 +54,9 @@ func TestGetVolumeInfo(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetGeneralInfo(t *testing.T) {
|
||||
config.Upload(map[string]interface{}{
|
||||
common.ReadOnly: false,
|
||||
})
|
||||
apiTest := newHarborAPI()
|
||||
code, body, err := apiTest.GetGeneralInfo()
|
||||
assert := assert.New(t)
|
||||
|
@ -24,6 +24,8 @@ import (
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/rbac/project"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
@ -339,6 +341,57 @@ func (ua *UserAPI) ToggleUserAdminRole() {
|
||||
}
|
||||
}
|
||||
|
||||
// ListUserPermissions handles GET to /api/users/{}/permissions
|
||||
func (ua *UserAPI) ListUserPermissions() {
|
||||
if ua.userID != ua.currentUserID {
|
||||
log.Warningf("Current user, id: %d can not view other user's permissions", ua.currentUserID)
|
||||
ua.RenderError(http.StatusForbidden, "User does not have permission")
|
||||
return
|
||||
}
|
||||
|
||||
relative := ua.Ctx.Input.Query("relative") == "true"
|
||||
|
||||
scope := rbac.Resource(ua.Ctx.Input.Query("scope"))
|
||||
policies := []*rbac.Policy{}
|
||||
|
||||
namespace, err := scope.GetNamespace()
|
||||
if err == nil {
|
||||
switch namespace.Kind() {
|
||||
case "project":
|
||||
for _, policy := range project.GetAllPolicies(namespace) {
|
||||
if ua.SecurityCtx.Can(policy.Action, policy.Resource) {
|
||||
policies = append(policies, policy)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results := []map[string]string{}
|
||||
for _, policy := range policies {
|
||||
var resource rbac.Resource
|
||||
|
||||
// for resource `/project/1/repository` if `relative` is `true` then the resource in response will be `repository`
|
||||
if relative {
|
||||
relativeResource, err := policy.Resource.RelativeTo(scope)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resource = relativeResource
|
||||
} else {
|
||||
resource = policy.Resource
|
||||
}
|
||||
|
||||
results = append(results, map[string]string{
|
||||
"resource": resource.String(),
|
||||
"action": policy.Action.String(),
|
||||
})
|
||||
}
|
||||
|
||||
ua.Data["json"] = results
|
||||
ua.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// modifiable returns whether the modify is allowed based on current auth mode and context
|
||||
func (ua *UserAPI) modifiable() bool {
|
||||
if ua.AuthMode == common.DBAuth {
|
||||
|
@ -27,6 +27,8 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/astaxie/beego"
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
)
|
||||
|
||||
var testUser0002ID, testUser0003ID int
|
||||
@ -39,7 +41,9 @@ func TestUsersPost(t *testing.T) {
|
||||
|
||||
assert := assert.New(t)
|
||||
apiTest := newHarborAPI()
|
||||
|
||||
config.Upload(map[string]interface{}{
|
||||
common.AUTHMode: "db_auth",
|
||||
})
|
||||
// case 1: register a new user without admin auth, expect 400, because self registration is on
|
||||
fmt.Println("Register user without admin auth")
|
||||
code, err := apiTest.UsersPost(testUser0002)
|
||||
@ -572,3 +576,28 @@ func TestModifiable(t *testing.T) {
|
||||
}
|
||||
assert.True(ua4.modifiable())
|
||||
}
|
||||
|
||||
func TestUsersCurrentPermissions(t *testing.T) {
|
||||
fmt.Println("Testing Get Users Current Permissions")
|
||||
|
||||
assert := assert.New(t)
|
||||
apiTest := newHarborAPI()
|
||||
|
||||
httpStatusCode, permissions, err := apiTest.UsersGetPermissions("current", "/project/library", *projAdmin)
|
||||
assert.Nil(err)
|
||||
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
|
||||
assert.NotEmpty(permissions, "permissions should not be empty")
|
||||
|
||||
httpStatusCode, permissions, err = apiTest.UsersGetPermissions("current", "/unsupport-scope", *projAdmin)
|
||||
assert.Nil(err)
|
||||
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
|
||||
assert.Empty(permissions, "permissions should be empty")
|
||||
|
||||
httpStatusCode, _, err = apiTest.UsersGetPermissions(projAdminID, "/project/library", *projAdmin)
|
||||
assert.Nil(err)
|
||||
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
|
||||
|
||||
httpStatusCode, _, err = apiTest.UsersGetPermissions(projDeveloperID, "/project/library", *projAdmin)
|
||||
assert.Nil(err)
|
||||
assert.Equal(int(403), httpStatusCode, "httpStatusCode should be 403")
|
||||
}
|
||||
|
@ -17,34 +17,12 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var l = NewUserLock(2 * time.Second)
|
||||
|
||||
var adminServerLdapTestConfig = map[string]interface{}{
|
||||
common.ExtEndpoint: "host01.com",
|
||||
common.AUTHMode: "ldap_auth",
|
||||
common.DatabaseType: "postgresql",
|
||||
common.PostGreSQLHOST: "127.0.0.1",
|
||||
common.PostGreSQLPort: 5432,
|
||||
common.PostGreSQLUsername: "postgres",
|
||||
common.PostGreSQLPassword: "root123",
|
||||
common.PostGreSQLDatabase: "registry",
|
||||
common.LDAPURL: "ldap://127.0.0.1",
|
||||
common.LDAPSearchDN: "cn=admin,dc=example,dc=com",
|
||||
common.LDAPSearchPwd: "admin",
|
||||
common.LDAPBaseDN: "dc=example,dc=com",
|
||||
common.LDAPUID: "uid",
|
||||
common.LDAPFilter: "",
|
||||
common.LDAPScope: 3,
|
||||
common.LDAPTimeout: 30,
|
||||
common.CfgExpiration: 5,
|
||||
common.AdminInitialPassword: "password",
|
||||
}
|
||||
|
||||
func TestLock(t *testing.T) {
|
||||
t.Log("Locking john")
|
||||
l.Lock("john")
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user