feat(auth): add edition overlay auth credentials and token marker for embedded mode
- Add AuthClientID and AuthClientFromMCP fields to edition Hooks, allowing overlays to override the default OAuth client ID and route auth through MCP endpoints. - Introduce token marker file (token.json) mechanism so the host application in embedded mode can detect authentication state without accessing the keychain. - Update SaveTokenData/DeleteTokenData to write/remove the marker file when running in embedded mode. - Update ClientID() and IsClientIDFromMCP() to respect edition overrides.
This commit is contained in:
1 parent
953b422295
commit
aec852f971
3 files changed
+56
-8
No files matched your search
@@ -18,6 +18,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -110,7 +112,7 @@ func SetClientIDFromMCP(id string) {
|
|||||||
func IsClientIDFromMCP() bool {
|
func IsClientIDFromMCP() bool {
|
||||||
clientMu.RLock()
|
clientMu.RLock()
|
||||||
defer clientMu.RUnlock()
|
defer clientMu.RUnlock()
|
||||||
return clientIDFromMCP
|
return clientIDFromMCP || edition.Get().AuthClientFromMCP
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserAccessTokenURL returns the appropriate token exchange URL.
|
// GetUserAccessTokenURL returns the appropriate token exchange URL.
|
||||||
@@ -189,6 +191,9 @@ func ClientID() string {
|
|||||||
if override != "" {
|
if override != "" {
|
||||||
return override
|
return override
|
||||||
}
|
}
|
||||||
|
if id := edition.Get().AuthClientID; id != "" {
|
||||||
|
return id
|
||||||
|
}
|
||||||
// Try loading from persisted app config
|
// Try loading from persisted app config
|
||||||
if id, _ := ResolveAppCredentials(getDefaultConfigDir()); id != "" {
|
if id, _ := ResolveAppCredentials(getDefaultConfigDir()); id != "" {
|
||||||
return id
|
return id
|
||||||
|
|||||||
+46
-7
@@ -20,7 +20,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TokenData holds the OAuth token set persisted to disk.
|
// TokenData holds the OAuth token set persisted to disk.
|
||||||
@@ -61,10 +65,46 @@ func (t *TokenData) HasPersistentCode() bool {
|
|||||||
return t != nil && t.PersistentCode != ""
|
return t != nil && t.PersistentCode != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tokenJSONFile = "token.json"
|
||||||
|
|
||||||
|
// TokenMarker is a lightweight file the host application reads to detect
|
||||||
|
// whether the CLI has a valid token without accessing the keychain.
|
||||||
|
type TokenMarker struct {
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteTokenMarker writes a token.json marker containing only an updated_at
|
||||||
|
// timestamp. The host application uses this file's presence and mtime to
|
||||||
|
// decide whether it needs to trigger a new auth exchange.
|
||||||
|
func WriteTokenMarker(configDir string) error {
|
||||||
|
marker := TokenMarker{UpdatedAt: time.Now().Format(time.RFC3339)}
|
||||||
|
data, _ := json.MarshalIndent(marker, "", " ")
|
||||||
|
if err := os.MkdirAll(configDir, 0o700); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tmp := filepath.Join(configDir, tokenJSONFile+".tmp")
|
||||||
|
if err := os.WriteFile(tmp, data, 0o600); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Rename(tmp, filepath.Join(configDir, tokenJSONFile))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTokenMarker removes the token.json marker file.
|
||||||
|
func DeleteTokenMarker(configDir string) error {
|
||||||
|
return os.Remove(filepath.Join(configDir, tokenJSONFile))
|
||||||
|
}
|
||||||
|
|
||||||
// SaveTokenData saves TokenData to the platform keychain.
|
// SaveTokenData saves TokenData to the platform keychain.
|
||||||
// Uses the new keychain-based storage with random master key for better security.
|
// In embedded mode, it also writes a token.json marker file so the host
|
||||||
|
// application can detect authentication state without accessing the keychain.
|
||||||
func SaveTokenData(configDir string, data *TokenData) error {
|
func SaveTokenData(configDir string, data *TokenData) error {
|
||||||
return SaveTokenDataKeychain(data)
|
if err := SaveTokenDataKeychain(data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if edition.Get().IsEmbedded {
|
||||||
|
_ = WriteTokenMarker(configDir)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadTokenData reads TokenData from the platform keychain.
|
// LoadTokenData reads TokenData from the platform keychain.
|
||||||
@@ -91,14 +131,13 @@ func LoadTokenData(configDir string) (*TokenData, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeleteTokenData removes token data from both keychain and legacy storage.
|
// DeleteTokenData removes token data from both keychain and legacy storage.
|
||||||
|
// In embedded mode, it also removes the token.json marker file.
|
||||||
func DeleteTokenData(configDir string) error {
|
func DeleteTokenData(configDir string) error {
|
||||||
// Delete from keychain
|
|
||||||
keychainErr := DeleteTokenDataKeychain()
|
keychainErr := DeleteTokenDataKeychain()
|
||||||
|
|
||||||
// Also clean up any legacy .data file
|
|
||||||
legacyErr := DeleteSecureData(configDir)
|
legacyErr := DeleteSecureData(configDir)
|
||||||
|
if edition.Get().IsEmbedded {
|
||||||
// Return keychain error if any, otherwise legacy error
|
_ = DeleteTokenMarker(configDir)
|
||||||
|
}
|
||||||
if keychainErr != nil {
|
if keychainErr != nil {
|
||||||
return keychainErr
|
return keychainErr
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ type Hooks struct {
|
|||||||
OnAuthError func(configDir string, err error) error
|
OnAuthError func(configDir string, err error) error
|
||||||
TokenProvider func(ctx context.Context, fallback func() (string, error)) (string, error)
|
TokenProvider func(ctx context.Context, fallback func() (string, error)) (string, error)
|
||||||
|
|
||||||
|
// --- auth credentials (overlay-only) ---
|
||||||
|
AuthClientID string // non-empty overrides DefaultClientID
|
||||||
|
AuthClientFromMCP bool // true routes OAuth through MCP endpoints
|
||||||
|
|
||||||
// --- product & endpoint ---
|
// --- product & endpoint ---
|
||||||
StaticServers func() []ServerInfo // non-nil → skip Market discovery
|
StaticServers func() []ServerInfo // non-nil → skip Market discovery
|
||||||
VisibleProducts func() []string // non-nil → override help visibility
|
VisibleProducts func() []string // non-nil → override help visibility
|
||||||
|
|||||||
Reference in new issue
Block a user