core 组件
main.gorouter.go
common/apiBaseAPI
type BaseAPI struct {
	beego.Controller
}
复制代码
BaseAPIControllerInterfaceAPIBaseControllerBaseControllerBaseAPI
projectAPI
type ProjectAPI struct {
	BaseController
	project *models.Project
}
复制代码
BaseController
// BaseController ...
type BaseController struct {
	api.BaseAPI
	// SecurityCtx is the security context used to authN &authZ
	SecurityCtx security.Context
	// ProjectMgr is the project manager which abstracts the operations
	// related to projects
	ProjectMgr promgr.ProjectManager
}
复制代码

通过上述继承实现,用户在添加新的功能时,需要按照这样的实现逻辑添加新的控制器。

router.go

注册 harbor 支持的所有的路由,每个路由分为三个部分,具体实现如下所示:

beego.Router("/c/login", &controllers.CommonController{}, "post:Login")
复制代码

第一个参数是浏览器访问的地址,第二个参数是MVC 中的 C。Controller是用来处理不同URL的控制器,不同的路径有不同的控制器。在 harbor 中大致有一下二种控制器:

  • controllers.CommonController
  • api 中的不同操作的控制器(细分种类很多)

每个控制器都实现了一下的接口

type ControllerInterface interface {
	Init(ct *context.Context, controllerName, actionName string, app interface{})
	Prepare()
	Get()
	Post()
	Delete()
	Put()
	Head()
	Patch()
	Options()
	Finish()
	Render() error
	XSRFToken() string
	CheckXSRFCookie() bool
	HandlerFunc(fn string) bool
	URLMapping()
}
复制代码

实现了上述接口定义的函数的结构体,通过路由根据url执行相应的controller的原则,会依次执行下列函数

Init()      初始化
Prepare()   执行之前的初始化,每个继承的子类可以来实现该函数
method()    根据不同的method执行不同的函数:GET、POST、PUT、HEAD等,子类来实现这些函数,如果没实现,那么默认都是403
Render()    可选,根据全局变量AutoRender来判断是否执行
Finish()    执行完之后执行的操作,每个继承的子类可以来实现该函数
复制代码

每个控制器的 Prepare 函数中都实现了认证功能,具体实现有差别,认证这一逻辑会具体介绍。

api

大部分功能的路由功能都在这里实现。

/api/users

post

添加新的用户到数据库中,用户的数据结构定义如下:

type User struct {
	UserID   int    `orm:"pk;auto;column(user_id)" json:"user_id"`
	Username string `orm:"column(username)" json:"username"`
	Email    string `orm:"column(email)" json:"email"`
	Password string `orm:"column(password)" json:"password"`
	Realname string `orm:"column(realname)" json:"realname"`
	Comment  string `orm:"column(comment)" json:"comment"`
	Deleted  bool   `orm:"column(deleted)" json:"deleted"`
	Rolename string `orm:"-" json:"role_name"`
	// if this field is named as "RoleID", beego orm can not map role_id
	// to it.
	Role int `orm:"-" json:"role_id"`
	//	RoleList     []Role `json:"role_list"`
	HasAdminRole bool         `orm:"column(sysadmin_flag)" json:"has_admin_role"`
	ResetUUID    string       `orm:"column(reset_uuid)" json:"reset_uuid"`
	Salt         string       `orm:"column(salt)" json:"-"`
	CreationTime time.Time    `orm:"column(creation_time);auto_now_add" json:"creation_time"`
	UpdateTime   time.Time    `orm:"column(update_time);auto_now" json:"update_time"`
	GroupList    []*UserGroup `orm:"-" json:"-"`
}

复制代码
postdao.registry(user)

/api/users/:user_id/password

更改密码服务 **Put()**方法,先检查验证模式是否允许更改密码。

controllers

此文件夹实现了用户登录,登出,检查用户或邮箱是否存储,重置邮箱,重置密码等功能。

proxy

拦截器:拦截器是指对浏览器到服务器的请求数据或者服务器到浏览器的返回数据做一些更改,或将请求的数据做一些增强。

ServeHTTP
pull manifest

interceptors.go

