From 30d50b72a26fe99b61deaf752d3e623424d9f28e Mon Sep 17 00:00:00 2001 From: Robert Lin Date: Fri, 19 Jul 2024 13:41:28 -0700 Subject: [PATCH] feat/sg: add 'sg sams client create' (#63885) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an equivalent to the curl command we currently share, but in `sg`. If we add a better API around this later it's just an in-place replacement. Similar to https://github.com/sourcegraph/sourcegraph/pull/63883 this "just works" with zero configuration against SAMS-dev. Part https://linear.app/sourcegraph/issue/CORE-220, a spike into polishing some local-dev DX for SAMS. ## Test plan ``` sg sams client create -redirect-uris='https://sourcegraph.test:3443/.auth/callback' robert-testing ``` if you hit an error loading the secret, e.g. targeting the prod instance, you get a suggestion to get Entitle access: ``` sg sams client create -redirect-uris='https://sourcegraph.test:3443/.auth/callback' -sams='https://accounts.sourcegraph.com' robert-testing ⚠️ Running sg with a dev build, following flags have different default value unless explictly set: skip-auto-update, disable-analytics 👉 Failed to get secret - do you have Entitle access to the "sourcegraph-accounts-prod-csvc" project? See https://sourcegraph.notion.site/Sourcegraph-Accounts-infrastructure-operations-b90a571da30443a8b1e7c31ade3594fb ❌ google(sourcegraph-accounts-prod-csvc): failed to get secret "MANAGEMENT_SECRET": rpc error: code = PermissionDenied desc = Permission 'secretmanager.versions.access' denied for resource 'projects/sourcegraph-accounts-prod-csvc/secrets/MANAGEMENT_SECRET/versions/latest' (or it may not exist). ``` ## Changelog - `sg sams client create` can now be used to create IdP clients for SAMS. --------- Co-authored-by: Erik Seliger --- dev/sg/sams/BUILD.bazel | 2 + dev/sg/sams/samsflags/clientcredentials.go | 5 +- dev/sg/sams/sg_sams.go | 136 +++++++++++++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) diff --git a/dev/sg/sams/BUILD.bazel b/dev/sg/sams/BUILD.bazel index a0a43ff5009..43564bb7726 100644 --- a/dev/sg/sams/BUILD.bazel +++ b/dev/sg/sams/BUILD.bazel @@ -7,8 +7,10 @@ go_library( visibility = ["//visibility:public"], deps = [ "//dev/sg/internal/category", + "//dev/sg/internal/secrets", "//dev/sg/internal/std", "//dev/sg/sams/samsflags", + "//internal/httpcli", "//lib/errors", "@com_github_sourcegraph_sourcegraph_accounts_sdk_go//scopes", "@com_github_urfave_cli_v2//:cli", diff --git a/dev/sg/sams/samsflags/clientcredentials.go b/dev/sg/sams/samsflags/clientcredentials.go index 83cdcdaed77..a47bdcf0243 100644 --- a/dev/sg/sams/samsflags/clientcredentials.go +++ b/dev/sg/sams/samsflags/clientcredentials.go @@ -23,7 +23,10 @@ func ClientCredentials() []cli.Flag { Aliases: []string{"sams"}, EnvVars: []string{"SG_SAMS_SERVER_URL"}, Value: SAMSDevURL, - Usage: fmt.Sprintf("URL of the Sourcegraph Accounts Management System (SAMS) server - one of %q or %q", + Usage: fmt.Sprintf("URL of the Sourcegraph Accounts Management System (SAMS) server - one of %q or %q,"+ + // TODO: 9091 currently conflicts with embeddings, we may want + // to change the default in the future + " or http://127.0.0.1:9091 for a locally running server.", SAMSProdURL, SAMSDevURL), }, &cli.StringFlag{ diff --git a/dev/sg/sams/sg_sams.go b/dev/sg/sams/sg_sams.go index ed0e3d98a03..be73e3585bb 100644 --- a/dev/sg/sams/sg_sams.go +++ b/dev/sg/sams/sg_sams.go @@ -2,13 +2,21 @@ package sams import ( + "bytes" "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" "github.com/urfave/cli/v2" "github.com/sourcegraph/sourcegraph-accounts-sdk-go/scopes" "github.com/sourcegraph/sourcegraph/dev/sg/internal/category" + "github.com/sourcegraph/sourcegraph/dev/sg/internal/secrets" "github.com/sourcegraph/sourcegraph/dev/sg/internal/std" + "github.com/sourcegraph/sourcegraph/internal/httpcli" "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/sourcegraph/dev/sg/sams/samsflags" @@ -81,5 +89,133 @@ Please reach out to #discuss-core-services for assistance if you have any questi return std.Out.WriteCode("json", string(data)) }, }}, + }, { + Name: "client", + Usage: "Manage IdP clients registered in SAMS", + Subcommands: []*cli.Command{{ + Name: "create", + ArgsUsage: "", + Usage: "Create an IdP client in the target SAMS instance", + Flags: []cli.Flag{ + &cli.StringSliceFlag{ + Name: "redirect-uris", + Usage: "Redirect URIs to associate with the client, e.g. 'https://sourcegraph.test:3443/.auth/callback'", + Required: true, + }, + // Flags are special subset of clientCredentialsFlags(), as we + // use a static secret - in the future we should change this to + // require client credentials instead. + &cli.StringFlag{ + Name: "sams-server", + Aliases: []string{"sams"}, + EnvVars: []string{"SG_SAMS_SERVER_URL"}, + Value: samsflags.SAMSDevURL, + Usage: fmt.Sprintf("URL of the Sourcegraph Accounts Management System (SAMS) server - one of %q or %q", + samsflags.SAMSProdURL, samsflags.SAMSDevURL), + }, + &cli.StringSliceFlag{ + Name: "scopes", + Aliases: []string{"s"}, + Value: cli.NewStringSlice("openid", "profile", "email"), + Usage: "OAuth scopes ('$SERVICE::$PERM::$ACTION') to request from the Sourcegraph Accounts Management System (SAMS) server", + }, + }, + Action: func(c *cli.Context) error { + samsServer := c.String("sams-server") + displayName := c.Args().First() + if displayName == "" { + return errors.New("argument display name required") + } + + ss, err := secrets.FromContext(c.Context) + if err != nil { + return err + } + // Environments specified in + // https://github.com/sourcegraph/managed-services/blob/main/services/sourcegraph-accounts/service.yaml + samsManagementSecretSource := map[string]*secrets.ExternalSecret{ + samsflags.SAMSDevURL: { + Project: "sourcegraph-dev", + Name: "SAMS_MANAGEMENT_SECRET", + }, + samsflags.SAMSProdURL: { + Project: "sourcegraph-accounts-prod-csvc", + Name: "MANAGEMENT_SECRET", + }, + } + var managementSecret string + if externalSecret := samsManagementSecretSource[samsServer]; externalSecret != nil { + std.Out.WriteSuggestionf("Targeting SAMS instance at %q", samsServer) + managementSecret, err = ss.GetExternal(c.Context, *externalSecret) + if err != nil { + std.Out.WriteAlertf("Failed to get secret - do you have Entitle access to the %q project? See https://sourcegraph.notion.site/Sourcegraph-Accounts-infrastructure-operations-b90a571da30443a8b1e7c31ade3594fb", + externalSecret.Project) + return err + } + } else { + managementSecret, err = std.Out.PromptPasswordf(os.Stdin, + "Enter the SAMS management secret for your target SAMS server %q: ", + c.String("sams-server")) + if err != nil { + return err + } + } + + body := map[string]any{ + "name": displayName, + "scopes": c.StringSlice("scopes"), + "redirect_uris": c.StringSlice("redirect-uris"), + } + data, err := json.Marshal(body) + if err != nil { + return err + } + + req, err := http.NewRequest( + http.MethodPost, + samsServer+"/api/management/v1/identity-provider/clients", + bytes.NewReader(data), + ) + if err != nil { + return err + } + req.Header.Set("Authorization", "Bearer "+managementSecret) + + resp, err := httpcli.UncachedExternalDoer.Do(req.WithContext(c.Context)) + if err != nil { + return errors.Wrap(err, "do request") + } + defer resp.Body.Close() + responseData, err := io.ReadAll(resp.Body) + if err != nil { + return errors.Wrap(err, "read response") + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + std.Out.Write("Got responnse:") + std.Out.Write(string(responseData)) + return errors.Newf("got unexpected response status %d", resp.StatusCode) + } + + var v any + if err := json.Unmarshal(responseData, &v); err != nil { + return err + } + prettyResponseData, err := json.MarshalIndent(v, "", " ") + if err != nil { + return err + } + if err := std.Out.WriteCode("json", string(prettyResponseData)); err != nil { + return err + } + + if u, err := url.Parse(samsServer); err == nil && + (u.Hostname() == "127.0.0.1" || u.Hostname() == "localhost") { // CI:LOCALHOST_OK + std.Out.WriteSuggestionf("These client credentials can NOT be shown again - if you lose them you will need to create another one.") + } else { + std.Out.WriteWarningf("These client credentials are highly sensitive and can NOT be shown again. Please store them securely in Google Secret Manager or 1Password.") + } + return nil + }, + }}, }}, }