refactor(auth): delegate access control to be-system

Remove Agent Desk users, roles, login sessions, tokens, and local permission persistence. Expose the backend as an embeddable ai-agent module with host-provided subject lookup and operation authorization callbacks, and complete the frontend/backend repository split.
This commit is contained in:
t
2026-08-21 00:41:07 +08:00
parent 3d47227fbd
commit 2bbf42b741
447 changed files with 1901 additions and 8920 deletions
-256
View File
@@ -1,256 +0,0 @@
package wxwork
import (
"agent-desk/internal/pkg/dto/response"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/i18nx"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"net/url"
"strings"
"sync"
"time"
"github.com/silenceper/wechat/v2/work/oauth"
)
const (
StateTTL = 5 * time.Minute
LoginTicketTTL = 1 * time.Minute
defaultLoginNextPath = "/dashboard"
)
var (
errStateInvalid = i18nx.Errorf("error.e0110")
loginTicketStore sync.Map
)
type statePayload struct {
Next string `json:"next"`
Nonce string `json:"nonce"`
ExpiredAt int64 `json:"expiredAt"`
}
type loginTicket struct {
Response *response.LoginResponse
ExpiredAt time.Time
}
func BuildLoginURL(state string) (string, error) {
if !Enabled() {
return "", i18nx.Errorf("error.e0109")
}
if strings.TrimSpace(wxCfg.OAuthRedirect) == "" {
return "", i18nx.Errorf("error.e0107")
}
if strings.TrimSpace(wxCfg.AgentID) == "" {
return "", i18nx.Errorf("error.e0093")
}
return fmt.Sprintf(
"https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_privateinfo&agentid=%s&state=%s#wechat_redirect",
url.QueryEscape(strings.TrimSpace(wxCfg.CorpID)),
url.QueryEscape(strings.TrimSpace(wxCfg.OAuthRedirect)),
url.QueryEscape(strings.TrimSpace(wxCfg.AgentID)),
url.QueryEscape(strings.TrimSpace(state)),
), nil
}
func BuildQRCodeLoginURL(state string) (string, error) {
if !Enabled() {
return "", i18nx.Errorf("error.e0109")
}
if strings.TrimSpace(wxCfg.OAuthRedirect) == "" {
return "", i18nx.Errorf("error.e0107")
}
if strings.TrimSpace(wxCfg.AgentID) == "" {
return "", i18nx.Errorf("error.e0093")
}
return fmt.Sprintf(
"https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid=%s&agentid=%s&redirect_uri=%s&state=%s",
url.QueryEscape(strings.TrimSpace(wxCfg.CorpID)),
url.QueryEscape(strings.TrimSpace(wxCfg.AgentID)),
url.QueryEscape(strings.TrimSpace(wxCfg.OAuthRedirect)),
url.QueryEscape(strings.TrimSpace(state)),
), nil
}
func CreateState(next string) (string, error) {
secret := strings.TrimSpace(StateSecret())
if secret == "" {
return "", errorsx.BusinessErrorI18n(1, "error.wxwork.loginSecretMissing")
}
payload := statePayload{
Next: sanitizeNextPath(next),
ExpiredAt: time.Now().Add(StateTTL).Unix(),
}
nonce, err := randomToken("ws_")
if err != nil {
return "", err
}
payload.Nonce = nonce
body, err := json.Marshal(payload)
if err != nil {
return "", err
}
encoded := base64.RawURLEncoding.EncodeToString(body)
return encoded + "." + signState(encoded, secret), nil
}
func ParseState(state string) (string, error) {
secret := strings.TrimSpace(StateSecret())
if secret == "" {
return "", errStateInvalid
}
parts := strings.Split(strings.TrimSpace(state), ".")
if len(parts) != 2 {
return "", errStateInvalid
}
if !hmac.Equal([]byte(parts[1]), []byte(signState(parts[0], secret))) {
return "", errStateInvalid
}
body, err := base64.RawURLEncoding.DecodeString(parts[0])
if err != nil {
return "", errStateInvalid
}
payload := statePayload{}
if err = json.Unmarshal(body, &payload); err != nil {
return "", errStateInvalid
}
if payload.ExpiredAt <= time.Now().Unix() {
return "", errStateInvalid
}
return sanitizeNextPath(payload.Next), nil
}
func IssueLoginTicket(loginResp *response.LoginResponse) (string, error) {
if loginResp == nil {
return "", i18nx.Errorf("error.e0272")
}
ticket, err := randomToken("wlt_")
if err != nil {
return "", err
}
cleanupExpiredLoginTickets()
loginTicketStore.Store(ticket, loginTicket{
Response: loginResp,
ExpiredAt: time.Now().Add(LoginTicketTTL),
})
return ticket, nil
}
func ConsumeLoginTicket(ticket string) (*response.LoginResponse, error) {
ticket = strings.TrimSpace(ticket)
if ticket == "" {
return nil, errorsx.InvalidParamI18n("error.e0072")
}
value, ok := loginTicketStore.LoadAndDelete(ticket)
if !ok {
return nil, errorsx.UnauthorizedI18n("error.e0271")
}
record, ok := value.(loginTicket)
if !ok || record.Response == nil || time.Now().After(record.ExpiredAt) {
return nil, errorsx.UnauthorizedI18n("error.e0271")
}
return record.Response, nil
}
func signState(content, secret string) string {
mac := hmac.New(sha256.New, []byte(secret))
_, _ = mac.Write([]byte(content))
return hex.EncodeToString(mac.Sum(nil))
}
func cleanupExpiredLoginTickets() {
now := time.Now()
loginTicketStore.Range(func(key, value any) bool {
record, ok := value.(loginTicket)
if !ok || now.After(record.ExpiredAt) {
loginTicketStore.Delete(key)
}
return true
})
}
func sanitizeNextPath(next string) string {
next = strings.TrimSpace(next)
if next == "" || !strings.HasPrefix(next, "/") || strings.HasPrefix(next, "//") {
return defaultLoginNextPath
}
return next
}
func randomToken(prefix string) (string, error) {
buf := make([]byte, 24)
if _, err := rand.Read(buf); err != nil {
return "", err
}
return prefix + hex.EncodeToString(buf), nil
}
func GetUserDetail(code string) (*LoginUser, error) {
if !Enabled() {
return nil, i18nx.Errorf("error.e0109")
}
code = strings.TrimSpace(code)
if code == "" {
return nil, i18nx.Errorf("error.e0202")
}
oauthClient := w.GetOauth()
userInfo, err := oauthClient.GetUserInfo(code)
if err != nil {
return nil, err
}
if strings.TrimSpace(userInfo.UserID) == "" {
return nil, i18nx.Errorf("error.e0198")
}
ret := &LoginUser{
CorpID: wxCfg.CorpID,
UserID: strings.TrimSpace(userInfo.UserID),
OpenID: strings.TrimSpace(userInfo.OpenID),
ExternalUserID: strings.TrimSpace(userInfo.ExternalUserID),
UserTicket: strings.TrimSpace(userInfo.UserTicket),
UserInfo: userInfo,
}
if ret.UserTicket != "" {
if detail, detailErr := oauthClient.GetUserDetail(&oauth.GetUserDetailRequest{UserTicket: ret.UserTicket}); detailErr == nil {
ret.UserDetail = detail
ret.Avatar = strings.TrimSpace(detail.Avatar)
ret.Mobile = strings.TrimSpace(detail.Mobile)
ret.Email = strings.TrimSpace(detail.Email)
ret.BizMail = strings.TrimSpace(detail.BizMail)
}
}
if profile, profileErr := w.GetAddressList().UserGet(ret.UserID); profileErr == nil {
ret.UserProfile = profile
if strings.TrimSpace(profile.Name) != "" {
ret.Name = strings.TrimSpace(profile.Name)
}
if strings.TrimSpace(profile.Avatar) != "" {
ret.Avatar = strings.TrimSpace(profile.Avatar)
}
if ret.Mobile == "" && strings.TrimSpace(profile.Mobile) != "" {
ret.Mobile = strings.TrimSpace(profile.Mobile)
}
if ret.Email == "" && strings.TrimSpace(profile.Email) != "" {
ret.Email = strings.TrimSpace(profile.Email)
}
if ret.BizMail == "" && strings.TrimSpace(profile.BizMail) != "" {
ret.BizMail = strings.TrimSpace(profile.BizMail)
}
}
if ret.Name == "" {
ret.Name = ret.UserID
}
return ret, nil
}
+1 -26
View File
@@ -1,14 +1,12 @@
package wxwork
import (
"agent-desk/internal/pkg/config"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/config"
"strings"
"github.com/silenceper/wechat/v2/cache"
"github.com/silenceper/wechat/v2/work"
"github.com/silenceper/wechat/v2/work/addresslist"
wxconfig "github.com/silenceper/wechat/v2/work/config"
"github.com/silenceper/wechat/v2/work/oauth"
)
var (
@@ -16,22 +14,6 @@ var (
wxCfg config.WxWorkConfig
)
type LoginUser struct {
CorpID string `json:"corpId"`
UserID string `json:"userId"`
OpenID string `json:"openId,omitempty"`
ExternalUserID string `json:"externalUserId,omitempty"`
UserTicket string `json:"userTicket,omitempty"`
Name string `json:"name,omitempty"`
Avatar string `json:"avatar,omitempty"`
Mobile string `json:"mobile,omitempty"`
Email string `json:"email,omitempty"`
BizMail string `json:"bizMail,omitempty"`
UserInfo *oauth.GetUserInfoResponse `json:"userInfo,omitempty"`
UserDetail *oauth.GetUserDetailResponse `json:"userDetail,omitempty"`
UserProfile *addresslist.UserGetResponse `json:"userProfile,omitempty"`
}
func Init() {
w = nil
wxCfg = config.WxWorkConfig{}
@@ -58,13 +40,6 @@ func Enabled() bool {
return w != nil && wxCfg.Enabled
}
func StateSecret() string {
if strings.TrimSpace(wxCfg.StateSecret) != "" {
return strings.TrimSpace(wxCfg.StateSecret)
}
return strings.TrimSpace(wxCfg.CorpSecret)
}
func GetWorkCli() *work.Work {
return w
}