ServeHTTPrequest
ServeHTTPflag, repository, referencerepositorytokenUsernameNewRepositoryClientForUI()repository clientregistryclient"User-Agent" header
readonlyHandlerServeHTTPrequest
listReposHandlerServeHTTP
rw.Header().Set(http.CanonicalHeaderKey("Content-Length"), strconv.Itoa(clen))
rw.Write(respJSON)
复制代码
contentTrustHandlerServeHTTPNotary
vulnerableHandlerServeHTTPclair

最后二个等到需要实时再来仔细看。

auth

实现了用户的登录认证,在 harbor 中用户登录认证的方式分为二种:

  • 数据库
  • LDAP 方式
  • uaa 认证

认证器的接口如下,用户可以很方便的实现自己的认证方式只要实现了下面接口提供的方法即可。

// AuthenticateHelper provides interface for user management in different auth modes.
type AuthenticateHelper interface {

	// Authenticate authenticate the user based on data in m.  Only when the error returned is an instance
	// of ErrAuth, it will be considered a bad credentials, other errors will be treated as server side error.
	Authenticate(m models.AuthModel) (*models.User, error)
	// OnBoardUser will check if a user exists in user table, if not insert the user and
	// put the id in the pointer of user model, if it does exist, fill in the user model based
	// on the data record of the user
	OnBoardUser(u *models.User) error
	// Create a group in harbor DB, if altGroupName is not empty, take the altGroupName as groupName in harbor DB.
	OnBoardGroup(g *models.UserGroup, altGroupName string) error
	// Get user information from account repository
	SearchUser(username string) (*models.User, error)
	// Search a group based on specific authentication
	SearchGroup(groupDN string) (*models.UserGroup, error)
	// Update user information after authenticate, such as OnBoard or sync info etc
	PostAuthenticate(u *models.User) error
}
复制代码

在 harbor 中可以给每个项目增加新的成员,新的组进行细粒度管理。其中的权限认证就是通过上述的几种方法实现的。(猜测)

CommonControllerLoginauth

默认的验证方式为数据库验证,登录验证的逻辑如下:

  1. 检查认证方式
  2. 设置锁,避免短时间内有多次登录请求互相抢占
  3. 完成上述检查后,根据配置认证方式的不同调用对应的认证器

主要分析数据库认证

daoLoginByDbPostAuthenticate

promgr

ProjectManagerPMSDriverProjectManagerPMSDriver
type ProjectManager interface {
	Get(projectIDOrName interface{}) (*models.Project, error)
	Create(*models.Project) (int64, error)
	Delete(projectIDOrName interface{}) error
	Update(projectIDOrName interface{}, project *models.Project) error
	List(query *models.ProjectQueryParam) (*models.ProjectQueryResult, error)
	IsPublic(projectIDOrName interface{}) (bool, error)
	Exists(projectIDOrName interface{}) (bool, error)
	// get all public project
	GetPublic() ([]*models.Project, error)
	// if the project manager uses a metadata manager, return it, otherwise return nil
	GetMetadataManager() metamgr.ProjectMetadataManager
}
复制代码
admirallocal

ProjectManager中存储的数据如下所示:

  {
    "project_id": 1,
    "owner_id": 1,
    "name": "library",
    "creation_time": "2019-02-15T12:58:31.485307Z",
    "update_time": "2019-02-15T12:58:31.485307Z",
    "deleted": false,
    "owner_name": "",
    "togglable": true,
    "current_user_role_id": 1,
    "repo_count": 1,
    "metadata": {
      "auto_scan": "true",
      "enable_content_trust": "false",
      "prevent_vul": "false",
      "public": "true",
      "severity": "low"
    }
  },
复制代码

config

main.goconfig.Init()adminserverconfig.goadminserveradminserver
adminserverCORE_SECRETManager
// Manager manages configurations
type Manager struct {
	client client.Client
	Cache  bool
	cache  cache.Cache
	key    string
}
复制代码

配置文件是直接存储在内存上。

	if enableCache {
		m.Cache = true
		m.cache = cache.NewMemoryCache()
		m.key = "cfg"
	}
复制代码
ManagerGetCfgsadminserverjobservice
ProjectManageradmiralAdmiralClientAdmiralClient
driver = admiral.NewDriver(AdmiralClient, AdmiralEndpoint(), TokenReader)
复制代码
admiral

还有一些功能是 LDAP 的实现,这里不做介绍。

filter

用来对一些请求进行过滤,只有符合要求的请求才被允许访问 harbor。这里的过滤主要分为三种分别为:

