13 changed files with 1064 additions and 817 deletions
@ -0,0 +1,268 @@ |
|||
package client |
|||
|
|||
import ( |
|||
"context" |
|||
"encoding/json" |
|||
"fmt" |
|||
"net/http" |
|||
"net/url" |
|||
"time" |
|||
|
|||
oidc "github.com/coreos/go-oidc" |
|||
"gopkg.in/h2non/gentleman.v2" |
|||
"gopkg.in/h2non/gentleman.v2/plugin" |
|||
"gopkg.in/h2non/gentleman.v2/plugins/timeout" |
|||
) |
|||
|
|||
type HttpConfig struct { |
|||
Addr string |
|||
Username string |
|||
Password string |
|||
Timeout time.Duration |
|||
} |
|||
|
|||
type client struct { |
|||
username string |
|||
password string |
|||
accessToken string |
|||
oidcProvider *oidc.Provider |
|||
httpClient *gentleman.Client |
|||
} |
|||
|
|||
func New(config HttpConfig) (*client, error) { |
|||
var u *url.URL |
|||
{ |
|||
var err error |
|||
u, err = url.Parse(config.Addr) |
|||
if err != nil { |
|||
return nil, fmt.Errorf("could not parse URL: %v", err) |
|||
} |
|||
} |
|||
|
|||
if u.Scheme != "http" { |
|||
return nil, fmt.Errorf("protocol not supported, your address must start with http://, not %v", u.Scheme) |
|||
} |
|||
|
|||
var httpClient = gentleman.New() |
|||
{ |
|||
httpClient = httpClient.URL(u.String()) |
|||
httpClient = httpClient.Use(timeout.Request(config.Timeout)) |
|||
} |
|||
|
|||
var oidcProvider *oidc.Provider |
|||
{ |
|||
var err error |
|||
var issuer = fmt.Sprintf("%s/auth/realms/master", u.String()) |
|||
oidcProvider, err = oidc.NewProvider(context.Background(), issuer) |
|||
if err != nil { |
|||
return nil, fmt.Errorf("could not create oidc provider: %v", err) |
|||
} |
|||
} |
|||
|
|||
return &client{ |
|||
username: config.Username, |
|||
password: config.Password, |
|||
oidcProvider: oidcProvider, |
|||
httpClient: httpClient, |
|||
}, nil |
|||
} |
|||
|
|||
func (c *client) getToken() error { |
|||
var req *gentleman.Request |
|||
{ |
|||
var authPath = "/auth/realms/master/protocol/openid-connect/token" |
|||
req = c.httpClient.Post() |
|||
req = req.SetHeader("Content-Type", "application/x-www-form-urlencoded") |
|||
req = req.Path(authPath) |
|||
req = req.Type("urlencoded") |
|||
req = req.BodyString(fmt.Sprintf("username=%s&password=%s&grant_type=password&client_id=admin-cli", c.username, c.password)) |
|||
} |
|||
|
|||
var resp *gentleman.Response |
|||
{ |
|||
var err error |
|||
resp, err = req.Do() |
|||
if err != nil { |
|||
return fmt.Errorf("could not get token: %v", err) |
|||
} |
|||
} |
|||
defer resp.Close() |
|||
|
|||
var unmarshalledBody map[string]interface{} |
|||
{ |
|||
var err error |
|||
err = resp.JSON(&unmarshalledBody) |
|||
if err != nil { |
|||
return fmt.Errorf("could not unmarshal response: %v", err) |
|||
} |
|||
} |
|||
|
|||
var accessToken interface{} |
|||
{ |
|||
var ok bool |
|||
accessToken, ok = unmarshalledBody["access_token"] |
|||
if !ok { |
|||
return fmt.Errorf("could not find access token in response body") |
|||
} |
|||
} |
|||
|
|||
c.accessToken = accessToken.(string) |
|||
return nil |
|||
} |
|||
|
|||
func (c *client) verifyToken() error { |
|||
var v = c.oidcProvider.Verifier(&oidc.Config{SkipClientIDCheck: true}) |
|||
|
|||
var err error |
|||
_, err = v.Verify(context.Background(), c.accessToken) |
|||
return err |
|||
} |
|||
|
|||
func (c *client) get(data interface{}, plugins ...plugin.Plugin) error { |
|||
var req = c.httpClient.Get() |
|||
req = applyPlugins(req, c.accessToken, plugins...) |
|||
|
|||
var resp *gentleman.Response |
|||
{ |
|||
var err error |
|||
resp, err = req.Do() |
|||
if err != nil { |
|||
return fmt.Errorf("could not get response: %v", err) |
|||
} |
|||
|
|||
switch { |
|||
case resp.StatusCode == http.StatusUnauthorized: |
|||
// If the token is not valid (expired, ...) ask a new one.
|
|||
if err = c.verifyToken(); err != nil { |
|||
var err = c.getToken() |
|||
if err != nil { |
|||
return fmt.Errorf("could not get token: %v", err) |
|||
} |
|||
} |
|||
return c.get(data, plugins...) |
|||
case resp.StatusCode >= 400: |
|||
return handleError(resp) |
|||
case resp.StatusCode >= 200: |
|||
return json.Unmarshal(resp.Bytes(), data) |
|||
default: |
|||
return fmt.Errorf("unknown response status code: %v", resp.StatusCode) |
|||
} |
|||
} |
|||
} |
|||
|
|||
func (c *client) post(plugins ...plugin.Plugin) error { |
|||
var req = c.httpClient.Post() |
|||
req = applyPlugins(req, c.accessToken, plugins...) |
|||
|
|||
var resp *gentleman.Response |
|||
{ |
|||
var err error |
|||
resp, err = req.Do() |
|||
if err != nil { |
|||
return fmt.Errorf("could not get response: %v", err) |
|||
} |
|||
|
|||
switch { |
|||
case resp.StatusCode == http.StatusUnauthorized: |
|||
// If the token is not valid (expired, ...) ask a new one.
|
|||
if err = c.verifyToken(); err != nil { |
|||
var err = c.getToken() |
|||
if err != nil { |
|||
return fmt.Errorf("could not get token: %v", err) |
|||
} |
|||
} |
|||
return c.post(plugins...) |
|||
case resp.StatusCode >= 400: |
|||
return handleError(resp) |
|||
case resp.StatusCode >= 200: |
|||
return nil |
|||
default: |
|||
return fmt.Errorf("unknown response status code: %v", resp.StatusCode) |
|||
} |
|||
} |
|||
} |
|||
|
|||
func (c *client) delete(plugins ...plugin.Plugin) error { |
|||
var req = c.httpClient.Delete() |
|||
req = applyPlugins(req, c.accessToken, plugins...) |
|||
|
|||
var resp *gentleman.Response |
|||
{ |
|||
var err error |
|||
resp, err = req.Do() |
|||
if err != nil { |
|||
return fmt.Errorf("could not get response: %v", err) |
|||
} |
|||
|
|||
switch { |
|||
case resp.StatusCode == http.StatusUnauthorized: |
|||
// If the token is not valid (expired, ...) ask a new one.
|
|||
if err = c.verifyToken(); err != nil { |
|||
var err = c.getToken() |
|||
if err != nil { |
|||
return fmt.Errorf("could not get token: %v", err) |
|||
} |
|||
} |
|||
return c.delete(plugins...) |
|||
case resp.StatusCode >= 400: |
|||
return handleError(resp) |
|||
case resp.StatusCode >= 200: |
|||
return nil |
|||
default: |
|||
return fmt.Errorf("unknown response status code: %v", resp.StatusCode) |
|||
} |
|||
} |
|||
} |
|||
|
|||
func (c *client) put(plugins ...plugin.Plugin) error { |
|||
var req = c.httpClient.Put() |
|||
req = applyPlugins(req, c.accessToken, plugins...) |
|||
|
|||
var resp *gentleman.Response |
|||
{ |
|||
var err error |
|||
resp, err = req.Do() |
|||
if err != nil { |
|||
return fmt.Errorf("could not get response: %v", err) |
|||
} |
|||
|
|||
switch { |
|||
case resp.StatusCode == http.StatusUnauthorized: |
|||
// If the token is not valid (expired, ...) ask a new one.
|
|||
if err = c.verifyToken(); err != nil { |
|||
var err = c.getToken() |
|||
if err != nil { |
|||
return fmt.Errorf("could not get token: %v", err) |
|||
} |
|||
} |
|||
return c.put(plugins...) |
|||
case resp.StatusCode >= 400: |
|||
return handleError(resp) |
|||
case resp.StatusCode >= 200: |
|||
return nil |
|||
default: |
|||
return fmt.Errorf("unknown response status code: %v", resp.StatusCode) |
|||
} |
|||
} |
|||
} |
|||
|
|||
func applyPlugins(req *gentleman.Request, accessToken string, plugins ...plugin.Plugin) *gentleman.Request { |
|||
var r = req.SetHeader("Authorization", fmt.Sprintf("Bearer %s", accessToken)) |
|||
for _, p := range plugins { |
|||
r = r.Use(p) |
|||
} |
|||
return r |
|||
} |
|||
|
|||
func handleError(resp *gentleman.Response) error { |
|||
var m = map[string]string{} |
|||
var err = json.Unmarshal(resp.Bytes(), &m) |
|||
if err != nil { |
|||
return fmt.Errorf("invalid status code: %v; could not unmarshal response: %v", resp.StatusCode, err) |
|||
} |
|||
return fmt.Errorf("error message: %v", m) |
|||
} |
|||
|
|||
func str(s string) *string { |
|||
return &s |
|||
} |
|||
@ -1,147 +0,0 @@ |
|||
package client |
|||
|
|||
import ( |
|||
"fmt" |
|||
"net/url" |
|||
"github.com/pkg/errors" |
|||
"time" |
|||
"net/http" |
|||
"gopkg.in/h2non/gentleman.v2" |
|||
"gopkg.in/h2non/gentleman.v2/plugin" |
|||
"gopkg.in/h2non/gentleman.v2/plugins/timeout" |
|||
//"gopkg.in/h2non/gentleman.v2/plugins/multipart"
|
|||
) |
|||
|
|||
|
|||
type Client interface { |
|||
GetRealms() ([]RealmRepresentation, error) |
|||
GetUsers(realm string) ([]UserRepresentation, error) |
|||
} |
|||
|
|||
type HttpConfig struct { |
|||
Addr string |
|||
Username string |
|||
Password string |
|||
Timeout time.Duration |
|||
} |
|||
|
|||
type client struct { |
|||
username string |
|||
password string |
|||
accessToken string |
|||
httpClient *gentleman.Client |
|||
} |
|||
|
|||
|
|||
func NewHttpClient(config HttpConfig) (Client, error) { |
|||
var u *url.URL |
|||
{ |
|||
var err error |
|||
u, err = url.Parse(config.Addr) |
|||
if err != nil { |
|||
return nil, errors.Wrap(err, "Parse failed") |
|||
} |
|||
} |
|||
|
|||
if u.Scheme != "http" { |
|||
var m string = fmt.Sprintf("Unsupported protocol %s. Your address must start with http://", u.Scheme) |
|||
return nil, errors.New(m) |
|||
} |
|||
|
|||
var httpClient *gentleman.Client = gentleman.New() |
|||
{ |
|||
httpClient = httpClient.URL(u.String()) |
|||
httpClient = httpClient.Use(timeout.Request(config.Timeout)) |
|||
} |
|||
|
|||
return &client{ |
|||
username: config.Username, |
|||
password: config.Password, |
|||
httpClient: httpClient, |
|||
}, nil |
|||
} |
|||
|
|||
func (c *client) getToken() error { |
|||
var req *gentleman.Request |
|||
{ |
|||
var authPath string = "/auth/realms/master/protocol/openid-connect/token" |
|||
//var formData multipart.FormData = multipart.FormData{
|
|||
// Data: map[string]multipart.Values{
|
|||
// "username": multipart.Values{c.username},
|
|||
// "password": multipart.Values{c.password},
|
|||
// "grant_type": multipart.Values{"password"},
|
|||
// "client_id": multipart.Values{"admin-cli"},
|
|||
// },
|
|||
//}
|
|||
req = c.httpClient.Post() |
|||
req = req.SetHeader("Content-Type", "application/x-www-form-urlencoded") |
|||
req = req.Path(authPath) |
|||
req = req.Type("urlencoded") |
|||
req = req.BodyString(fmt.Sprintf("username=%s&password=%s&grant_type=password&client_id=admin-cli",c.username,c.password)) |
|||
} |
|||
|
|||
var resp *gentleman.Response |
|||
{ |
|||
var err error |
|||
resp, err = req.Do() |
|||
if err != nil { |
|||
return errors.Wrap(err, "Failed to get the token response") |
|||
} |
|||
} |
|||
defer resp.Close() |
|||
|
|||
var unmarshalledBody map[string]interface{} |
|||
{ |
|||
var err error |
|||
err = resp.JSON(&unmarshalledBody) |
|||
if err != nil { |
|||
return errors.Wrap(err, "Failed to unmarshal response json") |
|||
} |
|||
} |
|||
|
|||
var accessToken interface{} |
|||
{ |
|||
var ok bool |
|||
accessToken, ok = unmarshalledBody["access_token"] |
|||
if !ok { |
|||
return errors.New("No access token in reponse body") |
|||
} |
|||
} |
|||
|
|||
c.accessToken = accessToken.(string) |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func (c *client) do(path string, plugins ...plugin.Plugin) (*gentleman.Response, error) { |
|||
var req *gentleman.Request = c.httpClient.Get() |
|||
{ |
|||
req = req.Path(path) |
|||
req = req.SetHeader("Authorization", fmt.Sprintf("Bearer %s", c.accessToken)) |
|||
for _, p := range plugins { |
|||
req = req.Use(p) |
|||
} |
|||
} |
|||
var resp *gentleman.Response |
|||
{ |
|||
var err error |
|||
resp, err = req.Do() |
|||
if err != nil { |
|||
return nil, errors.Wrap(err, "Failed to get the response") |
|||
} |
|||
switch resp.StatusCode { |
|||
case http.StatusUnauthorized: |
|||
var err = c.getToken() |
|||
//This induces a potential infinite loop, where a new token gets requested and the
|
|||
//process gets delayed so much it expires before the recursion.
|
|||
//It is decided that should this happen, the machine would be considered to be in terrible shape
|
|||
//and the loop wouldn't be the biggest problem.
|
|||
if err != nil { |
|||
return nil, errors.Wrap(err, "Failed to get token") |
|||
} |
|||
return c.do(path) |
|||
default: |
|||
return resp, nil |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
package client |
|||
|
|||
import ( |
|||
"gopkg.in/h2non/gentleman.v2/plugins/body" |
|||
"gopkg.in/h2non/gentleman.v2/plugins/url" |
|||
) |
|||
|
|||
const ( |
|||
realmRootPath = "/auth/admin/realms" |
|||
realmPath = realmRootPath + "/:realm" |
|||
) |
|||
|
|||
func (c *client) GetRealms() ([]RealmRepresentation, error) { |
|||
var resp = []RealmRepresentation{} |
|||
var err = c.get(&resp, url.Path(realmRootPath)) |
|||
return resp, err |
|||
} |
|||
|
|||
func (c *client) CreateRealm(realm RealmRepresentation) error { |
|||
return c.post(url.Path(realmRootPath), body.JSON(realm)) |
|||
} |
|||
|
|||
func (c *client) GetRealm(realm string) (RealmRepresentation, error) { |
|||
var resp = RealmRepresentation{} |
|||
var err = c.get(&resp, url.Path(realmPath), url.Param("realm", realm)) |
|||
return resp, err |
|||
} |
|||
|
|||
func (c *client) UpdateRealm(realmName string, realm RealmRepresentation) error { |
|||
return c.put(url.Path(realmPath), url.Param("realm", realmName), body.JSON(realm)) |
|||
} |
|||
|
|||
func (c *client) DeleteRealm(realm string) error { |
|||
return c.delete(url.Path(realmPath), url.Param("realm", realm)) |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
package client |
|||
|
|||
import ( |
|||
"testing" |
|||
|
|||
"github.com/stretchr/testify/assert" |
|||
"github.com/stretchr/testify/require" |
|||
) |
|||
|
|||
func TestGetRealms(t *testing.T) { |
|||
var client = initTest(t) |
|||
var realms []RealmRepresentation |
|||
{ |
|||
var err error |
|||
realms, err = client.GetRealms() |
|||
require.Nil(t, err, "could not get realms") |
|||
assert.NotNil(t, realms) |
|||
} |
|||
} |
|||
|
|||
func TestCreateRealm(t *testing.T) { |
|||
var client = initTest(t) |
|||
var realm = RealmRepresentation{ |
|||
Realm: str("__internal"), |
|||
} |
|||
var err = client.CreateRealm(realm) |
|||
assert.Nil(t, err) |
|||
} |
|||
|
|||
func TestGetRealm(t *testing.T) { |
|||
var client = initTest(t) |
|||
|
|||
var realm, err = client.GetRealm("__internal") |
|||
assert.Nil(t, err) |
|||
assert.NotNil(t, realm) |
|||
} |
|||
|
|||
func TestUpdateRealm(t *testing.T) { |
|||
var client = initTest(t) |
|||
|
|||
var realm = RealmRepresentation{ |
|||
DisplayName: str("Test realm"), |
|||
} |
|||
var err = client.UpdateRealm("__internal", realm) |
|||
assert.Nil(t, err) |
|||
} |
|||
func TestDeleteRealm(t *testing.T) { |
|||
var client = initTest(t) |
|||
|
|||
var err = client.DeleteRealm("__internal") |
|||
assert.Nil(t, err) |
|||
} |
|||
@ -1,29 +0,0 @@ |
|||
package client |
|||
|
|||
|
|||
import ( |
|||
"github.com/pkg/errors" |
|||
gentleman "gopkg.in/h2non/gentleman.v2" |
|||
"encoding/json" |
|||
) |
|||
|
|||
func (c *client) GetRealms() ([]RealmRepresentation, error) { |
|||
var getRealms_Path string = "/auth/admin/realms" |
|||
var resp *gentleman.Response |
|||
{ |
|||
var err error |
|||
resp, err = c.do(getRealms_Path) |
|||
if err != nil { |
|||
return nil, errors.Wrap(err, "Get Realms failed.") |
|||
} |
|||
} |
|||
var result []RealmRepresentation |
|||
{ |
|||
var err error |
|||
err = json.Unmarshal(resp.Bytes(), &result) |
|||
if err != nil { |
|||
return nil, errors.Wrap(err, "Get Realms failed to unmarshal response.") |
|||
} |
|||
} |
|||
return result, nil |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
package client |
|||
|
|||
import ( |
|||
"gopkg.in/h2non/gentleman.v2/plugins/body" |
|||
"gopkg.in/h2non/gentleman.v2/plugins/url" |
|||
) |
|||
|
|||
const ( |
|||
userPath = "/auth/admin/realms/:realm/users" |
|||
userCountPath = userPath + "/count" |
|||
userIDPath = userPath + "/:id" |
|||
) |
|||
|
|||
func (c *client) GetUsers(realm string) ([]UserRepresentation, error) { |
|||
var resp = []UserRepresentation{} |
|||
var err = c.get(&resp, url.Path(userPath), url.Param("realm", realm)) |
|||
return resp, err |
|||
} |
|||
|
|||
func (c *client) CreateUser(realm string, user UserRepresentation) error { |
|||
return c.post(url.Path(userPath), url.Param("realm", realm), body.JSON(user)) |
|||
} |
|||
|
|||
func (c *client) CountUsers(realm string) (int, error) { |
|||
var resp = 0 |
|||
var err = c.get(&resp, url.Path(userCountPath), url.Param("realm", realm)) |
|||
return resp, err |
|||
} |
|||
|
|||
func (c *client) GetUser(realm, userID string) (UserRepresentation, error) { |
|||
var resp = UserRepresentation{} |
|||
var err = c.get(&resp, url.Path(userIDPath), url.Param("realm", realm), url.Param("id", userID)) |
|||
return resp, err |
|||
} |
|||
|
|||
func (c *client) UpdateUser(realm, userID string, user UserRepresentation) error { |
|||
return c.put(url.Path(userIDPath), url.Param("realm", realm), url.Param("id", userID), body.JSON(user)) |
|||
} |
|||
|
|||
func (c *client) DeleteUser(realm, userID string) error { |
|||
return c.delete(url.Path(userIDPath), url.Param("realm", realm), url.Param("id", userID)) |
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
package client |
|||
|
|||
import ( |
|||
"fmt" |
|||
"testing" |
|||
|
|||
"github.com/stretchr/testify/assert" |
|||
"github.com/stretchr/testify/require" |
|||
) |
|||
|
|||
func TestGetUsers(t *testing.T) { |
|||
var client = initTest(t) |
|||
var users []UserRepresentation |
|||
{ |
|||
var err error |
|||
users, err = client.GetUsers("__internal") |
|||
require.Nil(t, err, "could not get users") |
|||
} |
|||
for _, i := range users { |
|||
fmt.Println(*i.Username) |
|||
} |
|||
} |
|||
|
|||
func TestCreateUser(t *testing.T) { |
|||
var client = initTest(t) |
|||
var realm = "__internal" |
|||
var user = UserRepresentation{ |
|||
Username: str("johanr"), |
|||
} |
|||
var err = client.CreateUser(realm, user) |
|||
assert.Nil(t, err) |
|||
} |
|||
|
|||
func TestCountUsers(t *testing.T) { |
|||
var client = initTest(t) |
|||
var realm = "__internal" |
|||
|
|||
var count, err = client.CountUsers(realm) |
|||
assert.Nil(t, err) |
|||
assert.NotZero(t, count) |
|||
} |
|||
func TestGetUser(t *testing.T) { |
|||
var client = initTest(t) |
|||
var user UserRepresentation |
|||
{ |
|||
var err error |
|||
user, err = client.GetUser("__internal", "078f735b-ac07-4b39-88cb-88647c4ff47c") |
|||
require.Nil(t, err, "could not get users") |
|||
} |
|||
fmt.Println(*user.Username) |
|||
} |
|||
|
|||
func TestUpdateUser(t *testing.T) { |
|||
var client = initTest(t) |
|||
|
|||
var user = UserRepresentation{ |
|||
Email: str("john.doe@elca.ch"), |
|||
} |
|||
var err = client.UpdateUser("__internal", "078f735b-ac07-4b39-88cb-88647c4ff47c", user) |
|||
assert.Nil(t, err) |
|||
} |
|||
func TestDeleteUser(t *testing.T) { |
|||
var client = initTest(t) |
|||
|
|||
var err = client.DeleteUser("__internal", "078f735b-ac07-4b39-88cb-88647c4ff47c") |
|||
assert.Nil(t, err) |
|||
} |
|||
@ -1,29 +0,0 @@ |
|||
package client |
|||
|
|||
import ( |
|||
"fmt" |
|||
"gopkg.in/h2non/gentleman.v2" |
|||
"github.com/pkg/errors" |
|||
"encoding/json" |
|||
) |
|||
|
|||
func (c *client)GetUsers(realm string) ([]UserRepresentation, error) { |
|||
var getUsers_Path string = fmt.Sprintf("/auth/admin/realms/%s/users", realm) |
|||
var resp *gentleman.Response |
|||
{ |
|||
var err error |
|||
resp, err = c.do(getUsers_Path) |
|||
if err != nil { |
|||
return nil, errors.Wrap(err, "Get Realms failed.") |
|||
} |
|||
} |
|||
var result []UserRepresentation |
|||
{ |
|||
var err error |
|||
err = json.Unmarshal(resp.Bytes(), &result) |
|||
if err != nil { |
|||
return nil, errors.Wrap(err, "Get Users failed to unmarshal response.") |
|||
} |
|||
} |
|||
return result, nil |
|||
} |
|||
@ -0,0 +1,2 @@ |
|||
// Keycloak client is a client for keycloak.
|
|||
package keycloakclient |
|||
Loading…
Reference in new issue