Skip to content

Commit 3195cb5

Browse files
authored
Add support for sub mapper (#1323)
Signed-off-by: Eunseok Kang <[email protected]>
1 parent 226aa90 commit 3195cb5

File tree

5 files changed

+662
-0
lines changed

5 files changed

+662
-0
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
page_title: "keycloak_openid_sub_protocol_mapper Resource"
3+
---
4+
5+
# keycloak\_openid\_sub\_protocol\_mapper Resource
6+
7+
Allows for creating and managing sub protocol mappers within Keycloak.
8+
9+
Sub protocol mappers add the Subject (sub) claim to tokens. The sub claim contains the user ID and is a standard claim in OpenID Connect tokens.
10+
11+
Protocol mappers can be defined for a single client, or they can be defined for a client scope which can be shared between
12+
multiple different clients.
13+
14+
## Example Usage (Client)
15+
16+
```hcl
17+
resource "keycloak_realm" "realm" {
18+
realm = "my-realm"
19+
enabled = true
20+
}
21+
22+
resource "keycloak_openid_client" "openid_client" {
23+
realm_id = keycloak_realm.realm.id
24+
client_id = "client"
25+
26+
name = "client"
27+
enabled = true
28+
29+
access_type = "CONFIDENTIAL"
30+
valid_redirect_uris = [
31+
"http://localhost:8080/openid-callback"
32+
]
33+
}
34+
35+
resource "keycloak_openid_sub_protocol_mapper" "sub_mapper" {
36+
realm_id = keycloak_realm.realm.id
37+
client_id = keycloak_openid_client.openid_client.id
38+
name = "sub-mapper"
39+
}
40+
```
41+
42+
## Example Usage (Client Scope)
43+
44+
```hcl
45+
resource "keycloak_realm" "realm" {
46+
realm = "my-realm"
47+
enabled = true
48+
}
49+
50+
resource "keycloak_openid_client_scope" "client_scope" {
51+
realm_id = keycloak_realm.realm.id
52+
name = "client-scope"
53+
}
54+
55+
resource "keycloak_openid_sub_protocol_mapper" "sub_mapper" {
56+
realm_id = keycloak_realm.realm.id
57+
client_scope_id = keycloak_openid_client_scope.client_scope.id
58+
name = "sub-mapper"
59+
}
60+
```
61+
62+
## Argument Reference
63+
64+
- `realm_id` - (Required) The realm this protocol mapper exists within.
65+
- `name` - (Required) The display name of this protocol mapper in the GUI.
66+
- `client_id` - (Optional) The client this protocol mapper should be attached to. Conflicts with `client_scope_id`. One of `client_id` or `client_scope_id` must be specified.
67+
- `client_scope_id` - (Optional) The client scope this protocol mapper should be attached to. Conflicts with `client_id`. One of `client_id` or `client_scope_id` must be specified.
68+
- `add_to_access_token` - (Optional) Indicates if the sub claim should be added to the access token. Defaults to `true`.
69+
- `add_to_token_introspection` - (Optional) Indicates if the sub claim should be added to the token introspection response. Defaults to `true`.
70+
71+
## Import
72+
73+
Protocol mappers can be imported using one of the following formats:
74+
- Client: `{{realm_id}}/client/{{client_keycloak_id}}/{{protocol_mapper_id}}`
75+
- Client Scope: `{{realm_id}}/client-scope/{{client_scope_keycloak_id}}/{{protocol_mapper_id}}`
76+
77+
Example:
78+
79+
```bash
80+
$ terraform import keycloak_openid_sub_protocol_mapper.sub_mapper my-realm/client/a7202154-8793-4656-b655-1dd18c181e14/71602afa-f7d1-4788-8c49-ef8fd00af0f4
81+
$ terraform import keycloak_openid_sub_protocol_mapper.sub_mapper my-realm/client-scope/b799ea7e-73ee-4a73-990a-1eafebe8e20a/71602afa-f7d1-4788-8c49-ef8fd00af0f4
82+
```
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package keycloak
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strconv"
7+
)
8+
9+
type OpenIdSubProtocolMapper struct {
10+
Id string
11+
Name string
12+
RealmId string
13+
ClientId string
14+
ClientScopeId string
15+
16+
AddToAccessToken bool
17+
AddToTokenIntrospection bool
18+
}
19+
20+
func (mapper *OpenIdSubProtocolMapper) convertToGenericProtocolMapper() *protocolMapper {
21+
return &protocolMapper{
22+
Id: mapper.Id,
23+
Name: mapper.Name,
24+
Protocol: "openid-connect",
25+
ProtocolMapper: "oidc-sub-mapper",
26+
Config: map[string]string{
27+
addToAccessTokenField: strconv.FormatBool(mapper.AddToAccessToken),
28+
addToTokenIntrospectionField: strconv.FormatBool(mapper.AddToTokenIntrospection),
29+
},
30+
}
31+
}
32+
33+
func (protocolMapper *protocolMapper) convertToOpenIdSubProtocolMapper(realmId, clientId, clientScopeId string) (*OpenIdSubProtocolMapper, error) {
34+
addToAccessToken, err := parseBoolAndTreatEmptyStringAsFalse(protocolMapper.Config[addToAccessTokenField])
35+
if err != nil {
36+
return nil, err
37+
}
38+
39+
addToTokenIntrospection, err := parseBoolAndTreatEmptyStringAsFalse(protocolMapper.Config[addToTokenIntrospectionField])
40+
if err != nil {
41+
return nil, err
42+
}
43+
44+
return &OpenIdSubProtocolMapper{
45+
Id: protocolMapper.Id,
46+
Name: protocolMapper.Name,
47+
RealmId: realmId,
48+
ClientId: clientId,
49+
ClientScopeId: clientScopeId,
50+
51+
AddToAccessToken: addToAccessToken,
52+
AddToTokenIntrospection: addToTokenIntrospection,
53+
}, nil
54+
}
55+
56+
func (keycloakClient *KeycloakClient) GetOpenIdSubProtocolMapper(ctx context.Context, realmId, clientId, clientScopeId, mapperId string) (*OpenIdSubProtocolMapper, error) {
57+
var protoMapper *protocolMapper
58+
59+
err := keycloakClient.get(ctx, individualProtocolMapperPath(realmId, clientId, clientScopeId, mapperId), &protoMapper, nil)
60+
if err != nil {
61+
return nil, err
62+
}
63+
64+
return protoMapper.convertToOpenIdSubProtocolMapper(realmId, clientId, clientScopeId)
65+
}
66+
67+
func (keycloakClient *KeycloakClient) DeleteOpenIdSubProtocolMapper(ctx context.Context, realmId, clientId, clientScopeId, mapperId string) error {
68+
return keycloakClient.delete(ctx, individualProtocolMapperPath(realmId, clientId, clientScopeId, mapperId), nil)
69+
}
70+
71+
func (keycloakClient *KeycloakClient) NewOpenIdSubProtocolMapper(ctx context.Context, mapper *OpenIdSubProtocolMapper) error {
72+
path := protocolMapperPath(mapper.RealmId, mapper.ClientId, mapper.ClientScopeId)
73+
74+
_, location, err := keycloakClient.post(ctx, path, mapper.convertToGenericProtocolMapper())
75+
if err != nil {
76+
return err
77+
}
78+
79+
mapper.Id = getIdFromLocationHeader(location)
80+
81+
return nil
82+
}
83+
84+
func (keycloakClient *KeycloakClient) UpdateOpenIdSubProtocolMapper(ctx context.Context, mapper *OpenIdSubProtocolMapper) error {
85+
path := individualProtocolMapperPath(mapper.RealmId, mapper.ClientId, mapper.ClientScopeId, mapper.Id)
86+
87+
return keycloakClient.put(ctx, path, mapper.convertToGenericProtocolMapper())
88+
}
89+
90+
func (keycloakClient *KeycloakClient) ValidateOpenIdSubProtocolMapper(ctx context.Context, mapper *OpenIdSubProtocolMapper) error {
91+
if mapper.ClientId == "" && mapper.ClientScopeId == "" {
92+
return fmt.Errorf("validation error: one of ClientId or ClientScopeId must be set")
93+
}
94+
95+
protocolMappers, err := keycloakClient.listGenericProtocolMappers(ctx, mapper.RealmId, mapper.ClientId, mapper.ClientScopeId)
96+
if err != nil {
97+
return err
98+
}
99+
100+
for _, protocolMapper := range protocolMappers {
101+
if protocolMapper.Name == mapper.Name && protocolMapper.Id != mapper.Id {
102+
return fmt.Errorf("validation error: a protocol mapper with name %s already exists for this client", mapper.Name)
103+
}
104+
}
105+
106+
return nil
107+
}

