Skip to content

Commit

Permalink
Merge pull request #10462 from carlyin0801/issue_10418_api_permission…
Browse files Browse the repository at this point in the history
…_validate_fix

pref:研发商店敏感接口权限校验优化 #10418
  • Loading branch information
bkci-bot authored Jun 18, 2024
2 parents 61b52de + f70d0ad commit 7b0b9a0
Show file tree
Hide file tree
Showing 28 changed files with 467 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import com.tencent.devops.artifactory.util.BkRepoUtils.toFileDetail
import com.tencent.devops.artifactory.util.BkRepoUtils.toFileInfo
import com.tencent.devops.artifactory.util.DefaultPathUtils
import com.tencent.devops.common.api.constant.CommonMessageCode
import com.tencent.devops.common.api.constant.KEY_SHA_CONTENT
import com.tencent.devops.common.api.exception.ErrorCodeException
import com.tencent.devops.common.api.exception.RemoteServiceException
import com.tencent.devops.common.api.pojo.Page
Expand All @@ -64,6 +65,13 @@ import com.tencent.devops.common.archive.util.MimeUtil
import com.tencent.devops.common.auth.api.AuthPermission
import com.tencent.devops.common.auth.api.AuthResourceType
import com.tencent.devops.common.service.utils.HomeHostUtil
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.stereotype.Service
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.context.request.ServletRequestAttributes
import java.io.File
import java.io.OutputStream
import java.net.URLDecoder
Expand All @@ -73,13 +81,6 @@ import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import javax.servlet.http.HttpServletResponse
import javax.ws.rs.NotFoundException
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.stereotype.Service
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.context.request.ServletRequestAttributes

