Apply project level policies to standalone Harbor

The following features are only enabled in integration mode, this commit moves
these to standalone Harbor:
 - Content trust policy: only signed images can be pulled
 - Vulnerability policy: only images whose severity is below the threshold can be pulled
 - Automatic scan policy: automatic scan pushed images
This commit is contained in:
Wenkai Yin 2017-09-29 16:37:43 +08:00
parent a1b8969793
commit 66b2d0d3f3
43 changed files with 690 additions and 953 deletions

View File

@ -167,6 +167,38 @@ paths:
description: User need to log in first.
'500':
description: Internal errors.
put:
summary: Update properties for a selected project.
description: |
This endpoint is aimed to update the properties of a project.
parameters:
- name: project_id
in: path
type: integer
format: int64
required: true
description: Selected project ID.
- name: project
in: body
required: true
schema:
$ref: '#/definitions/Project'
description: Updates of project.
tags:
- Products
responses:
'200':
description: Updated project properties successfully.
'400':
description: Illegal format of provided ID value.
'401':
description: User need to log in first.
'403':
description: User does not have permission to the project.
'404':
description: Project ID does not exist.
'500':
description: Unexpected internal errors.
delete:
summary: Delete project by projectID
description: |
@ -193,39 +225,6 @@ paths:
description: 'Project contains policies, can not be deleted.'
'500':
description: Internal errors.
'/projects/{project_id}/publicity':
put:
summary: Update properties for a selected project.
description: |
This endpoint is aimed to toggle a project publicity status.
parameters:
- name: project_id
in: path
type: integer
format: int64
required: true
description: Selected project ID.
- name: project
in: body
required: true
schema:
$ref: '#/definitions/Project'
description: Updates of project.
tags:
- Products
responses:
'200':
description: Updated project publicity status successfully.
'400':
description: Illegal format of provided ID value.
'401':
description: User need to log in first.
'403':
description: User does not have permission to the project.
'404':
description: Project ID does not exist.
'500':
description: Unexpected internal errors.
'/projects/{project_id}/logs':
get:
summary: Get access logs accompany with a relevant project.
@ -2016,10 +2015,6 @@ definitions:
owner_name:
type: string
description: The owner name of the project.
public:
type: integer
format: int
description: The public status of the project.
Togglable:
type: boolean
description: >-
@ -2031,6 +2026,18 @@ definitions:
repo_count:
type: integer
description: The number of the repositories under this project.
metadata:
type: object
description: The metadata of the project.
items:
$ref: '#/definitions/ProjectMetadata'
ProjectMetadata:
type: object
properties:
public:
type: integer
format: int
description: The public status of the project.
enable_content_trust:
type: boolean
description: >-

View File

