Skip to content

Commit

Permalink
feat: 支持云桌面分享人上传文件 #2873
Browse files Browse the repository at this point in the history
  • Loading branch information
yaoxuwan committed Dec 25, 2024
1 parent c16a9df commit 8d74ed2
Show file tree
Hide file tree
Showing 8 changed files with 253 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -147,5 +147,9 @@ data class DevXProperties(
*/
var authToken: String = "",

/**
* 搜索云桌面接口url
*/
var workspaceSearchUrl: String = "",

)
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,13 @@

package com.tencent.bkrepo.websocket.controller

import com.tencent.bkrepo.common.api.constant.ANONYMOUS_USER
import com.tencent.bkrepo.common.api.constant.USER_KEY
import com.tencent.bkrepo.websocket.pojo.fs.CopyPDU
import com.tencent.bkrepo.websocket.pojo.fs.PastePDU
import com.tencent.bkrepo.websocket.service.ClipboardService
import org.springframework.messaging.handler.annotation.MessageMapping
import org.springframework.messaging.simp.SimpMessageHeaderAccessor
import org.springframework.stereotype.Controller

@Controller
Expand All @@ -40,8 +43,9 @@ class ClipboardController(
) {

@MessageMapping("/copy")
fun copy(copyPDU: CopyPDU) {
clipboardService.copy(copyPDU)
fun copy(copyPDU: CopyPDU, accessor: SimpMessageHeaderAccessor) {
val userId = accessor.sessionAttributes?.get(USER_KEY)?.toString() ?: ANONYMOUS_USER
clipboardService.copy(userId, copyPDU)
}

@MessageMapping("/paste")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,31 @@
package com.tencent.bkrepo.websocket.exception

import com.tencent.bkrepo.common.api.exception.ErrorCodeException
import com.tencent.bkrepo.common.api.pojo.Response
import com.tencent.bkrepo.common.service.exception.AbstractExceptionHandler
import com.tencent.bkrepo.common.api.message.CommonMessageCode
import com.tencent.bkrepo.common.service.log.LoggerHolder
import com.tencent.bkrepo.common.service.util.LocaleMessageUtils
import org.springframework.messaging.handler.annotation.MessageExceptionHandler
import org.springframework.messaging.simp.SimpMessagingTemplate
import org.springframework.web.bind.annotation.RestControllerAdvice

// TODO 异常处理传递到客户端
@RestControllerAdvice
class WebsocketExceptionHandler : AbstractExceptionHandler() {
class WebsocketExceptionHandler(
private val simpMessagingTemplate: SimpMessagingTemplate,
) {

@MessageExceptionHandler(ErrorCodeException::class)
fun handleException(exception: ErrorCodeException): Response<Void> {
return response(exception)
fun handleException(exception: ErrorCodeException) {
val errorMessage = LocaleMessageUtils.getLocalizedMessage(exception.messageCode, exception.params)
LoggerHolder.logErrorCodeException(exception, "[${exception.messageCode.getCode()}]$errorMessage")
simpMessagingTemplate.convertAndSend("/topic/error", errorMessage)
}

@MessageExceptionHandler(Exception::class)
fun handleException(exception: Exception): Response<Void> {
return response(exception)
fun handleException(exception: Exception) {
val errorMessage = LocaleMessageUtils.getLocalizedMessage(CommonMessageCode.SYSTEM_ERROR)
val code = CommonMessageCode.SYSTEM_ERROR.getCode()
LoggerHolder.logException(exception, "[$code]${exception.message}", true)
simpMessagingTemplate.convertAndSend("/topic/error", errorMessage)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ class SessionHandler(
val (accessKey, secretKey) = String(Base64.getDecoder().decode(platformToken)).split(COLON)
val appId = authenticationManager.checkPlatformAccount(accessKey, secretKey)
session.attributes[PLATFORM_KEY] = appId
session.attributes[USER_KEY] = session.handshakeHeaders[AUTH_HEADER_UID]
session.attributes[USER_KEY] = session.handshakeHeaders[AUTH_HEADER_UID]?.first()
}
uri.path.startsWith(APP_ENDPOINT) -> {
val token = session.handshakeHeaders[HttpHeaders.AUTHORIZATION]?.firstOrNull().orEmpty()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@ data class CopyPDU(
val files: Map<String, Long>,
val timestamp: Long,
val dstPath: String? = null,
val strategy: ConflictStrategy = ConflictStrategy.OVERWRITE
val strategy: ConflictStrategy = ConflictStrategy.OVERWRITE,
var token: String? = null
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available.
*
* Copyright (C) 2024 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.websocket.pojo.fs

import io.swagger.v3.oas.annotations.media.Schema

@Schema(title = "工作空间信息")
data class ProjectWorkspace(
@get:Schema(title = "工作空间ID<只读>", readOnly = true)
val workspaceId: Long?,
@get:Schema(title = "工作空间名称")
val workspaceName: String,
@get:Schema(title = "项目ID")
val projectId: String?,
@get:Schema(title = "工作空间备注名称")
val displayName: String? = null,
@get:Schema(title = "状态最近更新时间<只读>", readOnly = true)
val lastStatusUpdateTime: Long? = null,
@get:Schema(title = "休眠时间<只读>", readOnly = true)
val sleepingTime: Long? = null,
@get:Schema(title = "工作空间创建人<只读>", readOnly = true)
val createUserId: String,
@get:Schema(title = "工作空间对应的IP")
val hostName: String? = null,
@get:Schema(title = "拥有者")
val owner: String? = null,
@get:Schema(title = "拥有者_CN")
val ownerCN: String? = null,
@get:Schema(title = "查看者")
val viewers: List<String>? = emptyList(),
@get:Schema(title = "查看者_CN")
val viewersCN: List<String>? = emptyList(),
@get:Schema(title = "当前登陆者信息")
var currentLoginUsers: List<String>,
@get:Schema(title = "云桌面对应的mac地址")
val macAddress: String? = null,
@get:Schema(title = "工作空间备注")
val remark: String? = null,
@get:Schema(title = "标签")
var labels: List<String>? = null,
@get:Schema(title = "创建时间<只读>", readOnly = true)
val createTime: Long? = null,
@get:Schema(title = "镜像ID")
val imageId: String? = null,
@get:Schema(title = "是否开启了录屏")
val recordEnabled: Boolean
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available.
*
* Copyright (C) 2024 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.websocket.pojo.fs

import io.swagger.v3.oas.annotations.media.Schema

data class WorkspaceSearch(
@get:Schema(title = "工作空间名称")
val workspaceName: List<String>? = null,
@get:Schema(title = "工作空间备注名称")
val displayName: List<String>? = null,
@get:Schema(title = "区域简称,SZ,NJ")
var zoneShortName: List<String>? = null,
@get:Schema(title = "资源类型:MLXLS")
val size: List<String>? = null,
@get:Schema(title = "工作空间对应的IP,可带区域,也可不带区域进行模糊匹配, 可能NJ1.12.123.12.132")
val ips: List<String>? = null,
@get:Schema(title = "工作空间对应的IP 不带区域, 只能12.123.12.132")
val sips: List<String>? = null,
@get:Schema(title = "云桌面对应的mac地址")
val macAddress: List<String>? = null,
@get:Schema(title = "拥有者")
val owner: List<String>? = null,
@get:Schema(title = "查看者")
var viewers: List<String>? = null,
@get:Schema(title = "项目id")
var projectId: List<String>? = null,
@get:Schema(title = "协助工单,仅op有效")
var expertSupId: List<Long>? = null,
@get:Schema(title = "标签ids")
var labels: List<String>? = null,
@get:Schema(title = "工作空间所在部门")
var businessLineNames: List<String>? = null,
@get:Schema(title = "是否模糊匹配,可以关闭,查询会更快。")
val onFuzzyMatch: Boolean = false,
@get:Schema(title = "节点HashId")
val nodeHashIds: List<String>? = null,
@get:Schema(title = "为true返回公共云桌面实例,默认不会返回")
val onPublic: Boolean = false
)
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,50 @@

package com.tencent.bkrepo.websocket.service

import com.tencent.bkrepo.auth.pojo.enums.PermissionAction
import com.tencent.bkrepo.common.api.constant.HttpHeaders
import com.tencent.bkrepo.common.api.constant.MediaTypes
import com.tencent.bkrepo.common.api.pojo.Page
import com.tencent.bkrepo.common.api.util.readJsonString
import com.tencent.bkrepo.common.api.util.toJsonString
import com.tencent.bkrepo.common.metadata.permission.PermissionManager
import com.tencent.bkrepo.common.security.exception.PermissionException
import com.tencent.bkrepo.common.security.http.jwt.JwtAuthProperties
import com.tencent.bkrepo.common.security.interceptor.devx.ApiAuth
import com.tencent.bkrepo.common.security.interceptor.devx.DevXProperties
import com.tencent.bkrepo.common.security.util.JwtUtils
import com.tencent.bkrepo.common.service.util.okhttp.HttpClientBuilderFactory
import com.tencent.bkrepo.common.storage.innercos.http.toRequestBody
import com.tencent.bkrepo.fs.server.constant.JWT_CLAIMS_PERMIT
import com.tencent.bkrepo.fs.server.constant.JWT_CLAIMS_REPOSITORY
import com.tencent.bkrepo.websocket.dispatch.TransferDispatch
import com.tencent.bkrepo.websocket.dispatch.push.CopyPDUTransferPush
import com.tencent.bkrepo.websocket.dispatch.push.PastePDUTransferPush
import com.tencent.bkrepo.websocket.pojo.fs.CopyPDU
import com.tencent.bkrepo.websocket.pojo.fs.PastePDU
import com.tencent.bkrepo.websocket.pojo.fs.ProjectWorkspace
import com.tencent.bkrepo.websocket.pojo.fs.WorkspaceSearch
import com.tencent.devops.api.pojo.Response
import okhttp3.Request
import okio.IOException
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service

@Service
class ClipboardService(
private val transferDispatch: TransferDispatch
private val transferDispatch: TransferDispatch,
private val devXProperties: DevXProperties,
private val jwtAuthProperties: JwtAuthProperties,
private val permissionManager: PermissionManager,
) {

fun copy(copyPDU: CopyPDU) {
logger.info("CopyPDU: $copyPDU")
private val httpClient = HttpClientBuilderFactory.create().build()
private val signingKey = JwtUtils.createSigningKey(jwtAuthProperties.secretKey)

fun copy(userId: String, copyPDU: CopyPDU) {
logger.info("userId: $userId, CopyPDU: $copyPDU")
val token = generateToken(userId, copyPDU)
copyPDU.token = token
val copyPDUTransferPush = CopyPDUTransferPush(copyPDU)
transferDispatch.dispatch(copyPDUTransferPush)
}
Expand All @@ -52,7 +81,61 @@ class ClipboardService(
transferDispatch.dispatch(pastePDUTransferPush)
}

private fun generateToken(userId: String, copyPDU: CopyPDU): String? {
if (userId != copyPDU.userId) {
throw PermissionException("can't send copy pdu with userId[${copyPDU.userId}]")
}
if (devXProperties.workspaceSearchUrl.isEmpty()) {
return null
}
val apiAuth = ApiAuth(devXProperties.appCode, devXProperties.appSecret)
val token = apiAuth.toJsonString().replace(System.lineSeparator(), "")
val request = Request.Builder().url("${devXProperties.workspaceSearchUrl}?projectId=${copyPDU.projectId}")
.header("X-Bkapi-Authorization", token)
.header("X-Devops-Uid", userId)
.header(HttpHeaders.CONTENT_TYPE, MediaTypes.APPLICATION_JSON)
.post(WorkspaceSearch(workspaceName = listOf(copyPDU.workspaceName)).toJsonString().toRequestBody())
.build()
try {
httpClient.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
logger.error("request url failed: " +
"${request.url}, ${response.code}, ${response.headers["X-Devops-RID"]}")
return null
}
val workspace = response.body!!.string().readJsonString<Response<Page<ProjectWorkspace>>>()
.data?.records?.firstOrNull()
?: throw PermissionException("can't find workspace by name[${copyPDU.workspaceName}]")
logger.debug("workspace: {}", workspace)
if (workspace.owner != userId && workspace.viewers?.contains(userId) == false) {
throw PermissionException("user[$userId] is not the owner or viewer of [${copyPDU.workspaceName}]")
}
return if (workspace.owner == userId) {
null
} else {
createToken(copyPDU.projectId, userId)
}
}
} catch (e: IOException) {
logger.error("Error while processing request: ${e.message}")
return null
}
}

private fun createToken(projectId: String, userId: String): String {
val claims = mutableMapOf(JWT_CLAIMS_REPOSITORY to "$projectId/$REPO_NAME")
permissionManager.checkRepoPermission(
action = PermissionAction.DOWNLOAD,
projectId = projectId,
repoName = REPO_NAME,
userId = userId
)
claims[JWT_CLAIMS_PERMIT] = PermissionAction.READ.name
return JwtUtils.generateToken(signingKey, jwtAuthProperties.expiration, userId)
}

companion object {
private val logger = LoggerFactory.getLogger(ClipboardService::class.java)
private const val REPO_NAME = "lsync"
}
}

0 comments on commit 8d74ed2

Please sign in to comment.