Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -556,13 +556,26 @@ func (c *OryClient) ensureProjectClient() error {
return nil
}

// requireConsoleClient returns an error if the console API client is not configured.
// This prevents nil pointer panics when methods are called without a workspace API key.
func (c *OryClient) requireConsoleClient(operation string) error {
if c.consoleClient == nil {
return fmt.Errorf("%s: console API client not configured. "+
"Set workspace_api_key (ORY_WORKSPACE_API_KEY) in the provider configuration", operation)
}
return nil
}

// =============================================================================
// Project Operations (Console API)
// =============================================================================

// CreateProject creates a new Ory project.
// Returns the project, HTTP response (for status code inspection), and any error.
func (c *OryClient) CreateProject(ctx context.Context, name, environment, homeRegion string) (*ory.Project, *http.Response, error) {
if err := c.requireConsoleClient("creating project"); err != nil {
return nil, nil, err
}
body := ory.CreateProjectBody{
Name: name,
Environment: environment,
Expand All @@ -580,6 +593,9 @@ func (c *OryClient) CreateProject(ctx context.Context, name, environment, homeRe

// GetProject retrieves a project by ID.
func (c *OryClient) GetProject(ctx context.Context, projectID string) (*ory.Project, error) {
if err := c.requireConsoleClient("getting project"); err != nil {
return nil, err
}
project, httpResp, err := c.consoleClient.ProjectAPI.GetProject(ctx, projectID).Execute()
if httpResp != nil {
_ = httpResp.Body.Close()
Expand All @@ -589,6 +605,9 @@ func (c *OryClient) GetProject(ctx context.Context, projectID string) (*ory.Proj

// DeleteProject purges a project.
func (c *OryClient) DeleteProject(ctx context.Context, projectID string) error {
if err := c.requireConsoleClient("deleting project"); err != nil {
return err
}
httpResp, err := c.consoleClient.ProjectAPI.PurgeProject(ctx, projectID).Execute()
if httpResp != nil {
_ = httpResp.Body.Close()
Expand All @@ -599,6 +618,9 @@ func (c *OryClient) DeleteProject(ctx context.Context, projectID string) error {
// PatchProject applies JSON Patch operations to a project.
// The response is automatically cached to avoid stale GetProject reads.
func (c *OryClient) PatchProject(ctx context.Context, projectID string, patches []ory.JsonPatch) (*ory.SuccessfulProjectUpdate, error) {
if err := c.requireConsoleClient("patching project"); err != nil {
return nil, err
}
result, httpResp, err := c.consoleClient.ProjectAPI.PatchProject(ctx, projectID).
JsonPatch(patches).
Execute()
Expand Down Expand Up @@ -627,6 +649,9 @@ func (c *OryClient) GetCachedProject(projectID string) *ory.Project {

// CreateWorkspace creates a new workspace.
func (c *OryClient) CreateWorkspace(ctx context.Context, name string) (*ory.Workspace, error) {
if err := c.requireConsoleClient("creating workspace"); err != nil {
return nil, err
}
body := ory.CreateWorkspaceBody{
Name: name,
}
Expand All @@ -642,6 +667,9 @@ func (c *OryClient) CreateWorkspace(ctx context.Context, name string) (*ory.Work
// but not get a specific workspace. We fall back to listing and filtering if
// the direct GET fails with 403.
func (c *OryClient) GetWorkspace(ctx context.Context, workspaceID string) (*ory.Workspace, error) {
if err := c.requireConsoleClient("getting workspace"); err != nil {
return nil, err
}
workspace, httpResp, err := c.consoleClient.WorkspaceAPI.GetWorkspace(ctx, workspaceID).Execute()
if httpResp != nil {
_ = httpResp.Body.Close()
Expand Down Expand Up @@ -672,6 +700,9 @@ func (c *OryClient) GetWorkspace(ctx context.Context, workspaceID string) (*ory.

// UpdateWorkspace updates a workspace.
func (c *OryClient) UpdateWorkspace(ctx context.Context, workspaceID, name string) (*ory.Workspace, error) {
if err := c.requireConsoleClient("updating workspace"); err != nil {
return nil, err
}
body := ory.UpdateWorkspaceBody{
Name: name,
}
Expand Down Expand Up @@ -955,6 +986,9 @@ func (c *OryClient) DeleteOAuth2Client(ctx context.Context, clientID string) err

// CreateProjectAPIKey creates a new API key for a project.
func (c *OryClient) CreateProjectAPIKey(ctx context.Context, projectID string, body ory.CreateProjectApiKeyRequest) (*ory.ProjectApiKey, error) {
if err := c.requireConsoleClient("creating project API key"); err != nil {
return nil, err
}
key, httpResp, err := c.consoleClient.ProjectAPI.CreateProjectApiKey(ctx, projectID).CreateProjectApiKeyRequest(body).Execute()
if httpResp != nil {
_ = httpResp.Body.Close()
Expand All @@ -964,6 +998,9 @@ func (c *OryClient) CreateProjectAPIKey(ctx context.Context, projectID string, b

// ListProjectAPIKeys lists all API keys for a project.
func (c *OryClient) ListProjectAPIKeys(ctx context.Context, projectID string) ([]ory.ProjectApiKey, error) {
if err := c.requireConsoleClient("listing project API keys"); err != nil {
return nil, err
}
keys, httpResp, err := c.consoleClient.ProjectAPI.ListProjectApiKeys(ctx, projectID).Execute()
if httpResp != nil {
_ = httpResp.Body.Close()
Expand All @@ -973,6 +1010,9 @@ func (c *OryClient) ListProjectAPIKeys(ctx context.Context, projectID string) ([

// DeleteProjectAPIKey deletes an API key with retry logic for transient errors.
func (c *OryClient) DeleteProjectAPIKey(ctx context.Context, projectID, keyID string) error {
if err := c.requireConsoleClient("deleting project API key"); err != nil {
return err
}
var lastErr error
backoff := initialBackoff

Expand Down Expand Up @@ -1115,6 +1155,9 @@ func (c *OryClient) DeleteRelationships(ctx context.Context, namespace string, o

// CreateEventStream creates a new event stream for a project.
func (c *OryClient) CreateEventStream(ctx context.Context, projectID string, body ory.CreateEventStreamBody) (*ory.EventStream, error) {
if err := c.requireConsoleClient("creating event stream"); err != nil {
return nil, err
}
stream, httpResp, err := c.consoleClient.EventsAPI.CreateEventStream(ctx, projectID).CreateEventStreamBody(body).Execute()
if httpResp != nil {
_ = httpResp.Body.Close()
Expand All @@ -1128,6 +1171,9 @@ func (c *OryClient) CreateEventStream(ctx context.Context, projectID string, bod
// GetEventStream retrieves an event stream by listing all and filtering by ID.
// The Ory API does not have a direct GET endpoint for event streams.
func (c *OryClient) GetEventStream(ctx context.Context, projectID, streamID string) (*ory.EventStream, error) {
if err := c.requireConsoleClient("getting event stream"); err != nil {
return nil, err
}
list, httpResp, err := c.consoleClient.EventsAPI.ListEventStreams(ctx, projectID).Execute()
if httpResp != nil {
_ = httpResp.Body.Close()
Expand All @@ -1146,6 +1192,9 @@ func (c *OryClient) GetEventStream(ctx context.Context, projectID, streamID stri

// SetEventStream updates an event stream.
func (c *OryClient) SetEventStream(ctx context.Context, projectID, streamID string, body ory.SetEventStreamBody) (*ory.EventStream, error) {
if err := c.requireConsoleClient("updating event stream"); err != nil {
return nil, err
}
stream, httpResp, err := c.consoleClient.EventsAPI.SetEventStream(ctx, projectID, streamID).SetEventStreamBody(body).Execute()
if httpResp != nil {
_ = httpResp.Body.Close()
Expand All @@ -1158,6 +1207,9 @@ func (c *OryClient) SetEventStream(ctx context.Context, projectID, streamID stri

// DeleteEventStream deletes an event stream.
func (c *OryClient) DeleteEventStream(ctx context.Context, projectID, streamID string) error {
if err := c.requireConsoleClient("deleting event stream"); err != nil {
return err
}
httpResp, err := c.consoleClient.EventsAPI.DeleteEventStream(ctx, projectID, streamID).Execute()
if httpResp != nil {
_ = httpResp.Body.Close()
Expand All @@ -1167,6 +1219,9 @@ func (c *OryClient) DeleteEventStream(ctx context.Context, projectID, streamID s

// ListEventStreams lists all event streams for a project.
func (c *OryClient) ListEventStreams(ctx context.Context, projectID string) ([]ory.EventStream, error) {
if err := c.requireConsoleClient("listing event streams"); err != nil {
return nil, err
}
list, httpResp, err := c.consoleClient.EventsAPI.ListEventStreams(ctx, projectID).Execute()
if httpResp != nil {
_ = httpResp.Body.Close()
Expand Down Expand Up @@ -1325,6 +1380,9 @@ func (c *OryClient) ListOAuth2Clients(ctx context.Context) ([]ory.OAuth2Client,

// ListOrganizations lists all organizations in a project.
func (c *OryClient) ListOrganizations(ctx context.Context, projectID string) ([]ory.Organization, error) {
if err := c.requireConsoleClient("listing organizations"); err != nil {
return nil, err
}
resp, httpResp, err := c.consoleClient.ProjectAPI.ListOrganizations(ctx, projectID).Execute()
if httpResp != nil {
_ = httpResp.Body.Close()
Expand Down Expand Up @@ -1832,6 +1890,9 @@ func (c *OryClient) DeleteCustomDomain(ctx context.Context, projectID, domainID

// ListWorkspaces lists all workspaces.
func (c *OryClient) ListWorkspaces(ctx context.Context) ([]ory.Workspace, error) {
if err := c.requireConsoleClient("listing workspaces"); err != nil {
return nil, err
}
resp, httpResp, err := c.consoleClient.WorkspaceAPI.ListWorkspaces(ctx).Execute()
if httpResp != nil {
_ = httpResp.Body.Close()
Expand Down
105 changes: 105 additions & 0 deletions internal/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,111 @@ func TestOryClient_EnsureProjectClient_MissingKeyOnly(t *testing.T) {
}
}

func TestOryClient_RequireConsoleClient_NilPanics(t *testing.T) {
// This test verifies the fix for https://github.com/ory/terraform-provider-ory/issues/137
// where calling GetProject (and other console API methods) without a workspace
// API key caused a nil pointer dereference panic instead of a helpful error.
cfg := OryClientConfig{
ProjectAPIKey: testutil.TestProjectAPIKey,
ProjectSlug: testutil.TestProjectSlug,
// No WorkspaceAPIKey — consoleClient will be nil
}

client, err := NewOryClient(cfg)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if client.consoleClient != nil {
t.Fatal("consoleClient should be nil for this test")
}

ctx := context.Background()

t.Run("GetProject", func(t *testing.T) {
_, err := client.GetProject(ctx, "any-project-id")
if err == nil {
t.Fatal("expected error when consoleClient is nil")
}
if !contains(err.Error(), "console API client not configured") {
t.Errorf("unexpected error: %s", err.Error())
}
})

t.Run("PatchProject", func(t *testing.T) {
_, err := client.PatchProject(ctx, "any-project-id", nil)
if err == nil {
t.Fatal("expected error when consoleClient is nil")
}
if !contains(err.Error(), "console API client not configured") {
t.Errorf("unexpected error: %s", err.Error())
}
})

t.Run("CreateProject", func(t *testing.T) {
_, resp, err := client.CreateProject(ctx, "name", "prod", "")
if resp != nil && resp.Body != nil {
_ = resp.Body.Close()
}
if err == nil {
t.Fatal("expected error when consoleClient is nil")
}
if !contains(err.Error(), "console API client not configured") {
t.Errorf("unexpected error: %s", err.Error())
}
})

t.Run("DeleteProject", func(t *testing.T) {
err := client.DeleteProject(ctx, "any-project-id")
if err == nil {
t.Fatal("expected error when consoleClient is nil")
}
if !contains(err.Error(), "console API client not configured") {
t.Errorf("unexpected error: %s", err.Error())
}
})

t.Run("CreateWorkspace", func(t *testing.T) {
_, err := client.CreateWorkspace(ctx, "name")
if err == nil {
t.Fatal("expected error when consoleClient is nil")
}
if !contains(err.Error(), "console API client not configured") {
t.Errorf("unexpected error: %s", err.Error())
}
})

t.Run("GetWorkspace", func(t *testing.T) {
_, err := client.GetWorkspace(ctx, "any-workspace-id")
if err == nil {
t.Fatal("expected error when consoleClient is nil")
}
if !contains(err.Error(), "console API client not configured") {
t.Errorf("unexpected error: %s", err.Error())
}
})

t.Run("ListWorkspaces", func(t *testing.T) {
_, err := client.ListWorkspaces(ctx)
if err == nil {
t.Fatal("expected error when consoleClient is nil")
}
if !contains(err.Error(), "console API client not configured") {
t.Errorf("unexpected error: %s", err.Error())
}
})

t.Run("ListOrganizations", func(t *testing.T) {
_, err := client.ListOrganizations(ctx, "any-project-id")
if err == nil {
t.Fatal("expected error when consoleClient is nil")
}
if !contains(err.Error(), "console API client not configured") {
t.Errorf("unexpected error: %s", err.Error())
}
})
}

func TestIsRetryableError(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading