fix: load uid key from manager config
This commit is contained in:
@@ -46,6 +46,19 @@ X-Request-ID: {uuid}
|
|||||||
|
|
||||||
服务从 Redis 读取 `im:session:token:{sha256(token)}` 校验登录态。
|
服务从 Redis 读取 `im:session:token:{sha256(token)}` 校验登录态。
|
||||||
|
|
||||||
|
`usr...` 用户 ID 编解码优先从 IM Manager 内部配置接口读取 UID 加密 key,读不到时回退到
|
||||||
|
`auth.uid_key` 或环境变量 `IM_WORKBENCH_UID_KEY`。测试环境需要配置:
|
||||||
|
|
||||||
|
```text
|
||||||
|
IM_WORKBENCH_MANAGER_BASE_URL=https://admin.118im.com
|
||||||
|
IM_WORKBENCH_MANAGER_SERVICE_NAME=im-server
|
||||||
|
IM_WORKBENCH_MANAGER_SECRET={IM_CONFIG_API_SECRET}
|
||||||
|
```
|
||||||
|
|
||||||
|
默认读取 `GET /internal/im-config/third-party/im?keys=uid_encryption_key,UID_ENCRYPTION_KEY,uid_key`。
|
||||||
|
如果后台配置使用了其他 group/key,可通过 `IM_WORKBENCH_MANAGER_UID_KEY_GROUP` 和
|
||||||
|
`IM_WORKBENCH_MANAGER_UID_KEY_KEYS` 调整。
|
||||||
|
|
||||||
HotGo 后台访问 `/internal/admin/workbench/*` 时需要配置同一组内部鉴权参数:
|
HotGo 后台访问 `/internal/admin/workbench/*` 时需要配置同一组内部鉴权参数:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
@@ -19,11 +19,24 @@
|
|||||||
"auth": {
|
"auth": {
|
||||||
"api_keys": [],
|
"api_keys": [],
|
||||||
"session_key_pattern": "im:session:token:%s",
|
"session_key_pattern": "im:session:token:%s",
|
||||||
|
"uid_key": "la6YsO+bNX/+XIkOqc5Svw==",
|
||||||
"allow_dev_header_auth": true,
|
"allow_dev_header_auth": true,
|
||||||
"required_tenant_code": true,
|
"required_tenant_code": true,
|
||||||
"required_api_key": false,
|
"required_api_key": false,
|
||||||
"skip_session_in_dev": false,
|
"skip_session_in_dev": false,
|
||||||
"internal_service_name": "hotgo-admin",
|
"internal_service_name": "hotgo-admin",
|
||||||
"internal_secret": "local-workbench-secret"
|
"internal_secret": "local-workbench-secret"
|
||||||
|
},
|
||||||
|
"manager": {
|
||||||
|
"base_url": "",
|
||||||
|
"service_name": "im-server",
|
||||||
|
"secret": "",
|
||||||
|
"uid_key_group": "im",
|
||||||
|
"uid_key_keys": [
|
||||||
|
"uid_encryption_key",
|
||||||
|
"UID_ENCRYPTION_KEY",
|
||||||
|
"uid_key"
|
||||||
|
],
|
||||||
|
"timeout_ms": 3000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,11 +23,24 @@
|
|||||||
"auth": {
|
"auth": {
|
||||||
"api_keys": [],
|
"api_keys": [],
|
||||||
"session_key_pattern": "im:session:token:%s",
|
"session_key_pattern": "im:session:token:%s",
|
||||||
|
"uid_key": "la6YsO+bNX/+XIkOqc5Svw==",
|
||||||
"allow_dev_header_auth": false,
|
"allow_dev_header_auth": false,
|
||||||
"required_tenant_code": true,
|
"required_tenant_code": true,
|
||||||
"required_api_key": true,
|
"required_api_key": true,
|
||||||
"skip_session_in_dev": false,
|
"skip_session_in_dev": false,
|
||||||
"internal_service_name": "hotgo-admin",
|
"internal_service_name": "hotgo-admin",
|
||||||
"internal_secret": ""
|
"internal_secret": ""
|
||||||
|
},
|
||||||
|
"manager": {
|
||||||
|
"base_url": "https://admin.118im.com",
|
||||||
|
"service_name": "im-server",
|
||||||
|
"secret": "",
|
||||||
|
"uid_key_group": "im",
|
||||||
|
"uid_key_keys": [
|
||||||
|
"uid_encryption_key",
|
||||||
|
"UID_ENCRYPTION_KEY",
|
||||||
|
"uid_key"
|
||||||
|
],
|
||||||
|
"timeout_ms": 3000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ require (
|
|||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/redis/go-redis/v9 v9.12.1
|
github.com/redis/go-redis/v9 v9.12.1
|
||||||
github.com/segmentio/kafka-go v0.4.51
|
github.com/segmentio/kafka-go v0.4.51
|
||||||
|
golang.org/x/crypto v0.53.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -26,7 +27,7 @@ require (
|
|||||||
github.com/xdg-go/scram v1.1.2 // indirect
|
github.com/xdg-go/scram v1.1.2 // indirect
|
||||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||||
golang.org/x/sys v0.35.0 // indirect
|
golang.org/x/sys v0.46.0 // indirect
|
||||||
golang.org/x/text v0.28.0 // indirect
|
golang.org/x/text v0.38.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.8 // indirect
|
google.golang.org/protobuf v1.36.8 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -68,12 +68,14 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
|||||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||||
|
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -81,16 +83,16 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"git.118im.com/im_team/im_workbench/internal/config"
|
"git.118im.com/im_team/im_workbench/internal/config"
|
||||||
"git.118im.com/im_team/im_workbench/internal/events"
|
"git.118im.com/im_team/im_workbench/internal/events"
|
||||||
"git.118im.com/im_team/im_workbench/internal/httpapi"
|
"git.118im.com/im_team/im_workbench/internal/httpapi"
|
||||||
|
"git.118im.com/im_team/im_workbench/internal/managerconfig"
|
||||||
"git.118im.com/im_team/im_workbench/internal/store"
|
"git.118im.com/im_team/im_workbench/internal/store"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
@@ -22,6 +23,10 @@ type App struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New(cfg *config.Config) (*App, error) {
|
func New(cfg *config.Config) (*App, error) {
|
||||||
|
uidKey := managerconfig.ResolveUIDKey(context.Background(), cfg.Manager, cfg.Auth.UIDKey)
|
||||||
|
if err := httpapi.SetUIDKey(uidKey); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
st, err := store.Open(cfg.MySQL)
|
st, err := store.Open(cfg.MySQL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type Config struct {
|
|||||||
Redis RedisConfig `json:"redis"`
|
Redis RedisConfig `json:"redis"`
|
||||||
Kafka KafkaConfig `json:"kafka"`
|
Kafka KafkaConfig `json:"kafka"`
|
||||||
Auth AuthConfig `json:"auth"`
|
Auth AuthConfig `json:"auth"`
|
||||||
|
Manager ManagerConfig `json:"manager"`
|
||||||
ReadTimeout time.Duration `json:"-"`
|
ReadTimeout time.Duration `json:"-"`
|
||||||
WriteTimeout time.Duration `json:"-"`
|
WriteTimeout time.Duration `json:"-"`
|
||||||
}
|
}
|
||||||
@@ -46,6 +47,7 @@ type KafkaConfig struct {
|
|||||||
type AuthConfig struct {
|
type AuthConfig struct {
|
||||||
APIKeys []string `json:"api_keys"`
|
APIKeys []string `json:"api_keys"`
|
||||||
SessionKeyPattern string `json:"session_key_pattern"`
|
SessionKeyPattern string `json:"session_key_pattern"`
|
||||||
|
UIDKey string `json:"uid_key"`
|
||||||
AllowDevHeaderAuth bool `json:"allow_dev_header_auth"`
|
AllowDevHeaderAuth bool `json:"allow_dev_header_auth"`
|
||||||
RequiredTenantCode bool `json:"required_tenant_code"`
|
RequiredTenantCode bool `json:"required_tenant_code"`
|
||||||
RequiredAPIKey bool `json:"required_api_key"`
|
RequiredAPIKey bool `json:"required_api_key"`
|
||||||
@@ -55,6 +57,15 @@ type AuthConfig struct {
|
|||||||
InternalSecret string `json:"internal_secret"`
|
InternalSecret string `json:"internal_secret"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ManagerConfig struct {
|
||||||
|
BaseURL string `json:"base_url"`
|
||||||
|
ServiceName string `json:"service_name"`
|
||||||
|
Secret string `json:"secret"`
|
||||||
|
UIDKeyGroup string `json:"uid_key_group"`
|
||||||
|
UIDKeyKeys []string `json:"uid_key_keys"`
|
||||||
|
TimeoutMS int `json:"timeout_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
func Load() (*Config, error) {
|
func Load() (*Config, error) {
|
||||||
cfg := defaultConfig()
|
cfg := defaultConfig()
|
||||||
if path := strings.TrimSpace(os.Getenv("IM_WORKBENCH_CONFIG")); path != "" {
|
if path := strings.TrimSpace(os.Getenv("IM_WORKBENCH_CONFIG")); path != "" {
|
||||||
@@ -89,9 +100,16 @@ func defaultConfig() *Config {
|
|||||||
},
|
},
|
||||||
Auth: AuthConfig{
|
Auth: AuthConfig{
|
||||||
SessionKeyPattern: "im:session:token:%s",
|
SessionKeyPattern: "im:session:token:%s",
|
||||||
|
UIDKey: "la6YsO+bNX/+XIkOqc5Svw==",
|
||||||
RequiredTenantCode: true,
|
RequiredTenantCode: true,
|
||||||
RequiredAPIKey: true,
|
RequiredAPIKey: true,
|
||||||
},
|
},
|
||||||
|
Manager: ManagerConfig{
|
||||||
|
ServiceName: "im-server",
|
||||||
|
UIDKeyGroup: "im",
|
||||||
|
UIDKeyKeys: []string{"uid_encryption_key", "UID_ENCRYPTION_KEY", "uid_key"},
|
||||||
|
TimeoutMS: 3000,
|
||||||
|
},
|
||||||
ReadTimeout: 8 * time.Second,
|
ReadTimeout: 8 * time.Second,
|
||||||
WriteTimeout: 8 * time.Second,
|
WriteTimeout: 8 * time.Second,
|
||||||
}
|
}
|
||||||
@@ -110,26 +128,45 @@ func applyEnv(cfg *Config) {
|
|||||||
setString(&cfg.Kafka.SASL, "IM_WORKBENCH_KAFKA_SASL")
|
setString(&cfg.Kafka.SASL, "IM_WORKBENCH_KAFKA_SASL")
|
||||||
setString(&cfg.Kafka.ClientID, "IM_WORKBENCH_KAFKA_CLIENT_ID")
|
setString(&cfg.Kafka.ClientID, "IM_WORKBENCH_KAFKA_CLIENT_ID")
|
||||||
setString(&cfg.Auth.SessionKeyPattern, "IM_WORKBENCH_SESSION_KEY_PATTERN")
|
setString(&cfg.Auth.SessionKeyPattern, "IM_WORKBENCH_SESSION_KEY_PATTERN")
|
||||||
|
setString(&cfg.Auth.UIDKey, "IM_WORKBENCH_UID_KEY")
|
||||||
setString(&cfg.Auth.TrustedInternalAddr, "IM_WORKBENCH_TRUSTED_INTERNAL_ADDR")
|
setString(&cfg.Auth.TrustedInternalAddr, "IM_WORKBENCH_TRUSTED_INTERNAL_ADDR")
|
||||||
setString(&cfg.Auth.InternalServiceName, "IM_WORKBENCH_INTERNAL_SERVICE_NAME")
|
setString(&cfg.Auth.InternalServiceName, "IM_WORKBENCH_INTERNAL_SERVICE_NAME")
|
||||||
setString(&cfg.Auth.InternalSecret, "IM_WORKBENCH_INTERNAL_SECRET")
|
setString(&cfg.Auth.InternalSecret, "IM_WORKBENCH_INTERNAL_SECRET")
|
||||||
|
setString(&cfg.Manager.BaseURL, "IM_WORKBENCH_MANAGER_BASE_URL")
|
||||||
|
setString(&cfg.Manager.ServiceName, "IM_WORKBENCH_MANAGER_SERVICE_NAME")
|
||||||
|
setString(&cfg.Manager.Secret, "IM_WORKBENCH_MANAGER_SECRET")
|
||||||
|
setString(&cfg.Manager.UIDKeyGroup, "IM_WORKBENCH_MANAGER_UID_KEY_GROUP")
|
||||||
if strings.TrimSpace(cfg.Auth.InternalSecret) == "" {
|
if strings.TrimSpace(cfg.Auth.InternalSecret) == "" {
|
||||||
setString(&cfg.Auth.InternalSecret, "IM_INTERNAL_API_SECRET")
|
setString(&cfg.Auth.InternalSecret, "IM_INTERNAL_API_SECRET")
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(cfg.Auth.InternalSecret) == "" {
|
if strings.TrimSpace(cfg.Auth.InternalSecret) == "" {
|
||||||
setString(&cfg.Auth.InternalSecret, "IM_ADMIN_API_SECRET")
|
setString(&cfg.Auth.InternalSecret, "IM_ADMIN_API_SECRET")
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(cfg.Manager.Secret) == "" {
|
||||||
|
setString(&cfg.Manager.Secret, "IM_CONFIG_API_SECRET")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(cfg.Manager.Secret) == "" {
|
||||||
|
setString(&cfg.Manager.Secret, "IM_INTERNAL_API_SECRET")
|
||||||
|
}
|
||||||
if brokers := strings.TrimSpace(os.Getenv("IM_WORKBENCH_KAFKA_BROKERS")); brokers != "" {
|
if brokers := strings.TrimSpace(os.Getenv("IM_WORKBENCH_KAFKA_BROKERS")); brokers != "" {
|
||||||
cfg.Kafka.Brokers = splitCSV(brokers)
|
cfg.Kafka.Brokers = splitCSV(brokers)
|
||||||
}
|
}
|
||||||
if keys := strings.TrimSpace(os.Getenv("IM_WORKBENCH_API_KEYS")); keys != "" {
|
if keys := strings.TrimSpace(os.Getenv("IM_WORKBENCH_API_KEYS")); keys != "" {
|
||||||
cfg.Auth.APIKeys = splitCSV(keys)
|
cfg.Auth.APIKeys = splitCSV(keys)
|
||||||
}
|
}
|
||||||
|
if keys := strings.TrimSpace(os.Getenv("IM_WORKBENCH_MANAGER_UID_KEY_KEYS")); keys != "" {
|
||||||
|
cfg.Manager.UIDKeyKeys = splitCSV(keys)
|
||||||
|
}
|
||||||
if value := strings.TrimSpace(os.Getenv("IM_WORKBENCH_KAFKA_BATCH_SIZE")); value != "" {
|
if value := strings.TrimSpace(os.Getenv("IM_WORKBENCH_KAFKA_BATCH_SIZE")); value != "" {
|
||||||
if parsed, err := strconv.Atoi(value); err == nil {
|
if parsed, err := strconv.Atoi(value); err == nil {
|
||||||
cfg.Kafka.BatchSize = parsed
|
cfg.Kafka.BatchSize = parsed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if value := strings.TrimSpace(os.Getenv("IM_WORKBENCH_MANAGER_TIMEOUT_MS")); value != "" {
|
||||||
|
if parsed, err := strconv.Atoi(value); err == nil {
|
||||||
|
cfg.Manager.TimeoutMS = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
if value := strings.TrimSpace(os.Getenv("IM_WORKBENCH_REDIS_DB")); value != "" {
|
if value := strings.TrimSpace(os.Getenv("IM_WORKBENCH_REDIS_DB")); value != "" {
|
||||||
if parsed, err := strconv.Atoi(value); err == nil {
|
if parsed, err := strconv.Atoi(value); err == nil {
|
||||||
cfg.Redis.DB = parsed
|
cfg.Redis.DB = parsed
|
||||||
|
|||||||
+54
-6
@@ -7,10 +7,41 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/xtea"
|
||||||
)
|
)
|
||||||
|
|
||||||
type userID int64
|
type userID int64
|
||||||
|
|
||||||
|
const defaultUIDKey = "la6YsO+bNX/+XIkOqc5Svw=="
|
||||||
|
|
||||||
|
var (
|
||||||
|
uidCodecMu sync.RWMutex
|
||||||
|
uidCipher *xtea.Cipher
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if err := SetUIDKey(defaultUIDKey); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetUIDKey(encoded string) error {
|
||||||
|
key, err := base64.StdEncoding.DecodeString(strings.TrimSpace(encoded))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid uid key: %w", err)
|
||||||
|
}
|
||||||
|
cipher, err := xtea.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid uid key: %w", err)
|
||||||
|
}
|
||||||
|
uidCodecMu.Lock()
|
||||||
|
uidCipher = cipher
|
||||||
|
uidCodecMu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (id *userID) UnmarshalJSON(raw []byte) error {
|
func (id *userID) UnmarshalJSON(raw []byte) error {
|
||||||
value, err := parseUserIDJSON(raw)
|
value, err := parseUserIDJSON(raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -63,7 +94,7 @@ func parseUserID(value string) (int64, error) {
|
|||||||
if err != nil || len(decoded) < 8 {
|
if err != nil || len(decoded) < 8 {
|
||||||
return 0, fmt.Errorf("invalid user id")
|
return 0, fmt.Errorf("invalid user id")
|
||||||
}
|
}
|
||||||
return int64(binary.LittleEndian.Uint64(decoded[:8])), nil
|
return decryptUID(decoded[:8]), nil
|
||||||
}
|
}
|
||||||
return strconv.ParseInt(value, 10, 64)
|
return strconv.ParseInt(value, 10, 64)
|
||||||
}
|
}
|
||||||
@@ -72,11 +103,8 @@ func formatUserID(id int64) string {
|
|||||||
if id == 0 {
|
if id == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
src := make([]byte, 8)
|
encoded := encryptUID(id)
|
||||||
dst := make([]byte, base64.URLEncoding.WithPadding(base64.NoPadding).EncodedLen(8))
|
return "usr" + base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(encoded)
|
||||||
binary.LittleEndian.PutUint64(src, uint64(id))
|
|
||||||
base64.URLEncoding.WithPadding(base64.NoPadding).Encode(dst, src)
|
|
||||||
return "usr" + string(dst)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseUserIDList(values []userID) []int64 {
|
func parseUserIDList(values []userID) []int64 {
|
||||||
@@ -92,3 +120,23 @@ func parseUserIDList(values []userID) []int64 {
|
|||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decryptUID(encoded []byte) int64 {
|
||||||
|
dst := make([]byte, 8)
|
||||||
|
uidCodecMu.RLock()
|
||||||
|
cipher := uidCipher
|
||||||
|
uidCodecMu.RUnlock()
|
||||||
|
cipher.Decrypt(dst, encoded[:8])
|
||||||
|
return int64(binary.LittleEndian.Uint64(dst))
|
||||||
|
}
|
||||||
|
|
||||||
|
func encryptUID(id int64) []byte {
|
||||||
|
src := make([]byte, 8)
|
||||||
|
dst := make([]byte, 8)
|
||||||
|
binary.LittleEndian.PutUint64(src, uint64(id))
|
||||||
|
uidCodecMu.RLock()
|
||||||
|
cipher := uidCipher
|
||||||
|
uidCodecMu.RUnlock()
|
||||||
|
cipher.Encrypt(dst, src)
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,23 @@ func TestUserIDJSONAcceptsIMUserID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUserIDCodecMatchesIMServer(t *testing.T) {
|
||||||
|
const (
|
||||||
|
publicID = "usrOj2NWDNdlMU"
|
||||||
|
dbID = int64(1574592702383656960)
|
||||||
|
)
|
||||||
|
got, err := parseUserID(publicID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseUserID: %v", err)
|
||||||
|
}
|
||||||
|
if got != dbID {
|
||||||
|
t.Fatalf("parseUserID(%q)=%d, want %d", publicID, got, dbID)
|
||||||
|
}
|
||||||
|
if formatted := formatUserID(dbID); formatted != publicID {
|
||||||
|
t.Fatalf("formatUserID(%d)=%q, want %q", dbID, formatted, publicID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUserIDJSONRejectsNumericID(t *testing.T) {
|
func TestUserIDJSONRejectsNumericID(t *testing.T) {
|
||||||
var id userID
|
var id userID
|
||||||
if err := json.Unmarshal([]byte(`1573862062872858624`), &id); err == nil {
|
if err := json.Unmarshal([]byte(`1573862062872858624`), &id); err == nil {
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package managerconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/hmac"
|
||||||
|
cryptorand "crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.118im.com/im_team/im_workbench/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxResponseBody = 1 << 20
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
cfg config.ManagerConfig
|
||||||
|
http *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveUIDKey(ctx context.Context, cfg config.ManagerConfig, fallback string) string {
|
||||||
|
key, err := New(cfg).UIDKey(ctx)
|
||||||
|
if err != nil || strings.TrimSpace(key) == "" {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg config.ManagerConfig) *Client {
|
||||||
|
timeout := time.Duration(cfg.TimeoutMS) * time.Millisecond
|
||||||
|
if timeout <= 0 {
|
||||||
|
timeout = 3 * time.Second
|
||||||
|
}
|
||||||
|
return &Client{
|
||||||
|
cfg: cfg,
|
||||||
|
http: &http.Client{Timeout: timeout},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) UIDKey(ctx context.Context) (string, error) {
|
||||||
|
baseURL := strings.TrimRight(strings.TrimSpace(c.cfg.BaseURL), "/")
|
||||||
|
if baseURL == "" {
|
||||||
|
return "", fmt.Errorf("manager base url is empty")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(c.cfg.Secret) == "" {
|
||||||
|
return "", fmt.Errorf("manager secret is empty")
|
||||||
|
}
|
||||||
|
keys := cleanList(c.cfg.UIDKeyKeys)
|
||||||
|
if len(keys) == 0 {
|
||||||
|
keys = []string{"uid_encryption_key", "UID_ENCRYPTION_KEY", "uid_key"}
|
||||||
|
}
|
||||||
|
group := strings.TrimSpace(c.cfg.UIDKeyGroup)
|
||||||
|
if group == "" {
|
||||||
|
group = "im"
|
||||||
|
}
|
||||||
|
values := url.Values{}
|
||||||
|
values.Set("keys", strings.Join(keys, ","))
|
||||||
|
path := "/internal/im-config/third-party/" + url.PathEscape(group) + "?" + values.Encode()
|
||||||
|
req, err := c.newRequest(ctx, http.MethodGet, baseURL+path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
resp, err := c.http.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return "", fmt.Errorf("manager status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBody))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
valuesMap, err := parseValues(body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
for _, key := range keys {
|
||||||
|
if value := strings.TrimSpace(valuesMap[key]); value != "" {
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("uid key is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) newRequest(ctx context.Context, method string, rawURL string, body []byte) (*http.Request, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, rawURL, bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
timestamp := strconv.FormatInt(time.Now().UTC().Unix(), 10)
|
||||||
|
nonce, err := randomNonce()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bodyHash := sha256.Sum256(body)
|
||||||
|
signingText := strings.Join([]string{
|
||||||
|
method,
|
||||||
|
req.URL.RequestURI(),
|
||||||
|
hex.EncodeToString(bodyHash[:]),
|
||||||
|
timestamp,
|
||||||
|
nonce,
|
||||||
|
}, "\n")
|
||||||
|
mac := hmac.New(sha256.New, []byte(c.cfg.Secret))
|
||||||
|
_, _ = mac.Write([]byte(signingText))
|
||||||
|
serviceName := strings.TrimSpace(c.cfg.ServiceName)
|
||||||
|
if serviceName == "" {
|
||||||
|
serviceName = "im-server"
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
req.Header.Set("X-Service-Name", serviceName)
|
||||||
|
req.Header.Set("X-Timestamp", timestamp)
|
||||||
|
req.Header.Set("X-Nonce", nonce)
|
||||||
|
req.Header.Set("X-Signature", hex.EncodeToString(mac.Sum(nil)))
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseValues(body []byte) (map[string]string, error) {
|
||||||
|
var envelope struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &envelope); err == nil && len(envelope.Data) > 0 {
|
||||||
|
if envelope.Code != 0 {
|
||||||
|
return nil, fmt.Errorf("manager code %d", envelope.Code)
|
||||||
|
}
|
||||||
|
body = envelope.Data
|
||||||
|
}
|
||||||
|
var payload struct {
|
||||||
|
Values map[string]string `json:"values"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &payload); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if payload.Values == nil {
|
||||||
|
return map[string]string{}, nil
|
||||||
|
}
|
||||||
|
return payload.Values, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanList(values []string) []string {
|
||||||
|
items := make([]string, 0, len(values))
|
||||||
|
seen := make(map[string]bool, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if value == "" || seen[value] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[value] = true
|
||||||
|
items = append(items, value)
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomNonce() (string, error) {
|
||||||
|
var b [16]byte
|
||||||
|
if _, err := cryptorand.Read(b[:]); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(b[:]), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package managerconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.118im.com/im_team/im_workbench/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResolveUIDKeyPrefersManager(t *testing.T) {
|
||||||
|
const secret = "test-secret"
|
||||||
|
const managerKey = "manager-key"
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/internal/im-config/third-party/im" {
|
||||||
|
t.Fatalf("unexpected path %s", r.URL.Path)
|
||||||
|
}
|
||||||
|
if r.URL.Query().Get("keys") != "uid_encryption_key,UID_ENCRYPTION_KEY,uid_key" {
|
||||||
|
t.Fatalf("unexpected keys %s", r.URL.Query().Get("keys"))
|
||||||
|
}
|
||||||
|
if r.Header.Get("X-Service-Name") != "im-server" {
|
||||||
|
t.Fatalf("unexpected service %s", r.Header.Get("X-Service-Name"))
|
||||||
|
}
|
||||||
|
if !validSignature(t, r, secret) {
|
||||||
|
t.Fatal("invalid signature")
|
||||||
|
}
|
||||||
|
_, _ = w.Write([]byte(`{"code":0,"data":{"values":{"uid_encryption_key":"manager-key"}}}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
got := ResolveUIDKey(context.Background(), config.ManagerConfig{
|
||||||
|
BaseURL: server.URL,
|
||||||
|
ServiceName: "im-server",
|
||||||
|
Secret: secret,
|
||||||
|
UIDKeyGroup: "im",
|
||||||
|
UIDKeyKeys: []string{"uid_encryption_key", "UID_ENCRYPTION_KEY", "uid_key"},
|
||||||
|
}, "fallback-key")
|
||||||
|
if got != managerKey {
|
||||||
|
t.Fatalf("ResolveUIDKey() = %q, want %q", got, managerKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveUIDKeyFallsBack(t *testing.T) {
|
||||||
|
got := ResolveUIDKey(context.Background(), config.ManagerConfig{}, "fallback-key")
|
||||||
|
if got != "fallback-key" {
|
||||||
|
t.Fatalf("ResolveUIDKey() = %q, want fallback", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseValuesAcceptsUnwrappedResponse(t *testing.T) {
|
||||||
|
values, err := parseValues([]byte(`{"values":{"UID_ENCRYPTION_KEY":"key"}}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if values["UID_ENCRYPTION_KEY"] != "key" {
|
||||||
|
t.Fatalf("unexpected values %#v", values)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validSignature(t *testing.T, r *http.Request, secret string) bool {
|
||||||
|
t.Helper()
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
bodyHash := sha256.Sum256(body)
|
||||||
|
signingText := strings.Join([]string{
|
||||||
|
r.Method,
|
||||||
|
r.URL.RequestURI(),
|
||||||
|
hex.EncodeToString(bodyHash[:]),
|
||||||
|
r.Header.Get("X-Timestamp"),
|
||||||
|
r.Header.Get("X-Nonce"),
|
||||||
|
}, "\n")
|
||||||
|
mac := hmac.New(sha256.New, []byte(secret))
|
||||||
|
_, _ = mac.Write([]byte(signingText))
|
||||||
|
return hmac.Equal([]byte(r.Header.Get("X-Signature")), []byte(hex.EncodeToString(mac.Sum(nil))))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user