@ -73,14 +73,13 @@ create table project (
creation_time timestamp,
update_time timestamp,
deleted tinyint (1) DEFAULT 0 NOT NULL,
public tinyint (1) DEFAULT 0 NOT NULL,
primary key (project_id),
FOREIGN KEY (owner_id) REFERENCES user(user_id),
UNIQUE (name)
);
insert into project (owner_id, name, creation_time, update_time, public) values
(1, 'library', NOW(), NOW(), 1);
insert into project (owner_id, name, creation_time, update_time) values
(1, 'library', NOW(), NOW());
create table project_member (
project_id int NOT NULL,

View File

@ -71,13 +71,12 @@ create table project (
creation_time timestamp,
update_time timestamp,
deleted tinyint (1) DEFAULT 0 NOT NULL,
public tinyint (1) DEFAULT 0 NOT NULL,
FOREIGN KEY (owner_id) REFERENCES user(user_id),
UNIQUE (name)
);
insert into project (owner_id, name, creation_time, update_time, public) values
(1, 'library', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 1);
insert into project (owner_id, name, creation_time, update_time) values
(1, 'library', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
create table project_member (
project_id int NOT NULL,

View File

@ -420,19 +420,6 @@ func TestChangeUserPasswordWithIncorrectOldPassword(t *testing.T) {
}
}
func TestQueryRelevantProjectsWhenNoProjectAdded(t *testing.T) {
projects, err := GetHasReadPermProjects(currentUser.Username)
if err != nil {
t.Errorf("Error occurred in QueryRelevantProjects: %v", err)
}
if len(projects) != 1 {
t.Errorf("Expected only one project in DB, but actual: %d", len(projects))
}
if projects[0].Name != "library" {
t.Errorf("There name of the project does not match, expected: %s, actual: %s", "library", projects[0].Name)
}
}
func TestAddProject(t *testing.T) {
project := models.Project{
@ -657,43 +644,6 @@ func TestGetUserByProject(t *testing.T) {
}
func TestToggleProjectPublicity(t *testing.T) {
err := ToggleProjectPublicity(currentProject.ProjectID, publicityOn)
if err != nil {
t.Errorf("Error occurred in ToggleProjectPublicity: %v", err)
}
currentProject, err = GetProjectByName(projectName)
if err != nil {
t.Errorf("Error occurred in GetProjectByName: %v", err)
}
if currentProject.Public != publicityOn {
t.Errorf("project, id: %d, its publicity is not on", currentProject.ProjectID)
}
err = ToggleProjectPublicity(currentProject.ProjectID, publicityOff)
if err != nil {
t.Errorf("Error occurred in ToggleProjectPublicity: %v", err)
}
currentProject, err = GetProjectByName(projectName)
if err != nil {
t.Errorf("Error occurred in GetProjectByName: %v", err)
}
if currentProject.Public != publicityOff {
t.Errorf("project, id: %d, its publicity is not off", currentProject.ProjectID)
}
}
/*
func TestIsProjectPublic(t *testing.T) {
if isPublic := IsProjectPublic(projectName); isPublic {
t.Errorf("project, id: %d, its publicity is not false after turning off", currentProject.ProjectID)
}
}
*/
func TestGetUserProjectRoles(t *testing.T) {
r, err := GetUserProjectRoles(currentUser.UserID, currentProject.ProjectID)
if err != nil {
@ -710,17 +660,6 @@ func TestGetUserProjectRoles(t *testing.T) {
}
}
/*
func TestProjectPermission(t *testing.T) {
roleCode, err := GetPermission(currentUser.Username, currentProject.Name)
if err != nil {
t.Errorf("Error occurred in GetPermission: %v", err)
}
if roleCode != "MDRWS" {
t.Errorf("The expected role code is MDRWS,but actual: %s", roleCode)
}
}
*/
func TestGetTotalOfProjects(t *testing.T) {
total, err := GetTotalOfProjects(nil)
if err != nil {
@ -745,22 +684,6 @@ func TestGetProjects(t *testing.T) {
}
}
func TestGetPublicProjects(t *testing.T) {
value := true
projects, err := GetProjects(&models.ProjectQueryParam{
Public: &value,
})
if err != nil {
t.Errorf("Error occurred in getProjects: %v", err)
}
if len(projects) != 1 {
t.Errorf("Expected length of projects is 1, but actual: %d, the projects: %+v", len(projects), projects)
}
if projects[0].Name != "library" {
t.Errorf("Expected project name in the list: %s, actual: %s", "library", projects[0].Name)
}
}
func TestAddProjectMember(t *testing.T) {
err := AddProjectMember(currentProject.ProjectID, 1, models.DEVELOPER)
if err != nil {

View File

@ -91,3 +91,12 @@ func paramPlaceholder(n int) string {
}
return strings.Join(placeholders, ",")
}
// ListProjectMetadata ...
func ListProjectMetadata(name, value string) ([]*models.ProjectMetadata, error) {
sql := `select * from project_metadata
where name = ? and value = ? and deleted = 0`
metadatas := []*models.ProjectMetadata{}
_, err := GetOrmer().Raw(sql, name, value).QueryRows(&metadatas)
return metadatas, err
}

View File

@ -64,6 +64,12 @@ func TestProMetaDaoMethods(t *testing.T) {
assert.Equal(t, value1, m[name1].Value)
assert.Equal(t, value2, m[name2].Value)
// test list
metas, err = ListProjectMetadata(name1, value1)
require.Nil(t, err)
assert.Equal(t, 1, len(metas))
assert.Equal(t, int64(1), metas[0].ProjectID)
// test update
newValue1 := "new_value1"
meta1.Value = newValue1

View File

@ -30,13 +30,13 @@ import (
func AddProject(project models.Project) (int64, error) {
o := GetOrmer()
p, err := o.Raw("insert into project (owner_id, name, creation_time, update_time, deleted, public) values (?, ?, ?, ?, ?, ?)").Prepare()
p, err := o.Raw("insert into project (owner_id, name, creation_time, update_time, deleted) values (?, ?, ?, ?, ?)").Prepare()
if err != nil {
return 0, err
}
now := time.Now()
r, err := p.Exec(project.OwnerID, project.Name, now, now, project.Deleted, project.Public)
r, err := p.Exec(project.OwnerID, project.Name, now, now, project.Deleted)
if err != nil {
return 0, err
}
@ -50,49 +50,11 @@ func AddProject(project models.Project) (int64, error) {
return projectID, err
}
/*
// IsProjectPublic ...
func IsProjectPublic(projectName string) bool {
project, err := GetProjectByName(projectName)
if err != nil {
log.Errorf("Error occurred in GetProjectByName: %v", err)
return false
}
if project == nil {
return false
}
return project.Public == 1
}
//ProjectExists returns whether the project exists according to its name of ID.
func ProjectExists(nameOrID interface{}) (bool, error) {
o := GetOrmer()
type dummy struct{}
sql := `select project_id from project where deleted = 0 and `
switch nameOrID.(type) {
case int64:
sql += `project_id = ?`
case string:
sql += `name = ?`
default:
return false, fmt.Errorf("Invalid nameOrId: %v", nameOrID)
}
var d []dummy
num, err := o.Raw(sql, nameOrID).QueryRows(&d)
if err != nil {
return false, err
}
return num > 0, nil
}
*/
// GetProjectByID ...
func GetProjectByID(id int64) (*models.Project, error) {
o := GetOrmer()
sql := `select p.project_id, p.name, u.username as owner_name, p.owner_id, p.creation_time, p.update_time, p.public
sql := `select p.project_id, p.name, u.username as owner_name, p.owner_id, p.creation_time, p.update_time
from project p left join user u on p.owner_id = u.user_id where p.deleted = 0 and p.project_id = ?`
queryParam := make([]interface{}, 1)
queryParam = append(queryParam, id)
@ -127,94 +89,18 @@ func GetProjectByName(name string) (*models.Project, error) {
return &p[0], nil
}
/*
// GetPermission gets roles that the user has according to the project.
func GetPermission(username, projectName string) (string, error) {
o := GetOrmer()
sql := `select r.role_code from role as r
inner join project_member as pm on r.role_id = pm.role
inner join user as u on u.user_id = pm.user_id
inner join project p on p.project_id = pm.project_id
where u.username = ? and p.name = ? and u.deleted = 0 and p.deleted = 0`
var r []models.Role
n, err := o.Raw(sql, username, projectName).QueryRows(&r)
if err != nil {
return "", err
}
if n == 0 {
return "", nil
}
return r[0].RoleCode, nil
}
*/
// ToggleProjectPublicity toggles the publicity of the project.
func ToggleProjectPublicity(projectID int64, publicity int) error {
o := GetOrmer()
sql := "update project set public = ? where project_id = ?"
_, err := o.Raw(sql, publicity, projectID).Exec()
return err
}
// GetHasReadPermProjects returns a project list,
// which satisfies the following conditions:
// 1. the project is not deleted
// 2. the prject is public or the user is a member of the project
func GetHasReadPermProjects(username string) ([]*models.Project, error) {
user, err := GetUser(models.User{
Username: username,
})
if err != nil {
return nil, err
}
o := GetOrmer()
sql :=
`select distinct p.project_id, p.name, p.public,
p.owner_id, p.creation_time, p.update_time
from project p
left join project_member pm
on p.project_id = pm.project_id
where (pm.user_id = ? or p.public = 1)
and p.deleted = 0 `
var projects []*models.Project
if _, err := o.Raw(sql, user.UserID).QueryRows(&projects); err != nil {
return nil, err
}
return projects, nil
}
// GetTotalOfProjects returns the total count of projects
// according to the query conditions
func GetTotalOfProjects(query *models.ProjectQueryParam, base ...*models.BaseProjectCollection) (int64, error) {
var (
owner string
name string
public *bool
member string
role int
)
func GetTotalOfProjects(query *models.ProjectQueryParam) (int64, error) {
var pagination *models.Pagination
if query != nil {
owner = query.Owner
name = query.Name
public = query.Public
if query.Member != nil {
member = query.Member.Name
role = query.Member.Role
}
pagination = query.Pagination
query.Pagination = nil
}
sql, params := projectQueryConditions(query)
if query != nil {
query.Pagination = pagination
}
sql, params := projectQueryConditions(owner, name, public, member, role, base...)
sql = `select count(*) ` + sql
@ -224,86 +110,39 @@ func GetTotalOfProjects(query *models.ProjectQueryParam, base ...*models.BasePro
}
// GetProjects returns a project list according to the query conditions
func GetProjects(query *models.ProjectQueryParam, base ...*models.BaseProjectCollection) ([]*models.Project, error) {
func GetProjects(query *models.ProjectQueryParam) ([]*models.Project, error) {
sql, params := projectQueryConditions(query)
var (
owner string
name string
public *bool
member string
role int
page int64
size int64
)
if query != nil {
owner = query.Owner
name = query.Name
public = query.Public
if query.Member != nil {
member = query.Member.Name
role = query.Member.Role
}
if query.Pagination != nil {
page = query.Pagination.Page
size = query.Pagination.Size
}
}
sql, params := projectQueryConditions(owner, name, public, member, role, base...)
sql = `select distinct p.project_id, p.name, p.public, p.owner_id,
sql = `select distinct p.project_id, p.name, p.owner_id,
p.creation_time, p.update_time ` + sql
if size > 0 {
sql += ` limit ?`
params = append(params, size)
if page > 0 {
sql += ` offset ?`
params = append(params, (page-1)*size)
}
}
var projects []*models.Project
_, err := GetOrmer().Raw(sql, params).QueryRows(&projects)
return projects, err
}
func projectQueryConditions(owner, name string, public *bool, member string,
role int, base ...*models.BaseProjectCollection) (string, []interface{}) {
func projectQueryConditions(query *models.ProjectQueryParam) (string, []interface{}) {
params := []interface{}{}
// the base project collections:
// 1. all projects
// 2. public projects
// 3. public projects and projects which the user is a member of
collection := `project `
if len(base) != 0 && base[0] != nil {
if len(base[0].Member) == 0 && base[0].Public {
collection = `(select * from project pr
where pr.public=1) `
}
if len(base[0].Member) > 0 && base[0].Public {
collection = `(select pr.project_id, pr.owner_id, pr.name, pr.
creation_time, pr.update_time, pr.deleted, pr.public
from project pr
join project_member prm
on pr.project_id = prm.project_id
join user ur
on prm.user_id=ur.user_id
where ur.username=? or pr.public=1 )`
params = append(params, base[0].Member)
}
sql := ` from project as p`
if query == nil {
sql += ` where p.deleted=0 order by p.name`
return sql, params
}
sql := ` from ` + collection + ` as p`
// if query.ProjectIDs is not nil but has no element, the query will returns no rows
if query.ProjectIDs != nil && len(query.ProjectIDs) == 0 {
sql += ` where 1 = 0`
return sql, params
}
if len(owner) != 0 {
if len(query.Owner) != 0 {
sql += ` join user u1
on p.owner_id = u1.user_id`
}
if len(member) != 0 {
if query.Member != nil && len(query.Member.Name) != 0 {
sql += ` join project_member pm
on p.project_id = pm.project_id
join user u2
@ -311,33 +150,24 @@ func projectQueryConditions(owner, name string, public *bool, member string,
}
sql += ` where p.deleted=0`
if len(owner) != 0 {
if len(query.Owner) != 0 {
sql += ` and u1.username=?`
params = append(params, owner)
params = append(params, query.Owner)
}
if len(name) != 0 {
if len(query.Name) != 0 {
sql += ` and p.name like ?`
params = append(params, "%"+escape(name)+"%")
params = append(params, "%"+escape(query.Name)+"%")
}
if public != nil {
sql += ` and p.public = ?`
if *public {
params = append(params, 1)
} else {
params = append(params, 0)
}
}
if len(member) != 0 {
if query.Member != nil && len(query.Member.Name) != 0 {
sql += ` and u2.username=?`
params = append(params, member)
params = append(params, query.Member.Name)
if role > 0 {
if query.Member.Role > 0 {
sql += ` and pm.role = ?`
roleID := 0
switch role {
switch query.Member.Role {
case common.RoleProjectAdmin:
roleID = 1
case common.RoleDeveloper:
@ -350,8 +180,24 @@ func projectQueryConditions(owner, name string, public *bool, member string,
}
}
if len(query.ProjectIDs) > 0 {
sql += fmt.Sprintf(` and p.project_id in ( %s )`,
paramPlaceholder(len(query.ProjectIDs)))
params = append(params, query.ProjectIDs)
}
sql += ` order by p.name`
if query.Pagination != nil && query.Pagination.Size > 0 {
sql += ` limit ?`
params = append(params, query.Pagination.Size)
if query.Pagination.Page > 0 {
sql += ` offset ?`
params = append(params, (query.Pagination.Page-1)*query.Pagination.Size)
}
}
return sql, params
}

View File

@ -103,7 +103,6 @@ func TestGetTopRepos(t *testing.T) {
project1 := models.Project{
OwnerID: 1,
Name: "project1",
Public: 0,
}
project1.ProjectID, err = AddProject(project1)
require.NoError(err)
@ -112,7 +111,6 @@ func TestGetTopRepos(t *testing.T) {
project2 := models.Project{
OwnerID: 1,
Name: "project2",
Public: 0,
}
project2.ProjectID, err = AddProject(project2)
require.NoError(err)
@ -232,7 +230,6 @@ func TestGetAllRepositories(t *testing.T) {
project1 := models.Project{
OwnerID: 1,
Name: "projectRepo",
Public: 0,
}
var err2 error
project1.ProjectID, err2 = AddProject(project1)

View File

@ -18,13 +18,17 @@ import (
"time"
)
// keys of project metadata
// keys of project metadata and severity values
const (
ProMetaPublic = "public"
ProMetaEnableContentTrust = "enable_content_trust"
ProMetaPreventVul = "prevent_vul"
ProMetaPreventVul = "prevent_vul" //prevent vulnerable images from being pulled
ProMetaSeverity = "severity"
ProMetaAutoScan = "auto_scan"
SeverityNone = "negligible"
SeverityLow = "low"
SeverityMedium = "medium"
SeverityHigh = "high"
)
// ProjectMetadata holds the metadata of a project.

View File

@ -15,31 +15,91 @@
package models
import (
"strings"
"time"
)
// Project holds the details of a project.
// TODO remove useless attrs
type Project struct {
ProjectID int64 `orm:"pk;auto;column(project_id)" json:"project_id"`
OwnerID int `orm:"column(owner_id)" json:"owner_id"`
Name string `orm:"column(name)" json:"name"`
CreationTime time.Time `orm:"column(creation_time)" json:"creation_time"`
UpdateTime time.Time `orm:"update_time" json:"update_time"`
Deleted int `orm:"column(deleted)" json:"deleted"`
CreationTimeStr string `orm:"-" json:"creation_time_str"`
OwnerName string `orm:"-" json:"owner_name"`
Togglable bool `orm:"-"`
Role int `orm:"-" json:"current_user_role_id"`
RepoCount int `orm:"-" json:"repo_count"`
Metadata map[string]string `orm:"-" json:"metadata"`
ProjectID int64 `orm:"pk;auto;column(project_id)" json:"project_id"`
OwnerID int `orm:"column(owner_id)" json:"owner_id"`
Name string `orm:"column(name)" json:"name"`
CreationTime time.Time `orm:"column(creation_time)" json:"creation_time"`
UpdateTime time.Time `orm:"update_time" json:"update_time"`
Deleted int `orm:"column(deleted)" json:"deleted"`
OwnerName string `orm:"-" json:"owner_name"`
Togglable bool `orm:"-" json:"togglable"`
Role int `orm:"-" json:"current_user_role_id"`
RepoCount int `orm:"-" json:"repo_count"`
Metadata map[string]string `orm:"-" json:"metadata"`
}
// TODO remove
Public int `orm:"column(public)" json:"public"`
EnableContentTrust bool `orm:"-" json:"enable_content_trust"`
PreventVulnerableImagesFromRunning bool `orm:"-" json:"prevent_vulnerable_images_from_running"`
PreventVulnerableImagesFromRunningSeverity string `orm:"-" json:"prevent_vulnerable_images_from_running_severity"`
AutomaticallyScanImagesOnPush bool `orm:"-" json:"automatically_scan_images_on_push"`
// GetMetadata ...
func (p *Project) GetMetadata(key string) (string, bool) {
if len(p.Metadata) == 0 {
return "", false
}
value, exist := p.Metadata[key]
return value, exist
}
// SetMetadata ...
func (p *Project) SetMetadata(key, value string) {
if p.Metadata == nil {
p.Metadata = map[string]string{}
}
p.Metadata[key] = value
}
// IsPublic ...
func (p *Project) IsPublic() bool {
public, exist := p.GetMetadata(ProMetaPublic)
if !exist {
return false
}
return isTrue(public)
}
// ContentTrustEnabled ...
func (p *Project) ContentTrustEnabled() bool {
enabled, exist := p.GetMetadata(ProMetaEnableContentTrust)
if !exist {
return false
}
return isTrue(enabled)
}
// VulPrevented ...
func (p *Project) VulPrevented() bool {
prevent, exist := p.GetMetadata(ProMetaPreventVul)
if !exist {
return false
}
return isTrue(prevent)
}
// Severity ...
func (p *Project) Severity() string {
severity, exist := p.GetMetadata(ProMetaSeverity)
if !exist {
return ""
}
return severity
}
// AutoScan ...
func (p *Project) AutoScan() bool {
auto, exist := p.GetMetadata(ProMetaAutoScan)
if !exist {
return false
}
return isTrue(auto)
}
func isTrue(value string) bool {
return strings.ToLower(value) == "true" ||
strings.ToLower(value) == "1"
}
// ProjectSorter holds an array of projects
@ -79,6 +139,7 @@ type ProjectQueryParam struct {
Public *bool // the project is public or not, can be ture, false and nil
Member *MemberQuery // the member of project
Pagination *Pagination // pagination information
ProjectIDs []int64 // project ID list
}
// MemberQuery fitler by member's username and role
@ -103,12 +164,9 @@ type BaseProjectCollection struct {
// ProjectRequest holds informations that need for creating project API
type ProjectRequest struct {
Name string `json:"project_name"`
Public int `json:"public"`
EnableContentTrust bool `json:"enable_content_trust"`
PreventVulnerableImagesFromRunning bool `json:"prevent_vulnerable_images_from_running"`
PreventVulnerableImagesFromRunningSeverity string `json:"prevent_vulnerable_images_from_running_severity"`
AutomaticallyScanImagesOnPush bool `json:"automatically_scan_images_on_push"`
Name string `json:"project_name"`
Public *int `json:"public"` //deprecated, reserved for project creation in replication
Metadata map[string]string `json:"metadata"`
}
// ProjectQueryResult ...

View File

@ -30,13 +30,13 @@ import (
func ParseClairSev(clairSev string) models.Severity {
sev := strings.ToLower(clairSev)
switch sev {
case "negligible":
case models.SeverityNone:
return models.SevNone
case "low":
case models.SeverityLow:
return models.SevLow
case "medium":
case models.SeverityMedium:
return models.SevMedium
case "high":
case models.SeverityHigh:
return models.SevHigh
default:
return models.SevUnknown

View File

@ -22,6 +22,7 @@ import (
"fmt"
"io/ioutil"
"net/http"
"strconv"
"strings"
"github.com/docker/distribution"
@ -258,13 +259,23 @@ func getProject(name string) (*models.Project, error) {
}
func (c *Checker) createProject(project *models.Project) error {
pro := &models.ProjectRequest{
Name: project.Name,
Public: project.Public,
EnableContentTrust: project.EnableContentTrust,
PreventVulnerableImagesFromRunning: project.PreventVulnerableImagesFromRunning,
PreventVulnerableImagesFromRunningSeverity: project.PreventVulnerableImagesFromRunningSeverity,
AutomaticallyScanImagesOnPush: project.AutomaticallyScanImagesOnPush,
// only replicate the public property of project
pro := struct {
models.ProjectRequest
Public int `json:"public"`
}{
ProjectRequest: models.ProjectRequest{
Name: project.Name,
Metadata: map[string]string{
models.ProMetaPublic: strconv.FormatBool(project.IsPublic()),
},
},
}
// put "public" property in both metadata and public field to keep compatibility
// with old version API(<=1.2.0)
if project.IsPublic() {
pro.Public = 1
}
data, err := json.Marshal(pro)

View File

@ -93,12 +93,11 @@ func init() {
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")
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/sysadmin", &UserAPI{}, "put:ToggleUserAdminRole")
beego.Router("/api/projects/:id/publicity", &ProjectAPI{}, "put:ToggleProjectPublic")
beego.Router("/api/projects/:id([0-9]+)/logs", &ProjectAPI{}, "get:Logs")
beego.Router("/api/projects/:id([0-9]+)/_deletable", &ProjectAPI{}, "get:Deletable")
beego.Router("/api/projects/:pid([0-9]+)/members/?:mid", &ProjectMemberAPI{}, "get:Get;post:Post;delete:Delete;put:Put")
@ -359,18 +358,10 @@ func (a testapi) ProjectsGet(query *apilib.ProjectQuery, authInfo ...usrInfo) (i
}
//Update properties for a selected project.
func (a testapi) ToggleProjectPublicity(prjUsr usrInfo, projectID string, ispublic int32) (int, error) {
// create path and map variables
path := "/api/projects/" + projectID + "/publicity/"
_sling := sling.New().Put(a.basePath)
_sling = _sling.Path(path)
type QueryParams struct {
Public int32 `json:"public,omitempty"`
}
_sling = _sling.BodyJSON(&QueryParams{Public: ispublic})
func (a testapi) ProjectsPut(prjUsr usrInfo, projectID string,
project *models.Project) (int, error) {
path := "/api/projects/" + projectID
_sling := sling.New().Put(a.basePath).Path(path).BodyJSON(project)
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, prjUsr)
return httpStatusCode, err

View File

@ -19,6 +19,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/tests/apitests/apilib"
)
@ -38,7 +39,7 @@ func TestLogGet(t *testing.T) {
fmt.Println("add the project first.")
project := apilib.ProjectReq{
ProjectName: "project_for_test_log",
Public: 1,
Metadata: map[string]string{models.ProMetaPublic: "true"},
}
reply, err := apiTest.ProjectsPost(*testUser, project)

View File

@ -18,6 +18,7 @@ import (
"fmt"
"net/http"
"regexp"
"strings"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/dao"
@ -120,14 +121,23 @@ func (p *ProjectAPI) Post() {
return
}
if pro.Metadata == nil {
pro.Metadata = map[string]string{}
}
// accept the "public" property to make replication work well with old versions(<=1.2.0)
if pro.Public != nil && len(pro.Metadata[models.ProMetaPublic]) == 0 {
pro.Metadata[models.ProMetaPublic] = strconv.FormatBool(*pro.Public == 1)
}
// populate public metadata as false if it isn't set
if _, ok := pro.Metadata[models.ProMetaPublic]; !ok {
pro.Metadata[models.ProMetaPublic] = strconv.FormatBool(false)
}
projectID, err := p.ProjectMgr.Create(&models.Project{
Name: pro.Name,
Public: pro.Public,
OwnerName: p.SecurityCtx.GetUsername(),
EnableContentTrust: pro.EnableContentTrust,
PreventVulnerableImagesFromRunning: pro.PreventVulnerableImagesFromRunning,
PreventVulnerableImagesFromRunningSeverity: pro.PreventVulnerableImagesFromRunningSeverity,
AutomaticallyScanImagesOnPush: pro.AutomaticallyScanImagesOnPush,
Name: pro.Name,
OwnerName: p.SecurityCtx.GetUsername(),
Metadata: pro.Metadata,
})
if err != nil {
if err == errutil.ErrDupProject {
@ -178,7 +188,7 @@ func (p *ProjectAPI) Head() {
// Get ...
func (p *ProjectAPI) Get() {
if p.project.Public == 0 {
if !p.project.IsPublic() {
if !p.SecurityCtx.IsAuthenticated() {
p.HandleUnauthorized()
return
@ -311,21 +321,52 @@ func (p *ProjectAPI) List() {
query.Public = &pub
}
// base project collection from which filter is done
base := &models.BaseProjectCollection{}
if !p.SecurityCtx.IsAuthenticated() {
// not login, only get public projects
base.Public = true
} else {
if !(p.SecurityCtx.IsSysAdmin() || p.SecurityCtx.IsSolutionUser()) {
// login, but not system admin, get public projects and
// projects that the user is member of
base.Member = p.SecurityCtx.GetUsername()
base.Public = true
// standalone, filter projects according to the privilleges of the user first
if !config.WithAdmiral() {
var projects []*models.Project
if !p.SecurityCtx.IsAuthenticated() {
// not login, only get public projects
pros, err := p.ProjectMgr.GetPublic()
if err != nil {
p.HandleInternalServerError(fmt.Sprintf("failed to get public projects: %v", err))
return
}
projects = []*models.Project{}
projects = append(projects, pros...)
} else {
if !(p.SecurityCtx.IsSysAdmin() || p.SecurityCtx.IsSolutionUser()) {
projects = []*models.Project{}
// login, but not system admin or solution user, get public projects and
// projects that the user is member of
pros, err := p.ProjectMgr.GetPublic()
if err != nil {
p.HandleInternalServerError(fmt.Sprintf("failed to get public projects: %v", err))
return
}
projects = append(projects, pros...)
mps, err := p.ProjectMgr.List(&models.ProjectQueryParam{
Member: &models.MemberQuery{
Name: p.SecurityCtx.GetUsername(),
},
})
if err != nil {
p.HandleInternalServerError(fmt.Sprintf("failed to list projects: %v", err))
return
}
projects = append(projects, mps.Projects...)
}
}
if projects != nil {
projectIDs := []int64{}
for _, project := range projects {
projectIDs = append(projectIDs, project.ProjectID)
}
query.ProjectIDs = projectIDs
}
}
result, err := p.ProjectMgr.List(query, base)
result, err := p.ProjectMgr.List(query)
if err != nil {
p.ParseAndHandleError("failed to list projects", err)
return
@ -358,8 +399,8 @@ func (p *ProjectAPI) List() {
p.ServeJSON()
}
// ToggleProjectPublic ...
func (p *ProjectAPI) ToggleProjectPublic() {
// Put ...
func (p *ProjectAPI) Put() {
if !p.SecurityCtx.IsAuthenticated() {
p.HandleUnauthorized()
return
@ -372,16 +413,10 @@ func (p *ProjectAPI) ToggleProjectPublic() {
var req *models.ProjectRequest
p.DecodeJSONReq(&req)
if req.Public != 0 && req.Public != 1 {
p.HandleBadRequest("public should be 0 or 1")
return
}
if err := p.ProjectMgr.Update(p.project.ProjectID,
&models.Project{
Metadata: map[string]string{
models.ProMetaPublic: strconv.Itoa(req.Public),
},
Metadata: req.Metadata,
}); err != nil {
p.ParseAndHandleError(fmt.Sprintf("failed to update project %d",
p.project.ProjectID), err)
@ -464,5 +499,39 @@ func validateProjectReq(req *models.ProjectRequest) error {
if !legal {
return fmt.Errorf("project name is not in lower case or contains illegal characters")
}
if req.Metadata != nil {
metas := req.Metadata
req.Metadata = map[string]string{}
boolMetas := []string{
models.ProMetaPublic,
models.ProMetaEnableContentTrust,
models.ProMetaPreventVul,
models.ProMetaAutoScan}
for _, boolMeta := range boolMetas {
value, exist := metas[boolMeta]
if exist {
b, err := strconv.ParseBool(value)
if err != nil {
log.Errorf("failed to parse %s to bool: %v", value, err)
b = false
}
req.Metadata[boolMeta] = strconv.FormatBool(b)
}
}
value, exist := metas[models.ProMetaSeverity]
if exist {
switch strings.ToLower(value) {
case models.SeverityHigh, models.SeverityMedium, models.SeverityLow, models.SeverityNone:
req.Metadata[models.ProMetaSeverity] = strings.ToLower(value)
default:
return fmt.Errorf("invalid severity %s", value)
}
}
}
return nil
}

View File

@ -31,7 +31,7 @@ var addProject *apilib.ProjectReq
var addPID int
func InitAddPro() {
addProject = &apilib.ProjectReq{"add_project", 1}
addProject = &apilib.ProjectReq{"add_project", map[string]string{models.ProMetaPublic: "true"}}
}
func TestAddProject(t *testing.T) {
@ -82,7 +82,7 @@ func TestAddProject(t *testing.T) {
//case 4: reponse code = 400 : Project name is illegal in length
fmt.Println("case 4 : reponse code = 400 : Project name is illegal in length ")
result, err = apiTest.ProjectsPost(*admin, apilib.ProjectReq{"t", 1})
result, err = apiTest.ProjectsPost(*admin, apilib.ProjectReq{"t", map[string]string{models.ProMetaPublic: "true"}})
if err != nil {
t.Error("Error while creat project", err.Error())
t.Log(err)
@ -112,7 +112,7 @@ func TestListProjects(t *testing.T) {
assert.Nil(err)
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
assert.Equal(addProject.ProjectName, result[0].ProjectName, "Project name is wrong")
assert.Equal(int32(1), result[0].Public, "Public is wrong")
assert.Equal("true", result[0].Metadata[models.ProMetaPublic], "Public is wrong")
//find add projectID
addPID = int(result[0].ProjectId)
@ -130,7 +130,7 @@ func TestListProjects(t *testing.T) {
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
assert.Equal(addProject.ProjectName, result[0].ProjectName, "Project name is wrong")
assert.Equal(int32(1), result[0].Public, "Public is wrong")
assert.Equal("true", result[0].Metadata[models.ProMetaPublic], "Public is wrong")
assert.Equal(int32(1), result[0].CurrentUserRoleId, "User project role is wrong")
}
@ -155,7 +155,7 @@ func TestListProjects(t *testing.T) {
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
assert.Equal(addProject.ProjectName, result[0].ProjectName, "Project name is wrong")
assert.Equal(int32(1), result[0].Public, "Public is wrong")
assert.Equal("true", result[0].Metadata[models.ProMetaPublic], "Public is wrong")
assert.Equal(int32(2), result[0].CurrentUserRoleId, "User project role is wrong")
}
id := strconv.Itoa(CommonGetUserID())
@ -187,7 +187,7 @@ func TestProGetByID(t *testing.T) {
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
assert.Equal(addProject.ProjectName, result.ProjectName, "ProjectName is wrong")
assert.Equal(int32(1), result.Public, "Public is wrong")
assert.Equal("true", result.Metadata[models.ProMetaPublic], "Public is wrong")
}
fmt.Printf("\n")
}
@ -273,48 +273,37 @@ func TestProHead(t *testing.T) {
fmt.Printf("\n")
}
func TestToggleProjectPublicity(t *testing.T) {
func TestPut(t *testing.T) {
fmt.Println("\nTest for Project PUT API: Update properties for a selected project")
assert := assert.New(t)
apiTest := newHarborAPI()
//-------------------case1: Response Code=200------------------------------//
project := &models.Project{
Metadata: map[string]string{
models.ProMetaPublic: "true",
},
}
fmt.Println("case 1: respose code:200")
httpStatusCode, err := apiTest.ToggleProjectPublicity(*admin, "1", 1)
if err != nil {
t.Error("Error while search project by proId", err.Error())
t.Log(err)
} else {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
}
//-------------------case2: Response Code=401 User need to log in first. ------------------------------//
code, err := apiTest.ProjectsPut(*admin, "1", project)
require.Nil(t, err)
assert.Equal(int(200), code)
fmt.Println("case 2: respose code:401, User need to log in first.")
httpStatusCode, err = apiTest.ToggleProjectPublicity(*unknownUsr, "1", 1)
if err != nil {
t.Error("Error while search project by proId", err.Error())
t.Log(err)
} else {
assert.Equal(int(401), httpStatusCode, "httpStatusCode should be 401")
}
//-------------------case3: Response Code=400 Invalid project id------------------------------//
code, err = apiTest.ProjectsPut(*unknownUsr, "1", project)
require.Nil(t, err)
assert.Equal(int(401), code)
fmt.Println("case 3: respose code:400, Invalid project id")
httpStatusCode, err = apiTest.ToggleProjectPublicity(*admin, "cc", 1)
if err != nil {
t.Error("Error while search project by proId", err.Error())
t.Log(err)
} else {
assert.Equal(int(400), httpStatusCode, "httpStatusCode should be 400")
}
//-------------------case4: Response Code=404 Not found the project------------------------------//
code, err = apiTest.ProjectsPut(*admin, "cc", project)
require.Nil(t, err)
assert.Equal(int(400), code)
fmt.Println("case 4: respose code:404, Not found the project")
httpStatusCode, err = apiTest.ToggleProjectPublicity(*admin, "1234", 1)
if err != nil {
t.Error("Error while search project by proId", err.Error())
t.Log(err)
} else {
assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404")
}
code, err = apiTest.ProjectsPut(*admin, "1234", project)
require.Nil(t, err)
assert.Equal(int(404), code)
fmt.Printf("\n")
}

View File

@ -147,7 +147,7 @@ func filterRepositories(projects []*models.Project, keyword string) (
entry["repository_name"] = r.Name
entry["project_name"] = projects[j].Name
entry["project_id"] = projects[j].ProjectID
entry["project_public"] = projects[j].Public
entry["project_public"] = projects[j].IsPublic()
entry["pull_count"] = r.PullCount
tags, err := getTags(r.Name)

View File

@ -37,7 +37,7 @@ func TestSearch(t *testing.T) {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
assert.Equal(int64(1), result.Projects[0].ProjectID, "Project id should be equal")
assert.Equal("library", result.Projects[0].Name, "Project name should be library")
assert.Equal(1, result.Projects[0].Public, "Project public status should be 1 (true)")
assert.True(result.Projects[0].IsPublic(), "Project public status should be 1 (true)")
}
//--------case 2 : Response Code = 200, sysAdmin and search repo--------//
@ -49,7 +49,7 @@ func TestSearch(t *testing.T) {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
assert.Equal("library", result.Repositories[0].ProjectName, "Project name should be library")
assert.Equal("library/docker", result.Repositories[0].RepositoryName, "Repository name should be library/docker")
assert.Equal(int32(1), result.Repositories[0].ProjectPublic, "Project public status should be 1 (true)")
assert.True(result.Repositories[0].ProjectPublic, "Project public status should be 1 (true)")
}
//--------case 3 : Response Code = 200, normal user and search repo--------//
@ -61,7 +61,7 @@ func TestSearch(t *testing.T) {
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
assert.Equal("library", result.Repositories[0].ProjectName, "Project name should be library")
assert.Equal("library/docker", result.Repositories[0].RepositoryName, "Repository name should be library/docker")
assert.Equal(int32(1), result.Repositories[0].ProjectPublic, "Project public status should be 1 (true)")
assert.True(result.Repositories[0].ProjectPublic, "Project public status should be 1 (true)")
}
}

View File

@ -15,7 +15,7 @@
package config
import (
//"crypto/tls"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
@ -108,38 +108,32 @@ func initSecretStore() {
func initProjectManager() {
var driver pmsdriver.PMSDriver
if WithAdmiral() {
// TODO add support for admiral
/*
// integration with admiral
log.Info("initializing the project manager based on PMS...")
// TODO read ca/cert file and pass it to the TLS config
AdmiralClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
// integration with admiral
log.Info("initializing the project manager based on PMS...")
// TODO read ca/cert file and pass it to the TLS config
AdmiralClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
},
}
path := os.Getenv("SERVICE_TOKEN_FILE_PATH")
if len(path) == 0 {
path = defaultTokenFilePath
}
log.Infof("service token file path: %s", path)
TokenReader = &admiral.FileTokenReader{
Path: path,
}
GlobalProjectMgr = admiral.NewProjectManager(AdmiralClient,
AdmiralEndpoint(), TokenReader)
*/
GlobalProjectMgr = nil
path := os.Getenv("SERVICE_TOKEN_FILE_PATH")
if len(path) == 0 {
path = defaultTokenFilePath
}
log.Infof("service token file path: %s", path)
TokenReader = &admiral.FileTokenReader{
Path: path,
}
driver = admiral.NewDriver(AdmiralClient, AdmiralEndpoint(), TokenReader)
} else {
// standalone
log.Info("initializing the project manager based on local database...")
driver = local.NewDriver()
// TODO move the statement out of the else block when admiral driver is completed
GlobalProjectMgr = promgr.NewDefaultProjectManager(driver, true)
}
GlobalProjectMgr = promgr.NewDefaultProjectManager(driver, true)
}

View File

@ -33,7 +33,7 @@ import (
"github.com/vmware/harbor/src/ui/auth"
"github.com/vmware/harbor/src/ui/config"
"github.com/vmware/harbor/src/ui/promgr"
//"github.com/vmware/harbor/src/ui/promgr/pmsdriver/admiral"
"github.com/vmware/harbor/src/ui/promgr/pmsdriver/admiral"
)
type key string
@ -264,15 +264,13 @@ func (t *tokenReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
return false
}
/*
log.Debug("creating PMS project manager...")
pm := admiral.NewProjectManager(config.AdmiralClient,
config.AdmiralEndpoint(), &admiral.RawTokenReader{
Token: token,
})
*/
// TODO create the DefaultProjectManager with the real admiral PMSDriver
pm := promgr.NewDefaultProjectManager(nil, false)
log.Debug("creating PMS project manager...")
driver := admiral.NewDriver(config.AdmiralClient,
config.AdmiralEndpoint(), &admiral.RawTokenReader{
Token: token,
})
pm := promgr.NewDefaultProjectManager(driver, false)
log.Debug("creating admiral security context...")
securCtx := admr.NewSecurityContext(authContext, pm)
@ -291,13 +289,10 @@ func (u *unauthorizedReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
var pm promgr.ProjectManager
if config.WithAdmiral() {
// integration with admiral
/*
log.Debug("creating PMS project manager...")
pm = admiral.NewProjectManager(config.AdmiralClient,
config.AdmiralEndpoint(), nil)
*/
// TODO create the DefaultProjectManager with the real admiral PMSDriver
pm = promgr.NewDefaultProjectManager(nil, false)
log.Debug("creating PMS project manager...")
driver := admiral.NewDriver(config.AdmiralClient,
config.AdmiralEndpoint(), nil)
pm = promgr.NewDefaultProjectManager(driver, false)
log.Debug("creating admiral security context...")
securCtx = admr.NewSecurityContext(nil, pm)
} else {

View File

@ -15,15 +15,13 @@
package metamgr
import (
"strconv"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
)
// ProjectMetadataManaegr defines the operations that a project metadata manager should
// ProjectMetadataManager defines the operations that a project metadata manager should
// implement
type ProjectMetadataManaegr interface {
type ProjectMetadataManager interface {
// Add metadatas for project specified by projectID
Add(projectID int64, meta map[string]string) error
// Delete metadatas whose keys are specified in parameter meta, if it
@ -34,16 +32,18 @@ type ProjectMetadataManaegr interface {
// Get metadatas whose keys are specified in parameter meta, if it is
// absent, get all
Get(projectID int64, meta ...string) (map[string]string, error)
// List metadata according to the name and value
List(name, value string) ([]*models.ProjectMetadata, error)
}
type defaultProjectMetadataManaegr struct{}
type defaultProjectMetadataManager struct{}
// NewDefaultProjectMetadataManager ...
func NewDefaultProjectMetadataManager() ProjectMetadataManaegr {
return &defaultProjectMetadataManaegr{}
func NewDefaultProjectMetadataManager() ProjectMetadataManager {
return &defaultProjectMetadataManager{}
}
func (d *defaultProjectMetadataManaegr) Add(projectID int64, meta map[string]string) error {
func (d *defaultProjectMetadataManager) Add(projectID int64, meta map[string]string) error {
for k, v := range meta {
proMeta := &models.ProjectMetadata{
ProjectID: projectID,
@ -57,11 +57,11 @@ func (d *defaultProjectMetadataManaegr) Add(projectID int64, meta map[string]str
return nil
}
func (d *defaultProjectMetadataManaegr) Delete(projectID int64, meta ...string) error {
func (d *defaultProjectMetadataManager) Delete(projectID int64, meta ...string) error {
return dao.DeleteProjectMetadata(projectID, meta...)
}
func (d *defaultProjectMetadataManaegr) Update(projectID int64, meta map[string]string) error {
func (d *defaultProjectMetadataManager) Update(projectID int64, meta map[string]string) error {
for k, v := range meta {
if err := dao.UpdateProjectMetadata(&models.ProjectMetadata{
ProjectID: projectID,
@ -72,20 +72,10 @@ func (d *defaultProjectMetadataManaegr) Update(projectID int64, meta map[string]
}
}
// TODO remove the logic
public, ok := meta[models.ProMetaPublic]
if ok {
i, err := strconv.Atoi(public)
if err != nil {
return err
}
return dao.ToggleProjectPublicity(projectID, i)
}
return nil
}
func (d *defaultProjectMetadataManaegr) Get(projectID int64, meta ...string) (map[string]string, error) {
func (d *defaultProjectMetadataManager) Get(projectID int64, meta ...string) (map[string]string, error) {
proMetas, err := dao.GetProjectMetadata(projectID, meta...)
if err != nil {
return nil, nil
@ -98,3 +88,14 @@ func (d *defaultProjectMetadataManaegr) Get(projectID int64, meta ...string) (ma
return m, nil
}
func (d *defaultProjectMetadataManager) List(name, value string) ([]*models.ProjectMetadata, error) {
metas := []*models.ProjectMetadata{}
mds, err := dao.ListProjectMetadata(name, value)
if err != nil {
return nil, err
}
metas = append(metas, mds...)
return metas, nil
}

View File

@ -54,6 +54,12 @@ func TestMetaMgrMethods(t *testing.T) {
assert.Equal(t, 1, len(m))
assert.Equal(t, value, m[key])
// test list
metas, err := mgr.List(key, value)
require.Nil(t, err)
assert.Equal(t, 1, len(metas))
assert.Equal(t, int64(1), metas[0].ProjectID)
// test update
require.Nil(t, mgr.Update(1, map[string]string{
key: newValue,

View File

@ -30,13 +30,12 @@ import (
"github.com/vmware/harbor/src/common/utils"
er "github.com/vmware/harbor/src/common/utils/error"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/ui/promgr/pmsdriver"
)
const dupProjectPattern = `Project name '\w+' is already used`
// ProjectManager implements projectmanager.ProjecdtManager interface
// base on project management service
type ProjectManager struct {
type driver struct {
client *http.Client
endpoint string
tokenReader TokenReader
@ -57,10 +56,10 @@ type project struct {
Guests []*user `json:"viewers"`
}
// NewProjectManager returns an instance of ProjectManager
func NewProjectManager(client *http.Client, endpoint string,
tokenReader TokenReader) *ProjectManager {
return &ProjectManager{
// NewDriver returns an instance of driver
func NewDriver(client *http.Client, endpoint string,
tokenReader TokenReader) pmsdriver.PMSDriver {
return &driver{
client: client,
endpoint: strings.TrimRight(endpoint, "/"),
tokenReader: tokenReader,
@ -68,8 +67,8 @@ func NewProjectManager(client *http.Client, endpoint string,
}
// Get ...
func (p *ProjectManager) Get(projectIDOrName interface{}) (*models.Project, error) {
project, err := p.get(projectIDOrName)
func (d *driver) Get(projectIDOrName interface{}) (*models.Project, error) {
project, err := d.get(projectIDOrName)
if err != nil {
return nil, err
}
@ -77,10 +76,10 @@ func (p *ProjectManager) Get(projectIDOrName interface{}) (*models.Project, erro
}
// get Admiral project with Harbor project ID or name
func (p *ProjectManager) get(projectIDOrName interface{}) (*project, error) {
func (d *driver) get(projectIDOrName interface{}) (*project, error) {
// if token is provided, search project from my projects list first
if len(p.getToken()) != 0 {
project, err := p.getFromMy(projectIDOrName)
if len(d.getToken()) != 0 {
project, err := d.getFromMy(projectIDOrName)
if err != nil {
return nil, err
}
@ -90,18 +89,18 @@ func (p *ProjectManager) get(projectIDOrName interface{}) (*project, error) {
}
// try to get project from public projects list
return p.getFromPublic(projectIDOrName)
return d.getFromPublic(projectIDOrName)
}
// call GET /projects?$filter=xxx eq xxx, the API can only filter projects
// which the user is a member of
func (p *ProjectManager) getFromMy(projectIDOrName interface{}) (*project, error) {
return p.getAdmiralProject(projectIDOrName, false)
func (d *driver) getFromMy(projectIDOrName interface{}) (*project, error) {
return d.getAdmiralProject(projectIDOrName, false)
}
// call GET /projects?public=true&$filter=xxx eq xxx
func (p *ProjectManager) getFromPublic(projectIDOrName interface{}) (*project, error) {
project, err := p.getAdmiralProject(projectIDOrName, true)
func (d *driver) getFromPublic(projectIDOrName interface{}) (*project, error) {
project, err := d.getAdmiralProject(projectIDOrName, true)
if project != nil {
// the projects returned by GET /projects?public=true&xxx have no
// "public" property, populate it here
@ -110,7 +109,7 @@ func (p *ProjectManager) getFromPublic(projectIDOrName interface{}) (*project, e
return project, err
}
func (p *ProjectManager) getAdmiralProject(projectIDOrName interface{}, public bool) (*project, error) {
func (d *driver) getAdmiralProject(projectIDOrName interface{}, public bool) (*project, error) {
m := map[string]string{}
id, name, err := utils.ParseProjectIDOrName(projectIDOrName)
@ -126,7 +125,7 @@ func (p *ProjectManager) getAdmiralProject(projectIDOrName interface{}, public b
m["public"] = "true"
}
projects, err := p.filter(m)
projects, err := d.filter(m)
if err != nil {
return nil, err
}
@ -145,7 +144,7 @@ func (p *ProjectManager) getAdmiralProject(projectIDOrName interface{}, public b
return projects[0], nil
}
func (p *ProjectManager) filter(m map[string]string) ([]*project, error) {
func (d *driver) filter(m map[string]string) ([]*project, error) {
query := ""
for k, v := range m {
if len(query) == 0 {
@ -165,7 +164,7 @@ func (p *ProjectManager) filter(m map[string]string) ([]*project, error) {
}
path := "/projects" + query
data, err := p.send(http.MethodGet, path, nil)
data, err := d.send(http.MethodGet, path, nil)
if err != nil {
return nil, err
}
@ -201,7 +200,7 @@ func convert(p *project) (*models.Project, error) {
Name: p.Name,
}
if p.Public {
project.Public = 1
project.SetMetadata(models.ProMetaPublic, "true")
}
value := p.CustomProperties["__projectIndex"]
@ -221,7 +220,7 @@ func convert(p *project) (*models.Project, error) {
if err != nil {
return nil, fmt.Errorf("failed to parse __enableContentTrust %s to bool: %v", value, err)
}
project.EnableContentTrust = enable
project.SetMetadata(models.ProMetaEnableContentTrust, strconv.FormatBool(enable))
}
value = p.CustomProperties["__preventVulnerableImagesFromRunning"]
@ -230,12 +229,12 @@ func convert(p *project) (*models.Project, error) {
if err != nil {
return nil, fmt.Errorf("failed to parse __preventVulnerableImagesFromRunning %s to bool: %v", value, err)
}
project.PreventVulnerableImagesFromRunning = prevent
project.SetMetadata(models.ProMetaPreventVul, strconv.FormatBool(prevent))
}
value = p.CustomProperties["__preventVulnerableImagesFromRunningSeverity"]
if len(value) != 0 {
project.PreventVulnerableImagesFromRunningSeverity = value
project.SetMetadata(models.ProMetaSeverity, value)
}
value = p.CustomProperties["__automaticallyScanImagesOnPush"]
@ -244,93 +243,14 @@ func convert(p *project) (*models.Project, error) {
if err != nil {
return nil, fmt.Errorf("failed to parse __automaticallyScanImagesOnPush %s to bool: %v", value, err)
}
project.AutomaticallyScanImagesOnPush = scan
project.SetMetadata(models.ProMetaAutoScan, strconv.FormatBool(scan))
}
return project, nil
}
// IsPublic ...
func (p *ProjectManager) IsPublic(projectIDOrName interface{}) (bool, error) {
project, err := p.get(projectIDOrName)
if err != nil {
return false, err
}
if project == nil {
return false, nil
}
return project.Public, nil
}
// Exist ...
func (p *ProjectManager) Exist(projectIDOrName interface{}) (bool, error) {
project, err := p.get(projectIDOrName)
if err != nil {
return false, err
}
return project != nil, nil
}
/*
// GetRoles gets roles that the user has to the project
// This method is used in GET /projects API.
// Jobservice calls GET /projects API to get information of source
// project when trying to replicate the project. There is no auth
// context in this use case, so the method is needed.
func (p *ProjectManager) GetRoles(username string, projectIDOrName interface{}) ([]int, error) {
if len(username) == 0 || projectIDOrName == nil {
return nil, nil
}
id, err := p.getIDbyHarborIDOrName(projectIDOrName)
if err != nil {
return nil, err
}
// get expanded project which contains role info by GET /projects/id?expand=true
path := fmt.Sprintf("/projects/%s?expand=true", id)
data, err := p.send(http.MethodGet, path, nil)
if err != nil {
return nil, err
}
pro := &project{}
if err = json.Unmarshal(data, pro); err != nil {
return nil, err
}
roles := []int{}
for _, user := range pro.Administrators {
if user.Email == username {
roles = append(roles, common.RoleProjectAdmin)
break
}
}
for _, user := range pro.Developers {
if user.Email == username {
roles = append(roles, common.RoleDeveloper)
break
}
}
for _, user := range pro.Guests {
if user.Email == username {
roles = append(roles, common.RoleGuest)
break
}
}
return roles, nil
}
*/
func (p *ProjectManager) getIDbyHarborIDOrName(projectIDOrName interface{}) (string, error) {
pro, err := p.get(projectIDOrName)
func (d *driver) getIDbyHarborIDOrName(projectIDOrName interface{}) (string, error) {
pro, err := d.get(projectIDOrName)
if err != nil {
return "", err
}
@ -342,32 +262,24 @@ func (p *ProjectManager) getIDbyHarborIDOrName(projectIDOrName interface{}) (str
return pro.ID, nil
}
// GetPublic ...
func (p *ProjectManager) GetPublic() ([]*models.Project, error) {
t := true
return p.GetAll(&models.ProjectQueryParam{
Public: &t,
})
}
// Create ...
func (p *ProjectManager) Create(pro *models.Project) (int64, error) {
func (d *driver) Create(pro *models.Project) (int64, error) {
proj := &project{
CustomProperties: make(map[string]string),
}
proj.Name = pro.Name
proj.Public = pro.Public == 1
proj.CustomProperties["__enableContentTrust"] = strconv.FormatBool(pro.EnableContentTrust)
proj.CustomProperties["__preventVulnerableImagesFromRunning"] = strconv.FormatBool(pro.PreventVulnerableImagesFromRunning)
proj.CustomProperties["__preventVulnerableImagesFromRunningSeverity"] = pro.PreventVulnerableImagesFromRunningSeverity
proj.CustomProperties["__automaticallyScanImagesOnPush"] = strconv.FormatBool(pro.AutomaticallyScanImagesOnPush)
proj.Public = pro.IsPublic()
proj.CustomProperties["__enableContentTrust"] = strconv.FormatBool(pro.ContentTrustEnabled())
proj.CustomProperties["__preventVulnerableImagesFromRunning"] = strconv.FormatBool(pro.VulPrevented())
proj.CustomProperties["__preventVulnerableImagesFromRunningSeverity"] = pro.Severity()
proj.CustomProperties["__automaticallyScanImagesOnPush"] = strconv.FormatBool(pro.AutoScan())
data, err := json.Marshal(proj)
if err != nil {
return 0, err
}
b, err := p.send(http.MethodPost, "/projects", bytes.NewBuffer(data))
b, err := d.send(http.MethodPost, "/projects", bytes.NewBuffer(data))
if err != nil {
// when creating a project with a duplicate name in Admiral, a 500 error
// with a specific message will be returned for now.
@ -413,23 +325,23 @@ func (p *ProjectManager) Create(pro *models.Project) (int64, error) {
}
// Delete ...
func (p *ProjectManager) Delete(projectIDOrName interface{}) error {
id, err := p.getIDbyHarborIDOrName(projectIDOrName)
func (d *driver) Delete(projectIDOrName interface{}) error {
id, err := d.getIDbyHarborIDOrName(projectIDOrName)
if err != nil {
return err
}
_, err = p.send(http.MethodDelete, fmt.Sprintf("/projects/%s", id), nil)
_, err = d.send(http.MethodDelete, fmt.Sprintf("/projects/%s", id), nil)
return err
}
// Update ...
func (p *ProjectManager) Update(projectIDOrName interface{}, project *models.Project) error {
func (d *driver) Update(projectIDOrName interface{}, project *models.Project) error {
return errors.New("project update is unsupported")
}
// GetAll ...
func (p *ProjectManager) GetAll(query *models.ProjectQueryParam, base ...*models.BaseProjectCollection) ([]*models.Project, error) {
// List ...
func (d *driver) List(query *models.ProjectQueryParam) (*models.ProjectQueryResult, error) {
m := map[string]string{}
if query != nil {
if len(query.Name) > 0 {
@ -440,7 +352,7 @@ func (p *ProjectManager) GetAll(query *models.ProjectQueryParam, base ...*models
}
}
projects, err := p.filter(m)
projects, err := d.filter(m)
if err != nil {
return nil, err
}
@ -454,27 +366,24 @@ func (p *ProjectManager) GetAll(query *models.ProjectQueryParam, base ...*models
list = append(list, project)
}
return list, nil
return &models.ProjectQueryResult{
Total: int64(len(list)),
Projects: list,
}, nil
}
// GetTotal ...
func (p *ProjectManager) GetTotal(query *models.ProjectQueryParam, base ...*models.BaseProjectCollection) (int64, error) {
projects, err := p.GetAll(query)
return int64(len(projects)), err
}
func (p *ProjectManager) send(method, path string, body io.Reader) ([]byte, error) {
req, err := http.NewRequest(method, p.endpoint+path, body)
func (d *driver) send(method, path string, body io.Reader) ([]byte, error) {
req, err := http.NewRequest(method, d.endpoint+path, body)
if err != nil {
return nil, err
}
req.Header.Add("x-xenon-auth-token", p.getToken())
req.Header.Add("x-xenon-auth-token", d.getToken())
url := req.URL.String()
req.URL.RawQuery = req.URL.Query().Encode()
resp, err := p.client.Do(req)
resp, err := d.client.Do(req)
if err != nil {
log.Debugf("\"%s %s\" failed", req.Method, url)
return nil, err
@ -497,12 +406,12 @@ func (p *ProjectManager) send(method, path string, body io.Reader) ([]byte, erro
return b, nil
}
func (p *ProjectManager) getToken() string {
if p.tokenReader == nil {
func (d *driver) getToken() string {
if d.tokenReader == nil {
return ""
}
token, err := p.tokenReader.ReadToken()
token, err := d.tokenReader.ReadToken()
if err != nil {
token = ""
log.Errorf("failed to read token: %v", err)

View File

@ -101,12 +101,12 @@ func TestConvert(t *testing.T) {
assert.Nil(t, err)
assert.NotNil(t, pro)
assert.Equal(t, "test", pro.Name)
assert.Equal(t, 1, pro.Public)
assert.True(t, pro.IsPublic())
assert.Equal(t, int64(1), pro.ProjectID)
assert.True(t, pro.EnableContentTrust)
assert.True(t, pro.PreventVulnerableImagesFromRunning)
assert.Equal(t, "medium", pro.PreventVulnerableImagesFromRunningSeverity)
assert.True(t, pro.AutomaticallyScanImagesOnPush)
assert.True(t, pro.ContentTrustEnabled())
assert.True(t, pro.VulPrevented())
assert.Equal(t, "medium", pro.Severity())
assert.True(t, pro.AutoScan())
}
func TestParse(t *testing.T) {
@ -182,233 +182,130 @@ func TestParse(t *testing.T) {
}
func TestGet(t *testing.T) {
pm := NewProjectManager(client, endpoint, tokenReader)
d := NewDriver(client, endpoint, tokenReader)
name := "project_for_test_get"
id, err := pm.Create(&models.Project{
id, err := d.Create(&models.Project{
Name: name,
})
require.Nil(t, err)
defer delete(t, id)
// get by invalid input type
_, err = pm.Get([]string{})
_, err = d.Get([]string{})
assert.NotNil(t, err)
// get by invalid ID
project, err := pm.Get(int64(0))
project, err := d.Get(int64(0))
assert.Nil(t, err)
assert.Nil(t, project)
// get by invalid name
project, err = pm.Get("invalid_name")
project, err = d.Get("invalid_name")
assert.Nil(t, err)
assert.Nil(t, project)
// get by valid ID
project, err = pm.Get(id)
project, err = d.Get(id)
assert.Nil(t, err)
assert.Equal(t, id, project.ProjectID)
// get by valid name
project, err = pm.Get(name)
project, err = d.Get(name)
assert.Nil(t, err)
assert.Equal(t, id, project.ProjectID)
}
func TestIsPublic(t *testing.T) {
pm := NewProjectManager(client, endpoint, tokenReader)
// invalid input type
public, err := pm.IsPublic([]string{})
assert.NotNil(t, err)
assert.False(t, public)
// non-exist project
public, err = pm.IsPublic(int64(2))
assert.Nil(t, err)
assert.False(t, public)
// public project
name := "project_for_pm_based_on_pms_public"
id, err := pm.Create(&models.Project{
Name: name,
Public: 1,
})
require.Nil(t, err)
defer delete(t, id)
public, err = pm.IsPublic(id)
assert.Nil(t, err)
assert.True(t, public)
public, err = pm.IsPublic(name)
assert.Nil(t, err)
assert.True(t, public)
// private project
name = "project_for_pm_based_on_pms_private"
id, err = pm.Create(&models.Project{
Name: name,
Public: 0,
})
require.Nil(t, err)
defer delete(t, id)
public, err = pm.IsPublic(id)
assert.Nil(t, err)
assert.False(t, public)
public, err = pm.IsPublic(name)
assert.Nil(t, err)
assert.False(t, public)
}
func TestExist(t *testing.T) {
pm := NewProjectManager(client, endpoint, tokenReader)
// invalid input type
exist, err := pm.Exist([]string{})
assert.NotNil(t, err)
assert.False(t, exist)
// non-exist project
exist, err = pm.Exist(int64(2))
assert.Nil(t, err)
assert.False(t, exist)
// exist project
name := "project_for_test_exist"
id, err := pm.Create(&models.Project{
Name: name,
})
require.Nil(t, err)
defer delete(t, id)
exist, err = pm.Exist(id)
assert.Nil(t, err)
assert.True(t, exist)
exist, err = pm.Exist(name)
assert.Nil(t, err)
assert.True(t, exist)
}
func TestGetPublic(t *testing.T) {
pm := NewProjectManager(client, endpoint, tokenReader)
projects, err := pm.GetPublic()
assert.Nil(t, nil)
size := len(projects)
name := "project_for_test_get_public"
id, err := pm.Create(&models.Project{
Name: name,
Public: 1,
})
require.Nil(t, err)
defer delete(t, id)
projects, err = pm.GetPublic()
assert.Nil(t, nil)
assert.Equal(t, size+1, len(projects))
found := false
for _, project := range projects {
if project.ProjectID == id {
found = true
break
}
}
assert.True(t, found)
}
func TestCreate(t *testing.T) {
pm := NewProjectManager(client, endpoint, tokenReader)
d := NewDriver(client, endpoint, tokenReader)
name := "project_for_test_create"
id, err := pm.Create(&models.Project{
Name: name,
Public: 1,
EnableContentTrust: true,
PreventVulnerableImagesFromRunning: true,
PreventVulnerableImagesFromRunningSeverity: "medium",
AutomaticallyScanImagesOnPush: true,
id, err := d.Create(&models.Project{
Name: name,
Metadata: map[string]string{
models.ProMetaPublic: "true",
models.ProMetaEnableContentTrust: "true",
models.ProMetaPreventVul: "true",
models.ProMetaSeverity: "medium",
models.ProMetaAutoScan: "true",
},
})
require.Nil(t, err)
defer delete(t, id)
project, err := pm.Get(id)
project, err := d.Get(id)
assert.Nil(t, err)
assert.Equal(t, name, project.Name)
assert.Equal(t, 1, project.Public)
assert.True(t, project.EnableContentTrust)
assert.True(t, project.PreventVulnerableImagesFromRunning)
assert.Equal(t, "medium", project.PreventVulnerableImagesFromRunningSeverity)
assert.True(t, project.AutomaticallyScanImagesOnPush)
assert.True(t, project.IsPublic())
assert.True(t, project.ContentTrustEnabled())
assert.True(t, project.VulPrevented())
assert.Equal(t, "medium", project.Severity())
assert.True(t, project.AutoScan())
// duplicate project name
_, err = pm.Create(&models.Project{
_, err = d.Create(&models.Project{
Name: name,
})
assert.Equal(t, errutil.ErrDupProject, err)
}
func TestDelete(t *testing.T) {
pm := NewProjectManager(client, endpoint, tokenReader)
d := NewDriver(client, endpoint, tokenReader)
// non-exist project
err := pm.Delete(int64(0))
err := d.Delete(int64(0))
assert.NotNil(t, err)
// delete by ID
name := "project_for_pm_based_on_pms_id"
id, err := pm.Create(&models.Project{
id, err := d.Create(&models.Project{
Name: name,
})
require.Nil(t, err)
err = pm.Delete(id)
err = d.Delete(id)
assert.Nil(t, err)
// delete by name
name = "project_for_pm_based_on_pms_name"
id, err = pm.Create(&models.Project{
id, err = d.Create(&models.Project{
Name: name,
})
require.Nil(t, err)
err = pm.Delete(name)
err = d.Delete(name)
assert.Nil(t, err)
}
func TestUpdate(t *testing.T) {
pm := NewProjectManager(client, endpoint, tokenReader)
err := pm.Update(nil, nil)
d := NewDriver(client, endpoint, tokenReader)
err := d.Update(nil, nil)
assert.NotNil(t, err)
}
func TestGetAll(t *testing.T) {
pm := NewProjectManager(client, endpoint, tokenReader)
func TestList(t *testing.T) {
d := NewDriver(client, endpoint, tokenReader)
name1 := "project_for_test_get_all_01"
id1, err := pm.Create(&models.Project{
id1, err := d.Create(&models.Project{
Name: name1,
})
require.Nil(t, err)
defer delete(t, id1)
name2 := "project_for_test_get_all_02"
id2, err := pm.Create(&models.Project{
Name: name2,
Public: 1,
id2, err := d.Create(&models.Project{
Name: name2,
Metadata: map[string]string{
models.ProMetaPublic: "true",
},
})
require.Nil(t, err)
defer delete(t, id2)
// no filter
projects, err := pm.GetAll(nil)
result, err := d.List(nil)
require.Nil(t, err)
found1 := false
found2 := false
for _, project := range projects {
for _, project := range result.Projects {
if project.ProjectID == id1 {
found1 = true
}
@ -420,12 +317,12 @@ func TestGetAll(t *testing.T) {
assert.True(t, found2)
// filter by name
projects, err = pm.GetAll(&models.ProjectQueryParam{
result, err = d.List(&models.ProjectQueryParam{
Name: name1,
})
require.Nil(t, err)
found1 = false
for _, project := range projects {
for _, project := range result.Projects {
if project.ProjectID == id1 {
found1 = true
break
@ -435,12 +332,12 @@ func TestGetAll(t *testing.T) {
// filter by public
value := true
projects, err = pm.GetAll(&models.ProjectQueryParam{
result, err = d.List(&models.ProjectQueryParam{
Public: &value,
})
require.Nil(t, err)
found2 = false
for _, project := range projects {
for _, project := range result.Projects {
if project.ProjectID == id2 {
found2 = true
break
@ -449,27 +346,9 @@ func TestGetAll(t *testing.T) {
assert.True(t, found2)
}
func TestGetTotal(t *testing.T) {
pm := NewProjectManager(client, endpoint, tokenReader)
total1, err := pm.GetTotal(nil)
require.Nil(t, err)
name := "project_for_test_get_total"
id, err := pm.Create(&models.Project{
Name: name,
})
require.Nil(t, err)
defer delete(t, id)
total2, err := pm.GetTotal(nil)
require.Nil(t, err)
assert.Equal(t, total1+1, total2)
}
func delete(t *testing.T, id int64) {
pm := NewProjectManager(client, endpoint, tokenReader)
if err := pm.Delete(id); err != nil {
d := NewDriver(client, endpoint, tokenReader)
if err := d.Delete(id); err != nil {
t.Logf("failed to delete project %d: %v", id, err)
}
}

View File

@ -30,7 +30,5 @@ type PMSDriver interface {
// Update the properties of a project
Update(projectIDOrName interface{}, project *models.Project) error
// List lists projects according to the query conditions
// TODO remove base
List(query *models.ProjectQueryParam,
base ...*models.BaseProjectCollection) (*models.ProjectQueryResult, error)
List(query *models.ProjectQueryParam) (*models.ProjectQueryResult, error)
}

View File

@ -82,7 +82,6 @@ func (d *driver) Create(project *models.Project) (int64, error) {
t := time.Now()
pro := &models.Project{
Name: project.Name,
Public: project.Public,
OwnerID: project.OwnerID,
CreationTime: t,
UpdateTime: t,
@ -129,16 +128,15 @@ func (d *driver) Update(projectIDOrName interface{},
return nil
}
// TODO remove base
// List returns a project list according to the query parameters
func (d *driver) List(query *models.ProjectQueryParam,
base ...*models.BaseProjectCollection) (
func (d *driver) List(query *models.ProjectQueryParam) (
*models.ProjectQueryResult, error) {
total, err := dao.GetTotalOfProjects(query, base...)
total, err := dao.GetTotalOfProjects(query)
if err != nil {
return nil, err
}
projects, err := dao.GetProjects(query, base...)
projects, err := dao.GetProjects(query)
if err != nil {
return nil, err
}

View File

@ -156,7 +156,9 @@ func TestList(t *testing.T) {
id, err := pm.Create(&models.Project{
Name: "get_all_test",
OwnerID: 1,
Public: 1,
Metadata: map[string]string{
models.ProMetaPublic: "true",
},
})
assert.Nil(t, err)
defer pm.Delete(id)

View File

@ -16,6 +16,7 @@ package promgr
import (
"fmt"
"strconv"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
@ -30,9 +31,7 @@ type ProjectManager interface {
Create(*models.Project) (int64, error)
Delete(projectIDOrName interface{}) error
Update(projectIDOrName interface{}, project *models.Project) error
// TODO remove base
List(query *models.ProjectQueryParam,
base ...*models.BaseProjectCollection) (*models.ProjectQueryResult, error)
List(query *models.ProjectQueryParam) (*models.ProjectQueryResult, error)
IsPublic(projectIDOrName interface{}) (bool, error)
Exists(projectIDOrName interface{}) (bool, error)
// get all public project
@ -42,7 +41,7 @@ type ProjectManager interface {
type defaultProjectManager struct {
pmsDriver pmsdriver.PMSDriver
metaMgrEnabled bool // if metaMgrEnabled is enabled, metaMgr will be used to CURD metadata
metaMgr metamgr.ProjectMetadataManaegr
metaMgr metamgr.ProjectMetadataManager
}
// NewDefaultProjectManager returns an instance of defaultProjectManager,
@ -117,7 +116,25 @@ func (d *defaultProjectManager) Update(projectIDOrName interface{}, project *mod
if pro == nil {
return fmt.Errorf("project %v not found", projectIDOrName)
}
if err = d.metaMgr.Update(pro.ProjectID, project.Metadata); err != nil {
// TODO transaction?
metaNeedUpdated := map[string]string{}
metaNeedCreated := map[string]string{}
if pro.Metadata == nil {
pro.Metadata = map[string]string{}
}
for key, value := range project.Metadata {
_, exist := pro.Metadata[key]
if exist {
metaNeedUpdated[key] = value
} else {
metaNeedCreated[key] = value
}
}
if err = d.metaMgr.Add(pro.ProjectID, metaNeedCreated); err != nil {
return err
}
if err = d.metaMgr.Update(pro.ProjectID, metaNeedUpdated); err != nil {
return err
}
}
@ -125,13 +142,32 @@ func (d *defaultProjectManager) Update(projectIDOrName interface{}, project *mod
return d.pmsDriver.Update(projectIDOrName, project)
}
// TODO remove base
func (d *defaultProjectManager) List(query *models.ProjectQueryParam,
base ...*models.BaseProjectCollection) (*models.ProjectQueryResult, error) {
result, err := d.pmsDriver.List(query, base...)
func (d *defaultProjectManager) List(query *models.ProjectQueryParam) (*models.ProjectQueryResult, error) {
// query by public/private property with ProjectMetadataManager first
if d.metaMgrEnabled && query != nil && query.Public != nil {
projectIDs, err := d.filterByPublic(*query.Public)
if err != nil {
return nil, err
}
if len(projectIDs) == 0 {
return &models.ProjectQueryResult{}, nil
}
if query.ProjectIDs == nil {
query.ProjectIDs = projectIDs
} else {
query.ProjectIDs = findInBoth(query.ProjectIDs, projectIDs)
}
}
// query by other properties
result, err := d.pmsDriver.List(query)
if err != nil {
return nil, err
}
// populate metadata
if d.metaMgrEnabled {
for _, project := range result.Projects {
meta, err := d.metaMgr.Get(project.ProjectID)
@ -144,6 +180,35 @@ func (d *defaultProjectManager) List(query *models.ProjectQueryParam,
return result, nil
}
func (d *defaultProjectManager) filterByPublic(public bool) ([]int64, error) {
metas, err := d.metaMgr.List(models.ProMetaPublic, strconv.FormatBool(public))
if err != nil {
return nil, err
}
projectIDs := []int64{}
for _, meta := range metas {
projectIDs = append(projectIDs, meta.ProjectID)
}
return projectIDs, nil
}
func findInBoth(ids1 []int64, ids2 []int64) []int64 {
m := map[int64]struct{}{}
for _, id := range ids1 {
m[id] = struct{}{}
}
ids := []int64{}
for _, id := range ids2 {
if _, exist := m[id]; exist {
ids = append(ids, id)
}
}
return ids
}
func (d *defaultProjectManager) IsPublic(projectIDOrName interface{}) (bool, error) {
project, err := d.Get(projectIDOrName)
if err != nil {
@ -152,7 +217,7 @@ func (d *defaultProjectManager) IsPublic(projectIDOrName interface{}) (bool, err
if project == nil {
return false, nil
}
return project.Public == 1, nil
return project.IsPublic(), nil
}
func (d *defaultProjectManager) Exists(projectIDOrName interface{}) (bool, error) {

View File

@ -32,7 +32,9 @@ func newFakePMSDriver() pmsdriver.PMSDriver {
project: &models.Project{
ProjectID: 1,
Name: "library",
Public: 1,
Metadata: map[string]string{
models.ProMetaPublic: "true",
},
},
}
}
@ -53,8 +55,7 @@ func (f *fakePMSDriver) Update(projectIDOrName interface{}, project *models.Proj
return nil
}
func (f *fakePMSDriver) List(query *models.ProjectQueryParam,
base ...*models.BaseProjectCollection) (*models.ProjectQueryResult, error) {
func (f *fakePMSDriver) List(query *models.ProjectQueryParam) (*models.ProjectQueryResult, error) {
return &models.ProjectQueryResult{
Total: 1,
Projects: []*models.Project{f.project},
@ -116,5 +117,5 @@ func TestGetPublic(t *testing.T) {
projects, err := proMgr.GetPublic()
require.Nil(t, err)
assert.Equal(t, 1, len(projects))
assert.Equal(t, 1, projects[0].Public)
assert.True(t, projects[0].IsPublic())
}

View File

@ -2,14 +2,14 @@ package proxy
import (
"github.com/stretchr/testify/assert"
//"github.com/stretchr/testify/require"
"github.com/stretchr/testify/require"
"github.com/vmware/harbor/src/adminserver/client"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
notarytest "github.com/vmware/harbor/src/common/utils/notary/test"
utilstest "github.com/vmware/harbor/src/common/utils/test"
"github.com/vmware/harbor/src/ui/config"
//"github.com/vmware/harbor/src/ui/promgr/pmsdriver/admiral"
"net/http"
"net/http/httptest"
@ -110,33 +110,19 @@ func TestMatchListRepos(t *testing.T) {
}
func TestEnvPolicyChecker(t *testing.T) {
assert := assert.New(t)
if err := os.Setenv("PROJECT_CONTENT_TRUST", "1"); err != nil {
t.Fatalf("Failed to set env variable: %v", err)
}
if err2 := os.Setenv("PROJECT_VULNERABLE", "1"); err2 != nil {
t.Fatalf("Failed to set env variable: %v", err2)
}
if err3 := os.Setenv("PROJECT_SEVERITY", "negligible"); err3 != nil {
t.Fatalf("Failed to set env variable: %v", err3)
}
contentTrustFlag := getPolicyChecker().contentTrustEnabled("whatever")
vulFlag, sev := getPolicyChecker().vulnerablePolicy("whatever")
assert.True(contentTrustFlag)
assert.True(vulFlag)
assert.Equal(sev, models.SevNone)
}
// TODO uncheck after admiral pms driver is implemented
/*
func TestPMSPolicyChecker(t *testing.T) {
var defaultConfigAdmiral = map[string]interface{}{
common.ExtEndpoint: "https://" + endpoint,
common.WithNotary: true,
common.CfgExpiration: 5,
common.AdmiralEndpoint: admiralEndpoint,
common.TokenExpiration: 30,
common.DatabaseType: "mysql",
common.MySQLHost: "127.0.0.1",
common.MySQLPort: 3306,
common.MySQLUsername: "root",
common.MySQLPassword: "root123",
common.MySQLDatabase: "registry",
common.SQLiteFile: "/tmp/registry.db",
}
adminServer, err := utilstest.NewAdminserver(defaultConfigAdmiral)
if err != nil {
@ -149,34 +135,38 @@ func TestPMSPolicyChecker(t *testing.T) {
if err := config.Init(); err != nil {
panic(err)
}
pm := admiral.NewProjectManager(http.DefaultClient,
admiralEndpoint, &admiral.RawTokenReader{
Token: "token",
})
database, err := config.Database()
if err != nil {
panic(err)
}
if err := dao.InitDatabase(database); err != nil {
panic(err)
}
name := "project_for_test_get_sev_low"
id, err := pm.Create(&models.Project{
Name: name,
EnableContentTrust: true,
PreventVulnerableImagesFromRunning: false,
PreventVulnerableImagesFromRunningSeverity: "low",
id, err := config.GlobalProjectMgr.Create(&models.Project{
Name: name,
OwnerID: 1,
Metadata: map[string]string{
models.ProMetaEnableContentTrust: "true",
models.ProMetaPreventVul: "true",
models.ProMetaSeverity: "low",
},
})
require.Nil(t, err)
defer func(id int64) {
if err := pm.Delete(id); err != nil {
if err := config.GlobalProjectMgr.Delete(id); err != nil {
t.Logf("failed to delete project %d: %v", id, err)
}
}(id)
project, err := pm.Get(id)
assert.Nil(t, err)
assert.Equal(t, id, project.ProjectID)
contentTrustFlag := getPolicyChecker().contentTrustEnabled("project_for_test_get_sev_low")
assert.True(t, contentTrustFlag)
projectVulnerableEnabled, projectVulnerableSeverity := getPolicyChecker().vulnerablePolicy("project_for_test_get_sev_low")
assert.False(t, projectVulnerableEnabled)
assert.True(t, projectVulnerableEnabled)
assert.Equal(t, projectVulnerableSeverity, models.SevLow)
}
*/
func TestMatchNotaryDigest(t *testing.T) {
assert := assert.New(t)
//The data from common/utils/notary/helper_test.go

View File

@ -16,7 +16,6 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"regexp"
"strconv"
"strings"
@ -38,9 +37,6 @@ var rec *httptest.ResponseRecorder
// NotaryEndpoint , exported for testing.
var NotaryEndpoint = config.InternalNotaryEndpoint()
// EnvChecker is the instance of envPolicyChecker
var EnvChecker = envPolicyChecker{}
// MatchPullManifest checks if the request looks like a request to pull manifest. If it is returns the image and tag/sha256 digest as 2nd and 3rd return values
func MatchPullManifest(req *http.Request) (bool, string, string) {
//TODO: add user agent check.
@ -77,16 +73,6 @@ type policyChecker interface {
vulnerablePolicy(name string) (bool, models.Severity)
}
//For testing
type envPolicyChecker struct{}
func (ec envPolicyChecker) contentTrustEnabled(name string) bool {
return os.Getenv("PROJECT_CONTENT_TRUST") == "1"
}
func (ec envPolicyChecker) vulnerablePolicy(name string) (bool, models.Severity) {
return os.Getenv("PROJECT_VULNERABLE") == "1", clair.ParseClairSev(os.Getenv("PROJECT_SEVERITY"))
}
type pmsPolicyChecker struct {
pm promgr.ProjectManager
}
@ -97,7 +83,7 @@ func (pc pmsPolicyChecker) contentTrustEnabled(name string) bool {
log.Errorf("Unexpected error when getting the project, error: %v", err)
return true
}
return project.EnableContentTrust
return project.ContentTrustEnabled()
}
func (pc pmsPolicyChecker) vulnerablePolicy(name string) (bool, models.Severity) {
project, err := pc.pm.Get(name)
@ -105,7 +91,7 @@ func (pc pmsPolicyChecker) vulnerablePolicy(name string) (bool, models.Severity)
log.Errorf("Unexpected error when getting the project, error: %v", err)
return true, models.SevUnknown
}
return project.PreventVulnerableImagesFromRunning, clair.ParseClairSev(project.PreventVulnerableImagesFromRunningSeverity)
return project.VulPrevented(), clair.ParseClairSev(project.Severity())
}
// newPMSPolicyChecker returns an instance of an pmsPolicyChecker
@ -116,10 +102,7 @@ func newPMSPolicyChecker(pm promgr.ProjectManager) policyChecker {
}
func getPolicyChecker() policyChecker {
if config.WithAdmiral() {
return newPMSPolicyChecker(config.GlobalProjectMgr)
}
return EnvChecker
return newPMSPolicyChecker(config.GlobalProjectMgr)
}
type imageInfo struct {

View File

@ -70,7 +70,6 @@ func initRouters() {
beego.Router("/api/projects/:pid([0-9]+)/members/?:mid", &api.ProjectMemberAPI{})
beego.Router("/api/projects/", &api.ProjectAPI{}, "head:Head")
beego.Router("/api/projects/:id([0-9]+)", &api.ProjectAPI{})
beego.Router("/api/projects/:id([0-9]+)/publicity", &api.ProjectAPI{}, "put:ToggleProjectPublic")
beego.Router("/api/users/:id", &api.UserAPI{}, "get:Get;delete:Delete;put:Put")
beego.Router("/api/users", &api.UserAPI{}, "get:List;post:Post")

View File

@ -16,7 +16,6 @@ package registry
import (
"encoding/json"
"os"
"regexp"
"strings"
"time"
@ -170,10 +169,8 @@ func autoScanEnabled(project *models.Project) bool {
log.Debugf("Auto Scan disabled because Harbor is not deployed with Clair")
return false
}
if config.WithAdmiral() {
return project.AutomaticallyScanImagesOnPush
}
return os.Getenv("ENABLE_HARBOR_SCAN_ON_PUSH") == "1"
return project.AutoScan()
}
// Render returns nil as it won't render any template.

View File

@ -23,7 +23,7 @@
<div class="form-group" style="padding-left: 135px;">
<label class="col-md-4 form-group-label-override">{{'PROJECT.ACCESS_LEVEL' | translate}}</label>
<div class="checkbox-inline">
<input type="checkbox" id="create_project_public" [(ngModel)]="project.public" name="public">
<input type="checkbox" id="create_project_public" [(ngModel)]="project.metadata.public" name="public">
<label for="create_project_public"></label>
<span class="access-level-label">{{ accessLevelDisplayText | translate}}</span>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-md tooltip-bottom-right" style="top:-8px; left:-8px;">

View File

@ -73,7 +73,7 @@ export class CreateProjectComponent implements AfterViewChecked, OnInit, OnDestr
private messageHandlerService: MessageHandlerService) { }
public get accessLevelDisplayText(): string {
return this.project.public ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE';
return this.project.metadata.public ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE';
}
ngOnInit(): void {
@ -115,7 +115,7 @@ export class CreateProjectComponent implements AfterViewChecked, OnInit, OnDestr
this.isSubmitOnGoing=true;
this.projectService
.createProject(this.project.name, this.project.public ? 1 : 0)
.createProject(this.project.name, this.project.metadata)
.subscribe(
status => {
this.isSubmitOnGoing=false;

View File

@ -7,11 +7,11 @@
<clr-dg-row *ngFor="let p of projects">
<clr-dg-action-overflow [hidden]="!(p.current_user_role_id === 1 || isSystemAdmin)">
<button class="action-item" (click)="newReplicationRule(p)" [hidden]="!isSystemAdmin">{{'PROJECT.REPLICATION_RULE' | translate}}</button>
<button class="action-item" (click)="toggleProject(p)">{{'PROJECT.MAKE' | translate}} {{(p.public === 0 ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE') | translate}} </button>
<button class="action-item" (click)="toggleProject(p)">{{'PROJECT.MAKE' | translate}} {{(p.metadata.public === 'false' ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE') | translate}} </button>
<button class="action-item" (click)="deleteProject(p)">{{'PROJECT.DELETE' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell><a href="javascript:void(0)" (click)="goToLink(p.project_id)">{{p.name}}</a></clr-dg-cell>
<clr-dg-cell>{{ (p.public === 1 ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE') | translate}}</clr-dg-cell>
<clr-dg-cell>{{ (p.metadata.public === 'true' ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE') | translate}}</clr-dg-cell>
<clr-dg-cell *ngIf="showRoleInfo">{{roleInfo[p.current_user_role_id] | translate}}</clr-dg-cell>
<clr-dg-cell>{{p.repo_count}}</clr-dg-cell>
<clr-dg-cell>{{p.creation_time | date: 'short'}}</clr-dg-cell>

View File

@ -173,15 +173,15 @@ export class ListProjectComponent implements OnDestroy {
toggleProject(p: Project) {
if (p) {
p.public === 0 ? p.public = 1 : p.public = 0;
p.metadata.public === 'true' ? p.metadata.public = 'false' : p.metadata.public = 'true';
this.proService
.toggleProjectPublic(p.project_id, p.public)
.toggleProjectPublic(p.project_id, p.metadata.public)
.subscribe(
response => {
this.msgHandler.showSuccess('PROJECT.TOGGLED_SUCCESS');
let pp: Project = this.projects.find((item: Project) => item.project_id === p.project_id);
if (pp) {
pp.public = p.public;
pp.metadata.public = p.metadata.public;
this.statisticHandler.refresh();
}
},
@ -263,4 +263,4 @@ export class ListProjectComponent implements OnDestroy {
return st;
}
}
}

View File

@ -58,20 +58,22 @@ export class ProjectService {
.catch(error=>Observable.throw(error));
}
createProject(name: string, isPublic: number): Observable<any> {
createProject(name: string, metadata: any): Observable<any> {
return this.http
.post(`/api/projects`,
JSON.stringify({'project_name': name, 'public': isPublic})
JSON.stringify({'project_name': name, 'metadata': {
public: metadata.public ? 'true' : 'false',
}})
, this.options)
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
toggleProjectPublic(projectId: number, isPublic: number): Observable<any> {
return this.http
.put(`/api/projects/${projectId}/publicity`, { 'public': isPublic }, this.options)
.map(response=>response.status)
.catch(error=>Observable.throw(error));
toggleProjectPublic(projectId: number, isPublic: string): Observable<any> {
return this.http
.put(`/api/projects/${projectId}`, { 'metadata': {'public': isPublic} }, this.options)
.map(response => response.status)
.catch(error => Observable.throw(error));
}
deleteProject(projectId: number): Observable<any> {

View File

@ -22,14 +22,14 @@
"deleted": 0,
"owner_name": "",
"public": 1,
"Togglable": true,
"togglable": true,
"update_time": "2017-02-10T07:57:56Z",
"current_user_role_id": 1,
"repo_count": 0
}
]
*/
export class Project {
export class Project {
project_id: number;
owner_id: number;
name: string;
@ -37,12 +37,22 @@ export class Project {
creation_time_str: string;
deleted: number;
owner_name: string;
public: number;
Togglable: boolean;
togglable: boolean;
update_time: Date;
current_user_role_id: number;
repo_count: number;
has_project_admin_role: boolean;
is_member: boolean;
role_name: string;
}
metadata: {
public: string | boolean;
enable_content_trust: string | boolean;
prevent_vul: string | boolean;
severity: string;
auto_scan: string | boolean;
};
constructor () {
this.metadata = <any>{};
this.metadata.public = false;
}
}

View File

@ -45,11 +45,11 @@ type Project struct {
// The owner name of the project.
OwnerName string `json:"owner_name,omitempty"`
// The public status of the project.
Public int32 `json:"public,omitempty"`
// The metadata of the project.
Metadata map[string]string `json:"metadata,omitempty"`
// Correspond to the UI about whether the project's publicity is updatable (for UI)
Togglable bool `json:"Togglable,omitempty"`
Togglable bool `json:"togglable,omitempty"`
// The role ID of the current user who triggered the API (for UI)
CurrentUserRoleId int32 `json:"current_user_role_id,omitempty"`

View File

@ -23,10 +23,8 @@
package apilib
type ProjectReq struct {
// The name of the project.
ProjectName string `json:"project_name,omitempty"`
// The public status of the project.
Public int32 `json:"public,omitempty"`
// The metadata of the project.
Metadata map[string]string `json:"metadata,omitempty"`
}

View File

@ -30,8 +30,8 @@ type SearchRepository struct {
// The name of the project that the repository belongs to
ProjectName string `json:"project_name,omitempty"`
// The flag to indicate the publicity of the project that the repository belongs to (1 is public, 0 is not)
ProjectPublic int32 `json:"project_public,omitempty"`
// The flag to indicate the publicity of the project that the repository belongs to
ProjectPublic bool `json:"project_public,omitempty"`
// The name of the repository
RepositoryName string `json:"repository_name,omitempty"`

View File

@ -53,4 +53,5 @@ Changelog for harbor database schema
## 1.3.0
- create table `project_metadata`
- insert data into table `project_metadata`
- insert data into table `project_metadata`
- delete column `public` from table `project`