cmd/tailscale: make cert subcommand give hints on access denied

Lot of people have been hitting this.

Now it says:

    $ tailscale cert tsdev.corp.ts.net
    Access denied: cert access denied

    Use 'sudo tailscale cert' or 'tailscale up --operator=$USER' to not require root.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2021-10-01 09:48:18 -07:00 committed by Brad Fitzpatrick
parent 7cf8ec8108
commit f62e6d83a9
2 changed files with 29 additions and 0 deletions

View File

@ -77,6 +77,20 @@ type errorJSON struct {
Error string Error string
} }
// AccessDeniedError is an error due to permissions.
type AccessDeniedError struct {
err error
}
func (e *AccessDeniedError) Error() string { return fmt.Sprintf("Access denied: %v", e.err) }
func (e *AccessDeniedError) Unwrap() error { return e.err }
// IsAccessDeniedError reports whether err is or wraps an AccessDeniedError.
func IsAccessDeniedError(err error) bool {
var ae *AccessDeniedError
return errors.As(err, &ae)
}
// bestError returns either err, or if body contains a valid JSON // bestError returns either err, or if body contains a valid JSON
// object of type errorJSON, its non-empty error body. // object of type errorJSON, its non-empty error body.
func bestError(err error, body []byte) error { func bestError(err error, body []byte) error {
@ -87,6 +101,14 @@ func bestError(err error, body []byte) error {
return err return err
} }
func errorMessageFromBody(body []byte) string {
var j errorJSON
if err := json.Unmarshal(body, &j); err == nil && j.Error != "" {
return j.Error
}
return strings.TrimSpace(string(body))
}
var onVersionMismatch func(clientVer, serverVer string) var onVersionMismatch func(clientVer, serverVer string)
// SetVersionMismatchHandler sets f as the version mismatch handler // SetVersionMismatchHandler sets f as the version mismatch handler
@ -123,6 +145,9 @@ func send(ctx context.Context, method, path string, wantStatus int, body io.Read
return nil, err return nil, err
} }
if res.StatusCode != wantStatus { if res.StatusCode != wantStatus {
if res.StatusCode == 403 {
return nil, &AccessDeniedError{errors.New(errorMessageFromBody(slurp))}
}
err := fmt.Errorf("HTTP %s: %s (expected %v)", res.Status, slurp, wantStatus) err := fmt.Errorf("HTTP %s: %s (expected %v)", res.Status, slurp, wantStatus)
return nil, bestError(err, slurp) return nil, bestError(err, slurp)
} }

View File

@ -13,6 +13,7 @@
"log" "log"
"net/http" "net/http"
"os" "os"
"runtime"
"strings" "strings"
"github.com/peterbourgon/ff/v3/ffcli" "github.com/peterbourgon/ff/v3/ffcli"
@ -85,6 +86,9 @@ func runCert(ctx context.Context, args []string) error {
certArgs.keyFile = domain + ".key" certArgs.keyFile = domain + ".key"
} }
certPEM, keyPEM, err := tailscale.CertPair(ctx, domain) certPEM, keyPEM, err := tailscale.CertPair(ctx, domain)
if tailscale.IsAccessDeniedError(err) && os.Getuid() != 0 && runtime.GOOS != "windows" {
return fmt.Errorf("%v\n\nUse 'sudo tailscale cert' or 'tailscale up --operator=$USER' to not require root.", err)
}
if err != nil { if err != nil {
return err return err
} }