Skip to content

Commit 7097630

Browse files
committed
feat: add project_id support to ory_json_web_key_set resource
The JWK resource previously required project_slug and project_api_key on the provider, with no way to specify project_id at the resource level. This made it impossible to use with project_id-based workflows. Changes: - Add optional project_id attribute to ory_json_web_key_set (falls back to provider's project_id when not set) - Add ResolveProjectSlug and ProjectClientForProject methods to the client for resolving project_id to slug via the console API - Cache resolved slugs to avoid redundant API calls within a run - Validate that project_id is set before CRUD operations - Support composite import format: project_id/set_id - Update acceptance tests, examples, and documentation
1 parent 2c6a7ec commit 7097630

File tree

7 files changed

+201
-34
lines changed

7 files changed

+201
-34
lines changed

docs/resources/json_web_key_set.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ JSON Web Keys are used for signing and encrypting tokens. This resource generate
1313

1414
-> **Plan:** Available on all Ory Network plans.
1515

16-
~> **Note:** This resource is **immutable**. Any change to `set_id`, `key_id`, `algorithm`, or `use` will destroy the existing key set and create a new one. Private keys in the old set will be permanently lost.
16+
~> **Note:** This resource is **immutable**. Any change to `project_id`, `set_id`, `key_id`, `algorithm`, or `use` will destroy the existing key set and create a new one. Private keys in the old set will be permanently lost.
1717

1818
## Algorithms
1919

@@ -46,20 +46,21 @@ Most configurations only need `use = "sig"`.
4646
## Example Usage
4747

4848
```terraform
49-
# RSA signing key set
49+
# RSA signing key set (project_id from provider config)
5050
resource "ory_json_web_key_set" "signing" {
5151
set_id = "token-signing-keys"
5252
key_id = "rsa-sig-1"
5353
algorithm = "RS256"
5454
use = "sig"
5555
}
5656
57-
# ECDSA signing key set (smaller, faster)
57+
# ECDSA signing key set with explicit project_id
5858
resource "ory_json_web_key_set" "ecdsa_signing" {
59-
set_id = "ecdsa-signing-keys"
60-
key_id = "ec-sig-1"
61-
algorithm = "ES256"
62-
use = "sig"
59+
project_id = var.ory_project_id
60+
set_id = "ecdsa-signing-keys"
61+
key_id = "ec-sig-1"
62+
algorithm = "ES256"
63+
use = "sig"
6364
}
6465
6566
# Encryption key set
@@ -98,9 +99,10 @@ On read, the provider extracts `algorithm`, `use`, and `key_id` from the **first
9899

99100
## Import
100101

101-
Import using the set ID:
102+
Import using the format `project_id/set_id` or just `set_id` (uses provider's project_id):
102103

103104
```shell
105+
terraform import ory_json_web_key_set.signing <project-id>/token-signing-keys
104106
terraform import ory_json_web_key_set.signing token-signing-keys
105107
```
106108

@@ -116,6 +118,10 @@ After import, `key_id` is populated from the first key in the set. If the set co
116118
- `set_id` (String) The ID of the JSON Web Key Set.
117119
- `use` (String) The intended use: sig (signature) or enc (encryption).
118120

121+
### Optional
122+
123+
- `project_id` (String) The project ID. If not set, uses the provider's project_id.
124+
119125
### Read-Only
120126

121127
- `id` (String) Internal Terraform ID (same as set_id).

examples/resources/ory_json_web_key_set/resource.tf

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
# RSA signing key set
1+
# RSA signing key set (project_id from provider config)
22
resource "ory_json_web_key_set" "signing" {
33
set_id = "token-signing-keys"
44
key_id = "rsa-sig-1"
55
algorithm = "RS256"
66
use = "sig"
77
}
88

9-
# ECDSA signing key set (smaller, faster)
9+
# ECDSA signing key set with explicit project_id
1010
resource "ory_json_web_key_set" "ecdsa_signing" {
11-
set_id = "ecdsa-signing-keys"
12-
key_id = "ec-sig-1"
13-
algorithm = "ES256"
14-
use = "sig"
11+
project_id = var.ory_project_id
12+
set_id = "ecdsa-signing-keys"
13+
key_id = "ec-sig-1"
14+
algorithm = "ES256"
15+
use = "sig"
1516
}
1617

1718
# Encryption key set

internal/client/client.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,11 @@ type OryClient struct {
329329
// immediately after PatchProject. This cache ensures Read operations
330330
// see the latest state after Create/Update.
331331
cachedProjects sync.Map
332+
333+
// cachedSlugs caches project ID -> slug mappings resolved via the console API.
334+
// This avoids redundant API calls when multiple CRUD operations resolve the
335+
// same project_id within a single Terraform run.
336+
cachedSlugs sync.Map
332337
}
333338

334339
// NewOryClient creates a new Ory API client.
@@ -454,6 +459,39 @@ func (c *OryClient) WorkspaceID() string {
454459
return c.config.WorkspaceID
455460
}
456461

462+
// ResolveProjectSlug resolves a project ID to its slug via the console API.
463+
// Results are cached to avoid redundant API calls within a single Terraform run.
464+
func (c *OryClient) ResolveProjectSlug(ctx context.Context, projectID string) (string, error) {
465+
if slug, ok := c.cachedSlugs.Load(projectID); ok {
466+
return slug.(string), nil
467+
}
468+
if c.consoleClient == nil {
469+
return "", fmt.Errorf("console API client not configured: workspace_api_key is required to resolve project_id to slug")
470+
}
471+
project, err := c.GetProject(ctx, projectID)
472+
if err != nil {
473+
return "", fmt.Errorf("resolving project slug for project %s: %w", projectID, err)
474+
}
475+
slug := project.GetSlug()
476+
c.cachedSlugs.Store(projectID, slug)
477+
return slug, nil
478+
}
479+
480+
// ProjectClientForProject returns a project-scoped client for the given project ID.
481+
// It resolves the project slug via the console API and uses the provider's project API key.
482+
func (c *OryClient) ProjectClientForProject(ctx context.Context, projectID string) (*OryClient, error) {
483+
slug, err := c.ResolveProjectSlug(ctx, projectID)
484+
if err != nil {
485+
return nil, err
486+
}
487+
apiKey := c.config.ProjectAPIKey
488+
if apiKey == "" {
489+
return nil, fmt.Errorf("project_api_key is required for project API operations (JWK, OAuth2, etc.): " +
490+
"set it on the provider or via ORY_PROJECT_API_KEY environment variable")
491+
}
492+
return c.WithProjectCredentials(slug, apiKey), nil
493+
}
494+
457495
// WithProjectCredentials returns a new OryClient that uses the given project
458496
// credentials. The returned client shares the console client with the parent
459497
// but has its own isolated project client (lazily initialized).

internal/resources/jwk/resource.go

Lines changed: 113 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"strings"
78

89
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
910
"github.com/hashicorp/terraform-plugin-framework/path"
@@ -38,6 +39,7 @@ type JWKResource struct {
3839
// JWKResourceModel describes the resource data model.
3940
type JWKResourceModel struct {
4041
ID types.String `tfsdk:"id"`
42+
ProjectID types.String `tfsdk:"project_id"`
4143
SetID types.String `tfsdk:"set_id"`
4244
KeyID types.String `tfsdk:"key_id"`
4345
Algorithm types.String `tfsdk:"algorithm"`
@@ -61,10 +63,11 @@ generate and manage custom key sets for your Ory project.
6163
6264
` + "```hcl" + `
6365
resource "ory_json_web_key_set" "signing" {
64-
set_id = "my-signing-keys"
65-
key_id = "sig-key-1"
66-
algorithm = "RS256"
67-
use = "sig"
66+
project_id = var.ory_project_id
67+
set_id = "my-signing-keys"
68+
key_id = "sig-key-1"
69+
algorithm = "RS256"
70+
use = "sig"
6871
}
6972
` + "```" + `
7073
@@ -81,9 +84,10 @@ resource "ory_json_web_key_set" "encryption" {
8184
8285
## Import
8386
84-
JWK sets can be imported using their set ID:
87+
JWK sets can be imported using the format ` + "`project_id/set_id`" + ` or just ` + "`set_id`" + `:
8588
8689
` + "```shell" + `
90+
terraform import ory_json_web_key_set.signing <project-id>/my-signing-keys
8791
terraform import ory_json_web_key_set.signing my-signing-keys
8892
` + "```" + `
8993
`
@@ -100,6 +104,15 @@ func (r *JWKResource) Schema(ctx context.Context, req resource.SchemaRequest, re
100104
stringplanmodifier.UseStateForUnknown(),
101105
},
102106
},
107+
"project_id": schema.StringAttribute{
108+
Description: "The project ID. If not set, uses the provider's project_id.",
109+
Optional: true,
110+
Computed: true,
111+
PlanModifiers: []planmodifier.String{
112+
stringplanmodifier.UseStateForUnknown(),
113+
stringplanmodifier.RequiresReplace(),
114+
},
115+
},
103116
"set_id": schema.StringAttribute{
104117
Description: "The ID of the JSON Web Key Set.",
105118
Required: true,
@@ -168,13 +181,31 @@ func (r *JWKResource) Create(ctx context.Context, req resource.CreateRequest, re
168181
return
169182
}
170183

184+
projectID := r.resolveProjectID(plan.ProjectID)
185+
if projectID == "" {
186+
resp.Diagnostics.AddError(
187+
"Missing Project ID",
188+
"project_id must be set either in the resource or provider configuration.",
189+
)
190+
return
191+
}
192+
193+
projectClient, err := r.resolveProjectClient(ctx, projectID)
194+
if err != nil {
195+
resp.Diagnostics.AddError(
196+
"Error Resolving Project",
197+
err.Error(),
198+
)
199+
return
200+
}
201+
171202
body := ory.CreateJsonWebKeySet{
172203
Alg: plan.Algorithm.ValueString(),
173204
Kid: plan.KeyID.ValueString(),
174205
Use: plan.Use.ValueString(),
175206
}
176207

177-
jwks, err := r.client.CreateJsonWebKeySet(ctx, plan.SetID.ValueString(), body)
208+
jwks, err := projectClient.CreateJsonWebKeySet(ctx, plan.SetID.ValueString(), body)
178209
if err != nil {
179210
resp.Diagnostics.AddError(
180211
"Error Creating JSON Web Key Set",
@@ -184,6 +215,7 @@ func (r *JWKResource) Create(ctx context.Context, req resource.CreateRequest, re
184215
}
185216

186217
plan.ID = types.StringValue(plan.SetID.ValueString())
218+
plan.ProjectID = types.StringValue(projectID)
187219

188220
// Serialize the keys to JSON
189221
if len(jwks.Keys) > 0 {
@@ -204,9 +236,26 @@ func (r *JWKResource) Read(ctx context.Context, req resource.ReadRequest, resp *
204236
return
205237
}
206238

207-
jwks, err := r.client.GetJsonWebKeySet(ctx, state.SetID.ValueString())
239+
projectID := r.resolveProjectID(state.ProjectID)
240+
if projectID == "" {
241+
resp.Diagnostics.AddError(
242+
"Missing Project ID",
243+
"project_id must be set either in the resource or provider configuration.",
244+
)
245+
return
246+
}
247+
248+
projectClient, err := r.resolveProjectClient(ctx, projectID)
249+
if err != nil {
250+
resp.Diagnostics.AddError(
251+
"Error Resolving Project",
252+
err.Error(),
253+
)
254+
return
255+
}
256+
257+
jwks, err := projectClient.GetJsonWebKeySet(ctx, state.SetID.ValueString())
208258
if err != nil {
209-
// Check if it's a 404
210259
resp.Diagnostics.AddError(
211260
"Error Reading JSON Web Key Set",
212261
"Could not read JWK set "+state.SetID.ValueString()+": "+err.Error(),
@@ -219,6 +268,8 @@ func (r *JWKResource) Read(ctx context.Context, req resource.ReadRequest, resp *
219268
return
220269
}
221270

271+
state.ProjectID = types.StringValue(projectID)
272+
222273
// Serialize the keys to JSON
223274
keysJSON, err := json.Marshal(jwks)
224275
if err == nil {
@@ -253,7 +304,25 @@ func (r *JWKResource) Delete(ctx context.Context, req resource.DeleteRequest, re
253304
return
254305
}
255306

256-
err := r.client.DeleteJsonWebKeySet(ctx, state.SetID.ValueString())
307+
projectID := r.resolveProjectID(state.ProjectID)
308+
if projectID == "" {
309+
resp.Diagnostics.AddError(
310+
"Missing Project ID",
311+
"project_id must be set either in the resource or provider configuration.",
312+
)
313+
return
314+
}
315+
316+
projectClient, err := r.resolveProjectClient(ctx, projectID)
317+
if err != nil {
318+
resp.Diagnostics.AddError(
319+
"Error Resolving Project",
320+
err.Error(),
321+
)
322+
return
323+
}
324+
325+
err = projectClient.DeleteJsonWebKeySet(ctx, state.SetID.ValueString())
257326
if err != nil {
258327
resp.Diagnostics.AddError(
259328
"Error Deleting JSON Web Key Set",
@@ -264,6 +333,39 @@ func (r *JWKResource) Delete(ctx context.Context, req resource.DeleteRequest, re
264333
}
265334

266335
func (r *JWKResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
267-
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("set_id"), req.ID)...)
268-
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), req.ID)...)
336+
// Import ID format: project_id/set_id or just set_id (uses provider's project_id)
337+
id := req.ID
338+
var projectID, setID string
339+
340+
if strings.Contains(id, "/") {
341+
parts := strings.SplitN(id, "/", 2)
342+
projectID = parts[0]
343+
setID = parts[1]
344+
} else {
345+
setID = id
346+
}
347+
348+
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("set_id"), setID)...)
349+
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), setID)...)
350+
if projectID != "" {
351+
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectID)...)
352+
}
353+
}
354+
355+
// resolveProjectID returns the project ID from the resource attribute or falls back to the provider's project_id.
356+
func (r *JWKResource) resolveProjectID(tfProjectID types.String) string {
357+
if !tfProjectID.IsNull() && !tfProjectID.IsUnknown() {
358+
return tfProjectID.ValueString()
359+
}
360+
return r.client.ProjectID()
361+
}
362+
363+
// resolveProjectClient returns a client configured for the given project.
364+
// If project_id is provided, it resolves the slug via the console API.
365+
// Otherwise, it falls back to the provider's project credentials.
366+
func (r *JWKResource) resolveProjectClient(ctx context.Context, projectID string) (*client.OryClient, error) {
367+
if projectID != "" {
368+
return r.client.ProjectClientForProject(ctx, projectID)
369+
}
370+
return r.client, nil
269371
}

0 commit comments

Comments
 (0)