chore: import upstream snapshot with attribution
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
lark "github.com/larksuite/oapi-sdk-go/v3"
|
||||
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
larkauth "github.com/larksuite/cli/internal/auth"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/errclass"
|
||||
)
|
||||
|
||||
// NewCmdAuth creates the auth command with subcommands.
|
||||
func NewCmdAuth(f *cmdutil.Factory) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "auth",
|
||||
Short: "OAuth credentials and authorization management",
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Replicate rootCmd's PersistentPreRun behaviour: cobra stops at the first
|
||||
// PersistentPreRun[E] found walking up the chain, so the root-level
|
||||
// SilenceUsage=true would be skipped without this line.
|
||||
cmd.SilenceUsage = true
|
||||
// cmd.Name() returns the subcommand name (e.g. "login"), not "auth".
|
||||
// Pass "auth" as a literal so the error message reads
|
||||
// `"auth" is not supported: ...`
|
||||
return f.RequireBuiltinCredentialProvider(cmd.Context(), "auth")
|
||||
},
|
||||
}
|
||||
cmdutil.DisableAuthCheck(cmd)
|
||||
|
||||
cmd.AddCommand(NewCmdAuthLogin(f, nil))
|
||||
cmd.AddCommand(NewCmdAuthLogout(f, nil))
|
||||
cmd.AddCommand(NewCmdAuthStatus(f, nil))
|
||||
cmd.AddCommand(NewCmdAuthScopes(f, nil))
|
||||
cmd.AddCommand(NewCmdAuthList(f, nil))
|
||||
cmd.AddCommand(NewCmdAuthCheck(f, nil))
|
||||
cmd.AddCommand(NewCmdAuthQRCode(f, nil))
|
||||
return cmd
|
||||
}
|
||||
|
||||
// userInfoResponse is the API response for /open-apis/authen/v1/user_info.
|
||||
type userInfoResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data struct {
|
||||
OpenID string `json:"open_id"`
|
||||
Name string `json:"name"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// getUserInfo fetches the current user's OpenID and name using the given access token.
|
||||
func getUserInfo(ctx context.Context, sdk *lark.Client, accessToken string) (openId, name string, err error) {
|
||||
apiResp, err := sdk.Do(ctx, &larkcore.ApiReq{
|
||||
HttpMethod: http.MethodGet,
|
||||
ApiPath: larkauth.PathUserInfoV1,
|
||||
SupportedAccessTokenTypes: []larkcore.AccessTokenType{larkcore.AccessTokenTypeUser},
|
||||
}, larkcore.WithUserAccessToken(accessToken))
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
var resp userInfoResponse
|
||||
if err := json.Unmarshal(apiResp.RawBody, &resp); err != nil {
|
||||
return "", "", fmt.Errorf("failed to parse user info: %w", err)
|
||||
}
|
||||
if resp.Code != 0 {
|
||||
return "", "", fmt.Errorf("failed to get user info [%d]: %s", resp.Code, resp.Msg)
|
||||
}
|
||||
if resp.Data.OpenID == "" {
|
||||
return "", "", fmt.Errorf("failed to get user info: missing open_id in response")
|
||||
}
|
||||
|
||||
name = resp.Data.Name
|
||||
if name == "" {
|
||||
name = "(unknown)"
|
||||
}
|
||||
return resp.Data.OpenID, name, nil
|
||||
}
|
||||
|
||||
// appInfo contains application information (owner, scopes).
|
||||
type appInfo struct {
|
||||
OwnerOpenId string
|
||||
UserScopes []string
|
||||
}
|
||||
|
||||
// appInfoResponse is the API response for /open-apis/application/v6/applications/:app_id.
|
||||
type appInfoResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data struct {
|
||||
App struct {
|
||||
Owner struct {
|
||||
OwnerID string `json:"owner_id"`
|
||||
} `json:"owner"`
|
||||
CreatorID string `json:"creator_id"`
|
||||
Scopes []struct {
|
||||
Scope string `json:"scope"`
|
||||
TokenTypes []string `json:"token_types"`
|
||||
} `json:"scopes"`
|
||||
} `json:"app"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// getAppInfoFn is the package-level seam used by callers (scopes.go) so tests
|
||||
// can substitute a fake without standing up a full SDK + httpmock pipeline.
|
||||
// Mirrors the pollDeviceToken pattern in login.go.
|
||||
var getAppInfoFn = getAppInfo
|
||||
|
||||
// getAppInfo queries app info from the Lark API.
|
||||
func getAppInfo(ctx context.Context, f *cmdutil.Factory, appId string) (*appInfo, error) {
|
||||
ac, err := f.NewAPIClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
queryParams := make(larkcore.QueryParams)
|
||||
queryParams.Set("lang", "zh_cn")
|
||||
|
||||
apiResp, err := ac.DoSDKRequest(ctx, &larkcore.ApiReq{
|
||||
HttpMethod: http.MethodGet,
|
||||
ApiPath: larkauth.ApplicationInfoPath(appId),
|
||||
QueryParams: queryParams,
|
||||
}, core.AsBot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp appInfoResponse
|
||||
if err := json.Unmarshal(apiResp.RawBody, &resp); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
if resp.Code != 0 {
|
||||
return nil, classifyAppInfoErr(apiResp.RawBody, resp.Code, resp.Msg, f, appId)
|
||||
}
|
||||
|
||||
app := resp.Data.App
|
||||
ownerOpenId := app.Owner.OwnerID
|
||||
if ownerOpenId == "" {
|
||||
ownerOpenId = app.CreatorID
|
||||
}
|
||||
|
||||
var userScopes []string
|
||||
for _, s := range app.Scopes {
|
||||
if s.Scope == "" || !slices.Contains(s.TokenTypes, "user") {
|
||||
continue
|
||||
}
|
||||
userScopes = append(userScopes, s.Scope)
|
||||
}
|
||||
|
||||
return &appInfo{OwnerOpenId: ownerOpenId, UserScopes: userScopes}, nil
|
||||
}
|
||||
|
||||
// classifyAppInfoErr re-decodes the raw body so BuildAPIError sees the
|
||||
// upstream `error` block — the typed appInfoResponse shape drops it.
|
||||
func classifyAppInfoErr(rawBody []byte, code int, msg string, f *cmdutil.Factory, appId string) error {
|
||||
var raw map[string]any
|
||||
_ = json.Unmarshal(rawBody, &raw)
|
||||
if raw == nil {
|
||||
raw = map[string]any{}
|
||||
}
|
||||
raw["code"] = code
|
||||
raw["msg"] = msg
|
||||
cc := errclass.ClassifyContext{Identity: string(core.AsBot)}
|
||||
if cfg, _ := f.Config(); cfg != nil {
|
||||
cc.Brand = string(cfg.Brand)
|
||||
cc.AppID = appId
|
||||
}
|
||||
return errclass.BuildAPIError(raw, cc)
|
||||
}
|
||||
Reference in New Issue
Block a user