mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
fix(cloud): actionable errors when sync credentials are missing or rejected (#388)
`pmg cloud sync` without usable credentials surfaced misleading errors:
missing credentials fell into the generic network fallback ("check your
network connectivity") and server-rejected credentials (gateway 403)
showed a bare "Permission denied" with no remediation.
Map dry's cloud.ErrMissingCredentials to a new CloudCredentialsNotFound
code pointing at 'pmg cloud login' and the SAFEDEP_API_KEY /
SAFEDEP_TENANT_ID environment variables, and rebrand authentication and
authorization failures with the same credential guidance. Entitlement,
quota and server errors still pass through unchanged.
This commit is contained in:
+31
-5
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/safedep/dry/cloud"
|
||||
"github.com/safedep/dry/usefulerror"
|
||||
"github.com/safedep/pmg/config"
|
||||
"github.com/safedep/pmg/errcodes"
|
||||
@@ -52,10 +53,10 @@ func runSync(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
// syncFailureError maps a DrainToCloud failure to a user-facing error. Errors
|
||||
// the backend already classified (authentication, entitlements, quota — via
|
||||
// gRPC status and usefulerror converters) pass through so the real cause is
|
||||
// shown; the network-flavored message is only a fallback for errors nothing
|
||||
// can classify.
|
||||
// the backend already classified (entitlements, quota — via gRPC status and
|
||||
// usefulerror converters) pass through so the real cause is shown, except
|
||||
// auth failures which get credential setup guidance; the network-flavored
|
||||
// message is only a fallback for errors nothing can classify.
|
||||
func syncFailureError(err error) error {
|
||||
if errors.Is(err, audit.ErrSyncInProgress) {
|
||||
return usefulerror.NewUsefulError().
|
||||
@@ -64,8 +65,16 @@ func syncFailureError(err error) error {
|
||||
WithHelp("Wait for the in-progress sync to finish, then try again")
|
||||
}
|
||||
|
||||
if errors.Is(err, cloud.ErrMissingCredentials) {
|
||||
return usefulerror.NewUsefulError().
|
||||
Wrap(err).
|
||||
WithCode(errcodes.CloudCredentialsNotFound).
|
||||
WithHumanError("SafeDep Cloud credentials are not configured").
|
||||
WithHelp("Run 'pmg cloud login' to store credentials, or set the SAFEDEP_API_KEY and SAFEDEP_TENANT_ID environment variables")
|
||||
}
|
||||
|
||||
if usefulErr, ok := usefulerror.AsUsefulError(err); ok {
|
||||
return usefulErr
|
||||
return withCredentialGuidance(usefulErr, err)
|
||||
}
|
||||
|
||||
return usefulerror.NewUsefulError().
|
||||
@@ -75,3 +84,20 @@ func syncFailureError(err error) error {
|
||||
WithHelp("Check your network connectivity and ensure SafeDep Cloud is reachable").
|
||||
WithAdditionalHelp("Override the cloud endpoint with SAFEDEP_CLOUD_DATA_ADDR if needed")
|
||||
}
|
||||
|
||||
// withCredentialGuidance rebrands generic auth failures (invalid API key,
|
||||
// wrong tenant, gateway 401/403) with how to fix credentials in pmg. Richer
|
||||
// classifications like missing entitlements keep their own code and help.
|
||||
func withCredentialGuidance(usefulErr usefulerror.UsefulError, err error) error {
|
||||
code := usefulErr.Code()
|
||||
if code != usefulerror.ErrAuthenticationFailed && code != usefulerror.ErrAuthorizationFailed {
|
||||
return usefulErr
|
||||
}
|
||||
|
||||
return usefulerror.NewUsefulError().
|
||||
Wrap(err).
|
||||
WithCode(code).
|
||||
WithHumanError("SafeDep Cloud rejected your credentials").
|
||||
WithHelp("Run 'pmg cloud login' to update credentials, or check the SAFEDEP_API_KEY and SAFEDEP_TENANT_ID environment variables").
|
||||
WithAdditionalHelp(usefulErr.AdditionalHelp())
|
||||
}
|
||||
|
||||
+19
-4
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/safedep/dry/cloud"
|
||||
"github.com/safedep/dry/usefulerror"
|
||||
"github.com/safedep/pmg/errcodes"
|
||||
"github.com/safedep/pmg/internal/audit"
|
||||
@@ -50,6 +51,7 @@ func TestSyncFailureError(t *testing.T) {
|
||||
err error
|
||||
expectedCode string
|
||||
expectedHuman string
|
||||
expectedHelp string
|
||||
}{
|
||||
{
|
||||
name: "sync already in progress",
|
||||
@@ -58,16 +60,25 @@ func TestSyncFailureError(t *testing.T) {
|
||||
expectedHuman: "Another cloud sync is already in progress",
|
||||
},
|
||||
{
|
||||
name: "authentication failure passes through",
|
||||
name: "missing credentials maps to setup guidance",
|
||||
err: fmt.Errorf("init cloud sync client: failed to resolve cloud credentials: %w", cloud.ErrMissingCredentials),
|
||||
expectedCode: errcodes.CloudCredentialsNotFound,
|
||||
expectedHuman: "SafeDep Cloud credentials are not configured",
|
||||
expectedHelp: "Run 'pmg cloud login' to store credentials, or set the SAFEDEP_API_KEY and SAFEDEP_TENANT_ID environment variables",
|
||||
},
|
||||
{
|
||||
name: "authentication failure gets credential guidance",
|
||||
err: wrap(status.Error(codes.Unauthenticated, "invalid API key")),
|
||||
expectedCode: usefulerror.ErrAuthenticationFailed,
|
||||
expectedHuman: "Authentication failed",
|
||||
expectedHuman: "SafeDep Cloud rejected your credentials",
|
||||
expectedHelp: "Run 'pmg cloud login' to update credentials, or check the SAFEDEP_API_KEY and SAFEDEP_TENANT_ID environment variables",
|
||||
},
|
||||
{
|
||||
name: "entitlement failure passes through",
|
||||
err: wrap(entitlementStatusErr(t, 0)),
|
||||
expectedCode: usefulerror.ErrMissingEntitlements,
|
||||
expectedHuman: "Permission denied",
|
||||
expectedHelp: "Access to this feature requires a SafeDep subscription. See https://safedep.io/pricing",
|
||||
},
|
||||
{
|
||||
name: "entitlement failure with re-wrapped details passes through",
|
||||
@@ -78,10 +89,11 @@ func TestSyncFailureError(t *testing.T) {
|
||||
expectedHuman: "Permission denied",
|
||||
},
|
||||
{
|
||||
name: "permission denied without entitlement detail",
|
||||
name: "permission denied without entitlement detail gets credential guidance",
|
||||
err: wrap(status.Error(codes.PermissionDenied, "no access")),
|
||||
expectedCode: usefulerror.ErrAuthorizationFailed,
|
||||
expectedHuman: "Permission denied",
|
||||
expectedHuman: "SafeDep Cloud rejected your credentials",
|
||||
expectedHelp: "Run 'pmg cloud login' to update credentials, or check the SAFEDEP_API_KEY and SAFEDEP_TENANT_ID environment variables",
|
||||
},
|
||||
{
|
||||
name: "server internal error passes through",
|
||||
@@ -107,6 +119,9 @@ func TestSyncFailureError(t *testing.T) {
|
||||
|
||||
assert.Equal(t, tt.expectedCode, usefulErr.Code())
|
||||
assert.Equal(t, tt.expectedHuman, usefulErr.HumanError())
|
||||
if tt.expectedHelp != "" {
|
||||
assert.Equal(t, tt.expectedHelp, usefulErr.Help())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,11 @@ const (
|
||||
// suspicious package) and the run was gated with --fail-on-violation.
|
||||
ProxyPolicyViolation = "ProxyPolicyViolation"
|
||||
|
||||
// Cloud error codes. CloudCredentialsNotFound is returned when a cloud
|
||||
// operation needs SafeDep Cloud credentials but none are configured in the
|
||||
// keychain or environment.
|
||||
CloudCredentialsNotFound = "CloudCredentialsNotFound"
|
||||
|
||||
// Unknown mirrors the default code that dry/usefulerror returns for errors
|
||||
// created without an explicit code, so unset and explicitly-unknown errors
|
||||
// classify identically (e.g. the bug-report hint in ui.ErrorExit).
|
||||
|
||||
Reference in New Issue
Block a user