"application/json", "multipart/form-data", "application/octet-stream"
/api/*

SecurityFilter

Modify(req)

secretReqCtxModifier对发送的请求增加了二种信息:

secretsecret

basicAuthReqCtxModifier 获取请求中的用户名和密码,然后使用账户信息进行登录验证,如果请求中发来的用户信息和数据库中一致。就可以对请求进行修改,给请求增加用户和 pm 信息。具体如下

securCtx := local.NewSecurityContext(user, pm)
	setSecurCtxAndPM(ctx.Request, securCtx, pm)
复制代码
reqsessionGlobalProjectMgrreq
	securCtx := local.NewSecurityContext(&user, pm)

	setSecurCtxAndPM(ctx.Request, securCtx, pm)
复制代码
reqpm
	securCtx = local.NewSecurityContext(nil, pm)
 	setSecurCtxAndPM(ctx.Request, securCtx, pm)

复制代码

ReadonlyFilter

开启 readonly 过滤后,当请求中有删除操作或重新打 label 操作时,请求都会被禁止。

GetSecurityContext

BaseControllerPreparesecurity contextsecurity contextsecurity context
SecurCtxKey=harbor_security_contextsecurity context

GetProjectManager

project managerPmKey=harbor_project_manager

日志的实现

  • 如何实现让日志服务成为单独一个容器,记录来自不同的容器的日志信息

主要需要考虑的就是在每个记录日志的地方,留意日志的输出是设置到哪里的。

src/common/utils/log

context 设计

commoncontext
// Context abstracts the operations related with authN and authZ
type Context interface {
	// IsAuthenticated returns whether the context has been authenticated or not
	IsAuthenticated() bool
	// GetUsername returns the username of user related to the context
	GetUsername() string
	// IsSysAdmin returns whether the user is system admin
	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
}
复制代码
context

重点分析 local 也就是数据库模式的实现。在local 模式中,定义了一个信息的结构体用来管理用户和项目。通过这个结构体将用户和项目关联在一个。

// SecurityContext implements security.Context interface based on database
type SecurityContext struct {
	user *models.User
	pm   promgr.ProjectManager
}
复制代码

IsAuthenticated

core/filtersecurity.goIsAuthenticatedsecurity.go
func (s *SecurityContext) IsAuthenticated() bool {
	return s.user != nil
}
复制代码

GetProjectRoles

用来查找摸个项目用多少用户。先根据请求提供的用户名在数据库中查找多对应的用户信息,查找的 sql 语句如下:

sql := `select user_id, username, password, email, realname, comment, reset_uuid, salt,
		sysadmin_flag, creation_time, update_time
		from harbor_user u
		where deleted = false  and user_id = ? and username = ? and reset_uuid = ? and email = ?`
复制代码
projectIDOrNameSecurityContextProjectManagerGetprojectIDOrNamepmsDriverpmsDriverlocal
// Get ...
func (d *driver) Get(projectIDOrName interface{}) (
	*models.Project, error) {
	id, name, err := utils.ParseProjectIDOrName(projectIDOrName)
	if err != nil {
		return nil, err
	}

	if id > 0 {
		return dao.GetProjectByID(id)
	}

	return dao.GetProjectByName(name)
}
复制代码

最后在根据上述查询到的用户名和项目名来查询此项目有多少用户。

最后返回给前端的 json 格式数据如下:

[
  {
    "id": 1,
    "project_id": 1,
    "entity_name": "admin",
    "role_name": "projectAdmin",
    "role_id": 1,
    "entity_id": 1,
    "entity_type": "u"
  },
  {
    "id": 2,
    "project_id": 1,
    "entity_name": "chenxu",
    "role_name": "developer",
    "role_id": 2,
    "entity_id": 3,
    "entity_type": "u"
  }
]
复制代码

展现出来的效果如下:

GetMyProjects

ProjectManagerListPSMlocal
result, err := s.pm.List(
		&models.ProjectQueryParam{
			Member: &models.MemberQuery{
				Name:      s.GetUsername(),
				GroupList: s.user.GroupList,
			},
		})
复制代码

前端接收到的json 数据格式如下:

[
  {
    "id": 1,
    "name": "library/centos",
    "project_id": 1,
    "description": "",
    "pull_count": 0,
    "star_count": 0,
    "tags_count": 1,
    "labels": [],
    "creation_time": "2019-05-10T04:21:34.499267Z",
    "update_time": "2019-05-10T04:21:34.499267Z"
  },
  {
    "id": 2,
    "name": "library/ubuntu",
    "project_id": 1,
    "description": "",
    "pull_count": 0,
    "star_count": 0,
    "tags_count": 1,
    "labels": [],
    "creation_time": "2019-05-10T04:22:17.538373Z",
    "update_time": "2019-05-10T04:22:17.538373Z"
  }
]
复制代码

展示界面如下

GetRolesByGroup

按组来查询用户,组的概念只有在 LADP 模式下才启用。

token 服务

registrynotary
pull/push
"/service/token"token.go
service="registry.docker.io"

具体解释一下Authorization Service 是如何创建token 的。

生成 token

func (g generalCreator) Create(r *http.Request) (*models.Token, error)
scope="repository:samalba/my-app:pull,push"securitycontextProjectManagersocpes
MakeToken(username, service string, access []*token.ResourceActions) (*models.Token, error)src/core/service/token/authutils.go#143
func makeTokenCore(issuer, subject, audience string, expiration int,
	access []*token.ResourceActions, signingKey libtrust.PrivateKey) (t *token.Token, expiresIn int, issuedAt *time.Time, err error)
复制代码

从传入的数据中构造出镜像的基本信息:命名空间,镜像名,标签。具体数据结构如下:

type image struct {
	namespace string
	repo      string
	tag       string
}
复制代码
registryFilterrepositoryFilterrwmRWR

通知服务

topichandlertopichandler
handler
type NotificationHandler interface {
	// Handle the event when it coming.
	// value might be optional, it depends on usages.
	Handle(value interface{}) error

	// IsStateful returns whether the handler is stateful or not.
	// If handler is stateful, it will not be triggerred in parallel.
	// Otherwise, the handler will be triggered concurrently if more
	// than one same handler are matched the topics.
	IsStateful() bool
}
复制代码

漏洞扫描

topicscan_all_policyhandlerScanPolicyNotificationHandler

前端设置为每日上午 10 点进行扫描,发送给后端的数据 json格式为:

 "scan_all_policy": {
    "value": {
      "parameter": {
        "daily_time": 7200
      },
      "type": "daily"
    },
    "editable": true
  },
复制代码
scan_all_policytypecroncron
    // 先取消所有扫描任务
		if err := cancelScanAllJobs(); err != nil {
			return fmt.Errorf("Failed to cancel scan_all jobs, error: %v", err)
		}
		// 解析前端传入的时间
		h, m, s := common_utils.ParseOfftime(notification.DailyTime)
		cron := fmt.Sprintf("%d %d %d * * *", s, m, h)
		// 创建定时的调度任务
		if err := utils.ScheduleScanAllImages(cron); err != nil {
			return fmt.Errorf("Failed to schedule scan_all job, error: %v", err)
		}
复制代码
ScheduleScanAllImagesjobservicejobservice/api/v1/jobsjobservice

/service/notifications/ 路由

adminjob

jobservice/service/notifications/jobs/adminjob/*jobservicejobidjobserviceUUID

clair

/service/notifications/clair/

jobs

/service/notifications/jobs/*

registry

/service/notifications/Post
/service/notifications/PostNotification
// Notification holds all events.
type Notification struct {
	Events []Event
}
复制代码

然后对传来的事件进行过滤,目前只支持二种事件。分别为:

  • 来自外部的 docker-client 的 pull or push 请求
  • 来自 jobservice 的 push 请求

完成事件过滤后,开始对事件进行加工提取,获取以下数据并持久化存储在数据库中:

dao.AddAccessLog(models.AccessLog{
				Username:  user,
				ProjectID: pro.ProjectID,
				RepoName:  repository,
				RepoTag:   tag,
				Operation: action,
				OpTime:    time.Now(),
			}
复制代码
pushpullpushrepository

等上述准备工作都完成之后,事件通知器发布事件。具体实现如下:

err := notifier.Publish(topic.ReplicationEventTopicOnPush, rep_notification.OnPushNotification{
					Image: image,
				})
复制代码

上述操作会调用对应的处理器对镜像执行操作。可能是处理用户从上传来的镜像,也可能是镜像仓库之间的复制操作,具体要看调用的什么。当有镜像上传到存储库之后会检查是否开启了镜像自动扫描功能。

pull repository 中的pull计数器加 1。