diff --git a/cmd/cloudx/client/api_key.go b/cmd/cloudx/client/api_key.go index 978d119c..34a2e341 100644 --- a/cmd/cloudx/client/api_key.go +++ b/cmd/cloudx/client/api_key.go @@ -7,18 +7,33 @@ import ( "context" "errors" "fmt" + "time" cloud "github.com/ory/client-go" "github.com/ory/x/cmdx" ) -func (h *CommandHelper) CreateProjectAPIKey(ctx context.Context, projectID, name string) (*cloud.ProjectApiKey, error) { +// CreateProjectAPIKey creates a project API key. If expiresIn is greater than +// zero, the key is set to expire that duration from now so it is cleaned up +// automatically on the server side even if local cleanup fails. An expiresIn of +// zero creates a key without expiry; a negative value is rejected. +func (h *CommandHelper) CreateProjectAPIKey(ctx context.Context, projectID, name string, expiresIn time.Duration) (*cloud.ProjectApiKey, error) { + if expiresIn < 0 { + return nil, errors.New("API key expiry must not be negative") + } + c, err := h.newConsoleAPIClient(ctx) if err != nil { return nil, err } - token, res, err := c.ProjectAPI.CreateProjectApiKey(ctx, projectID).CreateProjectApiKeyRequest(cloud.CreateProjectApiKeyRequest{Name: name}).Execute() + req := cloud.CreateProjectApiKeyRequest{Name: name} + if expiresIn > 0 { + expiresAt := time.Now().Add(expiresIn) + req.ExpiresAt = &expiresAt + } + + token, res, err := c.ProjectAPI.CreateProjectApiKey(ctx, projectID).CreateProjectApiKeyRequest(req).Execute() if err != nil { return nil, handleError("unable to create project API key", res, err) } @@ -64,7 +79,11 @@ func (h *CommandHelper) DeleteWorkspaceAPIKey(ctx context.Context, workspaceID, return nil } -func (h *CommandHelper) TemporaryAPIKey(ctx context.Context, name string) (apiKey string, cleanup func() error, err error) { +// TemporaryAPIKey creates a short-lived project API key that is deleted via the +// returned cleanup function. The key is additionally set to expire after +// expiresIn so that it is removed automatically should the cleanup fail. An +// expiresIn of zero creates a key without expiry. +func (h *CommandHelper) TemporaryAPIKey(ctx context.Context, name string, expiresIn time.Duration) (apiKey string, cleanup func() error, err error) { if h.projectAPIKey != nil { return *h.projectAPIKey, noop, nil } @@ -95,7 +114,7 @@ func (h *CommandHelper) TemporaryAPIKey(ctx context.Context, name string) (apiKe if err != nil { return "", noop, err } - ak, err := h.CreateProjectAPIKey(ctx, projectID, name) + ak, err := h.CreateProjectAPIKey(ctx, projectID, name, expiresIn) if err != nil { _, _ = fmt.Fprintf(h.VerboseErrWriter, "Unable to create API key. Do you have the required permissions to use the Ory CLI with project %q? Continuing without API key.", projectID) return "", noop, nil diff --git a/cmd/cloudx/client/command_helper_test.go b/cmd/cloudx/client/command_helper_test.go index 489117ec..a5dc9bef 100644 --- a/cmd/cloudx/client/command_helper_test.go +++ b/cmd/cloudx/client/command_helper_test.go @@ -155,7 +155,7 @@ func TestCommandHelper(t *testing.T) { require.Len(t, projects, 2) assert.ElementsMatch(t, []string{p0.Id, p1.Id}, []string{projects[0].Id, projects[1].Id}) - pjKey, err := authenticated.CreateProjectAPIKey(ctx, p0.Id, "test key") + pjKey, err := authenticated.CreateProjectAPIKey(ctx, p0.Id, "test key", 0) require.NoError(t, err) pjKeyH, err := client.NewCommandHelper(ctx, client.WithProjectAPIKey(*pjKey.Value)) @@ -192,7 +192,7 @@ func TestCommandHelper(t *testing.T) { require.Len(t, projects, 2) assert.ElementsMatch(t, []string{p0.Id, p1.Id}, []string{projects[0].Id, projects[1].Id}) - pjKey, err := authenticated.CreateProjectAPIKey(ctx, p0.Id, "test key") + pjKey, err := authenticated.CreateProjectAPIKey(ctx, p0.Id, "test key", 0) require.NoError(t, err) pjKeyH, err := client.NewCommandHelper(ctx, client.WithProjectAPIKey(*pjKey.Value)) @@ -310,10 +310,13 @@ func TestCommandHelper(t *testing.T) { keyName := "a test key" - key, err := authenticated.CreateProjectAPIKey(ctx, defaultProject.Id, keyName) + key, err := authenticated.CreateProjectAPIKey(ctx, defaultProject.Id, keyName, time.Hour) require.NoError(t, err) assert.Equal(t, keyName, key.Name) assert.NotNil(t, keyName, key.Value) + // The requested expiry should be reflected on the returned key. + require.NotNil(t, key.ExpiresAt) + assert.WithinDuration(t, time.Now().Add(time.Hour), *key.ExpiresAt, 5*time.Minute) // check that the key works ctxWithKey := client.ContextWithOptions(ctx, @@ -333,7 +336,7 @@ func TestCommandHelper(t *testing.T) { t.Run("func=GetProject", func(t *testing.T) { wsKeyH, err := client.NewCommandHelper(ctx, client.WithWorkspaceAPIKey(*defaultWorkspaceAPIKey.Value)) require.NoError(t, err) - pjKey, err := authenticated.CreateProjectAPIKey(ctx, defaultProject.Id, "test key") + pjKey, err := authenticated.CreateProjectAPIKey(ctx, defaultProject.Id, "test key", 0) require.NoError(t, err) pjKeyH, err := client.NewCommandHelper(ctx, client.WithProjectAPIKey(*pjKey.Value)) require.NoError(t, err) diff --git a/cmd/cloudx/proxy/helpers.go b/cmd/cloudx/proxy/helpers.go index 01690921..8f2b4951 100644 --- a/cmd/cloudx/proxy/helpers.go +++ b/cmd/cloudx/proxy/helpers.go @@ -53,8 +53,13 @@ const ( CORSFlag = "allowed-cors-origins" AdditionalCORSHeadersFlag = "additional-cors-headers" RewriteHostFlag = "rewrite-host" + APIKeyExpiryFlag = "api-key-expiry" ) +// defaultAPIKeyExpiry is the default lifetime of the temporary API key the +// proxy and tunnel create to configure the project. +const defaultAPIKeyExpiry = 12 * time.Hour + type config struct { port int open bool @@ -70,6 +75,11 @@ type config struct { corsOrigins []string additionalCorsHeaders []string + // apiKeyExpiry is the lifetime of the temporary API key the proxy/tunnel + // creates. The key is deleted on shutdown; the expiry ensures it is removed + // automatically should that cleanup fail. A value of zero disables expiry. + apiKeyExpiry time.Duration + // rewriteHost means the host header will be rewritten to the upstream host. // This is useful in cases where upstream resolves requests based on Host. rewriteHost bool @@ -89,6 +99,7 @@ func registerConfigFlags(conf *config, flags *pflag.FlagSet) { flags.BoolVar(&conf.isDev, DevFlag, true, "This flag is deprecated as the command is only supposed to be used during development.") flags.BoolVar(&conf.isDebug, DebugFlag, false, "Use this flag to debug, for example, CORS requests.") flags.BoolVar(&conf.rewriteHost, RewriteHostFlag, false, "Use this flag to rewrite the host header to the upstream host.") + flags.DurationVar(&conf.apiKeyExpiry, APIKeyExpiryFlag, defaultAPIKeyExpiry, "Sets the expiry of the temporary API key the Ory CLI creates to configure your project. The key is deleted on shutdown; this expiry ensures it is removed automatically if that cleanup fails. Set to 0 to disable expiry.") } func portFromEnv() int { @@ -115,7 +126,7 @@ func runReverseProxy(ctx context.Context, h *client.CommandHelper, stdErr io.Wri return err } - apiKey, removeAPIKey, err := h.TemporaryAPIKey(ctx, fmt.Sprintf("Ory %s temporary API key - %s", name, h.UserName(ctx))) + apiKey, removeAPIKey, err := h.TemporaryAPIKey(ctx, fmt.Sprintf("Ory %s temporary API key - %s", name, h.UserName(ctx)), conf.apiKeyExpiry) if err != nil { return err }