feat/sg: add 'sg sams client create' (#63885)

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 <erikseliger@me.com>
This commit is contained in:
Robert Lin 2024-07-19 13:41:28 -07:00 committed by GitHub
parent f53e211804
commit 30d50b72a2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 142 additions and 1 deletions

View File

@ -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",

View File

@ -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{

View File

@ -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: "<display name>",
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
},
}},
}},
}