provider/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ func KeycloakProvider(client *keycloak.KeycloakClient) *schema.Provider {
7272
"keycloak_openid_user_property_protocol_mapper": resourceKeycloakOpenIdUserPropertyProtocolMapper(),
7373
"keycloak_openid_group_membership_protocol_mapper": resourceKeycloakOpenIdGroupMembershipProtocolMapper(),
7474
"keycloak_openid_full_name_protocol_mapper": resourceKeycloakOpenIdFullNameProtocolMapper(),
75+
"keycloak_openid_sub_protocol_mapper": resourceKeycloakOpenIdSubProtocolMapper(),
7576
"keycloak_openid_hardcoded_claim_protocol_mapper": resourceKeycloakOpenIdHardcodedClaimProtocolMapper(),
7677
"keycloak_openid_audience_protocol_mapper": resourceKeycloakOpenIdAudienceProtocolMapper(),
7778
"keycloak_openid_audience_resolve_protocol_mapper": resourceKeycloakOpenIdAudienceResolveProtocolMapper(),
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
8+
9+
"github.com/keycloak/terraform-provider-keycloak/keycloak"
10+
)
11+
12+
func resourceKeycloakOpenIdSubProtocolMapper() *schema.Resource {
13+
return &schema.Resource{
14+
CreateContext: resourceKeycloakOpenIdSubProtocolMapperCreate,
15+
ReadContext: resourceKeycloakOpenIdSubProtocolMapperRead,
16+
UpdateContext: resourceKeycloakOpenIdSubProtocolMapperUpdate,
17+
DeleteContext: resourceKeycloakOpenIdSubProtocolMapperDelete,
18+
Importer: &schema.ResourceImporter{
19+
// import a mapper tied to a client:
20+
// {{realmId}}/client/{{clientId}}/{{protocolMapperId}}
21+
// or a client scope:
22+
// {{realmId}}/client-scope/{{clientScopeId}}/{{protocolMapperId}}
23+
StateContext: genericProtocolMapperImport,
24+
},
25+
Schema: map[string]*schema.Schema{
26+
"name": {
27+
Type: schema.TypeString,
28+
Required: true,
29+
Description: "A human-friendly name that will appear in the Keycloak console.",
30+
},
31+
"realm_id": {
32+
Type: schema.TypeString,
33+
Required: true,
34+
ForceNew: true,
35+
Description: "The realm id where the associated client or client scope exists.",
36+
},
37+
"client_id": {
38+
Type: schema.TypeString,
39+
Optional: true,
40+
ForceNew: true,
41+
Description: "The mapper's associated client. Cannot be used at the same time as client_scope_id.",
42+
ConflictsWith: []string{"client_scope_id"},
43+
},
44+
"client_scope_id": {
45+
Type: schema.TypeString,
46+
Optional: true,
47+
ForceNew: true,
48+
Description: "The mapper's associated client scope. Cannot be used at the same time as client_id.",
49+
ConflictsWith: []string{"client_id"},
50+
},
51+
"add_to_access_token": {
52+
Type: schema.TypeBool,
53+
Optional: true,
54+
Default: true,
55+
Description: "Indicates if the attribute should be a claim in the access token.",
56+
},
57+
"add_to_token_introspection": {
58+
Type: schema.TypeBool,
59+
Optional: true,
60+
Default: true,
61+
Description: "Indicates if the attribute should be a claim in the token introspection response body.",
62+
},
63+
},
64+
}
65+
}
66+
67+
func mapFromDataToOpenIdSubProtocolMapper(data *schema.ResourceData) *keycloak.OpenIdSubProtocolMapper {
68+
return &keycloak.OpenIdSubProtocolMapper{
69+
Id: data.Id(),
70+
Name: data.Get("name").(string),
71+
RealmId: data.Get("realm_id").(string),
72+
ClientId: data.Get("client_id").(string),
73+
ClientScopeId: data.Get("client_scope_id").(string),
74+
AddToAccessToken: data.Get("add_to_access_token").(bool),
75+
AddToTokenIntrospection: data.Get("add_to_token_introspection").(bool),
76+
}
77+
}
78+
79+
func mapFromOpenIdSubMapperToData(mapper *keycloak.OpenIdSubProtocolMapper, data *schema.ResourceData) {
80+
data.SetId(mapper.Id)
81+
data.Set("name", mapper.Name)
82+
data.Set("realm_id", mapper.RealmId)
83+
84+
if mapper.ClientId != "" {
85+
data.Set("client_id", mapper.ClientId)
86+
} else {
87+
data.Set("client_scope_id", mapper.ClientScopeId)
88+
}
89+
90+
data.Set("add_to_access_token", mapper.AddToAccessToken)
91+
data.Set("add_to_token_introspection", mapper.AddToTokenIntrospection)
92+
}
93+
94+
func resourceKeycloakOpenIdSubProtocolMapperCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
95+
keycloakClient := meta.(*keycloak.KeycloakClient)
96+
97+
openIdSubMapper := mapFromDataToOpenIdSubProtocolMapper(data)
98+
99+
err := keycloakClient.ValidateOpenIdSubProtocolMapper(ctx, openIdSubMapper)
100+
if err != nil {
101+
return diag.FromErr(err)
102+
}
103+
104+
err = keycloakClient.NewOpenIdSubProtocolMapper(ctx, openIdSubMapper)
105+
if err != nil {
106+
return diag.FromErr(err)
107+
}
108+
109+
mapFromOpenIdSubMapperToData(openIdSubMapper, data)
110+
111+
return resourceKeycloakOpenIdSubProtocolMapperRead(ctx, data, meta)
112+
}
113+
114+
func resourceKeycloakOpenIdSubProtocolMapperRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
115+
keycloakClient := meta.(*keycloak.KeycloakClient)
116+
realmId := data.Get("realm_id").(string)
117+
clientId := data.Get("client_id").(string)
118+
clientScopeId := data.Get("client_scope_id").(string)
119+
120+
openIdSubMapper, err := keycloakClient.GetOpenIdSubProtocolMapper(ctx, realmId, clientId, clientScopeId, data.Id())
121+
if err != nil {
122+
return handleNotFoundError(ctx, err, data)
123+
}
124+
125+
mapFromOpenIdSubMapperToData(openIdSubMapper, data)
126+
127+
return nil
128+
}
129+
130+
func resourceKeycloakOpenIdSubProtocolMapperUpdate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
131+
keycloakClient := meta.(*keycloak.KeycloakClient)
132+
133+
openIdSubMapper := mapFromDataToOpenIdSubProtocolMapper(data)
134+
135+
err := keycloakClient.ValidateOpenIdSubProtocolMapper(ctx, openIdSubMapper)
136+
if err != nil {
137+
return diag.FromErr(err)
138+
}
139+
140+
err = keycloakClient.UpdateOpenIdSubProtocolMapper(ctx, openIdSubMapper)
141+
if err != nil {
142+
return diag.FromErr(err)
143+
}
144+
145+
return resourceKeycloakOpenIdSubProtocolMapperRead(ctx, data, meta)
146+
}
147+
148+
func resourceKeycloakOpenIdSubProtocolMapperDelete(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
149+
keycloakClient := meta.(*keycloak.KeycloakClient)
150+
151+
realmId := data.Get("realm_id").(string)
152+
clientId := data.Get("client_id").(string)
153+
clientScopeId := data.Get("client_scope_id").(string)
154+
155+
return diag.FromErr(keycloakClient.DeleteOpenIdSubProtocolMapper(ctx, realmId, clientId, clientScopeId, data.Id()))
156+
}

0 commit comments

Comments
 (0)