![JAR search and dependency download from the Maven repository](/logo.png)
com.tencent.bkrepo.common.security.manager.PermissionManager.kt Maven / Gradle / Ivy
/*
* Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available.
*
* Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
*
* BK-CI 蓝鲸持续集成平台 is licensed under the MIT license.
*
* A copy of the MIT License is included in this file.
*
*
* Terms of the MIT License:
* ---------------------------------------------------
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.tencent.bkrepo.common.security.manager
import com.google.common.cache.CacheBuilder
import com.google.common.cache.CacheLoader
import com.google.common.cache.LoadingCache
import com.tencent.bkrepo.auth.api.ServiceExternalPermissionClient
import com.tencent.bkrepo.auth.api.ServicePermissionClient
import com.tencent.bkrepo.auth.api.ServiceUserClient
import com.tencent.bkrepo.auth.pojo.enums.PermissionAction
import com.tencent.bkrepo.auth.pojo.enums.ResourceType
import com.tencent.bkrepo.auth.pojo.externalPermission.ExternalPermission
import com.tencent.bkrepo.auth.pojo.permission.CheckPermissionRequest
import com.tencent.bkrepo.common.api.constant.ANONYMOUS_USER
import com.tencent.bkrepo.common.api.constant.MediaTypes
import com.tencent.bkrepo.common.api.pojo.Response
import com.tencent.bkrepo.common.api.util.readJsonString
import com.tencent.bkrepo.common.api.util.toJsonString
import com.tencent.bkrepo.common.artifact.constant.PIPELINE
import com.tencent.bkrepo.common.artifact.exception.NodeNotFoundException
import com.tencent.bkrepo.common.artifact.exception.RepoNotFoundException
import com.tencent.bkrepo.common.artifact.path.PathUtils
import com.tencent.bkrepo.common.security.exception.AuthenticationException
import com.tencent.bkrepo.common.security.exception.PermissionException
import com.tencent.bkrepo.common.security.http.core.HttpAuthProperties
import com.tencent.bkrepo.common.security.permission.PrincipalType
import com.tencent.bkrepo.common.security.util.SecurityUtils
import com.tencent.bkrepo.common.service.util.HttpContextHolder
import com.tencent.bkrepo.common.service.util.LocaleMessageUtils
import com.tencent.bkrepo.repository.api.NodeClient
import com.tencent.bkrepo.repository.api.RepositoryClient
import com.tencent.bkrepo.repository.constant.NODE_DETAIL_LIST_KEY
import com.tencent.bkrepo.repository.constant.SYSTEM_USER
import com.tencent.bkrepo.repository.pojo.node.NodeDetail
import com.tencent.bkrepo.repository.pojo.node.NodeListOption
import com.tencent.bkrepo.repository.pojo.repo.RepositoryInfo
import okhttp3.Headers
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.slf4j.LoggerFactory
import java.io.IOException
import java.util.concurrent.TimeUnit
/**
* 权限管理类
*/
open class PermissionManager(
private val repositoryClient: RepositoryClient,
private val permissionResource: ServicePermissionClient,
private val externalPermissionResource: ServiceExternalPermissionClient,
private val userResource: ServiceUserClient,
private val nodeClient: NodeClient,
private val httpAuthProperties: HttpAuthProperties
) {
private val httpClient =
OkHttpClient.Builder().connectTimeout(10L, TimeUnit.SECONDS).readTimeout(10L, TimeUnit.SECONDS).build()
private val externalPermissionCache: LoadingCache> by lazy {
val cacheLoader = object : CacheLoader>() {
override fun load(key: String): List =
externalPermissionResource.listExternalPermission().data!!
}
CacheBuilder.newBuilder().maximumSize(1).expireAfterWrite(30L, TimeUnit.MINUTES).build(cacheLoader)
}
/**
* 校验项目权限
* @param action 动作
* @param projectId 项目id
*/
open fun checkProjectPermission(
action: PermissionAction,
projectId: String,
userId: String = SecurityUtils.getUserId()
) {
checkPermission(
type = ResourceType.PROJECT,
action = action,
projectId = projectId,
userId = userId
)
}
/**
* 校验仓库权限
* @param action 动作
* @param projectId 项目id
* @param repoName 仓库名称
* @param public 仓库是否为public
* @param anonymous 是否允许匿名
*/
open fun checkRepoPermission(
action: PermissionAction,
projectId: String,
repoName: String,
public: Boolean? = null,
anonymous: Boolean = false,
userId: String = SecurityUtils.getUserId()
) {
val repoInfo = queryRepositoryInfo(projectId, repoName)
if (isReadPublicRepo(action, repoInfo, public)) {
return
}
if (allowReadSystemRepo(action, repoInfo, userId)) {
return
}
checkPermission(
type = ResourceType.REPO,
action = action,
projectId = projectId,
repoName = repoName,
anonymous = anonymous,
userId = userId
)
}
/**
* 校验节点权限
* @param action 动作
* @param projectId 项目id
* @param repoName 仓库名称
* @param path 节点路径
* @param public 仓库是否为public
* @param anonymous 是否允许匿名
*/
open fun checkNodePermission(
action: PermissionAction,
projectId: String,
repoName: String,
vararg path: String,
public: Boolean? = null,
anonymous: Boolean = false,
userId: String = SecurityUtils.getUserId()
) {
val repoInfo = queryRepositoryInfo(projectId, repoName)
if (isReadPublicRepo(action, repoInfo, public)) {
return
}
if (allowReadSystemRepo(action, repoInfo, userId)) {
return
}
// 禁止批量下载流水线节点
if (path.size > 1 && repoName == PIPELINE) {
throw PermissionException()
}
checkPermission(
type = ResourceType.NODE,
action = action,
projectId = projectId,
repoName = repoName,
paths = path.toList(),
anonymous = anonymous,
userId = userId
)
}
/**
* 校验身份
* @param userId 用户id
* @param principalType 身份类型
*/
open fun checkPrincipal(userId: String, principalType: PrincipalType) {
val platformId = SecurityUtils.getPlatformId()
checkAnonymous(userId, platformId)
if (principalType == PrincipalType.ADMIN) {
if (!isAdminUser(userId)) {
throw PermissionException()
}
} else if (principalType == PrincipalType.PLATFORM) {
if (userId.isEmpty()) {
logger.warn("platform auth with empty userId[$platformId,$userId]")
}
if (platformId == null && !isAdminUser(userId)) {
throw PermissionException()
}
} else if (principalType == PrincipalType.GENERAL) {
if (userId.isEmpty() || userId == ANONYMOUS_USER) {
throw PermissionException()
}
}
}
/**
* 判断是否为public仓库且为READ操作
*/
private fun isReadPublicRepo(
action: PermissionAction,
repoInfo: RepositoryInfo,
public: Boolean? = null
): Boolean {
if (action != PermissionAction.READ) {
return false
}
return public ?: repoInfo.public
}
/**
* 判断是否为系统级公开仓库且为READ操作
*/
@Suppress("TooGenericExceptionCaught")
private fun allowReadSystemRepo(
action: PermissionAction,
repoInfo: RepositoryInfo,
userId: String = SecurityUtils.getUserId()
): Boolean {
if (SecurityUtils.isServiceRequest()) {
return true
}
if (action != PermissionAction.READ) {
return false
}
val platformId = SecurityUtils.getPlatformId()
checkAnonymous(userId, platformId)
// 加载仓库信息
val systemValue = repoInfo.configuration.settings["system"]
val system = try {
systemValue as? Boolean
} catch (e: Exception) {
logger.error("Repo configuration system field trans failed: $systemValue", e)
false
}
return true == system
}
/**
* 查询仓库信息
*/
open fun queryRepositoryInfo(projectId: String, repoName: String): RepositoryInfo {
return repositoryClient.getRepoInfo(projectId, repoName).data ?: throw RepoNotFoundException(repoName)
}
/**
* 去auth微服务校验资源权限
*/
private fun checkPermission(
type: ResourceType,
action: PermissionAction,
projectId: String,
repoName: String? = null,
paths: List? = null,
anonymous: Boolean = false,
userId: String = SecurityUtils.getUserId()
) {
// 判断是否开启认证
if (!httpAuthProperties.enabled) {
return
}
val platformId = SecurityUtils.getPlatformId()
checkAnonymous(userId, platformId)
if (userId == ANONYMOUS_USER && platformId != null && anonymous) {
return
}
// 校验Oauth token对应权限
val authorities = SecurityUtils.getAuthorities()
if (authorities.isNotEmpty() && !authorities.contains(type.toString())) {
throw PermissionException()
}
// 自定义外部权限校验
val externalPermission = getExternalPermission(projectId, repoName)
if (externalPermission != null) {
checkExternalPermission(externalPermission, userId, type, action, projectId, repoName, paths)
return
}
// 去auth微服务校验资源权限
val checkRequest = CheckPermissionRequest(
uid = userId,
appId = platformId,
resourceType = type.toString(),
action = action.toString(),
projectId = projectId,
repoName = repoName,
path = paths?.first()
)
if (checkPermissionFromAuthService(checkRequest) != true) {
// 无权限,响应403错误
val reason: String?
if (repoName.isNullOrEmpty()) {
val param = arrayOf(userId, action, projectId )
reason = LocaleMessageUtils.getLocalizedMessage("permission.project.denied", param)
} else {
val param = arrayOf(userId, action, projectId, repoName )
reason = LocaleMessageUtils.getLocalizedMessage("permission.repo.denied", param)
}
throw PermissionException(reason)
}
if (logger.isDebugEnabled) {
logger.debug("User[${SecurityUtils.getPrincipal()}] check permission success.")
}
}
open fun checkPermissionFromAuthService(request: CheckPermissionRequest): Boolean? {
return permissionResource.checkPermission(request).data
}
/**
* 获取当前项目、仓库的自定义外部权限
*/
open fun getExternalPermission(projectId: String, repoName: String?): ExternalPermission? {
val externalPermissionList = externalPermissionCache.get(SYSTEM_USER)
val platformId = SecurityUtils.getPlatformId()
val ext = externalPermissionList.firstOrNull { p ->
p.enabled.and(projectId.matches(wildcardToRegex(p.projectId)))
.and(repoName?.matches(wildcardToRegex(p.repoName)) ?: true).and(matchApi(p.scope))
.and(p.platformWhiteList.isNullOrEmpty() || !p.platformWhiteList!!.contains(platformId))
}
return ext
}
/**
* 匹配需要自定义鉴权的接口
* 通过straceTrace获取接口名称
* 1. 过滤包名为com.tencent.bkrepo的接口
* 2. 使用注解鉴权的接口是由Spring cglib生成的,类名中包含$$EnhancerBySpringCGLIB$$xxxx, 需要替换掉
* 例如com.tencent.bkrepo.generic.controller.GenericController$$EnhancerBySpringCGLIB$$bccb61f5.download()
* 3. 去掉括号,得到接口名称
* 例如com.tencent.bkrepo.generic.controller.GenericController.download
* 然后scope与接口名称匹配进行正则匹配
*/
private fun matchApi(scope: String): Boolean {
val stackTraceElements =
Thread.currentThread().stackTrace.toList().filter { it.toString().startsWith(PACKAGE_NAME_PREFIX) }.map {
it.toString().replace(Regex("\\\$\\\$(.*)\\\$\\\$[a-z0-9]+"), "")
.substringBefore("(")
}
logger.debug("stack trace elements: $stackTraceElements")
val pattern = wildcardToRegex(scope)
stackTraceElements.forEach {
if (pattern.matches(it)) {
logger.debug("scope[$scope] match api: $it")
return true
}
}
return false
}
/**
* 检查外部权限
*/
private fun checkExternalPermission(
externalPermission: ExternalPermission,
userId: String,
type: ResourceType,
action: PermissionAction,
projectId: String,
repoName: String?,
paths: List?
) {
var errorMsg = "user[$userId] does not have $action permission in project[$projectId] repo[$repoName]"
paths?.let { errorMsg = errorMsg.plus(" path$paths") }
val nodes = getNodeDetailList(projectId, repoName, paths)
val request = buildRequest(externalPermission, type, action, userId, projectId, repoName, nodes)
callbackToAuth(request, projectId, repoName, paths, errorMsg)
}
private fun getNodeDetailList(
projectId: String,
repoName: String?,
paths: List?
): List? {
val nodeDetailList = if (repoName.isNullOrBlank() || paths.isNullOrEmpty()) {
null
} else if (paths.size == 1) {
val node = nodeClient.getNodeDetail(projectId, repoName, paths.first()).data ?: throw NodeNotFoundException(
paths.first()
)
listOf(node)
} else {
queryNodeDetailList(projectId, repoName, paths)
}
if (!nodeDetailList.isNullOrEmpty()) {
HttpContextHolder.getRequest().setAttribute(NODE_DETAIL_LIST_KEY, nodeDetailList)
}
return nodeDetailList
}
private fun queryNodeDetailList(
projectId: String,
repoName: String,
paths: List
): List {
val prefix = PathUtils.getCommonParentPath(paths)
var pageNumber = 1
val nodeDetailList = mutableListOf()
do {
val option = NodeListOption(
pageNumber = pageNumber, pageSize = 1000, includeFolder = true, includeMetadata = true, deep = true
)
val records = nodeClient.listNodePage(projectId, repoName, prefix, option).data?.records
if (records.isNullOrEmpty()) {
break
}
nodeDetailList.addAll(records.filter { paths.contains(it.fullPath) }.map { NodeDetail(it) })
pageNumber++
} while (nodeDetailList.size < paths.size)
return nodeDetailList
}
private fun callbackToAuth(
request: Request,
projectId: String,
repoName: String?,
paths: List?,
errorMsg: String
) {
try {
httpClient.newCall(request).execute().use {
val content = it.body?.string()
if (it.isSuccessful && checkResponse(content)) {
return
}
logger.info(
"check external permission error, url[${request.url}], project[$projectId], repo[$repoName]," +
" nodes$paths, code[${it.code}], response[$content]"
)
throw PermissionException(errorMsg)
}
} catch (e: IOException) {
logger.error(
"check external permission error," + "url[${request.url}], project[$projectId], " +
"repo[$repoName], nodes$paths, $e"
)
throw PermissionException(errorMsg)
}
}
private fun checkResponse(content: String?): Boolean {
if (content.isNullOrBlank()) {
return true
}
logger.debug("response content: $content")
val data = content.readJsonString>()
if (data.isNotOk()) {
return false
}
return true
}
private fun buildRequest(
externalPermission: ExternalPermission,
type: ResourceType,
action: PermissionAction,
userId: String,
projectId: String,
repoName: String?,
nodes: List?
): Request {
val headersBuilder = Headers.Builder()
externalPermission.headers?.forEach { (k, v) ->
headersBuilder[k] = v
}
val requestData = mutableMapOf()
requestData[USER_ID] = userId
requestData[TYPE] = type.toString()
requestData[ACTION] = action.toString()
requestData[PROJECT_ID] = projectId
repoName?.let { requestData[REPO_NAME] = repoName }
nodes?.let {
val nodeMaps = mutableListOf
© 2015 - 2025 Weber Informatics LLC | Privacy Policy