mirror of
https://github.com/larksuite/cli.git
synced 2026-08-03 08:32:46 +08:00
fix(vc): align meeting query scopes by identity (#1850)
* fix(vc): align meeting query scopes by identity * docs(vc): simplify meeting query scope guidance * fix: align meeting query scopes by identity * fix: harden vc meeting query scope preflight * test: assert vc meeting query permission category * fix: declare empty vc meeting query scopes * fix: align vc meeting query scope metadata * docs: simplify vc meeting query scope guidance * fix: preflight vc meeting query tat scopes * fix: make vc scope metadata lookup best effort * fix(vc): accept compatible meeting query scopes * test(vc): cover meeting query validate scope checks * refactor(vc): align meeting query precheck with framework * fix(vc): clarify meeting query scope recovery * docs(vc): use gray access as permission fallback * fix(vc): use user-only scope preflight for meeting queries * fix(vc): route meeting scope hints by error code * fix(vc): clarify compatible scope application hint * fix(vc): simplify meeting scope recovery * chore(vc): centralize meeting scope guidance * fix(vc): preserve upstream meeting scope messages * fix(vc): align meeting scope guidance by identity * docs(vc): clarify meeting gray access guidance * docs(vc): scope meeting query permission guidance * refactor(vc): simplify meeting permission hints * refactor(vc): remove unreachable permission guard * fix(vc): guard missing meeting permission runtime * fix(vc): guard typed nil meeting permission errors * fix(vc): preserve app scope console URL * refactor(vc): preserve original permission errors * docs(vc): prioritize permission recovery hints * docs(vc): simplify permission guidance * docs(vc): align permission check order * fix(vc): clarify meeting permission messages * docs(vc): prioritize meeting permission guidance * fix(vc): align meeting scope application link * docs(vc): scope user permission guidance to queries * fix(vc): narrow meeting missing scopes by identity
This commit is contained in:
committed by
GitHub
parent
64e10a0954
commit
64caef1526
50
shortcuts/vc/helpers.go
Normal file
50
shortcuts/vc/helpers.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package vc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/internal/registry"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
const (
|
||||
meetingQueryUserScope = "vc:meeting.meetingevent:read"
|
||||
meetingQueryBotScope = "vc:meeting.bot.join:write"
|
||||
)
|
||||
|
||||
func normalizeMeetingQueryPermissionError(runtime *common.RuntimeContext, err error) error {
|
||||
if runtime == nil {
|
||||
return err
|
||||
}
|
||||
var permissionErr *errs.PermissionError
|
||||
if !errors.As(err, &permissionErr) || permissionErr == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch {
|
||||
case runtime.As() == core.AsUser && permissionErr.Code == output.LarkErrUserScopeInsufficient:
|
||||
permissionErr.Message = "access denied for user identity; recommended scope: " + meetingQueryUserScope
|
||||
permissionErr.WithHint("for user identity, run `lark-cli auth login --scope %q` in the background. It blocks and outputs a verification URL — retrieve the URL and open it in a browser to complete login.", meetingQueryUserScope)
|
||||
permissionErr.WithMissingScopes(meetingQueryUserScope)
|
||||
return err
|
||||
case runtime.As() == core.AsBot && permissionErr.Code == output.LarkErrAppScopeNotEnabled:
|
||||
permissionErr.Message = "access denied for bot identity; recommended scope: " + meetingQueryBotScope
|
||||
permissionErr.WithHint("ask the app developer to enable scope %s", meetingQueryBotScope)
|
||||
permissionErr.WithMissingScopes(meetingQueryBotScope)
|
||||
if runtime.Config != nil {
|
||||
consoleURL := registry.BuildConsoleScopeURL(runtime.Config.Brand, runtime.Config.AppID, meetingQueryBotScope)
|
||||
if consoleURL != "" {
|
||||
permissionErr.WithConsoleURL(consoleURL)
|
||||
}
|
||||
}
|
||||
return err
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
207
shortcuts/vc/helpers_test.go
Normal file
207
shortcuts/vc/helpers_test.go
Normal file
@@ -0,0 +1,207 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package vc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
func bareMeetingQueryRuntime(as core.Identity) *common.RuntimeContext {
|
||||
return common.TestNewRuntimeContextWithIdentity(&cobra.Command{Use: "test"}, defaultConfig(), as)
|
||||
}
|
||||
|
||||
func TestNormalizeMeetingQueryPermissionError_NilRuntimeReturnsOriginalError(t *testing.T) {
|
||||
original := errs.NewPermissionError(errs.SubtypeMissingScope, "permission failure").
|
||||
WithCode(output.LarkErrUserScopeInsufficient)
|
||||
if got := normalizeMeetingQueryPermissionError(nil, original); got != original {
|
||||
t.Fatalf("got %v, want original error %v", got, original)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeMeetingQueryPermissionError_TypedNilReturnsOriginalError(t *testing.T) {
|
||||
var permissionErr *errs.PermissionError
|
||||
var original error = permissionErr
|
||||
if got := normalizeMeetingQueryPermissionError(bareMeetingQueryRuntime(core.AsUser), original); got != original {
|
||||
t.Fatalf("got %v, want original error %v", got, original)
|
||||
}
|
||||
}
|
||||
|
||||
func assertMeetingQueryPermissionError(t *testing.T, err error, identity core.Identity, code int) {
|
||||
t.Helper()
|
||||
|
||||
var pe *errs.PermissionError
|
||||
if !errors.As(err, &pe) {
|
||||
t.Fatalf("expected *errs.PermissionError, got %T: %v", err, err)
|
||||
}
|
||||
if pe.Category != errs.CategoryAuthorization {
|
||||
t.Fatalf("Category = %q, want %q", pe.Category, errs.CategoryAuthorization)
|
||||
}
|
||||
if pe.Subtype != errs.SubtypeMissingScope && pe.Subtype != errs.SubtypeAppScopeNotApplied {
|
||||
t.Fatalf("Subtype = %q, want a missing-scope subtype", pe.Subtype)
|
||||
}
|
||||
if pe.Identity != string(identity) {
|
||||
t.Fatalf("Identity = %q, want %q", pe.Identity, identity)
|
||||
}
|
||||
|
||||
wantScope := meetingQueryUserScope
|
||||
if identity.IsBot() {
|
||||
wantScope = meetingQueryBotScope
|
||||
}
|
||||
if !strings.Contains(pe.Hint, wantScope) {
|
||||
t.Fatalf("Hint = %q, want recommended scope %q", pe.Hint, wantScope)
|
||||
}
|
||||
if len(pe.MissingScopes) != 1 || pe.MissingScopes[0] != wantScope {
|
||||
t.Fatalf("MissingScopes = %v, want only recommended scope %q", pe.MissingScopes, wantScope)
|
||||
}
|
||||
if strings.Contains(pe.Hint, "either compatible scope") {
|
||||
t.Fatalf("Hint = %q, must not repeat the OR-scope explanation from message", pe.Hint)
|
||||
}
|
||||
switch code {
|
||||
case output.LarkErrAppScopeNotEnabled:
|
||||
if strings.Contains(pe.Hint, "auth login") {
|
||||
t.Fatalf("Hint = %q, app-scope error must not recommend user login", pe.Hint)
|
||||
}
|
||||
if !strings.Contains(pe.Hint, "app developer") {
|
||||
t.Fatalf("Hint = %q, want app developer guidance", pe.Hint)
|
||||
}
|
||||
if pe.ConsoleURL == "" {
|
||||
t.Fatal("ConsoleURL is empty, want identity-specific developer-console URL")
|
||||
}
|
||||
if strings.Contains(pe.ConsoleURL, url.QueryEscape(meetingQueryUserScope)) || !strings.Contains(pe.ConsoleURL, url.QueryEscape(meetingQueryBotScope)) {
|
||||
t.Fatalf("ConsoleURL = %q, want only bot scope", pe.ConsoleURL)
|
||||
}
|
||||
case output.LarkErrUserScopeInsufficient:
|
||||
if !strings.Contains(pe.Hint, "auth login --scope") {
|
||||
t.Fatalf("Hint = %q, want auth login guidance", pe.Hint)
|
||||
}
|
||||
if pe.ConsoleURL != "" {
|
||||
t.Fatalf("ConsoleURL = %q, user-scope error must not expose a developer-console URL", pe.ConsoleURL)
|
||||
}
|
||||
default:
|
||||
t.Fatalf("unexpected code %d", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeMeetingQueryPermissionError_RecommendsScopeForMatchingIdentity(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
identity core.Identity
|
||||
code int
|
||||
subtype errs.Subtype
|
||||
}{
|
||||
{name: "user_with_user_scope_error", identity: core.AsUser, code: output.LarkErrUserScopeInsufficient, subtype: errs.SubtypeMissingScope},
|
||||
{name: "bot_with_app_scope_error", identity: core.AsBot, code: output.LarkErrAppScopeNotEnabled, subtype: errs.SubtypeAppScopeNotApplied},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
wantScope := meetingQueryUserScope
|
||||
if tc.identity == core.AsBot {
|
||||
wantScope = meetingQueryBotScope
|
||||
}
|
||||
wantMessage := "access denied for " + string(tc.identity) + " identity; recommended scope: " + wantScope
|
||||
original := errs.NewPermissionError(tc.subtype, "upstream permission failure").
|
||||
WithCode(tc.code).
|
||||
WithLogID("log-id").
|
||||
WithRetryable().
|
||||
WithIdentity(string(tc.identity)).
|
||||
WithMissingScopes(meetingQueryUserScope, meetingQueryBotScope).
|
||||
WithRequestedScopes("requested:scope").
|
||||
WithGrantedScopes("granted:scope")
|
||||
if tc.identity == core.AsBot {
|
||||
original.ConsoleURL = "https://example.com/scopes"
|
||||
}
|
||||
original.Troubleshooter = "https://example.com/troubleshoot"
|
||||
|
||||
got := normalizeMeetingQueryPermissionError(bareMeetingQueryRuntime(tc.identity), original)
|
||||
var pe *errs.PermissionError
|
||||
if !errors.As(got, &pe) {
|
||||
t.Fatalf("got %T, want *errs.PermissionError", got)
|
||||
}
|
||||
if got != original || pe != original {
|
||||
t.Fatal("normalizer did not return the original permission error")
|
||||
}
|
||||
if pe.Code != tc.code || pe.Subtype != tc.subtype || pe.LogID != "log-id" || !pe.Retryable {
|
||||
t.Fatalf("diagnostics changed: %+v", pe.Problem)
|
||||
}
|
||||
if pe.Troubleshooter != original.Troubleshooter {
|
||||
t.Fatalf("Troubleshooter = %q, want %q", pe.Troubleshooter, original.Troubleshooter)
|
||||
}
|
||||
if pe.Message != wantMessage {
|
||||
t.Fatalf("Message = %q, want %q", pe.Message, wantMessage)
|
||||
}
|
||||
if tc.identity == core.AsBot {
|
||||
consoleURL, err := url.Parse(pe.ConsoleURL)
|
||||
if err != nil {
|
||||
t.Fatalf("ConsoleURL = %q is invalid: %v", pe.ConsoleURL, err)
|
||||
}
|
||||
if consoleURL.Host == "" || consoleURL.Query().Get("clientID") != "test-app" || consoleURL.Query().Get("scopes") != meetingQueryBotScope {
|
||||
t.Fatalf("ConsoleURL = %q, want test-app and only bot scope", pe.ConsoleURL)
|
||||
}
|
||||
} else if pe.ConsoleURL != "" {
|
||||
t.Fatalf("ConsoleURL = %q, user-scope error must not expose a developer-console URL", pe.ConsoleURL)
|
||||
}
|
||||
assertMeetingQueryPermissionError(t, got, tc.identity, tc.code)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeMeetingQueryPermissionError_PassesThroughNonMatchingErrors(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
identity core.Identity
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "user_with_app_scope_error",
|
||||
identity: core.AsUser,
|
||||
err: errs.NewPermissionError(errs.SubtypeAppScopeNotApplied, "app scope error").
|
||||
WithCode(output.LarkErrAppScopeNotEnabled),
|
||||
},
|
||||
{
|
||||
name: "bot_with_user_scope_error",
|
||||
identity: core.AsBot,
|
||||
err: errs.NewPermissionError(errs.SubtypeMissingScope, "user scope error").
|
||||
WithCode(output.LarkErrUserScopeInsufficient),
|
||||
},
|
||||
{
|
||||
name: "auto_with_user_scope_error",
|
||||
identity: core.AsAuto,
|
||||
err: errs.NewPermissionError(errs.SubtypeMissingScope, "auto identity").
|
||||
WithCode(output.LarkErrUserScopeInsufficient),
|
||||
},
|
||||
{
|
||||
name: "bot_not_in_meeting",
|
||||
err: errs.NewPermissionError(errs.SubtypePermissionDenied, "not in meeting").WithCode(10005),
|
||||
},
|
||||
{
|
||||
name: "not_in_gray",
|
||||
err: errs.NewPermissionError(errs.SubtypePermissionDenied, "not in gray").
|
||||
WithCode(20017),
|
||||
},
|
||||
{name: "plain_error", err: errors.New("boom")},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
identity := tc.identity
|
||||
if identity == "" {
|
||||
identity = core.AsBot
|
||||
}
|
||||
if got := normalizeMeetingQueryPermissionError(bareMeetingQueryRuntime(identity), tc.err); got != tc.err {
|
||||
t.Fatalf("got %T %v, want original error %T %v", got, got, tc.err, tc.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -52,9 +52,13 @@ var VCMeetingEvents = common.Shortcut{
|
||||
Command: "+meeting-events",
|
||||
Description: "List meeting events by meeting ID",
|
||||
Risk: "read",
|
||||
Scopes: []string{"vc:meeting.meetingevent:read"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
HasFormat: true,
|
||||
// UAT exposes user-granted scopes, so the framework can preflight the user
|
||||
// recommendation. TAT has no scope metadata; keep the bot recommendation
|
||||
// conditional so it is available to diagnostics without a local preflight.
|
||||
UserScopes: []string{meetingQueryUserScope},
|
||||
ConditionalBotScopes: []string{meetingQueryBotScope},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
HasFormat: true,
|
||||
Flags: []common.Flag{
|
||||
{Name: "meeting-id", Required: true, Desc: "meeting ID to query"},
|
||||
{Name: "start", Desc: "time lower bound (ISO 8601, YYYY-MM-DD, or Unix seconds)"},
|
||||
@@ -101,7 +105,7 @@ var VCMeetingEvents = common.Shortcut{
|
||||
}
|
||||
data, events, hasMore, pageToken, err := fetchMeetingEvents(ctx, runtime, startTime, endTime)
|
||||
if err != nil {
|
||||
return err
|
||||
return normalizeMeetingQueryPermissionError(runtime, err)
|
||||
}
|
||||
events = compactMeetingEvents(events)
|
||||
identity, identityWarning := meetingEventsCurrentIdentity(runtime)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/httpmock"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
@@ -418,6 +420,21 @@ func TestMeetingEvents_Validation_PageAllIgnoresInvalidPageSize(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeetingEvents_UsesUserScopePreflightAndBotScopeHint(t *testing.T) {
|
||||
if got := VCMeetingEvents.ScopesForIdentity("user"); !reflect.DeepEqual(got, []string{meetingQueryUserScope}) {
|
||||
t.Fatalf("ScopesForIdentity(user) = %v, want %v", got, []string{meetingQueryUserScope})
|
||||
}
|
||||
if got := VCMeetingEvents.ScopesForIdentity("bot"); len(got) != 0 {
|
||||
t.Fatalf("ScopesForIdentity(bot) = %v, want no bot preflight scopes", got)
|
||||
}
|
||||
if got := VCMeetingEvents.DeclaredScopesForIdentity("user"); !reflect.DeepEqual(got, []string{meetingQueryUserScope}) {
|
||||
t.Fatalf("DeclaredScopesForIdentity(user) = %v, want %v", got, []string{meetingQueryUserScope})
|
||||
}
|
||||
if got := VCMeetingEvents.DeclaredScopesForIdentity("bot"); !reflect.DeepEqual(got, []string{meetingQueryBotScope}) {
|
||||
t.Fatalf("DeclaredScopesForIdentity(bot) = %v, want %v", got, []string{meetingQueryBotScope})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeetingEvents_Validation_InvalidPageSizeReturnsFlagError(t *testing.T) {
|
||||
runtime := newMeetingEventsRuntime()
|
||||
mustSetMeetingEventsFlag(t, runtime, "meeting-id", "7628568141510692381")
|
||||
@@ -637,6 +654,63 @@ func TestMeetingEvents_ExecuteJSON(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeetingEvents_Execute_NormalizesMeetingScopeError(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: vcMeetingEventsAPIPath,
|
||||
Status: 400,
|
||||
Body: map[string]interface{}{
|
||||
"code": output.LarkErrAppScopeNotEnabled,
|
||||
"msg": "access denied",
|
||||
"error": map[string]interface{}{
|
||||
"permission_violations": []interface{}{
|
||||
map[string]interface{}{"subject": meetingQueryUserScope},
|
||||
map[string]interface{}{"subject": meetingQueryBotScope},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
err := mountAndRun(t, VCMeetingEvents, []string{
|
||||
"+meeting-events",
|
||||
"--meeting-id", "7628568141510692381",
|
||||
"--format", "json",
|
||||
"--as", "bot",
|
||||
}, f, stdout)
|
||||
if err == nil {
|
||||
t.Fatal("expected permission error")
|
||||
}
|
||||
reg.Verify(t)
|
||||
|
||||
var permissionErr *errs.PermissionError
|
||||
if !errors.As(err, &permissionErr) {
|
||||
t.Fatalf("error = %T %v, want *errs.PermissionError", err, err)
|
||||
}
|
||||
if permissionErr.Code != output.LarkErrAppScopeNotEnabled {
|
||||
t.Fatalf("Code = %d, want %d", permissionErr.Code, output.LarkErrAppScopeNotEnabled)
|
||||
}
|
||||
if permissionErr.Identity != "bot" {
|
||||
t.Fatalf("Identity = %q, want bot", permissionErr.Identity)
|
||||
}
|
||||
wantMessage := "access denied for bot identity; recommended scope: " + meetingQueryBotScope
|
||||
if permissionErr.Message != wantMessage {
|
||||
t.Fatalf("Message = %q, want %q", permissionErr.Message, wantMessage)
|
||||
}
|
||||
if !strings.Contains(permissionErr.Hint, meetingQueryBotScope) {
|
||||
t.Fatalf("Hint = %q, want bot scope %q", permissionErr.Hint, meetingQueryBotScope)
|
||||
}
|
||||
if len(permissionErr.MissingScopes) != 1 || permissionErr.MissingScopes[0] != meetingQueryBotScope {
|
||||
t.Fatalf("MissingScopes = %v, want only bot scope %q", permissionErr.MissingScopes, meetingQueryBotScope)
|
||||
}
|
||||
if permissionErr.ConsoleURL == "" {
|
||||
t.Fatal("ConsoleURL is empty, want identity-specific developer-console URL")
|
||||
}
|
||||
if strings.Contains(permissionErr.ConsoleURL, url.QueryEscape(meetingQueryUserScope)) || !strings.Contains(permissionErr.ConsoleURL, url.QueryEscape(meetingQueryBotScope)) {
|
||||
t.Fatalf("ConsoleURL = %q, want only bot scope", permissionErr.ConsoleURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeetingEvents_ExecuteJSON_BotIdentityErrorDoesNotBlockEvents(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
|
||||
reg.Register(meetingEventsStub([]interface{}{participantJoinedEvent()}, false, ""))
|
||||
|
||||
@@ -23,9 +23,13 @@ var VCMeetingListActive = common.Shortcut{
|
||||
Command: "+meeting-list-active",
|
||||
Description: "List active meetings for the current identity or target user",
|
||||
Risk: "read",
|
||||
Scopes: []string{"vc:meeting.meetingevent:read"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
HasFormat: true,
|
||||
// UAT exposes user-granted scopes, so the framework can preflight the user
|
||||
// recommendation. TAT has no scope metadata; keep the bot recommendation
|
||||
// conditional so it is available to diagnostics without a local preflight.
|
||||
UserScopes: []string{meetingQueryUserScope},
|
||||
ConditionalBotScopes: []string{meetingQueryBotScope},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
HasFormat: true,
|
||||
Flags: []common.Flag{
|
||||
{Name: "user-id", Desc: "target user ID when using bot identity"},
|
||||
},
|
||||
@@ -50,7 +54,7 @@ var VCMeetingListActive = common.Shortcut{
|
||||
}
|
||||
data, err := runtime.CallAPITyped(http.MethodGet, vcMeetingListActiveAPIPath, params, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return normalizeMeetingQueryPermissionError(runtime, err)
|
||||
}
|
||||
if data == nil {
|
||||
data = map[string]interface{}{}
|
||||
|
||||
@@ -608,9 +608,18 @@ func TestMeetingListActive_DryRun_UserIdentity(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeetingListActive_ScopeMatchesEventReadPermission(t *testing.T) {
|
||||
if len(VCMeetingListActive.Scopes) != 1 || VCMeetingListActive.Scopes[0] != "vc:meeting.meetingevent:read" {
|
||||
t.Fatalf("scopes = %#v, want [vc:meeting.meetingevent:read]", VCMeetingListActive.Scopes)
|
||||
func TestMeetingListActive_UsesUserScopePreflightAndBotScopeHint(t *testing.T) {
|
||||
if got := VCMeetingListActive.ScopesForIdentity("user"); len(got) != 1 || got[0] != meetingQueryUserScope {
|
||||
t.Fatalf("ScopesForIdentity(user) = %v, want [%s]", got, meetingQueryUserScope)
|
||||
}
|
||||
if got := VCMeetingListActive.ScopesForIdentity("bot"); len(got) != 0 {
|
||||
t.Fatalf("ScopesForIdentity(bot) = %v, want no bot preflight scopes", got)
|
||||
}
|
||||
if got := VCMeetingListActive.DeclaredScopesForIdentity("user"); len(got) != 1 || got[0] != meetingQueryUserScope {
|
||||
t.Fatalf("DeclaredScopesForIdentity(user) = %v, want [%s]", got, meetingQueryUserScope)
|
||||
}
|
||||
if got := VCMeetingListActive.DeclaredScopesForIdentity("bot"); len(got) != 1 || got[0] != meetingQueryBotScope {
|
||||
t.Fatalf("DeclaredScopesForIdentity(bot) = %v, want [%s]", got, meetingQueryBotScope)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,13 @@ metadata:
|
||||
- 如果 `lark-cli` 提示 `missing required scope(s)` / `missing_scopes`,不要走普通权限申请流程;先提示用户加入早鸟群确认内测权限已开通,再按“应用身份权限配置检查”处理应用权限、安装和数据范围。
|
||||
- 如果 `lark-cli` 返回 `error.code=20017` / `ErrNotInGray`,提示用户加入早鸟群:`https://go.larkoffice.com/join-chat/2f4nb0e1-fe00-4f67-bed7-25beaf533fbd`。
|
||||
|
||||
## 会议查询权限
|
||||
|
||||
`+meeting-list-active` 和 `+meeting-events` 缺少权限时,先按上面的内测提示确认功能已开通,再读取 CLI 错误中的 `hint`,并根据当前调用身份处理:
|
||||
|
||||
- 用户身份 `--as user`:按 CLI 提示为当前用户授权 `vc:meeting.meetingevent:read`。
|
||||
- 应用身份 `--as bot`:请应用开发者开通 `vc:meeting.bot.join:write`,不要执行 `auth login`;随后按“应用身份权限配置检查”确认应用发布、安装和数据范围。
|
||||
|
||||
## 定位
|
||||
|
||||
本 skill 与 [`lark-vc`](../lark-vc/SKILL.md) 并列:
|
||||
@@ -172,7 +179,7 @@ Shortcut 是对常用操作的高级封装(`lark-cli vc +<verb> [flags]`)。
|
||||
|
||||
应用身份 `--as bot` 报 `no permission`、`missing required scope(s)`、`missing_scopes`、`ErrNotInGray` 或 `20017` 时,不要引导用户执行 `auth login`。按顺序检查:
|
||||
|
||||
1. 以 CLI 返回的 metadata / error envelope 为准,确认提示的 VC Agent 相关权限已开通。常见读取 active meeting / events 需要会中事件读取权限;应用机器人入会 / 离会需要 bot 入会写权限。
|
||||
1. 确认内测权限后,按 CLI 错误中的 `hint` 处理;返回 `console_url` 时将其原样提供给用户。
|
||||
2. 应用已发布并安装到当前租户。
|
||||
3. 开放平台“权限可访问的数据范围”已开通并保存。
|
||||
4. 数据范围选择“按条件筛选”,条件配置为:**会议的归属者 包含 与应用的可用范围一致**。
|
||||
@@ -180,7 +187,7 @@ Shortcut 是对常用操作的高级封装(`lark-cli vc +<verb> [flags]`)。
|
||||
|
||||
## 用户身份被拒绝时
|
||||
|
||||
用户身份 `--as user` 报权限或身份不支持类错误时,不要反复引导用户执行 `auth login`。先以 CLI 返回的 metadata / error envelope 为准判断:如果错误表明当前接口不支持用户身份访问,再按用户意图切换处理:
|
||||
用户身份 `--as user` 调用 `+meeting-list-active` 或 `+meeting-events` 报普通 scope 缺失时,按“会议查询权限”处理;其他 shortcut 的 scope 缺失按各自 CLI `hint` 处理。普通 scope 缺失不表示接口不支持用户身份,只有 CLI 明确表明当前接口不支持用户身份访问时,才按用户意图切换处理:
|
||||
|
||||
1. 如果用户只是查询当前登录用户所在的进行中会议,说明当前接口链路不支持用户身份访问,改用应用身份流程;需要目标用户 open_id,并要求应用机器人已在会中或先按用户确认执行入会。
|
||||
2. 如果用户明确要求应用机器人入会、旁听、代参会或读取应用机器人可见事件,直接切到 `--as bot`,并按上面的应用身份权限配置检查处理。
|
||||
|
||||
@@ -290,7 +290,7 @@ lark-cli vc +meeting-events \
|
||||
| 用户身份无权限 / 不可见 | 当前用户不是该会议的可见参与者,或 `meeting_id` 不是从用户身份路径获得 | 不要反复执行 `auth login`。先确认 `meeting_id` 是否来自 `+meeting-list-active --as user`;如果用户明确要切到应用身份,再通过 `+meeting-list-active --as bot --user-id <user_open_id>` 获取应用身份可读的 `meeting_id`,或在用户明确同意后让应用机器人入会,再用 `+meeting-events --as bot` 读取 |
|
||||
| `20001 meeting_status_MEETING_END` | 会议已结束且已超出后端允许的 5 分钟宽限窗口 | 本接口不再适合继续拉取事件。先用 `lark-cli vc +detail --meeting-ids <meeting.id>` 获取会议产物信息,再根据 `note_display_type` / `note_id` / `minute_token` 和用户意图选择纪要正文、逐字稿或妙记;参会人请用 `lark-cli vc meeting get --params '{"meeting_id":"<meeting.id>"}' --with-participants` |
|
||||
| `20002 meeting not exist` | `meeting_id` 错误,或会议实例当前已不可获取(常见于把 9 位会议号当 meeting_id 传) | 确认传入的是长数字 `meeting_id`,不是 9 位会议号 |
|
||||
| 应用身份权限不足 | 应用权限、租户安装、权限可访问的数据范围或 VC Agent privilege 未配置完整 | 不要执行 `auth login`。以 CLI 返回的 metadata / error envelope 为准确认缺失权限;检查应用发布/安装,以及开放平台“权限可访问的数据范围”:选择“按条件筛选”,条件为“会议的归属者 包含 与应用的可用范围一致”;仍失败再排查内测 privilege / 灰度 |
|
||||
| 应用身份权限不足 | 应用权限、租户安装、权限可访问的数据范围或 VC Agent privilege 未配置完整 | 不要执行 `auth login`。请应用开发者开通 `vc:meeting.bot.join:write`;再检查应用发布/安装和权限可访问的数据范围,均正确仍失败时再排查内测灰度权限 |
|
||||
| `HTTP 404` / `HTTP 500` | 服务端当前无法找到或处理该会议实例 | 换一个正在进行且 bot 可见的 meeting_id,或排查后端问题 |
|
||||
|
||||
## 提示
|
||||
|
||||
@@ -80,10 +80,10 @@ lark-cli vc +meeting-list-active --as bot --user-id <user_open_id> --format json
|
||||
|---------|---------|---------|
|
||||
| `--user-id is required when --as bot` | 应用身份未传目标用户 | 传入目标用户 open_id |
|
||||
| 用户身份返回空列表 | 当前登录用户没有可见的进行中会议 | 确认用户是否在会中,或是否切错身份 |
|
||||
| 用户身份无权限 / 不可见 | 当前登录用户没有可见的进行中会议,或当前身份无法读取该会议 | 不要反复执行 `auth login`。先确认当前登录用户是否在会中、是否切错 profile;如果用户明确要查询应用机器人可见的会议,再拿目标用户 open_id 执行 `+meeting-list-active --as bot --user-id <user_open_id>`,并按应用身份权限配置检查应用权限、安装、数据范围和灰度 |
|
||||
| 用户身份无权限 / 不可见 | 当前登录用户没有可见的进行中会议,或当前身份无法读取该会议 | 不要反复执行 `auth login`。确认用户是否在会中、是否切错 profile;用户明确要查询应用机器人可见的会议时,再拿目标用户 open_id 执行 `+meeting-list-active --as bot --user-id <user_open_id>` |
|
||||
| 应用身份返回空列表 | 没有满足“目标用户在会中且应用机器人也在会中”的当前会 | 先让应用机器人入会,或确认 `user_id` 和会议状态 |
|
||||
| `--user-id` 格式错误 | 传入了 internal user_id 或其他非 `ou_...` 值 | 改传目标用户 open_id |
|
||||
| 应用身份权限不足 | 应用权限、租户安装、权限可访问的数据范围或 VC Agent privilege 未配置完整 | 不要执行 `auth login`。以 CLI 返回的 metadata / error envelope 为准确认缺失权限;检查应用发布/安装,以及开放平台“权限可访问的数据范围”:选择“按条件筛选”,条件为“会议的归属者 包含 与应用的可用范围一致”;仍失败再排查内测 privilege / 灰度 |
|
||||
| 应用身份权限不足 | 应用权限、租户安装、权限可访问的数据范围或 VC Agent privilege 未配置完整 | 不要执行 `auth login`。请应用开发者开通 `vc:meeting.bot.join:write`;再检查应用发布/安装和权限可访问的数据范围,均正确仍失败时再排查内测灰度权限 |
|
||||
|
||||
## 参考
|
||||
|
||||
|
||||
Reference in New Issue
Block a user