Files
dingtalk-workspace-cli/internal/auth/app_config_strict.go
aa6abc5ed6 feat(event): add personal event subscriptions (#589)
* dws event

* fix consume fail

* test: add stream ticket injection probe

* feat: add portal ticket stream mode

* user event

* fix: allow portal ticket normal without app secret

* event

* user event

* eventType filter

* refactor(event): 优化IPC端点路径处理和改进相关测试

- 用dwsevent.IPCEndpoint替代原先根据GOOS判断的路径逻辑
- 新增event包实现Unix socket路径长度限制及长路径fallback机制
- 添加endpoint_test.go覆盖路径短长及唯一性的单元测试
- 修改busctl模块使用统一的IPC端点获取方法,避免重复实现
- transport_unix.go新增checkSocketPath函数检查路径长度,防止EINVAL错误
- 在监听和连接Unix socket时加入路径限制检查,提升错误明晰度
- 去除多个文件中无用的runtime导入,简化代码依赖

* opt

* event skill

* default value

* install script event

* fix: remove subscribe id event fanout filter

* fix(personal): 修正指定发送人消息描述错误

* more im event

* filter subId

* fix: align personal event schema with stream payload

* fix: avoid duplicate app helper name

* feat: simplify personal event schemas

* feat: simplify event schema output

* docs: refine dingtalk event skill references

* feat: align personal event consume flags

* fix event stop and status visibility

* hide app event public entrypoints

* hide incomplete personal sender event

* remove external event reference comments

* chore(event): prepare official release

* fix(event): harden personal stream lifecycle

---------

Co-authored-by: 玉澜 <yulan.wqy@alibaba-inc.com>
Co-authored-by: zhengyubai <zhengyubai618@gmail.com>
2026-07-10 17:54:43 +08:00

158 lines
6.5 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
import (
"errors"
"fmt"
"os"
)
// CredentialSource identifies where a particular credential field
// (ClientID or ClientSecret) was loaded from. It is exposed in
// `dws event status` and the HelloAck IPC frame so users can verify which
// credential channel is actually in use — important because env vars,
// keychain, and config file can all coexist and silently override each
// other (see plan §1 决策 "凭证来源拆字段").
type CredentialSource string
const (
CredentialSourceUnknown CredentialSource = "unknown"
CredentialSourceEnv CredentialSource = "env"
CredentialSourceAppConfig CredentialSource = "app_config" // value pulled from app config (plain or SecretRef metadata)
CredentialSourceKeychain CredentialSource = "keychain" // SecretRef resolved through OS keychain
CredentialSourcePlainConfig CredentialSource = "plain_config" // SecretInput stored as plaintext in config file (insecure but supported)
)
// Strict resolver error sentinels. Use errors.Is to distinguish failure
// modes; see plan §8 strict resolver decision (4 classes).
var (
// ErrAppConfigMissing — no app config file on disk AND no env-var
// credentials present. Prompt the user to either `dws config init` or
// set DWS_CLIENT_ID + DWS_CLIENT_SECRET.
ErrAppConfigMissing = errors.New("app config missing: run `dws config init` or set DWS_CLIENT_ID/DWS_CLIENT_SECRET env vars")
// ErrClientIDEmpty — neither env nor config supplies a non-empty ClientID.
ErrClientIDEmpty = errors.New("ClientID is empty")
// ErrClientSecretEmpty — there's a ClientID but ClientSecret resolved to "".
ErrClientSecretEmpty = errors.New("ClientSecret is empty")
// ErrSecretResolve — the secret-resolution backend (keychain) failed
// unrecoverably. Typically headless Linux without gnome-keyring, locked
// macOS keychain, or CI sandboxes. Suggest the env-var fallback.
ErrSecretResolve = errors.New("ClientSecret resolution failed (keychain unavailable?); try DWS_CLIENT_ID/DWS_CLIENT_SECRET env vars")
)
// Env var names used by the env fallback channel. Must be set as a pair —
// any single-variable configuration is rejected so users cannot accidentally
// "set the env half-way" and silently fall back to keychain.
const (
EnvClientID = "DWS_CLIENT_ID"
EnvClientSecret = "DWS_CLIENT_SECRET"
)
// ResolveAppCredentialsStrict is the credentials channel used by the event
// subsystem (and by future commands that need fine-grained failure
// reporting). It distinguishes 4 failure classes and reports the source of
// each successfully-resolved field separately.
//
// Resolution order:
// 1. Env var override: if BOTH DWS_CLIENT_ID and DWS_CLIENT_SECRET are
// set non-empty, use them as a pair and skip keychain/config entirely.
// Single-variable configuration is detected and reported via the
// EnvHalfSet flag in the warning channel (callers MAY log a warning).
// 2. App config from disk:
// - ClientID from cfg.ClientID
// - ClientSecret from ResolveSecret(cfg.ClientSecret):
// - SecretInput.IsPlain() → CredentialSourcePlainConfig
// - SecretRef → CredentialSourceKeychain (or whatever Ref.Source says)
//
// Empty returns: clientID and secret may be empty when err is non-nil;
// callers must NOT use them in that case.
func ResolveAppCredentialsStrict(configDir string) (
clientID, secret string,
clientIDSource, secretSource CredentialSource,
err error,
) {
// Step 1: env var fallback (atomic pair)
envID := os.Getenv(EnvClientID)
envSecret := os.Getenv(EnvClientSecret)
if envID != "" && envSecret != "" {
return envID, envSecret, CredentialSourceEnv, CredentialSourceEnv, nil
}
// Note: if only one of the two is set we explicitly do NOT use it.
// The half-set warning is surfaced via EnvHalfSet() so the CLI can
// stderr-warn the user during preflight.
// Step 2: app config from disk
cfg, loadErr := LoadAppConfig(configDir)
if loadErr != nil {
return "", "", CredentialSourceUnknown, CredentialSourceUnknown,
fmt.Errorf("load app config: %w", loadErr)
}
if cfg == nil {
return "", "", CredentialSourceUnknown, CredentialSourceUnknown, ErrAppConfigMissing
}
if cfg.ClientID == "" {
return "", "", CredentialSourceUnknown, CredentialSourceUnknown, ErrClientIDEmpty
}
clientID = cfg.ClientID
clientIDSource = CredentialSourceAppConfig
// Resolve secret. Source depends on the SecretInput shape:
// - IsPlain (no Ref) → it's stored as plaintext in the config file
// - has Ref → it's a SecretRef pointing at keychain/file
wasPlain := cfg.ClientSecret.IsPlain()
resolved, resolveErr := ResolveSecret(cfg.ClientSecret)
if resolveErr != nil {
return "", "", CredentialSourceUnknown, CredentialSourceUnknown,
fmt.Errorf("%w: %v", ErrSecretResolve, resolveErr)
}
if resolved == "" {
return "", "", clientIDSource, CredentialSourceUnknown, ErrClientSecretEmpty
}
secret = resolved
if wasPlain {
secretSource = CredentialSourcePlainConfig
} else {
// For SecretRef we map Source verbatim (keychain / file / future)
switch cfg.ClientSecret.Ref.Source {
case "keychain":
secretSource = CredentialSourceKeychain
default:
// File-backed secrets share the "plain_config" category from
// the consumer's perspective: stored as readable bytes outside
// keychain. Status output renders them as "plain_config" so
// users see "secret is not in keychain".
secretSource = CredentialSourcePlainConfig
}
}
return clientID, secret, clientIDSource, secretSource, nil
}
// EnvHalfSet reports whether exactly one of (DWS_CLIENT_ID, DWS_CLIENT_SECRET)
// is set. Used by CLI preflight to emit a clear stderr warning of the form:
//
// WARN: DWS_CLIENT_ID is set but DWS_CLIENT_SECRET is not — env fallback
// disabled; using keychain/app config. Set both or unset both to
// avoid this warning.
//
// The strict resolver itself does NOT log; logging is the caller's job.
func EnvHalfSet() bool {
id := os.Getenv(EnvClientID) != ""
secret := os.Getenv(EnvClientSecret) != ""
return id != secret
}