@Service
@Suppress("TooManyFunctions", "MagicNumber", "ComplexMethod")
Expand Down Expand Up @@ -115,7 +116,7 @@ class BkRepoArchiveFileServiceImpl @Autowired constructor(
val pathSplit = file.name.split('.')
val destPath = filePath ?: DefaultPathUtils.randomFileName(pathSplit[pathSplit.size - 1])
val metadata = mutableMapOf<String, String>()
metadata["shaContent"] = file.inputStream().use { ShaUtils.sha1InputStream(it) }
metadata[KEY_SHA_CONTENT] = file.inputStream().use { ShaUtils.sha1InputStream(it) }
props?.forEach {
metadata[it.key] = it.value!!
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import com.tencent.devops.artifactory.pojo.enums.FileChannelTypeEnum
import com.tencent.devops.artifactory.pojo.enums.FileTypeEnum
import com.tencent.devops.artifactory.util.DefaultPathUtils
import com.tencent.devops.common.api.constant.CommonMessageCode
import com.tencent.devops.common.api.constant.KEY_SHA_CONTENT
import com.tencent.devops.common.api.exception.ErrorCodeException
import com.tencent.devops.common.api.pojo.Page
import com.tencent.devops.common.api.util.PageUtil
Expand Down Expand Up @@ -383,7 +384,7 @@ class DiskArchiveFileServiceImpl : ArchiveFileServiceImpl() {
uploadFileToRepo(destPath, file)
val shaContent = file.inputStream().use { ShaUtils.sha1InputStream(it) }
var fileProps: Map<String, String?> = props ?: mapOf()
fileProps = fileProps.plus("shaContent" to shaContent)
fileProps = fileProps.plus(KEY_SHA_CONTENT to shaContent)
val path = destPath.substring(getBasePath().length)
val fileId = UUIDUtil.generate()
dslContext.transaction { t ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import com.tencent.devops.artifactory.pojo.PackageFileInfo
import com.tencent.devops.artifactory.pojo.ReArchiveAtomRequest
import com.tencent.devops.artifactory.store.service.ArchiveAtomService
import com.tencent.devops.common.api.constant.CommonMessageCode
import com.tencent.devops.common.api.constant.KEY_SHA_CONTENT
import com.tencent.devops.common.api.constant.STATIC
import com.tencent.devops.common.api.exception.ErrorCodeException
import com.tencent.devops.common.api.pojo.Result
Expand Down Expand Up @@ -214,7 +215,7 @@ abstract class ArchiveAtomServiceImpl : ArchiveAtomService {
dslContext = context,
userId = userId,
fileId = fileId,
props = mapOf("shaContent" to packageFileInfo.shaContent)
props = mapOf(KEY_SHA_CONTENT to packageFileInfo.shaContent)
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,10 @@ const val REFERER = "referer" // 来源

const val DEVX_HEADER_GW_TOKEN = "X-DEVOPS-DEVX-GW-TOKEN"
const val DEVX_HEADER_NGGW_CLIENT_ADDRESS = "X-BK-NGGW-CLIENT-ADDRESS"

const val AUTH_HEADER_DEVOPS_SHA_CONTENT: String = "X-DEVOPS-SHA-CONTENT"
const val AUTH_HEADER_DEVOPS_OS_NAME: String = "X-DEVOPS-OS-NAME"
const val AUTH_HEADER_DEVOPS_OS_ARCH: String = "X-DEVOPS-OS-ARCH"
const val AUTH_HEADER_DEVOPS_STORE_CODE: String = "X-DEVOPS-STORE-CODE"
const val AUTH_HEADER_DEVOPS_STORE_TYPE: String = "X-DEVOPS-STORE-TYPE"
const val AUTH_HEADER_DEVOPS_STORE_VERSION: String = "X-DEVOPS-STORE-VERSION"
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ const val KEY_ARCHIVE = "archive"
const val KEY_BRANCH_TEST_FLAG = "branchTestFlag"
const val KEY_TASK_ATOM = "taskAtom"
const val KEY_ELEMENT_ENABLE = "elementEnable"
const val KEY_SHA_CONTENT = "shaContent"
const val KEY_INSTALLED_PKG_SHA_CONTENT = "installedPkgShaContent"

const val BK_BUILD_ENV_START_FAILED = "bkBuildEnvStartFailed" // 构建环境启动失败
const val BK_START_PULL_IMAGE = "bkStartPullImage" // 开始拉取镜像,镜像名称:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,5 +105,14 @@ class SpringContextUtil : ApplicationContextAware {
fun getValue(key: String): String? {
return applicationContext?.environment?.get(key)
}

/**
* 根据bean名称判断bean是否存在
* @param beanName bean名称
* @return 布尔值
*/
fun isBeanExist(beanName: String): Boolean {
return applicationContext?.containsBean(beanName) ?: false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,22 @@ package com.tencent.devops.common.web.aop

import com.github.benmanes.caffeine.cache.Caffeine
import com.tencent.devops.common.api.auth.AUTH_HEADER_DEVOPS_BUILD_ID
import com.tencent.devops.common.api.auth.AUTH_HEADER_DEVOPS_OS_ARCH
import com.tencent.devops.common.api.auth.AUTH_HEADER_DEVOPS_OS_NAME
import com.tencent.devops.common.api.auth.AUTH_HEADER_DEVOPS_SHA_CONTENT
import com.tencent.devops.common.api.auth.AUTH_HEADER_DEVOPS_STORE_CODE
import com.tencent.devops.common.api.auth.AUTH_HEADER_DEVOPS_STORE_TYPE
import com.tencent.devops.common.api.auth.AUTH_HEADER_DEVOPS_STORE_VERSION
import com.tencent.devops.common.api.auth.AUTH_HEADER_DEVOPS_VM_SEQ_ID
import com.tencent.devops.common.api.auth.REFERER
import com.tencent.devops.common.api.auth.SIGN_HEADER_NONCE
import com.tencent.devops.common.api.auth.SIGN_HEADER_TIMESTAMP
import com.tencent.devops.common.api.auth.SING_HEADER_SIGNATURE
import com.tencent.devops.common.api.constant.CommonMessageCode
import com.tencent.devops.common.api.exception.ErrorCodeException
import com.tencent.devops.common.client.Client
import com.tencent.devops.common.redis.RedisOperation
import com.tencent.devops.common.service.PROFILE_DEVX
import com.tencent.devops.common.util.ApiSignUtil
import com.tencent.devops.common.web.annotation.SensitiveApiPermission
import com.tencent.devops.common.web.service.ServiceSensitiveApiPermissionResource
Expand All @@ -63,53 +71,91 @@ class SensitiveApiPermissionAspect constructor(

private val apiPermissionCache = Caffeine.newBuilder()
.maximumSize(CACHE_MAX_SIZE)
.build<String/*atomCode:apiName*/, Boolean>()
.build<String, Boolean>()

@Before("pointCut()")
fun doBefore(jp: JoinPoint) {
val request = (RequestContextHolder.getRequestAttributes() as ServletRequestAttributes).request
val buildId = request.getHeader(AUTH_HEADER_DEVOPS_BUILD_ID)
val vmSeqId = request.getHeader(AUTH_HEADER_DEVOPS_VM_SEQ_ID)
val method = (jp.signature as MethodSignature).method
val apiName = method.getAnnotation(SensitiveApiPermission::class.java)?.value

val (atomCode, signToken) = if (buildId != null && vmSeqId != null) {
AtomRuntimeUtil.getRunningAtomValue(
redisOperation = redisOperation, buildId = buildId, vmSeqId = vmSeqId
) ?: Pair(null, null)
val referer = request.getHeader(REFERER)
var verifyFlag = false
var storeCode: String? = request.getHeader(AUTH_HEADER_DEVOPS_STORE_CODE)
if (referer?.contains(PROFILE_DEVX) == true) {
verifyFlag = doShaValidateBus(request, storeCode, apiName)
} else {
Pair(null, null)
val buildId = request.getHeader(AUTH_HEADER_DEVOPS_BUILD_ID)
val vmSeqId = request.getHeader(AUTH_HEADER_DEVOPS_VM_SEQ_ID)
val (atomCode, signToken) = if (buildId != null && vmSeqId != null) {
AtomRuntimeUtil.getRunningAtomValue(
redisOperation = redisOperation, buildId = buildId, vmSeqId = vmSeqId
) ?: Pair(null, null)
} else {
Pair(null, null)
}
storeCode = atomCode
if (apiName != null && storeCode != null) {
verifyFlag = verifyToken(request, signToken) || verifyApi("ATOM", storeCode, apiName)
}
}

if (apiName != null && atomCode != null) {
logger.info("$buildId|$vmSeqId|$atomCode|$apiName|using sensitive api")
if (enableSensitiveApi &&
!verifyToken(request, signToken) &&
!verifyApi(buildId, vmSeqId, atomCode, apiName)
) {
logger.warn("$buildId|$vmSeqId|$atomCode|$apiName|verify sensitive api failed")
throw ErrorCodeException(
statusCode = 401,
errorCode = CommonMessageCode.ERROR_SENSITIVE_API_NO_AUTH,
params = arrayOf(atomCode, apiName),
defaultMessage = "Unauthorized: sensitive api $apiName cannot be used by $atomCode"
)
}
logger.info("$storeCode|$apiName|using sensitive api")
if (enableSensitiveApi && !verifyFlag) {
logger.warn("$storeCode|$apiName|verify sensitive api failed")
throw ErrorCodeException(
statusCode = 401,
errorCode = CommonMessageCode.ERROR_SENSITIVE_API_NO_AUTH,
params = arrayOf(storeCode ?: "", apiName ?: ""),
defaultMessage = "Unauthorized: sensitive api $apiName cannot be used by $storeCode"
)
}
}

private fun doShaValidateBus(
request: HttpServletRequest,
storeCode: String?,
apiName: String?
): Boolean {
val storeType = request.getHeader(AUTH_HEADER_DEVOPS_STORE_TYPE)
val version = request.getHeader(AUTH_HEADER_DEVOPS_STORE_VERSION)
val checkParamFlag = !storeType.isNullOrBlank() && !version.isNullOrBlank()
if (checkParamFlag && !apiName.isNullOrBlank() && !storeCode.isNullOrBlank()) {
val installedPkgShaContent = request.getHeader(AUTH_HEADER_DEVOPS_SHA_CONTENT)
val osName = request.getHeader(AUTH_HEADER_DEVOPS_OS_NAME)
val osArch = request.getHeader(AUTH_HEADER_DEVOPS_OS_ARCH)
return verifyApi(
storeType = storeType,
storeCode = storeCode,
apiName = apiName,
version = version,
installedPkgShaContent = installedPkgShaContent,
osName = osName,
osArch = osArch
)
}
return false
}

@Suppress("LongParameterList")
private fun verifyApi(
buildId: String,
vmSeqId: String,
atomCode: String,
apiName: String
storeType: String,
storeCode: String,
apiName: String,
version: String? = null,
installedPkgShaContent: String? = null,
osName: String? = null,
osArch: String? = null
): Boolean {
val cacheKey = "$atomCode:$apiName"
logger.info("buildId:$buildId|vmSeqId:$vmSeqId|atomCode:$atomCode|apiName:$apiName|verify sensitive api")
val cacheKey = "$storeType:$storeCode:$apiName"
return apiPermissionCache.getIfPresent(cacheKey) ?: run {
val apiPermission = client.get(ServiceSensitiveApiPermissionResource::class).verifyApi(
atomCode = atomCode,
apiName = apiName
installedPkgShaContent = installedPkgShaContent,
osName = osName,
osArch = osArch,
storeCode = storeCode,
apiName = apiName,
storeType = storeType,
version = version
).data == true
// 只有验证通过的插件才缓存,没有验证通过的插件状态是动态的
if (apiPermission) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,22 @@
package com.tencent.devops.common.web.service

import com.tencent.devops.common.api.annotation.ServiceInterface
import com.tencent.devops.common.api.auth.AUTH_HEADER_DEVOPS_OS_ARCH
import com.tencent.devops.common.api.auth.AUTH_HEADER_DEVOPS_OS_NAME
import com.tencent.devops.common.api.auth.AUTH_HEADER_DEVOPS_SHA_CONTENT
import com.tencent.devops.common.api.pojo.Result
import io.swagger.v3.oas.annotations.Parameter
import javax.ws.rs.Consumes
import javax.ws.rs.GET
import javax.ws.rs.HeaderParam
import javax.ws.rs.Path
import javax.ws.rs.PathParam
import javax.ws.rs.Produces
import javax.ws.rs.QueryParam
import javax.ws.rs.core.MediaType

/**
* 验证插件是否有调用敏感接口的权限,在store中实现
* 验证组件是否有调用敏感接口的权限,在store中实现
*/
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
Expand All @@ -46,17 +52,40 @@ import javax.ws.rs.core.MediaType
interface ServiceSensitiveApiPermissionResource {

/**
* 验证api是否已经审批
* 验证组件是否有该api接口的权限
*
* @param atomCode 组件编码
* @param installedPkgShaContent 已安装组件包sha1摘要值
* @param osName 操作系统名称
* @param osArch 操作系统CPU架构
* @param storeCode 组件标识
* @param apiName api接口名称
* @param storeType 组件类型
* @param version 组件版本
*/
@Path("verify/{atomCode}/{apiName}")
@Path("verify/{storeCode}/{apiName}")
@GET
@Suppress("LongParameterList")
fun verifyApi(
@PathParam("atomCode")
atomCode: String,
@Parameter(description = "已安装组件包sha1摘要值", required = false)
@HeaderParam(AUTH_HEADER_DEVOPS_SHA_CONTENT)
installedPkgShaContent: String? = null,
@Parameter(description = "操作系统名称", required = false)
@HeaderParam(AUTH_HEADER_DEVOPS_OS_NAME)
osName: String? = null,
@Parameter(description = "操作系统CPU架构", required = false)
@HeaderParam(AUTH_HEADER_DEVOPS_OS_ARCH)
osArch: String? = null,
@PathParam("storeCode")
@Parameter(description = "组件标识", required = true)
storeCode: String,
@PathParam("apiName")
apiName: String
@Parameter(description = "api接口名称", required = true)
apiName: String,
@QueryParam("storeType")
@Parameter(description = "组件类型", required = true)
storeType: String = "ATOM",
@QueryParam("version")
@Parameter(description = "组件版本", required = false)
version: String? = null
): Result<Boolean>
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ import com.tencent.devops.common.api.pojo.Page
import com.tencent.devops.common.api.pojo.Result
import com.tencent.devops.common.web.annotation.BkField
import com.tencent.devops.common.web.constant.BkStyleEnum
import com.tencent.devops.store.pojo.common.InstalledPkgShaContentRequest
import com.tencent.devops.store.pojo.common.MyStoreComponent
import com.tencent.devops.store.pojo.common.StoreBaseInfoUpdateRequest
import com.tencent.devops.store.pojo.common.StoreDetailInfo
import com.tencent.devops.store.pojo.common.enums.StoreSortTypeEnum
import com.tencent.devops.store.pojo.common.enums.StoreTypeEnum
import com.tencent.devops.store.pojo.common.publication.StoreApproveReleaseRequest
import com.tencent.devops.store.pojo.common.publication.StoreOfflineRequest
import com.tencent.devops.store.pojo.common.version.StoreDeskVersionItem
Expand Down Expand Up @@ -206,4 +208,26 @@ interface OpStoreComponentResource {
@Valid
storeOfflineRequest: StoreOfflineRequest
): Result<Boolean>

@Operation(summary = "更新组件已安装包sha1摘要值")
@PUT
@Path("/types/{storeType}/codes/{storeCode}/versions/{version}/component/installed/pkg/sha/update")
fun updateComponentInstalledPkgShaContent(
@Parameter(description = "用户ID", required = true, example = AUTH_HEADER_USER_ID_DEFAULT_VALUE)
@HeaderParam(AUTH_HEADER_USER_ID)
userId: String,
@Parameter(description = "组件类型", required = true)
@PathParam("storeType")
@BkField(patternStyle = BkStyleEnum.CODE_STYLE)
storeType: StoreTypeEnum,
@Parameter(description = "组件代码", required = true)
@PathParam("storeCode")
@BkField(patternStyle = BkStyleEnum.CODE_STYLE)
storeCode: String,
@Parameter(description = "组件版本", required = true)
@PathParam("version")
version: String,
@Parameter(description = "更新组件已安装包sha1摘要值请求报文", required = true)
installedPkgShaContentRequest: InstalledPkgShaContentRequest
): Result<Boolean>
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ interface UserSensitiveApiResource {
@Parameter(description = "组件标识", required = true)
@PathParam("storeCode")
storeCode: String,
@Parameter(description = "组件标识", required = true)
@Parameter(description = "开发语言", required = true)
@QueryParam("language")
language: String
language: String = ""
): Result<List<SensitiveApiNameInfo>>

@Operation(summary = "敏感API申请")
Expand Down
Loading

0 comments on commit 7b0b9a0

Please sign in to comment.