Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added default realm, added password grant type #88

Merged
merged 24 commits into from
Feb 13, 2019
Merged
30 changes: 24 additions & 6 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
version: 2
workflows:
version: 2
test:
jobs:
- client-credentials
- password
jobs:
build:
test-template: &test-template
docker:
- image: circleci/golang:1.11.4
- image: jboss/keycloak:4.2.1.Final
Expand All @@ -10,11 +16,6 @@ jobs:
KEYCLOAK_USER: keycloak
KEYCLOAK_PASSWORD: password
working_directory: /go/src/github.com/mrparkers/terraform-provider-keycloak
environment:
GO111MODULE: "on"
KEYCLOAK_CLIENT_ID: "terraform"
KEYCLOAK_CLIENT_SECRET: "884e0f95-0f42-4a63-9b1f-94274655669e"
KEYCLOAK_URL: "http://localhost:8080"
steps:
- checkout
- restore_cache:
Expand All @@ -30,3 +31,20 @@ jobs:
./scripts/wait-for-local-keycloak.sh
./scripts/create-terraform-client.sh
make testacc
client-credentials:
<<: *test-template
environment:
GO111MODULE: "on"
KEYCLOAK_CLIENT_ID: terraform
KEYCLOAK_URL: http://localhost:8080
KEYCLOAK_REALM: master
KEYCLOAK_CLIENT_SECRET: 884e0f95-0f42-4a63-9b1f-94274655669e
password:
<<: *test-template
environment:
GO111MODULE: "on"
KEYCLOAK_CLIENT_ID: admin-cli
KEYCLOAK_URL: http://localhost:8080
KEYCLOAK_REALM: master
KEYCLOAK_USER: keycloak
KEYCLOAK_PASSWORD: password
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this idea. I'll have to update the branch protection to require these new statuses set by CircleCI.

67 changes: 48 additions & 19 deletions keycloak/keycloak_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,42 +7,58 @@ import (
"io/ioutil"
"log"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"time"
)

type KeycloakClient struct {
baseUrl string
realm string
clientCredentials *ClientCredentials
httpClient *http.Client
}

type ClientCredentials struct {
ClientId string
ClientSecret string
Username string
Password string
GrantType string
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
}

const (
apiUrl = "/auth/admin"
tokenUrl = "/auth/realms/master/protocol/openid-connect/token"
tokenUrl = "%s/auth/realms/%s/protocol/openid-connect/token"
)

