Files
larksuite-cli/internal/auth/errors.go
zhaoyukun.yk aeaed0884e feat(extension): present restricted commands as absent and trim skills
Integrators that build their own lark-cli distribution can now ship a
package where everything they cut out truly disappears, instead of
lingering as half-disabled stubs that mislead users and agents.

Commands an integrator plugin restricts no longer show up in help or
completion, and invoking one (including asking for its help) answers
"command not included in this build" (subtype command_unavailable) with
no policy vocabulary and no dead-end recovery steering. Integrators can
replace that message with their own product wording via
Rule.DeniedMessage. Cutting a whole domain also retires everything that
points at it: --profile hides and rejects use when the profile domain
goes, the skills-setup footer, update notices, and the auth-login
recovery hint stop rendering when their domains go.

The policy self-inspection commands (config policy show / plugins show)
stay executable by default so operators can still see which rule locked
the build, hidden from help but available on request. Fully-managed
distributions can retire even those with HideDiagnostics().

Plugins can also trim the embedded skills to match: EmbeddedSkills
removes, overlays, or replaces entries over the CLI's built-in set, and
skills list/read plus every help pointer serve the same trimmed tree.

Users who write their own ~/.lark-cli/policy.yml see no change: their
denials stay visible and keep explaining how to adjust the policy they
own. Default builds are byte-for-byte unchanged.
2026-07-11 20:24:01 +08:00

73 lines
2.6 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package auth
import (
"errors"
"fmt"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/policystate"
)
const (
needUserAuthorizationMarker = "need_user_authorization"
)
// TokenRetryCodes contains error codes that allow retry after token refresh.
var TokenRetryCodes = map[int]bool{
output.LarkErrTokenInvalid: true,
output.LarkErrTokenExpired: true,
}
// NeedAuthorizationError is the sentinel preserved in the Cause chain of the
// typed missing-UAT error so existing errors.As(&NeedAuthorizationError{})
// consumers keep matching after the construction site moved to the typed
// taxonomy. It is never surfaced on the wire on its own.
type NeedAuthorizationError struct {
UserOpenId string
}
// Error returns the error message for NeedAuthorizationError.
func (e *NeedAuthorizationError) Error() string {
return fmt.Sprintf("%s (user: %s)", needUserAuthorizationMarker, e.UserOpenId)
}
// NewNeedUserAuthorizationError builds the typed *errs.AuthenticationError
// returned when no valid UAT exists for userOpenID. The Message keeps the
// need_user_authorization marker, the Hint converges on the same auth-login
// recovery vocabulary as the token-missing surface in internal/client, and the
// legacy *NeedAuthorizationError sentinel is preserved in the Cause chain for
// errors.As / errors.Is traversal.
func NewNeedUserAuthorizationError(userOpenID string) *errs.AuthenticationError {
e := errs.NewAuthenticationError(errs.SubtypeTokenMissing,
"%s (user: %s)", needUserAuthorizationMarker, userOpenID).
WithUserOpenID(userOpenID).
WithCause(&NeedAuthorizationError{UserOpenId: userOpenID})
// No recovery hint when the auth domain is absent from this build.
if !policystate.DomainDeniedByPlugin("auth") {
e = e.WithHint("run: lark-cli auth login to re-authorize")
}
return e
}
// IsNeedUserAuthorizationError reports whether err represents a missing-UAT
// failure. It matches the legacy *NeedAuthorizationError sentinel, which is
// preserved in the Cause chain of the typed missing-UAT error, so errors.As
// traverses into the typed *errs.AuthenticationError as well.
func IsNeedUserAuthorizationError(err error) bool {
if err == nil {
return false
}
var needAuthErr *NeedAuthorizationError
return errors.As(err, &needAuthErr)
}
// SecurityPolicyError is preserved as a Go type alias so existing
// errors.As(&SecurityPolicyError{}) consumers (cmd/root.go etc.) keep working.
// The concrete struct lives in errs/types.go.
type SecurityPolicyError = errs.SecurityPolicyError