Skip to content

Commit

Permalink
Pass configuration when enrolling a provider with a token (#3388)
Browse files Browse the repository at this point in the history
* Use providerClass, not provider name when validating token

This will make reusing the method later easier across providers as well
as allows us to enroll providers whose name is not github and verify
their tokens.

* Enroll providers with token and config

This allows to enroll providers and pass a config at the same time.

For example:
```
echo '{"github": {"one": 1}}' | ./bin/minder provider enroll -c github -n my-little-github -o jakubtestorg -t ghp_secret --config - --yes
```
  • Loading branch information
jhrozek authored May 21, 2024
1 parent a771115 commit b4ef6ec
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 9 deletions.
89 changes: 84 additions & 5 deletions cmd/cli/app/provider/provider_enroll.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,25 @@ package provider

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"slices"
"time"

"github.com/pkg/browser"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/structpb"

"github.com/stacklok/minder/internal/db"
"github.com/stacklok/minder/internal/providers"
"github.com/stacklok/minder/internal/util"
"github.com/stacklok/minder/internal/util/cli"
"github.com/stacklok/minder/internal/util/rand"
minderv1 "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1"
Expand Down Expand Up @@ -67,11 +76,16 @@ func EnrollProviderCommand(ctx context.Context, cmd *cobra.Command, _ []string,
if provider == "" {
provider = viper.GetString("class")
}
providerName := viper.GetString("name")
if providerName == "" {
providerName = provider
}
project := viper.GetString("project")
token := viper.GetString("token")
owner := viper.GetString("owner")
yesFlag := viper.GetBool("yes")
skipBrowser := viper.GetBool("skip-browser")
cfgFlag := viper.GetString("config")

// No longer print usage on returned error, since we've parsed our inputs
// See https://github.com/spf13/cobra/issues/340#issuecomment-374617413
Expand All @@ -98,9 +112,20 @@ func EnrollProviderCommand(ctx context.Context, cmd *cobra.Command, _ []string,
}
}

config, err := providerConfigFromArg(cfgFlag, cmd.InOrStdin())
if err != nil {
return cli.MessageAndError("Error reading provider configuration", err)
}

// the token only applies to the old flow
if token != "" && provider == legacyGitHubProvider.ToString() {
return enrollUsingToken(ctx, cmd, oauthClient, provider, project, token, owner)
// TODO: allow for token to be passed in if the provider allows it, don't hardcode
userFlow, err := supportsToken(provider)
if err != nil {
return cli.MessageAndError("Error checking provider support", err)
}

if token != "" && userFlow {
return enrollUsingToken(ctx, cmd, oauthClient, providerClient, providerName, provider, project, token, owner, config)
}

// This will have a different timeout
Expand All @@ -113,13 +138,35 @@ func enrollUsingToken(
ctx context.Context,
cmd *cobra.Command,
client minderv1.OAuthServiceClient,
provider string,
provClient minderv1.ProvidersServiceClient,
providerName string,
providerClass string,
project string,
token string,
owner string,
providerConfig *structpb.Struct,
) error {
_, err := client.StoreProviderToken(ctx, &minderv1.StoreProviderTokenRequest{
Context: &minderv1.Context{Provider: &provider, Project: &project},
_, err := provClient.CreateProvider(ctx, &minderv1.CreateProviderRequest{
Context: &minderv1.Context{Provider: &providerName, Project: &project},
Provider: &minderv1.Provider{
Name: providerName,
Class: providerClass,
Config: providerConfig,
},
})
st, ok := status.FromError(err)
if !ok {
// We can't parse the error, so just return it
return err
}

if st.Code() != codes.AlreadyExists {
return err
}

// the provider already exists, turn this call into an update of the token
_, err = client.StoreProviderToken(ctx, &minderv1.StoreProviderTokenRequest{
Context: &minderv1.Context{Provider: &providerName, Project: &project},
AccessToken: token,
Owner: &owner,
})
Expand Down Expand Up @@ -284,13 +331,45 @@ func callBackServer(ctx context.Context, cmd *cobra.Command, project string, por
}
}

func providerConfigFromArg(configSource string, dashReader io.Reader) (*structpb.Struct, error) {
if configSource == "" {
return nil, nil
}

reader, closer, err := util.OpenFileArg(configSource, dashReader)
if err != nil {
return nil, fmt.Errorf("error opening file arg: %w", err)
}
defer closer()

var config map[string]any

// TODO: handle YAML and JSON
err = json.NewDecoder(reader).Decode(&config)
if err != nil {
return nil, fmt.Errorf("error parsing provider configuration: %w", err)
}

return structpb.NewStruct(config)
}

func supportsToken(providerClass string) (bool, error) {
providerDef, err := providers.GetProviderClassDefinition(providerClass)
if err != nil {
return false, err
}
return slices.Contains(providerDef.AuthorizationFlows, db.AuthorizationFlowUserInput), nil
}

func init() {
ProviderCmd.AddCommand(enrollCmd)
// Flags
enrollCmd.Flags().StringP("token", "t", "", "Personal Access Token (PAT) to use for enrollment (Legacy GitHub only)")
enrollCmd.Flags().StringP("owner", "o", "", "Owner to filter on for provider resources (Legacy GitHub only)")
enrollCmd.Flags().BoolP("yes", "y", false, "Bypass any yes/no prompts when enrolling a new provider")
enrollCmd.Flags().StringP("class", "c", githubAppProvider.ToString(), "Provider class, defaults to github-app")
enrollCmd.Flags().StringP("config", "f", "", "Path to the provider configuration (or - for stdin)")
enrollCmd.Flags().StringP("name", "n", "", "Name of the new provider. (Only when using a token)")

// Bind flags
if err := viper.BindPFlag("token", enrollCmd.Flags().Lookup("token")); err != nil {
Expand Down
6 changes: 4 additions & 2 deletions internal/auth/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"golang.org/x/oauth2/google"

"github.com/stacklok/minder/internal/config"
"github.com/stacklok/minder/internal/db"
)

const (
Expand Down Expand Up @@ -149,8 +150,9 @@ func DeleteAccessToken(ctx context.Context, provider string, token string) error
}

// ValidateProviderToken validates the given token for the given provider
func ValidateProviderToken(_ context.Context, provider string, token string) error {
if provider == Github {
func ValidateProviderToken(_ context.Context, provider db.ProviderClass, token string) error {
// Fixme: this should really be handled by the provider. Should this be in the credentials API or the manager?
if provider == db.ProviderClassGithub {
// Create an OAuth2 token source with the PAT
tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})

Expand Down
4 changes: 2 additions & 2 deletions internal/controlplane/handlers_oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -465,9 +465,9 @@ func (s *Server) StoreProviderToken(ctx context.Context,
}

// validate token
err = auth.ValidateProviderToken(ctx, provider.Name, in.AccessToken)
err = auth.ValidateProviderToken(ctx, provider.Class, in.AccessToken)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid token provided")
return nil, status.Errorf(codes.InvalidArgument, "invalid token provided: %v", err)
}

ftoken := &oauth2.Token{
Expand Down

0 comments on commit b4ef6ec

Please sign in to comment.