From 0e831204283e8915a352ecee7dea0e52c54cd9bb Mon Sep 17 00:00:00 2001 From: ryanfkeepers Date: Wed, 1 Nov 2023 11:20:50 -0600 Subject: [PATCH] basic auth delegated token poc --- src/cli/cli.go | 29 ++++++++---- src/cli/config/account.go | 15 +++--- src/cmd/delegated/delegated.go | 73 +++++++++++++++++++++++++++++ src/pkg/account/m365.go | 6 +++ src/pkg/credentials/m365.go | 8 ++++ src/pkg/services/m365/api/access.go | 66 +++++++++++++++++++++++++- src/pkg/services/m365/api/client.go | 3 ++ 7 files changed, 181 insertions(+), 19 deletions(-) create mode 100644 src/cmd/delegated/delegated.go diff --git a/src/cli/cli.go b/src/cli/cli.go index 9b6eae05c..e4277967f 100644 --- a/src/cli/cli.go +++ b/src/cli/cli.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/spf13/cobra" + "go.uber.org/zap" "golang.org/x/exp/slices" "github.com/alcionai/corso/src/cli/backup" @@ -33,10 +34,10 @@ var corsoCmd = &cobra.Command{ Short: "Free, Secure, Open-Source Backup for M365.", Long: `Free, Secure, and Open-Source Backup for Microsoft 365.`, RunE: handleCorsoCmd, - PersistentPreRunE: preRun, + PersistentPreRunE: PreRun, } -func preRun(cc *cobra.Command, args []string) error { +func PreRun(cc *cobra.Command, args []string) error { if err := config.InitFunc(cc, args); err != nil { return err } @@ -104,6 +105,13 @@ func CorsoCommand() *cobra.Command { return c } +func AddSupportFlags(cmd *cobra.Command) { + config.AddConfigFlags(cmd) + logger.AddLoggingFlags(cmd) + observe.AddProgressBarFlags(cmd) + print.AddOutputFlag(cmd) +} + // BuildCommandTree builds out the command tree used by the Corso library. func BuildCommandTree(cmd *cobra.Command) { // want to order flags explicitly @@ -111,11 +119,8 @@ func BuildCommandTree(cmd *cobra.Command) { flags.AddRunModeFlag(cmd, true) cmd.Flags().BoolP("version", "v", false, "current version info") - cmd.PersistentPreRunE = preRun - config.AddConfigFlags(cmd) - logger.AddLoggingFlags(cmd) - observe.AddProgressBarFlags(cmd) - print.AddOutputFlag(cmd) + cmd.PersistentPreRunE = PreRun + AddSupportFlags(cmd) flags.AddGlobalOperationFlags(cmd) cmd.SetUsageTemplate(indentExamplesTemplate(corsoCmd.UsageTemplate())) @@ -132,14 +137,20 @@ func BuildCommandTree(cmd *cobra.Command) { // Running Corso // ------------------------------------------------------------------------------------------ -// Handle builds and executes the cli processor. -func Handle() { +func SeedCtx() (context.Context, *zap.SugaredLogger) { //nolint:forbidigo ctx := config.Seed(context.Background()) ctx, log := logger.Seed(ctx, logger.PreloadLoggingFlags(os.Args[1:])) ctx = print.SetRootCmd(ctx, corsoCmd) ctx = observe.SeedObserver(ctx, print.StderrWriter(ctx), observe.PreloadFlags()) + return ctx, log +} + +// Handle builds and executes the cli processor. +func Handle() { + ctx, log := SeedCtx() + BuildCommandTree(corsoCmd) defer func() { diff --git a/src/cli/config/account.go b/src/cli/config/account.go index 22a481b57..7f5703ba1 100644 --- a/src/cli/config/account.go +++ b/src/cli/config/account.go @@ -98,17 +98,16 @@ func configureAccount( // M365 is a helper for aggregating m365 secrets and credentials. func GetM365(m365Cfg account.M365Config) credentials.M365 { - AzureClientID := str.First( + creds := credentials.GetM365() + + creds.AzureClientID = str.First( flags.AzureClientIDFV, - os.Getenv(credentials.AzureClientID), + creds.AzureClientID, m365Cfg.AzureClientID) - AzureClientSecret := str.First( + creds.AzureClientSecret = str.First( flags.AzureClientSecretFV, - os.Getenv(credentials.AzureClientSecret), + creds.AzureClientSecret, m365Cfg.AzureClientSecret) - return credentials.M365{ - AzureClientID: AzureClientID, - AzureClientSecret: AzureClientSecret, - } + return creds } diff --git a/src/cmd/delegated/delegated.go b/src/cmd/delegated/delegated.go new file mode 100644 index 000000000..c44c5c622 --- /dev/null +++ b/src/cmd/delegated/delegated.go @@ -0,0 +1,73 @@ +package main + +import ( + "os" + + "github.com/spf13/cobra" + + "github.com/alcionai/corso/src/cli" + . "github.com/alcionai/corso/src/cli/print" + "github.com/alcionai/corso/src/cli/utils" + "github.com/alcionai/corso/src/internal/observe" + "github.com/alcionai/corso/src/pkg/count" + "github.com/alcionai/corso/src/pkg/path" + "github.com/alcionai/corso/src/pkg/services/m365/api" +) + +// The root-level command. +// `corso [] [] [...]` +var cmd = &cobra.Command{ + Use: "delegated", + Short: "delegated token POC", + RunE: getToken, + PersistentPreRunE: cli.PreRun, +} + +func main() { + cli.AddSupportFlags(cmd) + ctx, log := cli.SeedCtx() + + defer func() { + observe.Flush(ctx) // flush the progress bars + + _ = log.Sync() // flush all logs in the buffer + }() + + if err := cmd.ExecuteContext(ctx); err != nil { + os.Exit(1) + } +} + +func getToken(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + _, details, err := utils.GetAccountAndConnect( + ctx, + cmd, + path.ExchangeService) + if err != nil { + return Only(ctx, err) + } + + creds, err := details.Repo.Account.M365Config() + if err != nil { + return Only(ctx, err) + } + + ac, err := api.NewClient( + creds, + details.Opts, + count.New()) + if err != nil { + return Only(ctx, err) + } + + da, err := ac.Access().GetDelegatedToken(ctx) + if err != nil { + return Only(ctx, err) + } + + PrettyJSON(ctx, da) + + return nil +} diff --git a/src/pkg/account/m365.go b/src/pkg/account/m365.go index 38d6efc88..53ceb8935 100644 --- a/src/pkg/account/m365.go +++ b/src/pkg/account/m365.go @@ -21,6 +21,8 @@ const ( keyAzureClientID = "azure_clientid" keyAzureClientSecret = "azure_clientSecret" keyAzureTenantID = "azure_tenantid" + keyAzureUsername = "azure_username" + keyAzureUserPassword = "azure_userPassword" ) // StringConfig transforms a m365Config struct into a plain @@ -31,6 +33,8 @@ func (c M365Config) StringConfig() (map[string]string, error) { keyAzureClientID: c.AzureClientID, keyAzureClientSecret: c.AzureClientSecret, keyAzureTenantID: c.AzureTenantID, + keyAzureUsername: c.AzureUsername, + keyAzureUserPassword: c.AzureUserPassword, } return cfg, c.validate() @@ -52,6 +56,8 @@ func (a Account) M365Config() (M365Config, error) { c.AzureClientID = a.Config[keyAzureClientID] c.AzureClientSecret = a.Config[keyAzureClientSecret] c.AzureTenantID = a.Config[keyAzureTenantID] + c.AzureUsername = a.Config[keyAzureUsername] + c.AzureUserPassword = a.Config[keyAzureUserPassword] } return c, c.validate() diff --git a/src/pkg/credentials/m365.go b/src/pkg/credentials/m365.go index 19f034011..38c6538e9 100644 --- a/src/pkg/credentials/m365.go +++ b/src/pkg/credentials/m365.go @@ -10,12 +10,16 @@ import ( const ( AzureClientID = "AZURE_CLIENT_ID" AzureClientSecret = "AZURE_CLIENT_SECRET" + AzureUsername = "AZURE_USERNAME" + AzureUserPassword = "AZURE_USER_PASSWORD" ) // M365 aggregates m365 credentials from flag and env_var values. type M365 struct { AzureClientID string AzureClientSecret string + AzureUsername string + AzureUserPassword string } // M365 is a helper for aggregating m365 secrets and credentials. @@ -24,10 +28,14 @@ func GetM365() M365 { // var AzureClientID, AzureClientSecret string AzureClientID := os.Getenv(AzureClientID) AzureClientSecret := os.Getenv(AzureClientSecret) + AzureUserPassword := os.Getenv(AzureUserPassword) + AzureUsername := os.Getenv(AzureUsername) return M365{ AzureClientID: AzureClientID, AzureClientSecret: AzureClientSecret, + AzureUsername: AzureUsername, + AzureUserPassword: AzureUserPassword, } } diff --git a/src/pkg/services/m365/api/access.go b/src/pkg/services/m365/api/access.go index 956f9db05..4cee23657 100644 --- a/src/pkg/services/m365/api/access.go +++ b/src/pkg/services/m365/api/access.go @@ -2,6 +2,7 @@ package api import ( "context" + "encoding/json" "fmt" "net/http" "strings" @@ -15,8 +16,8 @@ import ( // controller // --------------------------------------------------------------------------- -func (c Client) Access() Access { - return Access{c} +func (c Client) Access() *Access { + return &Access{c} } // Access is an interface-compliant provider of the client. @@ -66,3 +67,64 @@ func (c Access) GetToken( return nil } + +type delegatedAccess struct { + TokenType string `json:"token_type"` + Scope string `json:"scope"` + ExpiresIn string `json:"expires_in"` + ExpiresOn string `json:"expires_on"` + NotBefore string `json:"not_before"` + Resource string `json:"resource"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +} + +func (da delegatedAccess) MinimumPrintable() any { + return da +} + +func (c *Access) GetDelegatedToken( + ctx context.Context, +) (delegatedAccess, error) { + var ( + //nolint:lll + // https://dzone.com/articles/getting-access-token-for-microsoft-graph-using-oau + rawURL = fmt.Sprintf( + "https://login.microsoftonline.com/%s/oauth2/token", + c.Credentials.AzureTenantID) + headers = map[string]string{ + "Content-Type": "application/x-www-form-urlencoded", + } + body = strings.NewReader(fmt.Sprintf( + "client_id=%s"+ + "&client_secret=%s"+ + "&resource=https://graph.microsoft.com"+ + "&grant_type=password"+ + "&username=%s"+ + "&password=%s", + c.Credentials.AzureClientID, + c.Credentials.AzureClientSecret, + c.Credentials.AzureUsername, + c.Credentials.AzureUserPassword)) + ) + + resp, err := c.Post(ctx, rawURL, headers, body) + if err != nil { + return delegatedAccess{}, graph.Stack(ctx, err) + } + + if resp.StatusCode == http.StatusBadRequest { + return delegatedAccess{}, clues.New("incorrect tenant or credentials") + } + + if resp.StatusCode/100 == 4 || resp.StatusCode/100 == 5 { + return delegatedAccess{}, clues.New("non-2xx response: " + resp.Status) + } + + defer resp.Body.Close() + + var da delegatedAccess + err = json.NewDecoder(resp.Body).Decode(&da) + + return da, clues.Wrap(err, "undecodable body").WithClues(ctx).OrNil() +} diff --git a/src/pkg/services/m365/api/client.go b/src/pkg/services/m365/api/client.go index f35232098..42cf1cd21 100644 --- a/src/pkg/services/m365/api/client.go +++ b/src/pkg/services/m365/api/client.go @@ -40,6 +40,9 @@ type Client struct { // graph api client. Requester graph.Requester + delegated graph.Servicer + delegatedAccess delegatedAccess + counter *count.Bus options control.Options