Skip to content

Commit 169b453

Browse files
authored
Add keycloak_organization_identity_provider resource (#9)
* Add keycloak_organization_identity_provider resource --------- Co-authored-by: lucdew <[email protected]>
1 parent 8a8e273 commit 169b453

7 files changed

+506
-7
lines changed

docs/resources/organization.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ resource "keycloak_organization" "engineering" {
2323
description = "Organization for the engineering department"
2424
2525
domain {
26-
name = "engineering.example.com"
27-
verified = true
26+
name = "engineering.example.com"
27+
verified = true
2828
}
2929
3030
domain {
31-
name = "engineering-lab.example.com"
31+
name = "engineering-lab.example.com"
3232
}
3333
3434
attributes = {
@@ -37,8 +37,6 @@ resource "keycloak_organization" "engineering" {
3737
}
3838
}
3939
40-
41-
4240
```
4341

4442
## Argument Reference
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
page_title: "keycloak_organization_identity_provider Resource"
3+
---
4+
5+
# keycloak_organization_identity_provider Resource
6+
7+
Allows to link an identity provider to an organization.
8+
9+
## Example Usage
10+
11+
```hcl
12+
resource "keycloak_realm" "realm" {
13+
realm = "my-realm"
14+
enabled = true
15+
}
16+
17+
resource "keycloak_organization" "engineering" {
18+
realm_id = keycloak_realm.example.id
19+
name = "engineering"
20+
alias = "engineering"
21+
description = "Organization for the engineering department"
22+
23+
domain {
24+
name = "example.com"
25+
}
26+
27+
domain {
28+
name = "anotherexample.com"
29+
}
30+
}
31+
32+
resource "keycloak_oidc_identity_provider" "oidc" {
33+
realm_id = keycloak_realm.example.id
34+
provider_id = "oidc"
35+
alias = "myidp"
36+
authorization_url = "https://example.com/auth"
37+
token_url = "https://example.com/token"
38+
client_id = "example_id"
39+
client_secret = "example_token"
40+
default_scopes = "openid random"
41+
}
42+
43+
resource "keycloak_organization_identity_provider" "oidc" {
44+
realm_id = data.keycloak_realm.realm.id
45+
organization_id = keycloak_organization.engineering.id
46+
identity_provider_alias = keycloak_oidc_identity_provider.oidc.alias
47+
domain = "example.com"
48+
redirect_email_domain_matches = true
49+
}
50+
51+
```
52+
53+
## Argument Reference
54+
55+
- `realm_id` - (Required) The realm this identity provider and organization exist in.
56+
- `organization_id` - (Required) The unique ID of the organization.
57+
- `identity_provider_alias` - (Required) The alias of the identity provider to link to the organization.
58+
- `domain` - (Optional) the domain associated to the identity provider.
59+
- `redirect_email_domain_matches` - (Optional) When true, redirects users to the identity provider if the user's email matches the domain.
60+
61+
## Import
62+
63+
Organization identity providers can be imported using the format `{{realm_id}}/{{organization_id}}/{{identity_provider_alias}}`, where `organization` is the unique ID that Keycloak assigns to the organization upon creation. This value can be found in the URI when editing this organization in the GUI, and is typically a GUID.
64+
65+
Example:
66+
67+
```bash
68+
$ terraform import keycloak_organization_identity_provider.oidc my-realm/934a4a4e-28bd-4703-a0fa-332df153aabd/myidp
69+
```

keycloak/organization.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,28 @@ func (keycloakClient *KeycloakClient) CreateOrganization(ctx context.Context, or
3838
return nil
3939
}
4040

41+
func (keycloakClient *KeycloakClient) LinkIdentityProviderToOrganization(ctx context.Context, realmId string, orgId string, idpAlias string) error {
42+
path := fmt.Sprintf("/realms/%s/organizations/%s/identity-providers", realmId, orgId)
43+
44+
_, err := keycloakClient.sendRaw(ctx, path, []byte(idpAlias))
45+
46+
return err
47+
}
48+
49+
func (keycloakClient *KeycloakClient) UnlinkIdentityProviderToOrganization(ctx context.Context, realmId string, orgId string, idpAlias string) error {
50+
path := fmt.Sprintf("/realms/%s/organizations/%s/identity-providers/%s", realmId, orgId, idpAlias)
51+
52+
err := keycloakClient.delete(ctx, path, nil)
53+
54+
return err
55+
}
56+
57+
func (keycloakClient *KeycloakClient) CheckIdentityProviderLinkToOrganization(ctx context.Context, realmId string, orgId string, idpAlias string) error {
58+
path := fmt.Sprintf("/realms/%s/organizations/%s/identity-providers/%s", realmId, orgId, idpAlias)
59+
_, err := keycloakClient.getRaw(ctx, path, nil)
60+
return err
61+
}
62+
4163
// GetOrganizationsPath returns the URL for the organization API endpoint
4264
func (keycloakClient *KeycloakClient) GetOrganizationsPath(realmId string) string {
4365
return fmt.Sprintf("/realms/%s/organizations", realmId)

provider/generic_keycloak_identity_provider.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,21 +125,24 @@ func resourceKeycloakIdentityProvider() *schema.Resource {
125125
},
126126
"organization": {
127127
Type: schema.TypeList,
128+
Computed: true,
128129
Optional: true,
129130
MaxItems: 1,
130131
Elem: &schema.Resource{
131132
Schema: map[string]*schema.Schema{
132133
"organization_id": {
133134
Type: schema.TypeString,
134-
Required: true,
135+
Computed: true,
136+
Optional: true,
135137
},
136138
"domain": {
137139
Type: schema.TypeString,
140+
Computed: true,
138141
Optional: true,
139-
Default: "",
140142
},
141143
"redirect_email_domain_matches": {
142144
Type: schema.TypeBool,
145+
Computed: true,
143146
Optional: true,
144147
},
145148
},

provider/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ func KeycloakProvider(client *keycloak.KeycloakClient) *schema.Provider {
122122
"keycloak_group_permissions": resourceKeycloakGroupPermissions(),
123123
"keycloak_authentication_bindings": resourceKeycloakAuthenticationBindings(),
124124
"keycloak_organization": resourceKeycloakOrganization(),
125+
"keycloak_organization_identity_provider": resourceKeycloakOrganizationIdentityProvider(),
125126
},
126127
Schema: map[string]*schema.Schema{
127128
"client_id": {
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
11+
12+
"github.com/keycloak/terraform-provider-keycloak/keycloak"
13+
"github.com/keycloak/terraform-provider-keycloak/keycloak/types"
14+
)
15+
16+
func resourceKeycloakOrganizationIdentityProvider() *schema.Resource {
17+
return &schema.Resource{
18+
CreateContext: resourceKeycloakOrganizationIdentityProviderCreate,
19+
ReadContext: resourceKeycloakOrganizationIdentityProviderRead,
20+
UpdateContext: resourceKeycloakOrganizationIdentityProviderUpdate,
21+
DeleteContext: resourceKeycloakOrganizationIdentityProviderDelete,
22+
Importer: &schema.ResourceImporter{
23+
StateContext: resourceKeycloakOrganizationIdentityProviderImport,
24+
},
25+
Schema: map[string]*schema.Schema{
26+
"realm_id": {
27+
Type: schema.TypeString,
28+
Required: true,
29+
ForceNew: true,
30+
ValidateFunc: validation.StringIsNotEmpty,
31+
},
32+
"organization_id": {
33+
Type: schema.TypeString,
34+
Required: true,
35+
ForceNew: true,
36+
ValidateFunc: validation.StringIsNotEmpty,
37+
},
38+
"identity_provider_alias": {
39+
Type: schema.TypeString,
40+
Required: true,
41+
ForceNew: true,
42+
ValidateFunc: validation.StringIsNotEmpty,
43+
},
44+
"domain": {
45+
Type: schema.TypeString,
46+
Optional: true,
47+
Default: "",
48+
},
49+
"redirect_email_domain_matches": {
50+
Type: schema.TypeBool,
51+
Optional: true,
52+
},
53+
},
54+
}
55+
}
56+
57+
func resourceKeycloakOrganizationIdentityProviderCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
58+
keycloakClient := meta.(*keycloak.KeycloakClient)
59+
60+
realmId, orgId, idpAlias := getOrganizationIdentityProviderFromData(data)
61+
62+
idp, err := keycloakClient.GetIdentityProvider(ctx, realmId, idpAlias)
63+
if err != nil {
64+
return diag.FromErr(err)
65+
}
66+
67+
idp.Config.OrgDomain = data.Get("domain").(string)
68+
idp.Config.OrgRedirectEmailMatches = types.KeycloakBoolQuoted(data.Get("redirect_email_domain_matches").(bool))
69+
keycloakClient.UpdateIdentityProvider(ctx, idp)
70+
71+
err = keycloakClient.LinkIdentityProviderToOrganization(ctx, realmId, orgId, idpAlias)
72+
if err != nil {
73+
return diag.FromErr(err)
74+
}
75+
76+
data.SetId(fmt.Sprintf("%s/%s/%s", realmId, orgId, idpAlias))
77+
78+
return resourceKeycloakOrganizationIdentityProviderRead(ctx, data, meta)
79+
}
80+
81+
func resourceKeycloakOrganizationIdentityProviderRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
82+
keycloakClient := meta.(*keycloak.KeycloakClient)
83+
84+
realmId, orgId, idpAlias := getOrganizationIdentityProviderFromData(data)
85+
86+
err := keycloakClient.CheckIdentityProviderLinkToOrganization(ctx, realmId, orgId, idpAlias)
87+
if err != nil {
88+
return handleNotFoundError(ctx, err, data)
89+
}
90+
91+
idp, err := keycloakClient.GetIdentityProvider(ctx, realmId, idpAlias)
92+
if err != nil {
93+
return diag.FromErr(err)
94+
}
95+
data.Set("domain", idp.Config.OrgDomain)
96+
data.Set("redirect_email_domain_matches", idp.Config.OrgRedirectEmailMatches)
97+
98+
return nil
99+
}
100+
101+
func resourceKeycloakOrganizationIdentityProviderUpdate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
102+
keycloakClient := meta.(*keycloak.KeycloakClient)
103+
104+
realmId, orgId, idpAlias := getOrganizationIdentityProviderFromData(data)
105+
106+
err := keycloakClient.CheckIdentityProviderLinkToOrganization(ctx, realmId, orgId, idpAlias)
107+
if err != nil {
108+
return handleNotFoundError(ctx, err, data)
109+
}
110+
111+
idp, err := keycloakClient.GetIdentityProvider(ctx, realmId, idpAlias)
112+
if err != nil {
113+
return diag.FromErr(err)
114+
}
115+
idp.Config.OrgDomain = data.Get("domain").(string)
116+
idp.Config.OrgRedirectEmailMatches = types.KeycloakBoolQuoted(data.Get("redirect_email_domain_matches").(bool))
117+
keycloakClient.UpdateIdentityProvider(ctx, idp)
118+
119+
return nil
120+
}
121+
122+
func resourceKeycloakOrganizationIdentityProviderDelete(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
123+
keycloakClient := meta.(*keycloak.KeycloakClient)
124+
125+
realmId, orgId, idpAlias := getOrganizationIdentityProviderFromData(data)
126+
127+
err := keycloakClient.UnlinkIdentityProviderToOrganization(ctx, realmId, orgId, idpAlias)
128+
if err != nil {
129+
return diag.FromErr(err)
130+
}
131+
132+
return nil
133+
}
134+
135+
func resourceKeycloakOrganizationIdentityProviderImport(ctx context.Context, data *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
136+
parts := strings.Split(data.Id(), "/")
137+
if len(parts) != 3 {
138+
return nil, fmt.Errorf("invalid import. Supported format: {{realm}}/{{organizationId}}/{{identityProviderAlias}}")
139+
}
140+
141+
data.Set("realm_id", parts[0])
142+
data.Set("organization_id", parts[1])
143+
data.Set("identity_provider_alias", parts[2])
144+
145+
return []*schema.ResourceData{data}, nil
146+
}
147+
148+
func getOrganizationIdentityProviderFromData(data *schema.ResourceData) (realmId, orgId, idpAlias string) {
149+
realmId = data.Get("realm_id").(string)
150+
orgId = data.Get("organization_id").(string)
151+
idpAlias = data.Get("identity_provider_alias").(string)
152+
153+
return
154+
}

0 commit comments

Comments
 (0)