func NewKeycloakClient(baseUrl, clientId, clientSecret string) (*KeycloakClient, error) {
func NewKeycloakClient(baseUrl, clientId, clientSecret, realm, username, password string) (*KeycloakClient, error) {
httpClient := &http.Client{
Timeout: time.Second * 5,
}
clientCredentials := &ClientCredentials{
ClientId: clientId,
}
if password != "" && username != "" {
clientCredentials.Username = username
clientCredentials.Password = password
clientCredentials.GrantType = "password"
} else if clientSecret != "" {
clientCredentials.ClientSecret = clientSecret
clientCredentials.GrantType = "client_credentials"
} else {
return nil, fmt.Errorf("must specify client id, username and password for password grant, or client id and secret for client credentials grant")
}

keycloakClient := KeycloakClient{
baseUrl: baseUrl,
clientCredentials: &ClientCredentials{
ClientId: clientId,
ClientSecret: clientSecret,
},
httpClient: httpClient,
baseUrl: baseUrl,
clientCredentials: clientCredentials,
httpClient: httpClient,
realm: realm,
}

err := keycloakClient.login()
Expand All @@ -54,13 +70,16 @@ func NewKeycloakClient(baseUrl, clientId, clientSecret string) (*KeycloakClient,
}

func (keycloakClient *KeycloakClient) login() error {
accessTokenUrl := keycloakClient.baseUrl + tokenUrl

accessTokenUrl := fmt.Sprintf(tokenUrl, keycloakClient.baseUrl, keycloakClient.realm)
accessTokenData := url.Values{}

accessTokenData.Set("client_id", keycloakClient.clientCredentials.ClientId)
accessTokenData.Set("client_secret", keycloakClient.clientCredentials.ClientSecret)
accessTokenData.Set("grant_type", "client_credentials")
accessTokenData.Set("grant_type", keycloakClient.clientCredentials.GrantType)
if keycloakClient.clientCredentials.GrantType == "password" {
accessTokenData.Set("username", keycloakClient.clientCredentials.Username)
accessTokenData.Set("password", keycloakClient.clientCredentials.Password)
} else if keycloakClient.clientCredentials.GrantType == "client_credentials" {
accessTokenData.Set("client_secret", keycloakClient.clientCredentials.ClientSecret)
}

log.Printf("[DEBUG] Login request: %s", accessTokenData.Encode())

Expand Down Expand Up @@ -93,14 +112,16 @@ func (keycloakClient *KeycloakClient) login() error {
}

func (keycloakClient *KeycloakClient) refresh() error {
refreshTokenUrl := keycloakClient.baseUrl + tokenUrl

refreshTokenUrl := fmt.Sprintf(tokenUrl, keycloakClient.baseUrl, keycloakClient.realm)
refreshTokenData := url.Values{}

refreshTokenData.Set("grant_type", "refresh_token")
refreshTokenData.Set("client_id", keycloakClient.clientCredentials.ClientId)
refreshTokenData.Set("client_secret", keycloakClient.clientCredentials.ClientSecret)
refreshTokenData.Set("refresh_token", keycloakClient.clientCredentials.RefreshToken)
refreshTokenData.Set("grant_type", keycloakClient.clientCredentials.GrantType)
if keycloakClient.clientCredentials.GrantType == "password" {
refreshTokenData.Set("username", keycloakClient.clientCredentials.Username)
refreshTokenData.Set("password", keycloakClient.clientCredentials.Password)
} else if keycloakClient.clientCredentials.GrantType == "client_credentials" {
refreshTokenData.Set("client_secret", keycloakClient.clientCredentials.ClientSecret)
}

log.Printf("[DEBUG] Refresh request: %s", refreshTokenData.Encode())

Expand Down Expand Up @@ -158,7 +179,9 @@ func (keycloakClient *KeycloakClient) sendRequest(request *http.Request) ([]byte
requestPath := request.URL.Path

log.Printf("[DEBUG] Sending %s to %s", requestMethod, requestPath)
showBody := false
if request.Body != nil {
showBody = true
requestBody, err := request.GetBody()
if err != nil {
return nil, "", err
Expand All @@ -172,6 +195,12 @@ func (keycloakClient *KeycloakClient) sendRequest(request *http.Request) ([]byte

keycloakClient.addRequestHeaders(request)

dump, err := httputil.DumpRequest(request, showBody)
if err != nil {
return nil, "", err
}
log.Printf("[DEBUG] %s", dump)

response, err := keycloakClient.httpClient.Do(request)
if err != nil {
return nil, "", err
Expand Down
47 changes: 31 additions & 16 deletions keycloak/keycloak_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import (

var requiredEnvironmentVariables = []string{
"KEYCLOAK_CLIENT_ID",
"KEYCLOAK_CLIENT_SECRET",
"KEYCLOAK_URL",
"KEYCLOAK_REALM",
}

// Some actions, such as creating a realm, require a refresh
Expand All @@ -29,13 +29,22 @@ func TestAccKeycloakApiClientRefresh(t *testing.T) {
}
}

if v := os.Getenv("KEYCLOAK_CLIENT_SECRET"); v == "" {
if v := os.Getenv("KEYCLOAK_USER"); v == "" {
t.Fatal("KEYCLOAK_USER must be set for acceptance tests")
}
if v := os.Getenv("KEYCLOAK_PASSWORD"); v == "" {
t.Fatal("KEYCLOAK_PASSWORD must be set for acceptance tests")
}
}

// Disable [DEBUG] logs which terraform typically handles for you. Re-enable when finished
if tfLogLevel := os.Getenv("TF_LOG"); tfLogLevel == "" {
log.SetOutput(ioutil.Discard)
defer log.SetOutput(os.Stdout)
}

keycloakClient, err := NewKeycloakClient(os.Getenv("KEYCLOAK_URL"), os.Getenv("KEYCLOAK_CLIENT_ID"), os.Getenv("KEYCLOAK_CLIENT_SECRET"))
keycloakClient, err := NewKeycloakClient(os.Getenv("KEYCLOAK_URL"), os.Getenv("KEYCLOAK_CLIENT_ID"), os.Getenv("KEYCLOAK_CLIENT_SECRET"), os.Getenv("KEYCLOAK_REALM"), os.Getenv("KEYCLOAK_USER"), os.Getenv("KEYCLOAK_PASSWORD"))
if err != nil {
t.Fatalf("%s", err)
}
Expand All @@ -51,10 +60,14 @@ func TestAccKeycloakApiClientRefresh(t *testing.T) {
t.Fatalf("%s", err)
}

var oldAccessToken, oldRefreshToken, oldTokenType string

// A following GET for this realm will result in a 403, so we should save the current access and refresh token
oldAccessToken := keycloakClient.clientCredentials.AccessToken
oldRefreshToken := keycloakClient.clientCredentials.RefreshToken
oldTokenType := keycloakClient.clientCredentials.TokenType
if keycloakClient.clientCredentials.GrantType == "client_credentials" {
oldAccessToken = keycloakClient.clientCredentials.AccessToken
oldRefreshToken = keycloakClient.clientCredentials.RefreshToken
oldTokenType = keycloakClient.clientCredentials.TokenType
}

_, err = keycloakClient.GetRealm(realmName) // This should not fail since it will automatically refresh and try again
if err != nil {
Expand All @@ -67,19 +80,21 @@ func TestAccKeycloakApiClientRefresh(t *testing.T) {
t.Fatalf("%s", err)
}

newAccessToken := keycloakClient.clientCredentials.AccessToken
newRefreshToken := keycloakClient.clientCredentials.RefreshToken
newTokenType := keycloakClient.clientCredentials.TokenType
if keycloakClient.clientCredentials.GrantType == "client_credentials" {
newAccessToken := keycloakClient.clientCredentials.AccessToken
newRefreshToken := keycloakClient.clientCredentials.RefreshToken
newTokenType := keycloakClient.clientCredentials.TokenType

if oldAccessToken == newAccessToken {
t.Fatalf("expected access token to update after refresh")
}
if oldAccessToken == newAccessToken {
t.Fatalf("expected access token to update after refresh")
}

if oldRefreshToken == newRefreshToken {
t.Fatalf("expected refresh token to update after refresh")
}
if oldRefreshToken == newRefreshToken {
t.Fatalf("expected refresh token to update after refresh")
}

if oldTokenType != newTokenType {
t.Fatalf("expected token type to remain the same after refresh")
if oldTokenType != newTokenType {
t.Fatalf("expected token type to remain the same after refresh")
}
}
}
9 changes: 3 additions & 6 deletions provider/generic_protocol_mapper_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,15 @@ import (

func genericProtocolMapperImport(data *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) {
parts := strings.Split(data.Id(), "/")

if len(parts) != 4 {
return nil, fmt.Errorf("invalid import. supported import formats: {{realmId}}/client/{{clientId}}/{{protocolMapperId}} or {{realmId}}/client-scope/{{clientScopeId}}/{{protocolMapperId}}")
return nil, fmt.Errorf("invalid import. supported import formats: {{realmId}}/client/{{clientId}}/{{protocolMapperId}}, {{realmId}}/client-scope/{{clientScopeId}}/{{protocolMapperId}}")
}

realmId := parts[0]
parentResourceType := parts[1]
parentResourceId := parts[2]
mapperId := parts[3]

data.Set("realm_id", realmId)
data.SetId(mapperId)
data.Set("realm_id", parts[0])
data.SetId(parts[3])

if parentResourceType == "client" {
data.Set("client_id", parentResourceId)
Expand Down
10 changes: 6 additions & 4 deletions provider/keycloak_custom_user_federation.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package provider

import (
"fmt"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/helper/validation"
"github.com/mrparkers/terraform-provider-keycloak/keycloak"
Expand Down Expand Up @@ -154,11 +155,12 @@ func resourceKeycloakCustomUserFederationDelete(data *schema.ResourceData, meta
func resourceKeycloakCustomUserFederationImport(d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) {
parts := strings.Split(d.Id(), "/")

realm := parts[0]
id := parts[1]
if len(parts) != 2 {
return nil, fmt.Errorf("Invalid import. Supported import formats: {{realmId}}/{{userFederationId}}")
}

d.Set("realm_id", realm)
d.SetId(id)
d.Set("realm_id", parts[0])
d.SetId(parts[1])

return []*schema.ResourceData{d}, nil
}
12 changes: 6 additions & 6 deletions provider/keycloak_group.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package provider

import (
"fmt"
"github.com/hashicorp/terraform/helper/schema"
"github.com/mrparkers/terraform-provider-keycloak/keycloak"
"strings"
Expand Down Expand Up @@ -120,11 +121,10 @@ func resourceKeycloakGroupDelete(data *schema.ResourceData, meta interface{}) er
func resourceKeycloakGroupImport(d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) {
parts := strings.Split(d.Id(), "/")

realm := parts[0]
id := parts[1]

d.Set("realm_id", realm)
d.SetId(id)

if len(parts) != 2 {
return nil, fmt.Errorf("Invalid import. Supported import formats: {{realmId}}/{{groupId}}")
}
d.Set("realm_id", parts[0])
d.SetId(parts[1])
return []*schema.ResourceData{d}, nil
}
2 changes: 1 addition & 1 deletion provider/keycloak_group_memberships.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ func resourceKeycloakGroupMemberships() *schema.Resource {
func resourceKeycloakGroupMembershipsCreate(data *schema.ResourceData, meta interface{}) error {
keycloakClient := meta.(*keycloak.KeycloakClient)

realmId := data.Get("realm_id").(string)
groupId := data.Get("group_id").(string)
members := data.Get("members").(*schema.Set).List()
realmId := data.Get("realm_id").(string)

err := keycloakClient.ValidateGroupMembers(members)
if err != nil {
Expand Down
13 changes: 7 additions & 6 deletions provider/keycloak_ldap_full_name_mapper.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package provider

import (
"fmt"
"github.com/hashicorp/terraform/helper/schema"
"github.com/mrparkers/terraform-provider-keycloak/keycloak"
"strings"
Expand Down Expand Up @@ -146,13 +147,13 @@ func resourceKeycloakLdapFullNameMapperDelete(data *schema.ResourceData, meta in
func resourceKeycloakLdapGenericMapperImport(d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) {
parts := strings.Split(d.Id(), "/")

realm := parts[0]
ldapUserFederationId := parts[1]
id := parts[2]
if len(parts) != 3 {
return nil, fmt.Errorf("Invalid import. Supported import formats: {{realmId}}/{{userFederationId}}/{{userFederationMapperId}}")
}

d.Set("realm_id", realm)
d.Set("ldap_user_federation_id", ldapUserFederationId)
d.SetId(id)
d.Set("realm_id", parts[0])
d.Set("ldap_user_federation_id", parts[1])
d.SetId(parts[2])

return []*schema.ResourceData{d}, nil
}
Loading