Skip to content

Commit 7ff90dc

Browse files
committedMay 31, 2020
Add initial implementation
1 parent 4aeb0af commit 7ff90dc

File tree

11 files changed

+963
-0
lines changed

11 files changed

+963
-0
lines changed
 

‎.gitignore

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/bin
2+
/vendor
3+
4+
.terraform
5+
terraform-provider-oryhydra
6+
terraform.tfstate
7+
terraform.tfstate.*

‎Makefile

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.PHONY: build
2+
build:
3+
@go build -o ./bin/terraform-provider-oryhydra
4+
5+
.PHONY: prepare-examples
6+
prepare-examples:
7+
@ln -s $(shell pwd)/bin/terraform-provider-oryhydra ./examples/oryhydra_oauth2_client/terraform-provider-oryhydra

‎README.md

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# ORY Hydra Terraform Provider
2+
3+
Terraform provider for [ORY Hydra](https://github.com/ory/hydra).
4+
5+
## Usage
6+
7+
For example usage see [examples](examples/README.md).
8+
9+
## Implemented resources
10+
11+
- OAuth2 Client - `oryhydra_oauth2_client`

‎examples/README.md

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Examples
2+
3+
First of all you need to build the provider and link it to the examples:
4+
5+
```shell script
6+
make build
7+
make prepare-examples
8+
```
9+
10+
To test examples, you need a running Hydra instance. For demonstrational purposes,
11+
we can run Hydra locally as a Docker container:
12+
13+
```shell script
14+
docker container run -i -t --rm --name hydra \
15+
-p 4444:4444 \
16+
-p 4445:4445 \
17+
-e LOG_LEVEL=debug \
18+
-e DSN=memory \
19+
oryd/hydra:v1.4.10 serve all --dangerous-force-http
20+
```
21+
22+
Then you can simply run examples as usual terraform project. Enter a particular example directory (e.g. `cd examples/oryhydra_oauth2_client`)
23+
and run:
24+
25+
```shell script
26+
terraform init
27+
28+
terraform plan
29+
30+
terraform apply
31+
```
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
provider "oryhydra" {
2+
url = "http://localhost:4445"
3+
}
4+
5+
resource "oryhydra_oauth2_client" "example_1" {
6+
client_name = "Example 1"
7+
}
8+
9+
resource "oryhydra_oauth2_client" "example_2" {
10+
client_id = "example_2"
11+
client_name = "Example 2"
12+
client_secret = "super-secret!"
13+
}
14+
15+
resource "oryhydra_oauth2_client" "example_3" {
16+
client_name = "Example 3"
17+
client_metadata = {
18+
"Foo" = "Bar"
19+
}
20+
21+
scopes = ["offline", "openid"]
22+
grant_types = ["refresh_token", "authorization_code"]
23+
response_types = ["code"]
24+
25+
audience = ["http://localhost:8080"]
26+
redirect_uris = ["http://localhost:8080/redirect.html"]
27+
28+
subject_type = "public"
29+
token_endpoint_auth_method = "none"
30+
}

‎go.mod

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module github.com/hypnoglow/terraform-provider-oryhydra
2+
3+
go 1.14
4+
5+
require (
6+
github.com/go-openapi/runtime v0.19.15
7+
github.com/hashicorp/go-cleanhttp v0.5.1
8+
github.com/hashicorp/terraform-plugin-sdk v1.13.0
9+
github.com/ory/hydra-client-go v1.4.10
10+
)

‎go.sum

+405
Large diffs are not rendered by default.

‎main.go

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package main
2+
3+
import (
4+
"github.com/hashicorp/terraform-plugin-sdk/plugin"
5+
"github.com/hashicorp/terraform-plugin-sdk/terraform"
6+
7+
"github.com/hypnoglow/terraform-provider-oryhydra/oryhydra"
8+
)
9+
10+
func main() {
11+
plugin.Serve(&plugin.ServeOpts{
12+
ProviderFunc: func() terraform.ResourceProvider {
13+
return oryhydra.Provider()
14+
},
15+
})
16+
}

‎oryhydra/logger.go

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package oryhydra
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"os"
7+
"time"
8+
)
9+
10+
var (
11+
lg logger = nopLogger{}
12+
)
13+
14+
func init() {
15+
fpath := os.Getenv("TERRAFORM_PROVIDER_ORYHYDRA_LOG_FILE_PATH")
16+
if fpath == "" {
17+
return
18+
}
19+
20+
f, err := os.OpenFile(fpath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
21+
if err != nil {
22+
panic(fmt.Errorf("open log file: %v", err))
23+
}
24+
25+
lg = log.New(f, "", 0)
26+
lg.Printf("----- %s -----", time.Now().Format(time.RFC3339))
27+
28+
log.SetOutput(f)
29+
}
30+
31+
type logger interface {
32+
Print(v ...interface{})
33+
Printf(format string, v ...interface{})
34+
}
35+
36+
type nopLogger struct{}
37+
38+
func (n nopLogger) Print(v ...interface{}) {
39+
return
40+
}
41+
42+
func (n nopLogger) Printf(format string, v ...interface{}) {
43+
return
44+
}

‎oryhydra/provider.go

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package oryhydra
2+
3+
import (
4+
"fmt"
5+
"net/url"
6+
7+
httptransport "github.com/go-openapi/runtime/client"
8+
"github.com/hashicorp/go-cleanhttp"
9+
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
10+
hydra "github.com/ory/hydra-client-go/client"
11+
"github.com/ory/hydra-client-go/client/admin"
12+
)
13+
14+
func Provider() *schema.Provider {
15+
return &schema.Provider{
16+
Schema: map[string]*schema.Schema{
17+
"url": {
18+
Type: schema.TypeString,
19+
Required: true,
20+
DefaultFunc: schema.EnvDefaultFunc("ORY_HYDRA_URL", nil),
21+
},
22+
},
23+
ResourcesMap: map[string]*schema.Resource{
24+
"oryhydra_oauth2_client": resourceOAuth2Client(),
25+
},
26+
ConfigureFunc: configure,
27+
}
28+
}
29+
30+
func configure(data *schema.ResourceData) (interface{}, error) {
31+
adminURL := data.Get("url").(string)
32+
client, err := newHydraClient(adminURL)
33+
return client, err
34+
}
35+
36+
// newHydraClient returns a new configured hydra client.
37+
func newHydraClient(hydraAdminURL string) (admin.ClientService, error) {
38+
u, err := url.Parse(hydraAdminURL)
39+
if err != nil {
40+
return nil, fmt.Errorf("parse hydra url: %v", err)
41+
}
42+
43+
config := hydra.DefaultTransportConfig()
44+
config.Schemes = []string{u.Scheme}
45+
config.Host = u.Host
46+
if u.Path != "" {
47+
config.BasePath = u.Path
48+
}
49+
50+
transport := httptransport.NewWithClient(
51+
config.Host,
52+
config.BasePath,
53+
config.Schemes,
54+
cleanhttp.DefaultClient(),
55+
)
56+
57+
client := hydra.New(transport, nil)
58+
return client.Admin, nil
59+
}
+343
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
package oryhydra
2+
3+
import (
4+
"strings"
5+
6+
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
7+
"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
8+
"github.com/ory/hydra-client-go/client/admin"
9+
"github.com/ory/hydra-client-go/models"
10+
)
11+
12+
func resourceOAuth2Client() *schema.Resource {
13+
return &schema.Resource{
14+
Create: resourceOAuth2ClientCreate,
15+
Read: resourceOAuth2ClientRead,
16+
Update: resourceOAuth2ClientUpdate,
17+
Delete: resourceOAuth2ClientDelete,
18+
Importer: &schema.ResourceImporter{
19+
State: schema.ImportStatePassthrough,
20+
},
21+
Schema: map[string]*schema.Schema{
22+
"client_id": {
23+
Type: schema.TypeString,
24+
Optional: true,
25+
Computed: true,
26+
ForceNew: true,
27+
// client_id must not be empty string. It must be either unspecified (to make hydra generate the id)
28+
// or specified as id string.
29+
ValidateFunc: validation.NoZeroValues,
30+
},
31+
"client_secret": {
32+
Type: schema.TypeString,
33+
Optional: true,
34+
Computed: true,
35+
Sensitive: true,
36+
},
37+
"client_name": {
38+
Type: schema.TypeString,
39+
Optional: true,
40+
},
41+
"client_metadata": {
42+
Type: schema.TypeMap,
43+
Optional: true,
44+
Elem: &schema.Schema{
45+
Type: schema.TypeString,
46+
},
47+
},
48+
"scopes": {
49+
Type: schema.TypeList,
50+
Optional: true,
51+
Computed: true,
52+
Elem: &schema.Schema{
53+
Type: schema.TypeString,
54+
},
55+
},
56+
"grant_types": {
57+
Type: schema.TypeList,
58+
Optional: true,
59+
Elem: &schema.Schema{
60+
Type: schema.TypeString,
61+
ValidateFunc: validation.StringInSlice([]string{
62+
"refresh_token", "authorization_code", "client_credentials", "implicit",
63+
}, false),
64+
},
65+
},
66+
"response_types": {
67+
Type: schema.TypeList,
68+
Optional: true,
69+
Elem: &schema.Schema{
70+
Type: schema.TypeString,
71+
ValidateFunc: validation.StringInSlice([]string{
72+
"token", "code", "id_token",
73+
}, false),
74+
},
75+
},
76+
"audience": {
77+
Type: schema.TypeList,
78+
Optional: true,
79+
Elem: &schema.Schema{
80+
Type: schema.TypeString,
81+
},
82+
},
83+
"post_logout_redirect_uris": {
84+
Type: schema.TypeList,
85+
Optional: true,
86+
Elem: &schema.Schema{
87+
Type: schema.TypeString,
88+
},
89+
},
90+
"redirect_uris": {
91+
Type: schema.TypeList,
92+
Optional: true,
93+
Elem: &schema.Schema{
94+
Type: schema.TypeString,
95+
},
96+
},
97+
"owner": {
98+
Type: schema.TypeString,
99+
Optional: true,
100+
},
101+
"policy_uri": {
102+
Type: schema.TypeString,
103+
Optional: true,
104+
},
105+
"allowed_cors_origins": {
106+
Type: schema.TypeList,
107+
Optional: true,
108+
Elem: &schema.Schema{
109+
Type: schema.TypeString,
110+
},
111+
},
112+
"tos_uri": {
113+
Type: schema.TypeString,
114+
Optional: true,
115+
},
116+
"client_uri": {
117+
Type: schema.TypeString,
118+
Optional: true,
119+
},
120+
"logo_uri": {
121+
Type: schema.TypeString,
122+
Optional: true,
123+
},
124+
"contacts": {
125+
Type: schema.TypeList,
126+
Optional: true,
127+
Elem: &schema.Schema{
128+
Type: schema.TypeString,
129+
},
130+
},
131+
"subject_type": {
132+
Type: schema.TypeString,
133+
Optional: true,
134+
Default: "public",
135+
Computed: true,
136+
ValidateFunc: validation.StringInSlice([]string{
137+
"public", "pairwise",
138+
}, false),
139+
},
140+
"token_endpoint_auth_method": {
141+
Type: schema.TypeString,
142+
Optional: true,
143+
Default: "none",
144+
Computed: true,
145+
ValidateFunc: validation.StringInSlice([]string{
146+
"none", "client_secret_basic", "client_secret_post", "private_key_jwt",
147+
}, false),
148+
},
149+
},
150+
}
151+
}
152+
153+
func resourceOAuth2ClientCreate(d *schema.ResourceData, m interface{}) error {
154+
lg.Print("resourceOAuth2ClientCreate")
155+
156+
cli := m.(*admin.Client)
157+
158+
resp, err := cli.CreateOAuth2Client(
159+
admin.NewCreateOAuth2ClientParams().
160+
WithBody(expandClient(d)),
161+
)
162+
if err != nil {
163+
return err
164+
}
165+
166+
client := resp.Payload
167+
168+
d.SetId(client.ClientID)
169+
170+
// NOTE: client secret is only returned on create/update, not read.
171+
d.Set("client_secret", client.ClientSecret)
172+
173+
return resourceOAuth2ClientRead(d, m)
174+
}
175+
176+
func resourceOAuth2ClientRead(d *schema.ResourceData, m interface{}) error {
177+
lg.Print("resourceOAuth2ClientRead")
178+
179+
cli := m.(*admin.Client)
180+
181+
resp, err := cli.GetOAuth2Client(
182+
admin.NewGetOAuth2ClientParams().
183+
WithID(d.Id()),
184+
)
185+
if err != nil {
186+
return err
187+
}
188+
189+
client := resp.Payload
190+
191+
flattenClient(d, client)
192+
193+
return nil
194+
}
195+
196+
func resourceOAuth2ClientUpdate(d *schema.ResourceData, m interface{}) error {
197+
lg.Print("resourceOAuth2ClientUpdate")
198+
199+
client := expandClient(d)
200+
201+
cli := m.(*admin.Client)
202+
203+
_, err := cli.UpdateOAuth2Client(
204+
admin.NewUpdateOAuth2ClientParams().
205+
WithID(d.Id()).
206+
WithBody(client),
207+
)
208+
if err != nil {
209+
return err
210+
}
211+
212+
// NOTE: client secret is only returned on create/update, not read.
213+
d.Set("client_secret", client.ClientSecret)
214+
215+
return resourceOAuth2ClientRead(d, m)
216+
}
217+
218+
func resourceOAuth2ClientDelete(d *schema.ResourceData, m interface{}) error {
219+
lg.Print("resourceOAuth2ClientDelete")
220+
221+
cli := m.(*admin.Client)
222+
223+
_, err := cli.DeleteOAuth2Client(
224+
admin.NewDeleteOAuth2ClientParams().
225+
WithID(d.Id()),
226+
)
227+
if err != nil {
228+
return err
229+
}
230+
231+
return nil
232+
}
233+
234+
func expandClient(d *schema.ResourceData) *models.OAuth2Client {
235+
lg.Print("expandClient")
236+
237+
clientID := d.Get("client_id").(string)
238+
clientSecret := d.Get("client_secret").(string)
239+
clientName := d.Get("client_name").(string)
240+
clientMetadata := d.Get("client_metadata")
241+
lg.Printf("metadata: %T %v", clientMetadata, clientMetadata)
242+
243+
var scopeArray []string
244+
for _, sc := range d.Get("scopes").([]interface{}) {
245+
scopeArray = append(scopeArray, sc.(string))
246+
}
247+
scope := strings.Join(scopeArray, " ")
248+
249+
var grantTypes []string
250+
for _, gt := range d.Get("grant_types").([]interface{}) {
251+
grantTypes = append(grantTypes, gt.(string))
252+
}
253+
254+
var responseTypes []string
255+
for _, rt := range d.Get("response_types").([]interface{}) {
256+
responseTypes = append(responseTypes, rt.(string))
257+
}
258+
259+
var audience []string
260+
for _, aud := range d.Get("audience").([]interface{}) {
261+
audience = append(audience, aud.(string))
262+
}
263+
264+
var postLogoutRedirectUris []string
265+
for _, ru := range d.Get("post_logout_redirect_uris").([]interface{}) {
266+
postLogoutRedirectUris = append(postLogoutRedirectUris, ru.(string))
267+
}
268+
269+
var redirectUris []string
270+
for _, ru := range d.Get("redirect_uris").([]interface{}) {
271+
redirectUris = append(redirectUris, ru.(string))
272+
}
273+
274+
owner := d.Get("owner").(string)
275+
policyURI := d.Get("policy_uri").(string)
276+
277+
var allowedCorsOrigins []string
278+
for _, aco := range d.Get("allowed_cors_origins").([]interface{}) {
279+
allowedCorsOrigins = append(allowedCorsOrigins, aco.(string))
280+
}
281+
282+
tosURI := d.Get("tos_uri").(string)
283+
clientURI := d.Get("client_uri").(string)
284+
logoURI := d.Get("logo_uri").(string)
285+
286+
var contacts []string
287+
for _, c := range d.Get("contacts").([]interface{}) {
288+
contacts = append(contacts, c.(string))
289+
}
290+
291+
subjectType := d.Get("subject_type").(string)
292+
tokenEndpointAuthMethod := d.Get("token_endpoint_auth_method").(string)
293+
294+
return &models.OAuth2Client{
295+
ClientID: clientID,
296+
ClientName: clientName,
297+
ClientSecret: clientSecret,
298+
Metadata: clientMetadata,
299+
Scope: scope,
300+
GrantTypes: grantTypes,
301+
ResponseTypes: responseTypes,
302+
Audience: audience,
303+
PostLogoutRedirectUris: postLogoutRedirectUris,
304+
RedirectUris: redirectUris,
305+
Owner: owner,
306+
PolicyURI: policyURI,
307+
AllowedCorsOrigins: allowedCorsOrigins,
308+
TosURI: tosURI,
309+
ClientURI: clientURI,
310+
LogoURI: logoURI,
311+
Contacts: contacts,
312+
SubjectType: subjectType,
313+
TokenEndpointAuthMethod: tokenEndpointAuthMethod,
314+
}
315+
}
316+
317+
func flattenClient(d *schema.ResourceData, client *models.OAuth2Client) {
318+
lg.Print("flattenClient")
319+
320+
d.Set("client_id", client.ClientID)
321+
d.Set("client_name", client.ClientName)
322+
d.Set("client_metadata", client.Metadata)
323+
324+
lg.Printf("metadata: %T %v", client.Metadata, client.Metadata)
325+
326+
// NOTE: client secret is never returned from read operations, so don't set this field.
327+
328+
d.Set("scopes", strings.Split(client.Scope, " "))
329+
d.Set("grant_types", client.GrantTypes)
330+
d.Set("response_types", client.ResponseTypes)
331+
d.Set("audience", client.Audience)
332+
d.Set("post_logout_redirect_uris", client.PostLogoutRedirectUris)
333+
d.Set("redirect_uris", client.RedirectUris)
334+
d.Set("owner", client.Owner)
335+
d.Set("policy_uri", client.PolicyURI)
336+
d.Set("allowed_cors_origins", client.AllowedCorsOrigins)
337+
d.Set("tos_uri", client.TosURI)
338+
d.Set("client_uri", client.ClientURI)
339+
d.Set("logo_uri", client.LogoURI)
340+
d.Set("contacts", client.Contacts)
341+
d.Set("subject_type", client.SubjectType)
342+
d.Set("token_endpoint_auth_method", client.TokenEndpointAuthMethod)
343+
}

0 commit comments

Comments
 (0)
Please sign in to comment.