diff --git a/CHANGELOG.md b/CHANGELOG.md index ddbdc67d3b..0a0ded5ba5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,8 @@ ## [Unreleased] -- No changes yet. +- Add `buf registry whoami` command, which checks if you are logged in to the Buf Schema + Registry at a given domain. ## [v1.45.0] - 2024-10-08 diff --git a/private/buf/bufprint/bufprint.go b/private/buf/bufprint/bufprint.go index 1647a41f93..75b95b573a 100644 --- a/private/buf/bufprint/bufprint.go +++ b/private/buf/bufprint/bufprint.go @@ -248,6 +248,15 @@ func NewOrganizationEntity(organization *ownerv1.Organization, remote string) En } } +// NewUserEntity returns a new user entity to print. +func NewUserEntity(user *registryv1alpha1.User) Entity { + return outputUser{ + Username: user.Username, + // We use the Username as the full name for the user when printing. + FullName: user.Username, + } +} + // CuratedPluginPrinter is a printer for curated plugins. type CuratedPluginPrinter interface { PrintCuratedPlugin(ctx context.Context, format Format, plugin *registryv1alpha1.CuratedPlugin) error @@ -456,3 +465,12 @@ type outputOrganization struct { func (o outputOrganization) fullName() string { return o.FullName } + +type outputUser struct { + Username string `json:"username,omitempty"` + FullName string `json:"-" bufprint:"Name"` +} + +func (o outputUser) fullName() string { + return o.FullName +} diff --git a/private/buf/cmd/buf/buf.go b/private/buf/cmd/buf/buf.go index 1793958ce2..277c88d653 100644 --- a/private/buf/cmd/buf/buf.go +++ b/private/buf/cmd/buf/buf.go @@ -85,6 +85,7 @@ import ( "github.com/bufbuild/buf/private/buf/cmd/buf/command/registry/registrylogin" "github.com/bufbuild/buf/private/buf/cmd/buf/command/registry/registrylogout" "github.com/bufbuild/buf/private/buf/cmd/buf/command/registry/sdk/version" + "github.com/bufbuild/buf/private/buf/cmd/buf/command/registry/whoami" "github.com/bufbuild/buf/private/bufpkg/bufcobra" "github.com/bufbuild/buf/private/bufpkg/bufconnect" "github.com/bufbuild/buf/private/bufpkg/bufmodule" @@ -176,6 +177,7 @@ func NewRootCommand(name string) *appcmd.Command { SubCommands: []*appcmd.Command{ registrylogin.NewCommand("login", builder), registrylogout.NewCommand("logout", builder), + whoami.NewCommand("whoami", builder), registrycc.NewCommand("cc", builder, ``, false), { Use: "commit", diff --git a/private/buf/cmd/buf/command/registry/registrylogin/registrylogin.go b/private/buf/cmd/buf/command/registry/registrylogin/registrylogin.go index 550c5016aa..184b98f03e 100644 --- a/private/buf/cmd/buf/command/registry/registrylogin/registrylogin.go +++ b/private/buf/cmd/buf/command/registry/registrylogin/registrylogin.go @@ -34,6 +34,7 @@ import ( "github.com/bufbuild/buf/private/pkg/netext" "github.com/bufbuild/buf/private/pkg/netrc" "github.com/bufbuild/buf/private/pkg/oauth2" + "github.com/bufbuild/buf/private/pkg/syserror" "github.com/bufbuild/buf/private/pkg/transport/http/httpclient" "github.com/pkg/browser" "github.com/spf13/pflag" @@ -210,7 +211,7 @@ func inner( } user := resp.Msg.User if user == nil { - return errors.New("no user found for provided token") + return syserror.New("no user found for registry login token") } if err := netrc.PutMachines( container, diff --git a/private/buf/cmd/buf/command/registry/whoami/usage.gen.go b/private/buf/cmd/buf/command/registry/whoami/usage.gen.go new file mode 100644 index 0000000000..d260922272 --- /dev/null +++ b/private/buf/cmd/buf/command/registry/whoami/usage.gen.go @@ -0,0 +1,19 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Generated. DO NOT EDIT. + +package whoami + +import _ "github.com/bufbuild/buf/private/usage" diff --git a/private/buf/cmd/buf/command/registry/whoami/whoami.go b/private/buf/cmd/buf/command/registry/whoami/whoami.go new file mode 100644 index 0000000000..9d2bbec32b --- /dev/null +++ b/private/buf/cmd/buf/command/registry/whoami/whoami.go @@ -0,0 +1,139 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package whoami + +import ( + "context" + "errors" + "fmt" + + "connectrpc.com/connect" + "github.com/bufbuild/buf/private/buf/bufcli" + "github.com/bufbuild/buf/private/buf/bufprint" + "github.com/bufbuild/buf/private/bufpkg/bufconnect" + "github.com/bufbuild/buf/private/gen/proto/connect/buf/alpha/registry/v1alpha1/registryv1alpha1connect" + registryv1alpha1 "github.com/bufbuild/buf/private/gen/proto/go/buf/alpha/registry/v1alpha1" + "github.com/bufbuild/buf/private/pkg/app/appcmd" + "github.com/bufbuild/buf/private/pkg/app/appext" + "github.com/bufbuild/buf/private/pkg/connectclient" + "github.com/bufbuild/buf/private/pkg/netext" + "github.com/spf13/pflag" +) + +const ( + formatFlagName = "format" + + loginCommand = "buf registry login" +) + +// NewCommand returns a new Command. +func NewCommand( + name string, + builder appext.SubCommandBuilder, +) *appcmd.Command { + flags := newFlags() + return &appcmd.Command{ + Use: name + " ", + Short: `Check if you are logged in to the Buf Schema Registry`, + Long: `This command checks if you are currently logged into the Buf Schema Registry at the provided . +The argument will default to buf.build if not specified.`, + Args: appcmd.MaximumNArgs(1), + Run: builder.NewRunFunc( + func(ctx context.Context, container appext.Container) error { + return run(ctx, container, flags) + }, + ), + BindFlags: flags.Bind, + } +} + +type flags struct { + Format string +} + +func newFlags() *flags { + return &flags{} +} + +func (f *flags) Bind(flagSet *pflag.FlagSet) { + flagSet.StringVar( + &f.Format, + formatFlagName, + bufprint.FormatText.String(), + fmt.Sprintf(`The output format to use. Must be one of %s`, bufprint.AllFormatsString), + ) +} + +func run( + ctx context.Context, + container appext.Container, + flags *flags, +) error { + remote := bufconnect.DefaultRemote + if container.NumArgs() == 1 { + remote = container.Arg(0) + if _, err := netext.ValidateHostname(remote); err != nil { + return err + } + } + clientConfig, err := bufcli.NewConnectClientConfig(container) + if err != nil { + return err + } + authnService := connectclient.Make(clientConfig, remote, registryv1alpha1connect.NewAuthnServiceClient) + currentUserResponse, err := authnService.GetCurrentUser(ctx, connect.NewRequest(®istryv1alpha1.GetCurrentUserRequest{})) + if err != nil { + if connectErr := new(connect.Error); errors.As(err, &connectErr) && connectErr.Code() == connect.CodeUnauthenticated { + return fmt.Errorf("Not currently logged in for %s.", remote) + } + return err + } + user := currentUserResponse.Msg.User + if user == nil { + return fmt.Errorf( + `No user is logged in to %s. Run %q to refresh your credentials. If you have the %s environment variable set, ensure that the token is valid.`, + remote, + loginCommandForRemote(remote), + bufconnect.TokenEnvKey, + ) + } + format, err := bufprint.ParseFormat(flags.Format) + if err != nil { + return appcmd.WrapInvalidArgumentError(err) + } + // ParseFormat always expects a format that is either text or json, otherwise it returns + // an error, so do not need a default case for this switch. + switch format { + case bufprint.FormatText: + _, err = fmt.Fprintf(container.Stdout(), "Logged in as %s.\n", user.Username) + return err + case bufprint.FormatJSON: + return bufprint.PrintEntity( + container.Stdout(), + format, + bufprint.NewUserEntity(user), + ) + } + return nil +} + +// loginCommandForRemote returns the login command for the given remote, +// the default remote is excluded in the command. +func loginCommandForRemote(remote string) string { + if remote == bufconnect.DefaultRemote { + return loginCommand + } + return fmt.Sprintf("%s %s", loginCommand, remote) +}