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
默认的验证方式为数据库验证,登录验证的逻辑如下:
- 检查认证方式
- 设置锁,避免短时间内有多次登录请求互相抢占
- 完成上述检查后,根据配置认证方式的不同调用对应的认证器
主要分析数据库认证
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。