Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
apache
pulsar
提交
afaa63fd
pulsar
项目概览
apache
/
pulsar
通知
129
Star
40
Fork
3
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Wiki
1
Wiki
分析
仓库
DevOps
项目成员
Pages
pulsar
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Pages
分析
分析
仓库分析
DevOps
Wiki
1
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
提交
体验新版 GitCode,发现更多精彩内容 >>
提交
afaa63fd
编写于
11月 09, 2016
作者:
R
Rajan
提交者:
Matteo Merli
11月 09, 2016
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Async authorization check while creation of producer/consumer (#98)
上级
15683fd0
变更
6
展开全部
隐藏空白更改
内联
并排
Showing
6 changed file
with
280 addition
and
223 deletion
+280
-223
pulsar-broker-common/src/main/java/com/yahoo/pulsar/broker/authorization/AuthorizationManager.java
...hoo/pulsar/broker/authorization/AuthorizationManager.java
+69
-35
pulsar-broker/src/main/java/com/yahoo/pulsar/broker/service/ServerCnx.java
.../main/java/com/yahoo/pulsar/broker/service/ServerCnx.java
+187
-172
pulsar-broker/src/test/java/com/yahoo/pulsar/broker/service/ServerCnxTest.java
...t/java/com/yahoo/pulsar/broker/service/ServerCnxTest.java
+6
-6
pulsar-websocket/src/main/java/com/yahoo/pulsar/websocket/AbstractWebSocketHandler.java
.../com/yahoo/pulsar/websocket/AbstractWebSocketHandler.java
+12
-6
pulsar-websocket/src/main/java/com/yahoo/pulsar/websocket/ConsumerHandler.java
...main/java/com/yahoo/pulsar/websocket/ConsumerHandler.java
+3
-2
pulsar-websocket/src/main/java/com/yahoo/pulsar/websocket/ProducerHandler.java
...main/java/com/yahoo/pulsar/websocket/ProducerHandler.java
+3
-2
未找到文件。
pulsar-broker-common/src/main/java/com/yahoo/pulsar/broker/authorization/AuthorizationManager.java
浏览文件 @
afaa63fd
...
@@ -16,8 +16,8 @@
...
@@ -16,8 +16,8 @@
package
com.yahoo.pulsar.broker.authorization
;
package
com.yahoo.pulsar.broker.authorization
;
import
java.util.Map
;
import
java.util.Map
;
import
java.util.Optional
;
import
java.util.Set
;
import
java.util.Set
;
import
java.util.concurrent.CompletableFuture
;
import
org.slf4j.Logger
;
import
org.slf4j.Logger
;
import
org.slf4j.LoggerFactory
;
import
org.slf4j.LoggerFactory
;
...
@@ -26,7 +26,6 @@ import com.yahoo.pulsar.broker.ServiceConfiguration;
...
@@ -26,7 +26,6 @@ import com.yahoo.pulsar.broker.ServiceConfiguration;
import
com.yahoo.pulsar.broker.cache.ConfigurationCacheService
;
import
com.yahoo.pulsar.broker.cache.ConfigurationCacheService
;
import
com.yahoo.pulsar.common.naming.DestinationName
;
import
com.yahoo.pulsar.common.naming.DestinationName
;
import
com.yahoo.pulsar.common.policies.data.AuthAction
;
import
com.yahoo.pulsar.common.policies.data.AuthAction
;
import
com.yahoo.pulsar.common.policies.data.Policies
;
/**
/**
*/
*/
...
@@ -50,9 +49,19 @@ public class AuthorizationManager {
...
@@ -50,9 +49,19 @@ public class AuthorizationManager {
* @param role
* @param role
* the app id used to send messages to the destination.
* the app id used to send messages to the destination.
*/
*/
public
boolean
canProduce
(
DestinationName
destination
,
String
role
)
{
public
CompletableFuture
<
Boolean
>
canProduceAsync
(
DestinationName
destination
,
String
role
)
{
return
checkAuthorization
(
destination
,
role
,
AuthAction
.
produce
);
return
checkAuthorization
(
destination
,
role
,
AuthAction
.
produce
);
}
}
public
boolean
canProduce
(
DestinationName
destination
,
String
role
)
{
try
{
return
canProduceAsync
(
destination
,
role
).
get
();
}
catch
(
Exception
e
)
{
log
.
warn
(
"Producer-client with Role - {} failed to get permissions for destination - {}"
,
role
,
destination
,
e
);
return
false
;
}
}
/**
/**
* Check if the specified role has permission to receive messages from the specified fully qualified destination
* Check if the specified role has permission to receive messages from the specified fully qualified destination
...
@@ -63,9 +72,19 @@ public class AuthorizationManager {
...
@@ -63,9 +72,19 @@ public class AuthorizationManager {
* @param role
* @param role
* the app id used to receive messages from the destination.
* the app id used to receive messages from the destination.
*/
*/
public
boolean
canConsume
(
DestinationName
destination
,
String
role
)
{
public
CompletableFuture
<
Boolean
>
canConsumeAsync
(
DestinationName
destination
,
String
role
)
{
return
checkAuthorization
(
destination
,
role
,
AuthAction
.
consume
);
return
checkAuthorization
(
destination
,
role
,
AuthAction
.
consume
);
}
}
public
boolean
canConsume
(
DestinationName
destination
,
String
role
)
{
try
{
return
canConsumeAsync
(
destination
,
role
).
get
();
}
catch
(
Exception
e
)
{
log
.
warn
(
"Consumer-client with Role - {} failed to get permissions for destination - {}"
,
role
,
destination
,
e
);
return
false
;
}
}
/**
/**
* Check whether the specified role can perform a lookup for the specified destination.
* Check whether the specified role can perform a lookup for the specified destination.
...
@@ -80,10 +99,14 @@ public class AuthorizationManager {
...
@@ -80,10 +99,14 @@ public class AuthorizationManager {
return
canProduce
(
destination
,
role
)
||
canConsume
(
destination
,
role
);
return
canProduce
(
destination
,
role
)
||
canConsume
(
destination
,
role
);
}
}
private
boolean
checkAuthorization
(
DestinationName
destination
,
String
role
,
AuthAction
action
)
{
private
CompletableFuture
<
Boolean
>
checkAuthorization
(
DestinationName
destination
,
String
role
,
if
(
isSuperUser
(
role
))
AuthAction
action
)
{
return
true
;
if
(
isSuperUser
(
role
))
{
return
checkPermission
(
destination
,
role
,
action
)
&&
checkCluster
(
destination
);
return
CompletableFuture
.
completedFuture
(
true
);
}
else
{
return
checkPermission
(
destination
,
role
,
action
)
.
thenApply
(
isPermission
->
isPermission
&&
checkCluster
(
destination
));
}
}
}
private
boolean
checkCluster
(
DestinationName
destination
)
{
private
boolean
checkCluster
(
DestinationName
destination
)
{
...
@@ -98,38 +121,49 @@ public class AuthorizationManager {
...
@@ -98,38 +121,49 @@ public class AuthorizationManager {
}
}
}
}
public
boolean
checkPermission
(
DestinationName
destination
,
String
role
,
AuthAction
action
)
{
public
CompletableFuture
<
Boolean
>
checkPermission
(
DestinationName
destination
,
String
role
,
AuthAction
action
)
{
CompletableFuture
<
Boolean
>
permissionFuture
=
new
CompletableFuture
<>();
try
{
try
{
Optional
<
Policies
>
policies
=
configCache
.
policiesCache
().
get
(
POLICY_ROOT
+
destination
.
getNamespace
());
configCache
.
policiesCache
().
getAsync
(
POLICY_ROOT
+
destination
.
getNamespace
()).
thenAccept
(
policies
->
{
if
(!
policies
.
isPresent
())
{
if
(!
policies
.
isPresent
())
{
if
(
log
.
isDebugEnabled
())
{
if
(
log
.
isDebugEnabled
())
{
log
.
debug
(
"Policies node couldn't be found for destination : {}"
,
destination
);
log
.
debug
(
"Policies node couldn't be found for destination : {}"
,
destination
);
}
permissionFuture
.
complete
(
false
);
}
else
{
Set
<
AuthAction
>
namespaceActions
=
policies
.
get
().
auth_policies
.
namespace_auth
.
get
(
role
);
if
(
namespaceActions
!=
null
&&
namespaceActions
.
contains
(
action
))
{
// The role has namespace level permission
permissionFuture
.
complete
(
true
);
}
else
{
Map
<
String
,
Set
<
AuthAction
>>
roles
=
policies
.
get
().
auth_policies
.
destination_auth
.
get
(
destination
.
toString
());
if
(
roles
==
null
)
{
// Destination has no custom policy
permissionFuture
.
complete
(
false
);
}
else
{
Set
<
AuthAction
>
resourceActions
=
roles
.
get
(
role
);
if
(
resourceActions
!=
null
&&
resourceActions
.
contains
(
action
))
{
// The role has destination level permission
permissionFuture
.
complete
(
true
);
}
else
{
permissionFuture
.
complete
(
false
);
}
}
}
}
}
return
false
;
}).
exceptionally
(
ex
->
{
}
log
.
warn
(
"Client with Role - {} failed to get permissions for destination - {}"
,
role
,
destination
,
ex
);
Set
<
AuthAction
>
namespaceActions
=
policies
.
get
().
auth_policies
.
namespace_auth
.
get
(
role
);
permissionFuture
.
complete
(
false
);
if
(
namespaceActions
!=
null
&&
namespaceActions
.
contains
(
action
))
{
return
null
;
// The role has namespace level permission
});
return
true
;
}
Map
<
String
,
Set
<
AuthAction
>>
roles
=
policies
.
get
().
auth_policies
.
destination_auth
.
get
(
destination
.
toString
());
if
(
roles
==
null
)
{
// Destination has no custom policy
return
false
;
}
Set
<
AuthAction
>
resourceActions
=
roles
.
get
(
role
);
if
(
resourceActions
!=
null
&&
resourceActions
.
contains
(
action
))
{
// The role has destination level permission
return
true
;
}
return
false
;
}
catch
(
Exception
e
)
{
}
catch
(
Exception
e
)
{
log
.
warn
(
"Client with Role - {} failed to get permissions for destination - {}"
,
role
,
destination
,
e
);
log
.
warn
(
"Client with Role - {} failed to get permissions for destination - {}"
,
role
,
destination
,
e
);
return
false
;
permissionFuture
.
complete
(
false
)
;
}
}
return
permissionFuture
;
}
}
/**
/**
...
...
pulsar-broker/src/main/java/com/yahoo/pulsar/broker/service/ServerCnx.java
浏览文件 @
afaa63fd
此差异已折叠。
点击以展开。
pulsar-broker/src/test/java/com/yahoo/pulsar/broker/service/ServerCnxTest.java
浏览文件 @
afaa63fd
...
@@ -381,7 +381,7 @@ public class ServerCnxTest {
...
@@ -381,7 +381,7 @@ public class ServerCnxTest {
@Test
(
timeOut
=
30000
)
@Test
(
timeOut
=
30000
)
public
void
testProducerCommandWithAuthorizationPositive
()
throws
Exception
{
public
void
testProducerCommandWithAuthorizationPositive
()
throws
Exception
{
AuthorizationManager
authorizationManager
=
mock
(
AuthorizationManager
.
class
);
AuthorizationManager
authorizationManager
=
mock
(
AuthorizationManager
.
class
);
doReturn
(
true
).
when
(
authorizationManager
).
canProduce
(
Mockito
.
any
(),
Mockito
.
any
());
doReturn
(
CompletableFuture
.
completedFuture
(
true
)).
when
(
authorizationManager
).
canProduceAsync
(
Mockito
.
any
(),
Mockito
.
any
());
doReturn
(
authorizationManager
).
when
(
brokerService
).
getAuthorizationManager
();
doReturn
(
authorizationManager
).
when
(
brokerService
).
getAuthorizationManager
();
doReturn
(
true
).
when
(
brokerService
).
isAuthenticationEnabled
();
doReturn
(
true
).
when
(
brokerService
).
isAuthenticationEnabled
();
resetChannel
();
resetChannel
();
...
@@ -409,7 +409,7 @@ public class ServerCnxTest {
...
@@ -409,7 +409,7 @@ public class ServerCnxTest {
ConfigurationCacheService
configCacheService
=
mock
(
ConfigurationCacheService
.
class
);
ConfigurationCacheService
configCacheService
=
mock
(
ConfigurationCacheService
.
class
);
doReturn
(
configCacheService
).
when
(
pulsar
).
getConfigurationCache
();
doReturn
(
configCacheService
).
when
(
pulsar
).
getConfigurationCache
();
doReturn
(
zkDataCache
).
when
(
configCacheService
).
policiesCache
();
doReturn
(
zkDataCache
).
when
(
configCacheService
).
policiesCache
();
do
Throw
(
new
NoNodeException
()).
when
(
zkDataCache
).
get
(
matches
(
".*nonexistent.*"
));
do
Return
(
CompletableFuture
.
completedFuture
(
Optional
.
empty
())).
when
(
zkDataCache
).
getAsync
(
matches
(
".*nonexistent.*"
));
AuthorizationManager
authorizationManager
=
spy
(
new
AuthorizationManager
(
svcConfig
,
configCacheService
));
AuthorizationManager
authorizationManager
=
spy
(
new
AuthorizationManager
(
svcConfig
,
configCacheService
));
doReturn
(
authorizationManager
).
when
(
brokerService
).
getAuthorizationManager
();
doReturn
(
authorizationManager
).
when
(
brokerService
).
getAuthorizationManager
();
...
@@ -440,7 +440,7 @@ public class ServerCnxTest {
...
@@ -440,7 +440,7 @@ public class ServerCnxTest {
doReturn
(
authorizationManager
).
when
(
brokerService
).
getAuthorizationManager
();
doReturn
(
authorizationManager
).
when
(
brokerService
).
getAuthorizationManager
();
doReturn
(
true
).
when
(
brokerService
).
isAuthorizationEnabled
();
doReturn
(
true
).
when
(
brokerService
).
isAuthorizationEnabled
();
doReturn
(
false
).
when
(
authorizationManager
).
isSuperUser
(
Mockito
.
anyString
());
doReturn
(
false
).
when
(
authorizationManager
).
isSuperUser
(
Mockito
.
anyString
());
doReturn
(
true
).
when
(
authorizationManager
).
checkPermission
(
any
(
DestinationName
.
class
),
Mockito
.
anyString
(),
doReturn
(
CompletableFuture
.
completedFuture
(
true
)
).
when
(
authorizationManager
).
checkPermission
(
any
(
DestinationName
.
class
),
Mockito
.
anyString
(),
any
(
AuthAction
.
class
));
any
(
AuthAction
.
class
));
resetChannel
();
resetChannel
();
...
@@ -493,7 +493,7 @@ public class ServerCnxTest {
...
@@ -493,7 +493,7 @@ public class ServerCnxTest {
public
void
testProducerCommandWithAuthorizationNegative
()
throws
Exception
{
public
void
testProducerCommandWithAuthorizationNegative
()
throws
Exception
{
AuthorizationManager
authorizationManager
=
mock
(
AuthorizationManager
.
class
);
AuthorizationManager
authorizationManager
=
mock
(
AuthorizationManager
.
class
);
doReturn
(
false
).
when
(
authorizationManager
).
canProduce
(
Mockito
.
any
(),
Mockito
.
any
());
doReturn
(
CompletableFuture
.
completedFuture
(
false
)).
when
(
authorizationManager
).
canProduceAsync
(
Mockito
.
any
(),
Mockito
.
any
());
doReturn
(
authorizationManager
).
when
(
brokerService
).
getAuthorizationManager
();
doReturn
(
authorizationManager
).
when
(
brokerService
).
getAuthorizationManager
();
doReturn
(
true
).
when
(
brokerService
).
isAuthenticationEnabled
();
doReturn
(
true
).
when
(
brokerService
).
isAuthenticationEnabled
();
doReturn
(
true
).
when
(
brokerService
).
isAuthorizationEnabled
();
doReturn
(
true
).
when
(
brokerService
).
isAuthorizationEnabled
();
...
@@ -1022,7 +1022,7 @@ public class ServerCnxTest {
...
@@ -1022,7 +1022,7 @@ public class ServerCnxTest {
@Test
(
timeOut
=
30000
)
@Test
(
timeOut
=
30000
)
public
void
testSubscribeCommandWithAuthorizationPositive
()
throws
Exception
{
public
void
testSubscribeCommandWithAuthorizationPositive
()
throws
Exception
{
AuthorizationManager
authorizationManager
=
mock
(
AuthorizationManager
.
class
);
AuthorizationManager
authorizationManager
=
mock
(
AuthorizationManager
.
class
);
doReturn
(
true
).
when
(
authorizationManager
).
canConsume
(
Mockito
.
any
(),
Mockito
.
any
());
doReturn
(
CompletableFuture
.
completedFuture
(
true
)).
when
(
authorizationManager
).
canConsumeAsync
(
Mockito
.
any
(),
Mockito
.
any
());
doReturn
(
authorizationManager
).
when
(
brokerService
).
getAuthorizationManager
();
doReturn
(
authorizationManager
).
when
(
brokerService
).
getAuthorizationManager
();
doReturn
(
true
).
when
(
brokerService
).
isAuthenticationEnabled
();
doReturn
(
true
).
when
(
brokerService
).
isAuthenticationEnabled
();
doReturn
(
true
).
when
(
brokerService
).
isAuthorizationEnabled
();
doReturn
(
true
).
when
(
brokerService
).
isAuthorizationEnabled
();
...
@@ -1042,7 +1042,7 @@ public class ServerCnxTest {
...
@@ -1042,7 +1042,7 @@ public class ServerCnxTest {
@Test
(
timeOut
=
30000
)
@Test
(
timeOut
=
30000
)
public
void
testSubscribeCommandWithAuthorizationNegative
()
throws
Exception
{
public
void
testSubscribeCommandWithAuthorizationNegative
()
throws
Exception
{
AuthorizationManager
authorizationManager
=
mock
(
AuthorizationManager
.
class
);
AuthorizationManager
authorizationManager
=
mock
(
AuthorizationManager
.
class
);
doReturn
(
false
).
when
(
authorizationManager
).
canConsume
(
Mockito
.
any
(),
Mockito
.
any
());
doReturn
(
CompletableFuture
.
completedFuture
(
false
)).
when
(
authorizationManager
).
canConsumeAsync
(
Mockito
.
any
(),
Mockito
.
any
());
doReturn
(
authorizationManager
).
when
(
brokerService
).
getAuthorizationManager
();
doReturn
(
authorizationManager
).
when
(
brokerService
).
getAuthorizationManager
();
doReturn
(
true
).
when
(
brokerService
).
isAuthenticationEnabled
();
doReturn
(
true
).
when
(
brokerService
).
isAuthenticationEnabled
();
doReturn
(
true
).
when
(
brokerService
).
isAuthorizationEnabled
();
doReturn
(
true
).
when
(
brokerService
).
isAuthorizationEnabled
();
...
...
pulsar-websocket/src/main/java/com/yahoo/pulsar/websocket/AbstractWebSocketHandler.java
浏览文件 @
afaa63fd
...
@@ -22,6 +22,7 @@ import java.io.IOException;
...
@@ -22,6 +22,7 @@ import java.io.IOException;
import
java.util.List
;
import
java.util.List
;
import
java.util.Map
;
import
java.util.Map
;
import
java.util.TreeMap
;
import
java.util.TreeMap
;
import
java.util.concurrent.CompletableFuture
;
import
javax.naming.AuthenticationException
;
import
javax.naming.AuthenticationException
;
import
javax.servlet.http.HttpServletRequest
;
import
javax.servlet.http.HttpServletRequest
;
...
@@ -73,11 +74,16 @@ public abstract class AbstractWebSocketHandler extends WebSocketAdapter implemen
...
@@ -73,11 +74,16 @@ public abstract class AbstractWebSocketHandler extends WebSocketAdapter implemen
}
}
}
}
if
(
service
.
isAuthorizationEnabled
()
&&
!
isAuthorized
(
authRole
))
{
if
(
service
.
isAuthorizationEnabled
())
{
log
.
warn
(
"[{}] WebSocket Client [{}] is not authorized on topic {}"
,
session
.
getRemoteAddress
(),
authRole
,
final
String
role
=
authRole
;
topic
);
isAuthorized
(
authRole
).
thenApply
(
isAuthorized
->
{
close
(
WebSocketError
.
NotAuthorizedError
);
if
(!
isAuthorized
)
{
return
;
log
.
warn
(
"[{}] WebSocket Client [{}] is not authorized on topic {}"
,
session
.
getRemoteAddress
(),
role
,
topic
);
close
(
WebSocketError
.
NotAuthorizedError
);
}
return
null
;
});
}
}
}
}
...
@@ -120,7 +126,7 @@ public abstract class AbstractWebSocketHandler extends WebSocketAdapter implemen
...
@@ -120,7 +126,7 @@ public abstract class AbstractWebSocketHandler extends WebSocketAdapter implemen
return
null
;
return
null
;
}
}
protected
abstract
boolean
isAuthorized
(
String
authRole
);
protected
abstract
CompletableFuture
<
Boolean
>
isAuthorized
(
String
authRole
);
private
String
extractTopicName
(
HttpServletRequest
request
)
{
private
String
extractTopicName
(
HttpServletRequest
request
)
{
String
uri
=
request
.
getRequestURI
();
String
uri
=
request
.
getRequestURI
();
...
...
pulsar-websocket/src/main/java/com/yahoo/pulsar/websocket/ConsumerHandler.java
浏览文件 @
afaa63fd
...
@@ -24,6 +24,7 @@ import java.time.ZoneId;
...
@@ -24,6 +24,7 @@ import java.time.ZoneId;
import
java.time.format.DateTimeFormatter
;
import
java.time.format.DateTimeFormatter
;
import
java.util.Base64
;
import
java.util.Base64
;
import
java.util.List
;
import
java.util.List
;
import
java.util.concurrent.CompletableFuture
;
import
java.util.concurrent.TimeUnit
;
import
java.util.concurrent.TimeUnit
;
import
java.util.concurrent.atomic.AtomicInteger
;
import
java.util.concurrent.atomic.AtomicInteger
;
...
@@ -177,8 +178,8 @@ public class ConsumerHandler extends AbstractWebSocketHandler {
...
@@ -177,8 +178,8 @@ public class ConsumerHandler extends AbstractWebSocketHandler {
}
}
@Override
@Override
protected
boolean
isAuthorized
(
String
authRole
)
{
protected
CompletableFuture
<
Boolean
>
isAuthorized
(
String
authRole
)
{
return
service
.
getAuthorizationManager
().
canConsume
(
DestinationName
.
get
(
topic
),
authRole
);
return
service
.
getAuthorizationManager
().
canConsume
Async
(
DestinationName
.
get
(
topic
),
authRole
);
}
}
private
static
String
extractSubscription
(
HttpServletRequest
request
)
{
private
static
String
extractSubscription
(
HttpServletRequest
request
)
{
...
...
pulsar-websocket/src/main/java/com/yahoo/pulsar/websocket/ProducerHandler.java
浏览文件 @
afaa63fd
...
@@ -17,6 +17,7 @@ package com.yahoo.pulsar.websocket;
...
@@ -17,6 +17,7 @@ package com.yahoo.pulsar.websocket;
import
java.io.IOException
;
import
java.io.IOException
;
import
java.util.Base64
;
import
java.util.Base64
;
import
java.util.concurrent.CompletableFuture
;
import
java.util.concurrent.TimeUnit
;
import
java.util.concurrent.TimeUnit
;
import
javax.servlet.http.HttpServletRequest
;
import
javax.servlet.http.HttpServletRequest
;
...
@@ -119,8 +120,8 @@ public class ProducerHandler extends AbstractWebSocketHandler {
...
@@ -119,8 +120,8 @@ public class ProducerHandler extends AbstractWebSocketHandler {
});
});
}
}
protected
boolean
isAuthorized
(
String
authRole
)
{
protected
CompletableFuture
<
Boolean
>
isAuthorized
(
String
authRole
)
{
return
service
.
getAuthorizationManager
().
canProduce
(
DestinationName
.
get
(
topic
),
authRole
);
return
service
.
getAuthorizationManager
().
canProduce
Async
(
DestinationName
.
get
(
topic
),
authRole
);
}
}
private
void
sendAckResponse
(
ProducerAck
response
)
{
private
void
sendAckResponse
(
ProducerAck
response
)
{
...
...
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录