Harbor Auth Token 分析

2016, Oct 24    

分析环境:

harbor:https://github.com/vmware/harbor
Tags:0.4.1

1.docker 鉴权请求分析

1.docker 直接 pull public repo,请求格式如下:

/service/token?scope=repository:test/repo:pull&service=token-service

2.docker pull private repo,请求格式如下:

/service/token?account=test&scope=repository:test/repo:push,pull&service=token-service

除此之外,对 private repo 的请求还会在 Request Header 中加入 Authorization HTTP Basic Authentication 格式如下:

//account:password
Basic YWNjb3VudDpwYXNzd29yZA==

两个请求共有的部分如下:
scope:指定类型(repository),repo(test/repo),请求的操作权限(pull && push)
service:即 JWT 验证中的 Audience,Token 接收方(即 registry)

2.Harbor 权限获取源码分析

Harbor 路由:/service/token(/service/token/token.go#39)

// Get 处理获取 Token 的请求
func (h *Handler) Get() {

    var username, password string
    request := h.Ctx.Request
    // 获取 JWT 接收方名称
    service := h.GetString("service")
    // 获取 scopes
    scopes := h.GetStrings("scope")
    // 根据 scopes 生成 ResourceActions 数组, 该结构体的定义位于 github.com/docker/distribution/registry/auth/token/token.go#32
    //type ResourceActions struct {
    //  Type    string   `json:"type"`// 资源类型, 此处为 repository
    //  Name    string   `json:"name"`// 资源名称, 即 repo 名称
    //  Actions []string `json:"actions"`// 资源可用权限数组, 即 pull,push
    //}
    access := GetResourceActions(scopes)
    log.Infof("request url: %v", request.URL.String())

    // 验证 Request 的 Cookie 中是否包含 uisecret, 其值为 Harbor 预定义的密钥
    // 该密钥在环境变量 UI_SECRET 中定义, 使用该密钥可以获得任意权限, 仅在 Harbor 的 Job Service 中使用
    if svc_utils.VerifySecret(request) {
        log.Debugf("Will grant all access as this request is from job service with legal secret.")
        username = "job-service-user"
    } else {
        // 从 HTTP Basic Authentication 中获取用户名密码
        username, password, _ = request.BasicAuth()
        // 验证用户名密码是否正确
        authenticated := authenticate(username, password)
        // 如果 scopes 为空并且用户不存在则直接终止请求
        if len(scopes) == 0 && !authenticated {
            log.Info("login request with invalid credentials")
            h.CustomAbort(http.StatusUnauthorized, "")
        }
        // 对用户请求的权限进行过滤, 确保用户不能获得不属于自身的权限
        for _, a := range access {
            FilterAccess(username, authenticated, a)
        }
    }
    // 根据用户名, 接收方名称, 可用权限生成 JWT
    h.serveToken(username, service, access)
}

FilterAccess 过滤方法:/service/token/authutils.go#99

// FilterAccess 过滤用户请求的权限
func FilterAccess(username string, authenticated bool, a *token.ResourceActions) {
    // 对于 registry 类型并且名为 catalog 的请求不进行任何过滤
    if a.Type == "registry" && a.Name == "catalog" {
        log.Infof("current access, type: %s, name:%s, actions:%v \n", a.Type, a.Name, a.Actions)
        return
    }

    // 直接对 Actions 重新赋值, 无视用户请求的权限
    a.Actions = []string{}
    if a.Type == "repository" {
        if strings.Contains(a.Name, "/") {
            //Harbor 目前不允许创建带有 / 的 project,projectName 为 a.Name 到最后一个 / 之间的字符串
            // 因此 Harbor 中, 只能识别类似 test/repo 的两级 repo
            // 对于非 2 级的 repo 名称全部无法 push 和 pull
            // 例如 repo  test/xxx/repo 都是无效的 (虽然 docker 允许创建这种 Tag)
            projectName := a.Name[0:strings.LastIndex(a.Name, "/")]
            var permission string
            if authenticated {
                isAdmin, err := dao.IsAdminRole(username)
                if err != nil {
                    log.Errorf("Error occurred in IsAdminRole: %v", err)
                }
                if isAdmin {
                    exist, err := dao.ProjectExists(projectName)
                    if err != nil {
                        log.Errorf("Error occurred in CheckExistProject: %v", err)
                        return
                    }
                    if exist {
                        //Admin 对于任何存在的项目都拥有所有权限
                        permission = "RWM"
                    } else {
                        permission = ""log.Infof("project %s does not exist, set empty permission for admin\n", projectName)
                    }
                } else {
                    // 普通用户根据用户在项目中的 Role 获得不同的权限
                    //projectAdmin MDRWS
                    //developer RWS
                    //guest RS
                    permission, err = dao.GetPermission(username, projectName)
                    if err != nil {
                        log.Errorf("Error occurred in GetPermission: %v", err)
                        return
                    }
                }
            }
            //push 权限
            if strings.Contains(permission, "W") {
                a.Actions = append(a.Actions, "push")
            }
            // 管理权限
            if strings.Contains(permission, "M") {
                a.Actions = append(a.Actions, "*")
            }
            //pull 权限, 权限中包含 R 或者该 project 为 public 的
            if strings.Contains(permission, "R") || dao.IsProjectPublic(projectName) {
                a.Actions = append(a.Actions, "pull")
            }
        }
    }
    log.Infof("current access, type: %s, name:%s, actions:%v \n", a.Type, a.Name, a.Actions)
}

3.Harbor Token 生成分析

MakeToken 方法:/service/token/authutils.go#161

// MakeToken 生成 JWT 字符串
func MakeToken(username, service string, access []*token.ResourceActions) (token string, expiresIn int, issuedAt *time.Time, err error) {
    // 读取用于 JWT 签名的私钥
    pk, err := libtrust.LoadKeyFile(privateKey)
    if err != nil {
        return "", 0, nil, err
    }
    // 生成 Token,expiration 是全局变量, 默认值为 30, 即 Token 过期时间为 30 分钟
    tk, expiresIn, issuedAt, err := makeTokenCore(issuer, username, service, expiration, access, pk)
    if err != nil {
        return "", 0, nil, err
    }
    // 组合 Token, 构成 Header.Claim.Sign 格式的字符串
    rs := fmt.Sprintf("%s.%s", tk.Raw, base64UrlEncode(tk.Signature))
    return rs, expiresIn, issuedAt, nil
}

//makeTokenCore 生成 Token
func makeTokenCore(issuer, subject, audience string, expiration int,
    access []*token.ResourceActions, signingKey libtrust.PrivateKey) (t *token.Token, expiresIn int, issuedAt *time.Time, err error) {
    //JWT 头部
    joseHeader := &token.Header{
        Type:       "JWT",// 类型
        SigningAlg: "RS256",// 签名方法
        KeyID:      signingKey.KeyID(),// 使用私钥生成的唯一 ID
    }
    // 生成一串随机的 JWT ID
    jwtID, err := randString(16)
    if err != nil {
        return nil, 0, nil, fmt.Errorf("Error to generate jwt id: %s", err)
    }

    now := time.Now().UTC()
    issuedAt = &now
    //expiration 为过期时间 (秒)
    expiresIn = expiration * 60
    // 填充 Token 结构
    claimSet := &token.ClaimSet{
        Issuer:     issuer,//Token 签发者
        Subject:    subject,// 获取 Token 的用户
        Audience:   audience,//Token 接收者
        Expiration: now.Add(time.Duration(expiration) * time.Minute).Unix(),//Token 过期时间
        NotBefore:  now.Unix(),
        IssuedAt:   now.Unix(),//Token 签发时间
        JWTID:      jwtID,
        Access:     access,// 权限
    }

    var joseHeaderBytes, claimSetBytes []byte
    // 将 Header 进行 Json 序列化
    if joseHeaderBytes, err = json.Marshal(joseHeader); err != nil {
        return nil, 0, nil, fmt.Errorf("unable to marshal jose header: %s", err)
    }
    // 将 Claim 进行 Json 序列化
    if claimSetBytes, err = json.Marshal(claimSet); err != nil {
        return nil, 0, nil, fmt.Errorf("unable to marshal claim set: %s", err)
    }
    // 将 Header 和 Claim 字节数组进行 base64 编码
    encodedJoseHeader := base64UrlEncode(joseHeaderBytes)
    encodedClaimSet := base64UrlEncode(claimSetBytes)
    // 将 Header 和 Claim 合并为 Header.Claim
    payload := fmt.Sprintf("%s.%s", encodedJoseHeader, encodedClaimSet)

    // 使用私钥对 payload 进行签名
    var signatureBytes []byte
    if signatureBytes, _, err = signingKey.Sign(strings.NewReader(payload), crypto.SHA256); err != nil {
        return nil, 0, nil, fmt.Errorf("unable to sign jwt payload: %s", err)
    }

    signature := base64UrlEncode(signatureBytes)
    // 组合 Token, 构成 Header.Claim.Sign 格式的字符串
    tokenString := fmt.Sprintf("%s.%s", payload, signature)
    // 创建 Token 实例
    t, err = token.NewToken(tokenString)
    return
}