17 changed files with 23 additions and 1163 deletions
@ -1,301 +0,0 @@ |
|||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. |
|
||||
|
|
||||
|
|
||||
[[projects]] |
|
||||
digest = "1:efcf35206d596370b758dc4dcc16ce9a5a20792992dd9bcf5090963895dc21a8" |
|
||||
name = "github.com/cloudtrust/common-service" |
|
||||
packages = ["errors"] |
|
||||
pruneopts = "" |
|
||||
revision = "0da35eaf6d16a949088ce7f68ac7ae43a3015847" |
|
||||
version = "v2.3.2" |
|
||||
|
|
||||
[[projects]] |
|
||||
digest = "1:bb7f91ab4d1c44a3bb2651c613463c134165bda0282fca891a63b88d1b501997" |
|
||||
name = "github.com/coreos/go-oidc" |
|
||||
packages = ["."] |
|
||||
pruneopts = "" |
|
||||
revision = "8d771559cf6e5111c9b9159810d0e4538e7cdc82" |
|
||||
version = "v2.2.1" |
|
||||
|
|
||||
[[projects]] |
|
||||
digest = "1:0deddd908b6b4b768cfc272c16ee61e7088a60f7fe2f06c547bd3d8e1f8b8e77" |
|
||||
name = "github.com/davecgh/go-spew" |
|
||||
packages = ["spew"] |
|
||||
pruneopts = "" |
|
||||
revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" |
|
||||
version = "v1.1.1" |
|
||||
|
|
||||
[[projects]] |
|
||||
digest = "1:0c07a9cb3d3c845439a4fcaae6c8bdd0e7727cbbd3acf1e032e5d4a2dc132306" |
|
||||
name = "github.com/gbrlsnchs/jwt" |
|
||||
packages = ["."] |
|
||||
pruneopts = "" |
|
||||
revision = "808efa0714baff8c25cc65ef8681966740beb9f9" |
|
||||
version = "v2.0.0" |
|
||||
|
|
||||
[[projects]] |
|
||||
digest = "1:324437ddda77526fea02c29a2208ef1260e7a96ff0202965143215941564c400" |
|
||||
name = "github.com/go-kit/kit" |
|
||||
packages = [ |
|
||||
"endpoint", |
|
||||
"log", |
|
||||
"transport", |
|
||||
"transport/http", |
|
||||
] |
|
||||
pruneopts = "" |
|
||||
revision = "cc938d52e0cdf4c811ab203f428fcd06f9d9a148" |
|
||||
version = "v0.10.0" |
|
||||
|
|
||||
[[projects]] |
|
||||
digest = "1:aa9a6ccd5fd7d29804a27cb0666bc4ac5eb4b73439b7edd54f4b377e5ef8bb47" |
|
||||
name = "github.com/go-logfmt/logfmt" |
|
||||
packages = ["."] |
|
||||
pruneopts = "" |
|
||||
revision = "3be5f6aae7db841d31dc5e1b3bb7b4cff19200b3" |
|
||||
version = "v0.5.0" |
|
||||
|
|
||||
[[projects]] |
|
||||
digest = "1:bc24d32292c699c0cc375ae09fef8340c37360f46ec60cad98312b699c039422" |
|
||||
name = "github.com/golang/mock" |
|
||||
packages = ["gomock"] |
|
||||
pruneopts = "" |
|
||||
revision = "f7b1909c82a8958747e5c87c6a5c3b2eaed8a33d" |
|
||||
version = "v1.4.4" |
|
||||
|
|
||||
[[projects]] |
|
||||
digest = "1:6532affeeaaccdc6919d5773516176b77de02b4af8cf9a7fac16bae77aa319c5" |
|
||||
name = "github.com/golang/protobuf" |
|
||||
packages = ["proto"] |
|
||||
pruneopts = "" |
|
||||
revision = "4846b58453b3708320bdb524f25cc5a1d9cda4d4" |
|
||||
version = "v1.4.3" |
|
||||
|
|
||||
[[projects]] |
|
||||
digest = "1:20e3b61797cba6a42b110320b703a0b86e2771f324411e75557c2acbc819b1b5" |
|
||||
name = "github.com/gorilla/mux" |
|
||||
packages = ["."] |
|
||||
pruneopts = "" |
|
||||
revision = "98cb6bf42e086f6af920b965c38cacc07402d51b" |
|
||||
version = "v1.8.0" |
|
||||
|
|
||||
[[projects]] |
|
||||
digest = "1:1d7e1867c49a6dd9856598ef7c3123604ea3daabf5b83f303ff457bcbc410b1d" |
|
||||
name = "github.com/pkg/errors" |
|
||||
packages = ["."] |
|
||||
pruneopts = "" |
|
||||
revision = "ba968bfe8b2f7e042a574c888954fccecfa385b4" |
|
||||
version = "v0.8.1" |
|
||||
|
|
||||
[[projects]] |
|
||||
digest = "1:256484dbbcd271f9ecebc6795b2df8cad4c458dd0f5fd82a8c2fa0c29f233411" |
|
||||
name = "github.com/pmezard/go-difflib" |
|
||||
packages = ["difflib"] |
|
||||
pruneopts = "" |
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2" |
|
||||
version = "v1.0.0" |
|
||||
|
|
||||
[[projects]] |
|
||||
branch = "master" |
|
||||
digest = "1:3957a6d09f51e1efa16c86156e22521402e6c13867213fd45fe5a37508013021" |
|
||||
name = "github.com/pquerna/cachecontrol" |
|
||||
packages = [ |
|
||||
".", |
|
||||
"cacheobject", |
|
||||
] |
|
||||
pruneopts = "" |
|
||||
revision = "ac21108117ac345f5739431ee1e9a5fd7f82ae1c" |
|
||||
|
|
||||
[[projects]] |
|
||||
digest = "1:688428eeb1ca80d92599eb3254bdf91b51d7e232fead3a73844c1f201a281e51" |
|
||||
name = "github.com/spf13/pflag" |
|
||||
packages = ["."] |
|
||||
pruneopts = "" |
|
||||
revision = "2e9d26c8c37aae03e3f9d4e90b7116f5accb7cab" |
|
||||
version = "v1.0.5" |
|
||||
|
|
||||
[[projects]] |
|
||||
digest = "1:83fd2513b9f6ae0997bf646db6b74e9e00131e31002116fda597175f25add42d" |
|
||||
name = "github.com/stretchr/testify" |
|
||||
packages = ["assert"] |
|
||||
pruneopts = "" |
|
||||
revision = "f654a9112bbeac49ca2cd45bfbe11533c4666cf8" |
|
||||
version = "v1.6.1" |
|
||||
|
|
||||
[[projects]] |
|
||||
branch = "master" |
|
||||
digest = "1:22902e9ca5ef0028bb8e6a3dacb884d166d0259841a15288d81ddb5d61fa68c0" |
|
||||
name = "golang.org/x/crypto" |
|
||||
packages = [ |
|
||||
"ed25519", |
|
||||
"ed25519/internal/edwards25519", |
|
||||
"pbkdf2", |
|
||||
] |
|
||||
pruneopts = "" |
|
||||
revision = "eec23a3978adcfd26c29f4153eaa3e3d9b2cc53a" |
|
||||
|
|
||||
[[projects]] |
|
||||
branch = "master" |
|
||||
digest = "1:87515e47f85690ee710603c4de5c631d034f203799768278e43ec8497078b47e" |
|
||||
name = "golang.org/x/net" |
|
||||
packages = [ |
|
||||
"context", |
|
||||
"context/ctxhttp", |
|
||||
"idna", |
|
||||
"publicsuffix", |
|
||||
] |
|
||||
pruneopts = "" |
|
||||
revision = "6772e930b67bb09bf22262c7378e7d2f67cf59d1" |
|
||||
|
|
||||
[[projects]] |
|
||||
branch = "master" |
|
||||
digest = "1:bf98155c68bf5f703fd3cd79f8a2cf4372460bf4de00ef0aeadbade1befc3c4c" |
|
||||
name = "golang.org/x/oauth2" |
|
||||
packages = [ |
|
||||
".", |
|
||||
"internal", |
|
||||
] |
|
||||
pruneopts = "" |
|
||||
revision = "01de73cf58bdca33ccc181d1bd6d63ebcf21ccca" |
|
||||
|
|
||||
[[projects]] |
|
||||
digest = "1:4101e2977ebdbb93018565cd1c03db0151143b303a7f7a171592d3160f1498b2" |
|
||||
name = "golang.org/x/text" |
|
||||
packages = [ |
|
||||
"collate", |
|
||||
"collate/build", |
|
||||
"internal/colltab", |
|
||||
"internal/gen", |
|
||||
"internal/language", |
|
||||
"internal/language/compact", |
|
||||
"internal/tag", |
|
||||
"internal/triegen", |
|
||||
"internal/ucd", |
|
||||
"language", |
|
||||
"secure/bidirule", |
|
||||
"transform", |
|
||||
"unicode/bidi", |
|
||||
"unicode/cldr", |
|
||||
"unicode/norm", |
|
||||
"unicode/rangetable", |
|
||||
] |
|
||||
pruneopts = "" |
|
||||
revision = "75a595aef632b07c6eeaaa805adb6f0f66e4130e" |
|
||||
version = "v0.3.5" |
|
||||
|
|
||||
[[projects]] |
|
||||
digest = "1:13ee408d4cf721c5b576dbaff8d88ced35aadc9675274cc6b4ad009ac0372955" |
|
||||
name = "google.golang.org/appengine" |
|
||||
packages = [ |
|
||||
"internal", |
|
||||
"internal/base", |
|
||||
"internal/datastore", |
|
||||
"internal/log", |
|
||||
"internal/remote_api", |
|
||||
"internal/urlfetch", |
|
||||
"urlfetch", |
|
||||
] |
|
||||
pruneopts = "" |
|
||||
revision = "5d1c1d03f8703c2e81478d9a30e9afa2d3e4bd8a" |
|
||||
version = "v1.6.7" |
|
||||
|
|
||||
[[projects]] |
|
||||
digest = "1:0d049ff01749ce1d6092c85cb90a02dfdb3b401939b13415b7e608f36bc8ecee" |
|
||||
name = "google.golang.org/protobuf" |
|
||||
packages = [ |
|
||||
"encoding/prototext", |
|
||||
"encoding/protowire", |
|
||||
"internal/descfmt", |
|
||||
"internal/descopts", |
|
||||
"internal/detrand", |
|
||||
"internal/encoding/defval", |
|
||||
"internal/encoding/messageset", |
|
||||
"internal/encoding/tag", |
|
||||
"internal/encoding/text", |
|
||||
"internal/errors", |
|
||||
"internal/fieldsort", |
|
||||
"internal/filedesc", |
|
||||
"internal/filetype", |
|
||||
"internal/flags", |
|
||||
"internal/genid", |
|
||||
"internal/impl", |
|
||||
"internal/mapsort", |
|
||||
"internal/pragma", |
|
||||
"internal/set", |
|
||||
"internal/strs", |
|
||||
"internal/version", |
|
||||
"proto", |
|
||||
"reflect/protoreflect", |
|
||||
"reflect/protoregistry", |
|
||||
"runtime/protoiface", |
|
||||
"runtime/protoimpl", |
|
||||
] |
|
||||
pruneopts = "" |
|
||||
revision = "3f7a61f89bb6813f89d981d1870ed68da0b3c3f1" |
|
||||
version = "v1.25.0" |
|
||||
|
|
||||
[[projects]] |
|
||||
digest = "1:95fa5eae3b22887e8aea55ad4f93bc1374d586f7dd3504cf0010845ccc0a95a8" |
|
||||
name = "gopkg.in/h2non/gentleman.v2" |
|
||||
packages = [ |
|
||||
".", |
|
||||
"context", |
|
||||
"middleware", |
|
||||
"mux", |
|
||||
"plugin", |
|
||||
"plugins/body", |
|
||||
"plugins/bodytype", |
|
||||
"plugins/cookies", |
|
||||
"plugins/headers", |
|
||||
"plugins/multipart", |
|
||||
"plugins/query", |
|
||||
"plugins/timeout", |
|
||||
"plugins/url", |
|
||||
"utils", |
|
||||
] |
|
||||
pruneopts = "" |
|
||||
revision = "34f7caeaf69f4668a88f8294ec18665fd2756b84" |
|
||||
version = "v2.0.4" |
|
||||
|
|
||||
[[projects]] |
|
||||
digest = "1:7436f519828cd59ec8f6beac99e397a1bc9a4ce8871d9fbca029bddeaba48a92" |
|
||||
name = "gopkg.in/square/go-jose.v2" |
|
||||
packages = [ |
|
||||
".", |
|
||||
"cipher", |
|
||||
"json", |
|
||||
] |
|
||||
pruneopts = "" |
|
||||
revision = "3a5ee095dcb5030a9de84fb92c222ac652fff176" |
|
||||
version = "v2.5.1" |
|
||||
|
|
||||
[[projects]] |
|
||||
branch = "v3" |
|
||||
digest = "1:3f081584f03a869274b31afe33c22ce278c53767cb58963916746109e1d50535" |
|
||||
name = "gopkg.in/yaml.v3" |
|
||||
packages = ["."] |
|
||||
pruneopts = "" |
|
||||
revision = "496545a6307b2a7d7a710fd516e5e16e8ab62dbc" |
|
||||
|
|
||||
[solve-meta] |
|
||||
analyzer-name = "dep" |
|
||||
analyzer-version = 1 |
|
||||
input-imports = [ |
|
||||
"github.com/cloudtrust/common-service/errors", |
|
||||
"github.com/coreos/go-oidc", |
|
||||
"github.com/gbrlsnchs/jwt", |
|
||||
"github.com/go-kit/kit/transport/http", |
|
||||
"github.com/golang/mock/gomock", |
|
||||
"github.com/gorilla/mux", |
|
||||
"github.com/pkg/errors", |
|
||||
"github.com/spf13/pflag", |
|
||||
"github.com/stretchr/testify/assert", |
|
||||
"gopkg.in/h2non/gentleman.v2", |
|
||||
"gopkg.in/h2non/gentleman.v2/plugin", |
|
||||
"gopkg.in/h2non/gentleman.v2/plugins/body", |
|
||||
"gopkg.in/h2non/gentleman.v2/plugins/headers", |
|
||||
"gopkg.in/h2non/gentleman.v2/plugins/query", |
|
||||
"gopkg.in/h2non/gentleman.v2/plugins/timeout", |
|
||||
"gopkg.in/h2non/gentleman.v2/plugins/url", |
|
||||
] |
|
||||
solver-name = "gps-cdcl" |
|
||||
solver-version = 1 |
|
||||
@ -1,37 +0,0 @@ |
|||||
|
|
||||
# Gopkg.toml example |
|
||||
# |
|
||||
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md |
|
||||
# for detailed Gopkg.toml documentation. |
|
||||
# |
|
||||
# required = ["github.com/user/thing/cmd/thing"] |
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] |
|
||||
# |
|
||||
# [[constraint]] |
|
||||
# name = "github.com/user/project" |
|
||||
# version = "1.0.0" |
|
||||
# |
|
||||
# [[constraint]] |
|
||||
# name = "github.com/user/project2" |
|
||||
# branch = "dev" |
|
||||
# source = "github.com/myfork/project2" |
|
||||
# |
|
||||
# [[override]] |
|
||||
# name = "github.com/x/y" |
|
||||
# version = "2.4.0" |
|
||||
|
|
||||
[[constraint]] |
|
||||
name = "github.com/cloudtrust/common-service" |
|
||||
version = "2.3.2" |
|
||||
|
|
||||
[[constraint]] |
|
||||
name = "github.com/pkg/errors" |
|
||||
version = "0.8.0" |
|
||||
|
|
||||
[[constraint]] |
|
||||
name = "gopkg.in/h2non/gentleman.v2" |
|
||||
version = "2.0.0" |
|
||||
|
|
||||
[[constraint]] |
|
||||
name = "github.com/gbrlsnchs/jwt" |
|
||||
version = "2.0.0" |
|
||||
@ -1,67 +0,0 @@ |
|||||
pipeline { |
|
||||
agent any |
|
||||
options { |
|
||||
timestamps() |
|
||||
timeout(time: 3600, unit: 'SECONDS') |
|
||||
} |
|
||||
environment{ |
|
||||
BUILD_PATH="/home/jenkins/gopath/src/github.com/cloudtrust/keycloak-client" |
|
||||
} |
|
||||
stages { |
|
||||
stage('Build') { |
|
||||
agent { |
|
||||
label 'jenkins-slave-go-ct' |
|
||||
} |
|
||||
steps { |
|
||||
script { |
|
||||
sh 'printenv' |
|
||||
def isBranch = "" |
|
||||
if (!env.CHANGE_ID) { |
|
||||
isBranch = " || true" |
|
||||
} |
|
||||
withCredentials([usernamePassword(credentialsId: 'cloudtrust-cicd-sonarqube', usernameVariable: 'USER', passwordVariable: 'PASS')]) { |
|
||||
sh """ |
|
||||
set -eo pipefail |
|
||||
|
|
||||
mkdir -p "${BUILD_PATH}" |
|
||||
cp -r "${WORKSPACE}/." "${BUILD_PATH}/" |
|
||||
cd "${BUILD_PATH}" |
|
||||
|
|
||||
golint ./... | tee golint.out || true |
|
||||
|
|
||||
dep ensure |
|
||||
|
|
||||
go generate ./... |
|
||||
|
|
||||
go test -coverprofile=coverage.out -json ./... | tee report.json |
|
||||
go tool cover -func=coverage.out |
|
||||
bash -c \"go vet ./... > >(cat) 2> >(tee govet.out)\" || true |
|
||||
gometalinter --vendor --disable=gotype --disable=golint --disable=vet --disable=gocyclo --exclude=/usr/local/go/src --deadline=300s ./... | tee gometalinter.out || true |
|
||||
|
|
||||
nancy -no-color Gopkg.lock || true |
|
||||
|
|
||||
JAVA_TOOL_OPTIONS="" sonar-scanner \ |
|
||||
-Dsonar.host.url=https://sonarqube-cloudtrust-cicd.openshift.west.ch.elca-cloud.com \ |
|
||||
-Dsonar.login="${USER}" \ |
|
||||
-Dsonar.password="${PASS}" \ |
|
||||
-Dsonar.sourceEncoding=UTF-8 \ |
|
||||
-Dsonar.projectKey=keycloak-client \ |
|
||||
-Dsonar.projectName=keycloak-client \ |
|
||||
-Dsonar.projectVersion="${env.GIT_COMMIT}" \ |
|
||||
-Dsonar.sources=. \ |
|
||||
-Dsonar.exclusions=**/*_test.go,**/vendor/**,**/mock/** \ |
|
||||
-Dsonar.tests=. \ |
|
||||
-Dsonar.test.inclusions=**/*_test.go \ |
|
||||
-Dsonar.test.exclusions=**/vendor/** \ |
|
||||
-Dsonar.go.coverage.reportPaths=./coverage.out \ |
|
||||
-Dsonar.go.tests.reportPaths=./report.json \ |
|
||||
-Dsonar.go.govet.reportPaths=./govet.out \ |
|
||||
-Dsonar.go.golint.reportPaths=./golint.out \ |
|
||||
-Dsonar.go.gometalinter.reportPaths=./gometalinter.out ${isBranch} |
|
||||
""" |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,107 +0,0 @@ |
|||||
package api |
|
||||
|
|
||||
import ( |
|
||||
"github.com/cloudtrust/keycloak-client/v3" |
|
||||
"gopkg.in/h2non/gentleman.v2/plugin" |
|
||||
"gopkg.in/h2non/gentleman.v2/plugins/body" |
|
||||
"gopkg.in/h2non/gentleman.v2/plugins/headers" |
|
||||
"gopkg.in/h2non/gentleman.v2/plugins/query" |
|
||||
"gopkg.in/h2non/gentleman.v2/plugins/url" |
|
||||
) |
|
||||
|
|
||||
const ( |
|
||||
accountExtensionAPIPath = "/auth/realms/master/api/account/realms/:realm" |
|
||||
accountExecuteActionsEmail = accountExtensionAPIPath + "/execute-actions-email" |
|
||||
accountSendEmail = accountExtensionAPIPath + "/send-email" |
|
||||
accountCredentialsPath = accountExtensionAPIPath + "/credentials" |
|
||||
accountPasswordPath = accountCredentialsPath + "/password" |
|
||||
accountCredentialsRegistratorsPath = accountCredentialsPath + "/registrators" |
|
||||
accountCredentialIDPath = accountCredentialsPath + "/:credentialID" |
|
||||
accountCredentialLabelPath = accountCredentialIDPath + "/label" |
|
||||
accountMoveFirstPath = accountCredentialIDPath + "/moveToFirst" |
|
||||
accountMoveAfterPath = accountCredentialIDPath + "/moveAfter/:previousCredentialID" |
|
||||
) |
|
||||
|
|
||||
var ( |
|
||||
hdrAcceptJSON = headers.Set("Accept", "application/json") |
|
||||
hdrContentTypeTextPlain = headers.Set("Content-Type", "text/plain") |
|
||||
) |
|
||||
|
|
||||
// GetCredentials returns the list of credentials of the user
|
|
||||
func (c *AccountClient) GetCredentials(accessToken string, realmName string) ([]keycloak.CredentialRepresentation, error) { |
|
||||
var resp = []keycloak.CredentialRepresentation{} |
|
||||
var err = c.client.get(accessToken, &resp, url.Path(accountCredentialsPath), url.Param("realm", realmName), hdrAcceptJSON) |
|
||||
return resp, err |
|
||||
} |
|
||||
|
|
||||
// GetCredentialRegistrators returns list of credentials types available for the user
|
|
||||
func (c *AccountClient) GetCredentialRegistrators(accessToken string, realmName string) ([]string, error) { |
|
||||
var resp = []string{} |
|
||||
var err = c.client.get(accessToken, &resp, url.Path(accountCredentialsRegistratorsPath), url.Param("realm", realmName), hdrAcceptJSON) |
|
||||
return resp, err |
|
||||
} |
|
||||
|
|
||||
// UpdateLabelCredential updates the label of credential
|
|
||||
func (c *AccountClient) UpdateLabelCredential(accessToken string, realmName string, credentialID string, label string) error { |
|
||||
return c.client.put(accessToken, url.Path(accountCredentialLabelPath), url.Param("realm", realmName), url.Param("credentialID", credentialID), body.String(label), hdrAcceptJSON, hdrContentTypeTextPlain) |
|
||||
} |
|
||||
|
|
||||
// DeleteCredential deletes the credential
|
|
||||
func (c *AccountClient) DeleteCredential(accessToken string, realmName string, credentialID string) error { |
|
||||
return c.client.delete(accessToken, url.Path(accountCredentialIDPath), url.Param("realm", realmName), url.Param("credentialID", credentialID), hdrAcceptJSON) |
|
||||
} |
|
||||
|
|
||||
// MoveToFirst moves the credential at the top of the list
|
|
||||
func (c *AccountClient) MoveToFirst(accessToken string, realmName string, credentialID string) error { |
|
||||
_, err := c.client.post(accessToken, nil, url.Path(accountMoveFirstPath), url.Param("realm", realmName), url.Param("credentialID", credentialID), hdrAcceptJSON) |
|
||||
return err |
|
||||
} |
|
||||
|
|
||||
// MoveAfter moves the credential after the specified one into the list
|
|
||||
func (c *AccountClient) MoveAfter(accessToken string, realmName string, credentialID string, previousCredentialID string) error { |
|
||||
_, err := c.client.post(accessToken, nil, url.Path(accountMoveAfterPath), url.Param("realm", realmName), url.Param("credentialID", credentialID), url.Param("previousCredentialID", previousCredentialID), hdrAcceptJSON) |
|
||||
return err |
|
||||
} |
|
||||
|
|
||||
// UpdatePassword updates the user's password
|
|
||||
// Parameters: realm, currentPassword, newPassword, confirmPassword
|
|
||||
func (c *AccountClient) UpdatePassword(accessToken, realm, currentPassword, newPassword, confirmPassword string) (string, error) { |
|
||||
var m = map[string]string{"currentPassword": currentPassword, "newPassword": newPassword, "confirmation": confirmPassword} |
|
||||
return c.client.post(accessToken, nil, url.Path(accountPasswordPath), url.Param("realm", realm), body.JSON(m)) |
|
||||
} |
|
||||
|
|
||||
// GetAccount provides the user's information
|
|
||||
func (c *AccountClient) GetAccount(accessToken string, realm string) (keycloak.UserRepresentation, error) { |
|
||||
var resp = keycloak.UserRepresentation{} |
|
||||
var err = c.client.get(accessToken, &resp, url.Path(accountExtensionAPIPath), url.Param("realm", realm), hdrAcceptJSON) |
|
||||
return resp, err |
|
||||
} |
|
||||
|
|
||||
// UpdateAccount updates the user's information
|
|
||||
func (c *AccountClient) UpdateAccount(accessToken string, realm string, user keycloak.UserRepresentation) error { |
|
||||
_, err := c.client.post(accessToken, nil, url.Path(accountExtensionAPIPath), url.Param("realm", realm), body.JSON(user)) |
|
||||
return err |
|
||||
} |
|
||||
|
|
||||
// DeleteAccount deletes current user
|
|
||||
func (c *AccountClient) DeleteAccount(accessToken string, realmName string) error { |
|
||||
return c.client.delete(accessToken, url.Path(accountExtensionAPIPath), url.Param("realm", realmName), hdrAcceptJSON) |
|
||||
} |
|
||||
|
|
||||
// ExecuteActionsEmail sends an email with required actions to the user
|
|
||||
func (c *AccountClient) ExecuteActionsEmail(accessToken string, realmName string, actions []string) error { |
|
||||
return c.client.put(accessToken, url.Path(accountExecuteActionsEmail), url.Param("realm", realmName), body.JSON(actions)) |
|
||||
} |
|
||||
|
|
||||
// SendEmail sends an email
|
|
||||
func (c *AccountClient) SendEmail(accessToken, realmName, template, subject string, recipient *string, attributes map[string]string) error { |
|
||||
var plugins []plugin.Plugin |
|
||||
plugins = append(plugins, url.Path(accountSendEmail), url.Param("realm", realmName)) |
|
||||
plugins = append(plugins, query.Add("template", template), query.Add("subject", subject)) |
|
||||
if recipient != nil && len(*recipient) >= 0 { |
|
||||
plugins = append(plugins, query.Add("recipient", *recipient)) |
|
||||
} |
|
||||
plugins = append(plugins, body.JSON(attributes)) |
|
||||
_, err := c.client.post(accessToken, nil, plugins...) |
|
||||
return err |
|
||||
} |
|
||||
Binary file not shown.
@ -1,66 +0,0 @@ |
|||||
package toolbox |
|
||||
|
|
||||
import ( |
|
||||
"errors" |
|
||||
"net/url" |
|
||||
"regexp" |
|
||||
"strings" |
|
||||
"time" |
|
||||
|
|
||||
"github.com/cloudtrust/keycloak-client/v3" |
|
||||
) |
|
||||
|
|
||||
// IssuerManager provides URL according to a given context
|
|
||||
type IssuerManager interface { |
|
||||
GetOidcVerifierProvider(issuer string) (OidcVerifierProvider, error) |
|
||||
} |
|
||||
|
|
||||
type issuerManager struct { |
|
||||
domainToVerifier map[string]OidcVerifierProvider |
|
||||
} |
|
||||
|
|
||||
func getProtocolAndDomain(URL string) string { |
|
||||
var r = regexp.MustCompile(`^\w+:\/\/[^\/]+`) |
|
||||
var match = r.FindStringSubmatch(URL) |
|
||||
if match != nil { |
|
||||
return strings.ToLower(match[0]) |
|
||||
} |
|
||||
// Best effort: if not found return the whole input string
|
|
||||
return URL |
|
||||
} |
|
||||
|
|
||||
// NewIssuerManager creates a new URLProvider
|
|
||||
func NewIssuerManager(config keycloak.Config) (IssuerManager, error) { |
|
||||
URLs := config.AddrTokenProvider |
|
||||
// Use default values when clients are not initializing these values
|
|
||||
cacheTTL := config.CacheTTL |
|
||||
if cacheTTL == 0 { |
|
||||
cacheTTL = 15 * time.Minute |
|
||||
} |
|
||||
errTolerance := config.ErrorTolerance |
|
||||
if errTolerance == 0 { |
|
||||
errTolerance = time.Minute |
|
||||
} |
|
||||
|
|
||||
var domainToVerifier = make(map[string]OidcVerifierProvider) |
|
||||
|
|
||||
for _, value := range strings.Split(URLs, " ") { |
|
||||
uToken, err := url.Parse(value) |
|
||||
if err != nil { |
|
||||
return nil, err |
|
||||
} |
|
||||
verifier := NewVerifierCache(uToken, cacheTTL, errTolerance) |
|
||||
domainToVerifier[getProtocolAndDomain(value)] = verifier |
|
||||
} |
|
||||
return &issuerManager{ |
|
||||
domainToVerifier: domainToVerifier, |
|
||||
}, nil |
|
||||
} |
|
||||
|
|
||||
func (im *issuerManager) GetOidcVerifierProvider(issuer string) (OidcVerifierProvider, error) { |
|
||||
issuerDomain := getProtocolAndDomain(issuer) |
|
||||
if verifier, ok := im.domainToVerifier[issuerDomain]; ok { |
|
||||
return verifier, nil |
|
||||
} |
|
||||
return nil, errors.New("Unknown issuer") |
|
||||
} |
|
||||
@ -1,51 +0,0 @@ |
|||||
package toolbox |
|
||||
|
|
||||
import ( |
|
||||
"fmt" |
|
||||
"testing" |
|
||||
|
|
||||
"github.com/cloudtrust/keycloak-client/v3" |
|
||||
"github.com/stretchr/testify/assert" |
|
||||
) |
|
||||
|
|
||||
type contextKey int |
|
||||
|
|
||||
const ( |
|
||||
keyContextIssuerDomain contextKey = iota |
|
||||
) |
|
||||
|
|
||||
func TestGetProtocolAndDomain(t *testing.T) { |
|
||||
var invalidURL = "not a valid URL" |
|
||||
assert.Equal(t, invalidURL, getProtocolAndDomain(invalidURL)) |
|
||||
assert.Equal(t, "https://elca.ch", getProtocolAndDomain("https://ELCA.CH/PATH/TO/TARGET")) |
|
||||
} |
|
||||
|
|
||||
func TestNewIssuerManager(t *testing.T) { |
|
||||
t.Run("Invalid URL", func(t *testing.T) { |
|
||||
_, err := NewIssuerManager(keycloak.Config{AddrTokenProvider: ":"}) |
|
||||
assert.NotNil(t, err) |
|
||||
}) |
|
||||
|
|
||||
defaultPath := "http://default.domain.com:5555" |
|
||||
myDomainPath := "http://my.domain.com/path/to/somewhere" |
|
||||
otherDomainPath := "http://other.domain.com:2120/" |
|
||||
allDomains := fmt.Sprintf("%s %s %s", defaultPath, myDomainPath, otherDomainPath) |
|
||||
|
|
||||
prov, err := NewIssuerManager(keycloak.Config{AddrTokenProvider: allDomains}) |
|
||||
assert.Nil(t, err) |
|
||||
assert.NotNil(t, prov) |
|
||||
|
|
||||
// No issuer provided with context
|
|
||||
issuerNoContext, _ := prov.GetOidcVerifierProvider("") |
|
||||
// Unrecognized issuer provided in context
|
|
||||
issuerDefault, _ := prov.GetOidcVerifierProvider("http://unknown.issuer.com/one/path") |
|
||||
// Case insensitive
|
|
||||
issuerMyDomain, _ := prov.GetOidcVerifierProvider("http://MY.DOMAIN.COM/issuer") |
|
||||
// Other domain
|
|
||||
issuerOtherDomain, _ := prov.GetOidcVerifierProvider("http://other.domain.com:2120/any/thing/here") |
|
||||
|
|
||||
assert.Equal(t, issuerNoContext, issuerDefault) |
|
||||
assert.NotEqual(t, issuerNoContext, issuerMyDomain) |
|
||||
assert.NotEqual(t, issuerNoContext, issuerOtherDomain) |
|
||||
assert.NotEqual(t, issuerMyDomain, issuerOtherDomain) |
|
||||
} |
|
||||
@ -1,8 +0,0 @@ |
|||||
package toolbox |
|
||||
|
|
||||
import "context" |
|
||||
|
|
||||
// Logger interface for logging with level
|
|
||||
type Logger interface { |
|
||||
Warn(ctx context.Context, keyvals ...interface{}) |
|
||||
} |
|
||||
@ -1 +0,0 @@ |
|||||
package mock |
|
||||
@ -1,3 +0,0 @@ |
|||||
package toolbox |
|
||||
|
|
||||
//go:generate mockgen -destination=./mock/logger.go -package=mock -mock_names=Logger=Logger github.com/cloudtrust/keycloak-client/v3/toolbox Logger
|
|
||||
@ -1,102 +0,0 @@ |
|||||
package toolbox |
|
||||
|
|
||||
import ( |
|
||||
"bytes" |
|
||||
"context" |
|
||||
"encoding/json" |
|
||||
"fmt" |
|
||||
"net/http" |
|
||||
"net/url" |
|
||||
"strings" |
|
||||
"time" |
|
||||
|
|
||||
errorhandler "github.com/cloudtrust/common-service/errors" |
|
||||
"github.com/cloudtrust/keycloak-client/v3" |
|
||||
) |
|
||||
|
|
||||
// OidcTokenProvider provides OIDC tokens
|
|
||||
type OidcTokenProvider interface { |
|
||||
ProvideToken(ctx context.Context) (string, error) |
|
||||
} |
|
||||
|
|
||||
type oidcToken struct { |
|
||||
AccessToken string `json:"access_token,omitempty"` |
|
||||
ExpiresIn int64 `json:"expires_in,omitempty"` |
|
||||
RefreshToken string `json:"refresh_token,omitempty"` |
|
||||
RefreshExpiresIn int64 `json:"refresh_expires_in,omitempty"` |
|
||||
TokenType string `json:"token_type,omitempty"` |
|
||||
NotBeforePolicy int `json:"not-before-policy,omitempty"` |
|
||||
SessionState string `json:"session_state,omitempty"` |
|
||||
Scope string `json:"scope,omitempty"` |
|
||||
} |
|
||||
|
|
||||
type oidcTokenProvider struct { |
|
||||
timeout time.Duration |
|
||||
tokenURL string |
|
||||
reqBody string |
|
||||
logger Logger |
|
||||
oidcToken oidcToken |
|
||||
validUntil int64 |
|
||||
} |
|
||||
|
|
||||
const ( |
|
||||
// Max processing delay: let's assume that the user of OidcTokenProvider will have a maximum of 5 seconds to use the provided OIDC token
|
|
||||
maxProcessingDelay = int64(5) |
|
||||
) |
|
||||
|
|
||||
// NewOidcTokenProvider creates an OidcTokenProvider
|
|
||||
func NewOidcTokenProvider(config keycloak.Config, realm, username, password, clientID string, logger Logger) OidcTokenProvider { |
|
||||
var urls = strings.Split(config.AddrTokenProvider, " ") |
|
||||
var keycloakPublicURL = urls[0] |
|
||||
|
|
||||
var tokenURL = fmt.Sprintf("%s/auth/realms/%s/protocol/openid-connect/token", keycloakPublicURL, realm) |
|
||||
// If needed, can add &client_secret={secret}
|
|
||||
var body = fmt.Sprintf("grant_type=password&client_id=%s&username=%s&password=%s", |
|
||||
url.QueryEscape(clientID), url.QueryEscape(username), url.QueryEscape(password)) |
|
||||
|
|
||||
return &oidcTokenProvider{ |
|
||||
timeout: config.Timeout, |
|
||||
tokenURL: tokenURL, |
|
||||
reqBody: body, |
|
||||
logger: logger, |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
func (o *oidcTokenProvider) ProvideToken(ctx context.Context) (string, error) { |
|
||||
if o.validUntil+maxProcessingDelay > time.Now().Unix() { |
|
||||
return o.oidcToken.AccessToken, nil |
|
||||
} |
|
||||
|
|
||||
var mimeType = "application/x-www-form-urlencoded" |
|
||||
var httpClient = http.Client{ |
|
||||
Timeout: o.timeout, |
|
||||
} |
|
||||
var resp, err = httpClient.Post(o.tokenURL, mimeType, strings.NewReader(o.reqBody)) |
|
||||
if err != nil { |
|
||||
o.logger.Warn(ctx, "msg", err.Error()) |
|
||||
return "", errorhandler.CreateInternalServerError("unexpected.httpResponse") |
|
||||
} |
|
||||
if err == nil && resp.StatusCode == http.StatusUnauthorized { |
|
||||
o.logger.Warn(ctx, "msg", "Technical user credentials are invalid") |
|
||||
return "", errorhandler.Error{ |
|
||||
Status: http.StatusUnauthorized, |
|
||||
Message: errorhandler.GetEmitter() + ".unauthorized", |
|
||||
} |
|
||||
} |
|
||||
if resp.StatusCode >= 400 || resp.Body == http.NoBody || resp.Body == nil { |
|
||||
o.logger.Warn(ctx, "msg", fmt.Sprintf("Unexpected behavior: unexpected http status (%d) or response has no body", resp.StatusCode)) |
|
||||
return "", errorhandler.CreateInternalServerError("unexpected.httpResponse") |
|
||||
} |
|
||||
|
|
||||
buf := new(bytes.Buffer) |
|
||||
_, _ = buf.ReadFrom(resp.Body) |
|
||||
|
|
||||
err = json.Unmarshal(buf.Bytes(), &o.oidcToken) |
|
||||
if err != nil { |
|
||||
o.logger.Warn(ctx, "msg", fmt.Sprintf("Can't deserialize token. JSON: %s", buf.String())) |
|
||||
return "", errorhandler.CreateInternalServerError("unexpected.oidcToken") |
|
||||
} |
|
||||
o.validUntil = time.Now().Unix() + o.oidcToken.ExpiresIn |
|
||||
|
|
||||
return o.oidcToken.AccessToken, nil |
|
||||
} |
|
||||
@ -1,107 +0,0 @@ |
|||||
package toolbox |
|
||||
|
|
||||
import ( |
|
||||
"context" |
|
||||
"encoding/json" |
|
||||
"fmt" |
|
||||
"net/http" |
|
||||
"net/http/httptest" |
|
||||
"testing" |
|
||||
"time" |
|
||||
|
|
||||
"github.com/golang/mock/gomock" |
|
||||
|
|
||||
"github.com/cloudtrust/keycloak-client/v3" |
|
||||
"github.com/cloudtrust/keycloak-client/v3/toolbox/mock" |
|
||||
"github.com/gorilla/mux" |
|
||||
"github.com/stretchr/testify/assert" |
|
||||
) |
|
||||
|
|
||||
type TestResponse struct { |
|
||||
StatusCode int |
|
||||
NoBody bool |
|
||||
ResponseBody string |
|
||||
} |
|
||||
|
|
||||
func (t *TestResponse) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
|
||||
w.WriteHeader(t.StatusCode) |
|
||||
if !t.NoBody { |
|
||||
w.Write([]byte(t.ResponseBody)) |
|
||||
} |
|
||||
time.Sleep(20 * time.Millisecond) |
|
||||
} |
|
||||
|
|
||||
func TestCreateToken(t *testing.T) { |
|
||||
var mockCtrl = gomock.NewController(t) |
|
||||
defer mockCtrl.Finish() |
|
||||
|
|
||||
var mockLogger = mock.NewLogger(mockCtrl) |
|
||||
mockLogger.EXPECT().Warn(gomock.Any(), gomock.Any()).AnyTimes() |
|
||||
|
|
||||
var oidcToken = oidcToken{ |
|
||||
AccessToken: `eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJZRTUtNUpBb2NOcG5zeEpEaGRyYVdyWlVCZTBMR2xfanNILUtnb1EwWi1FIn0.eyJqdGkiOiIxYjJkZDY2NS01ZGE1LTRiMzAtODY0MS0wNWQ4ZTk0NTQ2ZWQiLCJleHAiOjE1NzkyMDQ0ODYsIm5iZiI6MCwiaWF0IjoxNTc5MTY4NDg2LCJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjgwODAvYXV0aC9yZWFsbXMvbWFzdGVyIiwiYXVkIjpbInBhc3NmbG93LXJlYWxtIiwibXlfcmVhbG0tcmVhbG0iLCJDbG91ZHRydXN0LXJlYWxtIiwibWFzdGVyLXJlYWxtIiwiYWNjb3VudCJdLCJzdWIiOiI3OTU5MjhjMy03N2Y1LTRmMjQtOTI0NC02NzBkMGJmMDJhMmQiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJhZG1pbi1jbGkiLCJhdXRoX3RpbWUiOjAsInNlc3Npb25fc3RhdGUiOiI5ZTQ5NDA1MS1kZGQ1LTRhODctYTczZC1hOWU5YjMwYmFlZGEiLCJhY3IiOiIxIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImNyZWF0ZS1yZWFsbSIsIm9mZmxpbmVfYWNjZXNzIiwiYWRtaW4iLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7InBhc3NmbG93LXJlYWxtIjp7InJvbGVzIjpbInZpZXctcmVhbG0iLCJ2aWV3LWlkZW50aXR5LXByb3ZpZGVycyIsIm1hbmFnZS1pZGVudGl0eS1wcm92aWRlcnMiLCJpbXBlcnNvbmF0aW9uIiwiY3JlYXRlLWNsaWVudCIsIm1hbmFnZS11c2VycyIsInF1ZXJ5LXJlYWxtcyIsInZpZXctYXV0aG9yaXphdGlvbiIsInF1ZXJ5LWNsaWVudHMiLCJxdWVyeS11c2VycyIsIm1hbmFnZS1ldmVudHMiLCJtYW5hZ2UtcmVhbG0iLCJ2aWV3LWV2ZW50cyIsInZpZXctdXNlcnMiLCJ2aWV3LWNsaWVudHMiLCJtYW5hZ2UtYXV0aG9yaXphdGlvbiIsIm1hbmFnZS1jbGllbnRzIiwicXVlcnktZ3JvdXBzIl19LCJteV9yZWFsbS1yZWFsbSI6eyJyb2xlcyI6WyJ2aWV3LXJlYWxtIiwidmlldy1pZGVudGl0eS1wcm92aWRlcnMiLCJtYW5hZ2UtaWRlbnRpdHktcHJvdmlkZXJzIiwiaW1wZXJzb25hdGlvbiIsImNyZWF0ZS1jbGllbnQiLCJtYW5hZ2UtdXNlcnMiLCJxdWVyeS1yZWFsbXMiLCJ2aWV3LWF1dGhvcml6YXRpb24iLCJxdWVyeS1jbGllbnRzIiwicXVlcnktdXNlcnMiLCJtYW5hZ2UtZXZlbnRzIiwibWFuYWdlLXJlYWxtIiwidmlldy1ldmVudHMiLCJ2aWV3LXVzZXJzIiwidmlldy1jbGllbnRzIiwibWFuYWdlLWF1dGhvcml6YXRpb24iLCJtYW5hZ2UtY2xpZW50cyIsInF1ZXJ5LWdyb3VwcyJdfSwiQ2xvdWR0cnVzdC1yZWFsbSI6eyJyb2xlcyI6WyJ2aWV3LWlkZW50aXR5LXByb3ZpZGVycyIsInZpZXctcmVhbG0iLCJtYW5hZ2UtaWRlbnRpdHktcHJvdmlkZXJzIiwiaW1wZXJzb25hdGlvbiIsImNyZWF0ZS1jbGllbnQiLCJtYW5hZ2UtdXNlcnMiLCJxdWVyeS1yZWFsbXMiLCJ2aWV3LWF1dGhvcml6YXRpb24iLCJxdWVyeS1jbGllbnRzIiwicXVlcnktdXNlcnMiLCJtYW5hZ2UtZXZlbnRzIiwibWFuYWdlLXJlYWxtIiwidmlldy1ldmVudHMiLCJ2aWV3LXVzZXJzIiwidmlldy1jbGllbnRzIiwibWFuYWdlLWF1dGhvcml6YXRpb24iLCJtYW5hZ2UtY2xpZW50cyIsInF1ZXJ5LWdyb3VwcyJdfSwibWFzdGVyLXJlYWxtIjp7InJvbGVzIjpbInZpZXctcmVhbG0iLCJ2aWV3LWlkZW50aXR5LXByb3ZpZGVycyIsIm1hbmFnZS1pZGVudGl0eS1wcm92aWRlcnMiLCJpbXBlcnNvbmF0aW9uIiwiY3JlYXRlLWNsaWVudCIsIm1hbmFnZS11c2VycyIsInF1ZXJ5LXJlYWxtcyIsInZpZXctYXV0aG9yaXphdGlvbiIsInF1ZXJ5LWNsaWVudHMiLCJxdWVyeS11c2VycyIsIm1hbmFnZS1ldmVudHMiLCJtYW5hZ2UtcmVhbG0iLCJ2aWV3LWV2ZW50cyIsInZpZXctdXNlcnMiLCJ2aWV3LWNsaWVudHMiLCJtYW5hZ2UtYXV0aG9yaXphdGlvbiIsIm1hbmFnZS1jbGllbnRzIiwicXVlcnktZ3JvdXBzIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUgZ3JvdXBzIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJncm91cHMiOlsiL3RvZV9hZG1pbmlzdHJhdG9yIl0sInByZWZlcnJlZF91c2VybmFtZSI6ImFkbWluIn0.GrPUDdQUID0S38ZSwVqarAuSDXrl5cJju3uq32y6bNGPK9a8jcHBP5FEfMcZ3vieQPtWFeySMycwTEcH6x6lc-9bj1w5veL4yyTA1zk_ERPshfiobk0u94vuljnoz-PW7JvBLOy47Bk5cP9pHPbJMPY0kOFHTpXZHd6KwfcE_X8gizLw4rDhIpK1NEtABQVzUNvP9fDZOm2I1PHJbl0odRE7EFu9Xh5ya8DaUQ2RKUb0E5csnA3DYlFdEhtMV1MAKRzqplDzj8zLQ8f8fflzC9_g4vmnDUEKSBxq1f1qKmzm1-XUuqRYTNWHfOtRR9rXrEzn-6fymFcRHIVGW7kgzg`, |
|
||||
ExpiresIn: 3600, |
|
||||
} |
|
||||
var validJSON, _ = json.Marshal(oidcToken) |
|
||||
|
|
||||
r := mux.NewRouter() |
|
||||
r.Handle("/auth/realms/nobody/protocol/openid-connect/token", &TestResponse{StatusCode: http.StatusOK, NoBody: true}) |
|
||||
r.Handle("/auth/realms/invalid/protocol/openid-connect/token", &TestResponse{StatusCode: http.StatusUnauthorized}) |
|
||||
r.Handle("/auth/realms/bad-json/protocol/openid-connect/token", &TestResponse{StatusCode: http.StatusOK, ResponseBody: `{"truncated-`}) |
|
||||
r.Handle("/auth/realms/valid/protocol/openid-connect/token", &TestResponse{StatusCode: http.StatusOK, ResponseBody: string(validJSON)}) |
|
||||
|
|
||||
ts := httptest.NewServer(r) |
|
||||
defer ts.Close() |
|
||||
|
|
||||
t.Run("No body in HTTP response", func(t *testing.T) { |
|
||||
var p = NewOidcTokenProvider(keycloak.Config{AddrTokenProvider: ts.URL}, "nobody", "user", "passwd", "clientID", mockLogger) |
|
||||
var _, err = p.ProvideToken(context.TODO()) |
|
||||
assert.NotNil(t, err) |
|
||||
}) |
|
||||
|
|
||||
t.Run("Invalid credentials", func(t *testing.T) { |
|
||||
var p = NewOidcTokenProvider(keycloak.Config{AddrTokenProvider: ts.URL}, "invalid", "user", "passwd", "clientID", mockLogger) |
|
||||
var _, err = p.ProvideToken(context.TODO()) |
|
||||
assert.NotNil(t, err) |
|
||||
}) |
|
||||
|
|
||||
t.Run("Invalid JSON", func(t *testing.T) { |
|
||||
var p = NewOidcTokenProvider(keycloak.Config{AddrTokenProvider: ts.URL}, "bad-json", "user", "passwd", "clientID", mockLogger) |
|
||||
var _, err = p.ProvideToken(context.TODO()) |
|
||||
assert.NotNil(t, err) |
|
||||
}) |
|
||||
|
|
||||
t.Run("No HTTP response", func(t *testing.T) { |
|
||||
var p = NewOidcTokenProvider(keycloak.Config{AddrTokenProvider: ts.URL + "0"}, "bad-json", "user", "passwd", "clientID", mockLogger) |
|
||||
var _, err = p.ProvideToken(context.TODO()) |
|
||||
assert.NotNil(t, err) |
|
||||
}) |
|
||||
|
|
||||
t.Run("Valid credentials", func(t *testing.T) { |
|
||||
var p = NewOidcTokenProvider(keycloak.Config{AddrTokenProvider: ts.URL}, "valid", "user", "passwd", "clientID", mockLogger) |
|
||||
|
|
||||
var timeStart = time.Now() |
|
||||
|
|
||||
// First call
|
|
||||
var token, err = p.ProvideToken(context.TODO()) |
|
||||
assert.Nil(t, err) |
|
||||
assert.NotEqual(t, "", token) |
|
||||
|
|
||||
var timeAfterFirstCall = time.Now() |
|
||||
|
|
||||
// Second call
|
|
||||
token, err = p.ProvideToken(context.TODO()) |
|
||||
assert.Nil(t, err) |
|
||||
assert.NotEqual(t, "", token) |
|
||||
|
|
||||
var timeAfterSecondCall = time.Now() |
|
||||
|
|
||||
var withHTTPDuration = int64(20 * time.Millisecond) |
|
||||
var withoutHTTPDuration = int64(5 * time.Millisecond) |
|
||||
var duration1 = timeAfterFirstCall.Sub(timeStart).Nanoseconds() |
|
||||
var duration2 = timeAfterSecondCall.Sub(timeAfterFirstCall).Nanoseconds() |
|
||||
var msg = fmt.Sprintf("Durations: no valid token loaded yet:%d (expected > %d), token not expired:%d (expected < %d)", duration1, withHTTPDuration, duration2, withoutHTTPDuration) |
|
||||
assert.True(t, duration1 > withHTTPDuration, msg) |
|
||||
assert.True(t, duration2 < withoutHTTPDuration, msg) |
|
||||
}) |
|
||||
} |
|
||||
@ -1,94 +0,0 @@ |
|||||
package toolbox |
|
||||
|
|
||||
import ( |
|
||||
"context" |
|
||||
"fmt" |
|
||||
"net/url" |
|
||||
"sync" |
|
||||
"time" |
|
||||
|
|
||||
"github.com/cloudtrust/keycloak-client/v3" |
|
||||
oidc "github.com/coreos/go-oidc" |
|
||||
"github.com/pkg/errors" |
|
||||
) |
|
||||
|
|
||||
// OidcVerifierProvider is an interface for a provider of OidcVerifier instances
|
|
||||
type OidcVerifierProvider interface { |
|
||||
GetOidcVerifier(realm string) (OidcVerifier, error) |
|
||||
} |
|
||||
|
|
||||
// OidcVerifier is an interface for OIDC token verifiers
|
|
||||
type OidcVerifier interface { |
|
||||
Verify(accessToken string) error |
|
||||
} |
|
||||
|
|
||||
type verifierCache struct { |
|
||||
duration time.Duration |
|
||||
errorTolerance time.Duration |
|
||||
tokenURL *url.URL |
|
||||
verifiers map[string]cachedVerifier |
|
||||
verifiersMutex sync.RWMutex |
|
||||
} |
|
||||
|
|
||||
type cachedVerifier struct { |
|
||||
verifier *oidc.IDTokenVerifier |
|
||||
createdAt time.Time |
|
||||
expireAt time.Time |
|
||||
invalidateOnErrorAt time.Time |
|
||||
} |
|
||||
|
|
||||
// NewVerifierCache create an instance of OIDC verifier cache
|
|
||||
func NewVerifierCache(tokenURL *url.URL, timeToLive time.Duration, errorTolerance time.Duration) OidcVerifierProvider { |
|
||||
return &verifierCache{ |
|
||||
duration: timeToLive, |
|
||||
errorTolerance: errorTolerance, |
|
||||
tokenURL: tokenURL, |
|
||||
verifiers: make(map[string]cachedVerifier), |
|
||||
verifiersMutex: sync.RWMutex{}, |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
func (vc *verifierCache) GetOidcVerifier(realm string) (OidcVerifier, error) { |
|
||||
vc.verifiersMutex.RLock() |
|
||||
v, ok := vc.verifiers[realm] |
|
||||
vc.verifiersMutex.RUnlock() |
|
||||
if ok && v.isValid() { |
|
||||
return &v, nil |
|
||||
} |
|
||||
var oidcProvider *oidc.Provider |
|
||||
{ |
|
||||
var err error |
|
||||
var issuer = fmt.Sprintf("%s/auth/realms/%s", vc.tokenURL.String(), realm) |
|
||||
oidcProvider, err = oidc.NewProvider(context.Background(), issuer) |
|
||||
if err != nil { |
|
||||
return nil, errors.Wrap(err, keycloak.MsgErrCannotCreate+"."+keycloak.OIDCProvider) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
ov := oidcProvider.Verifier(&oidc.Config{SkipClientIDCheck: true}) |
|
||||
res := cachedVerifier{ |
|
||||
createdAt: time.Now(), |
|
||||
expireAt: time.Now().Add(vc.duration), |
|
||||
invalidateOnErrorAt: time.Now().Add(vc.errorTolerance), |
|
||||
verifier: ov, |
|
||||
} |
|
||||
vc.verifiersMutex.Lock() |
|
||||
vc.verifiers[realm] = res |
|
||||
vc.verifiersMutex.Unlock() |
|
||||
|
|
||||
return &res, nil |
|
||||
} |
|
||||
|
|
||||
func (cv *cachedVerifier) isValid() bool { |
|
||||
return time.Now().Before(cv.expireAt) |
|
||||
} |
|
||||
|
|
||||
func (cv *cachedVerifier) Verify(accessToken string) error { |
|
||||
_, err := cv.verifier.Verify(context.Background(), accessToken) |
|
||||
if err != nil && time.Now().After(cv.invalidateOnErrorAt) { |
|
||||
// An error occured and current time is after invalidateOnErrorAt
|
|
||||
// Let's make this verifier expire
|
|
||||
cv.expireAt = cv.createdAt |
|
||||
} |
|
||||
return err |
|
||||
} |
|
||||
@ -1,112 +0,0 @@ |
|||||
package toolbox |
|
||||
|
|
||||
import ( |
|
||||
"context" |
|
||||
"encoding/json" |
|
||||
"net/http" |
|
||||
"net/http/httptest" |
|
||||
"net/url" |
|
||||
"strings" |
|
||||
"testing" |
|
||||
"time" |
|
||||
|
|
||||
http_transport "github.com/go-kit/kit/transport/http" |
|
||||
"github.com/gorilla/mux" |
|
||||
"github.com/stretchr/testify/assert" |
|
||||
) |
|
||||
|
|
||||
func decodeRequest(_ context.Context, req *http.Request) (interface{}, error) { |
|
||||
res := map[string]string{"realm": mux.Vars(req)["realm"], "host": req.Host} |
|
||||
return res, nil |
|
||||
} |
|
||||
|
|
||||
func encodeReply(_ context.Context, w http.ResponseWriter, rep interface{}) error { |
|
||||
if rep == nil { |
|
||||
w.WriteHeader(404) |
|
||||
return nil |
|
||||
} |
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8") |
|
||||
w.WriteHeader(200) |
|
||||
|
|
||||
var json, err = json.Marshal(rep) |
|
||||
if err == nil { |
|
||||
w.Write(json) |
|
||||
} |
|
||||
return nil |
|
||||
} |
|
||||
|
|
||||
func errorHandler(_ context.Context, _ error, w http.ResponseWriter) { |
|
||||
w.WriteHeader(500) |
|
||||
} |
|
||||
|
|
||||
func endpoint(_ context.Context, request interface{}) (response interface{}, err error) { |
|
||||
var query = request.(map[string]string) |
|
||||
if !strings.Contains(query["realm"], "realm") { |
|
||||
return nil, nil |
|
||||
} |
|
||||
return map[string]string{ |
|
||||
"issuer": "http://" + query["host"] + "/auth/realms/" + query["realm"], |
|
||||
"authorization_endpoint": "", |
|
||||
"token_endpoint": "", |
|
||||
"jwks_uri": "", |
|
||||
"userinfo_endpoint": "", |
|
||||
}, nil |
|
||||
} |
|
||||
|
|
||||
func TestGetOidcVerifier(t *testing.T) { |
|
||||
verifierHandler := http_transport.NewServer(endpoint, decodeRequest, encodeReply, http_transport.ServerErrorEncoder(errorHandler)) |
|
||||
|
|
||||
r := mux.NewRouter() |
|
||||
r.Handle("/auth/realms/{realm}/.well-known/openid-configuration", verifierHandler) |
|
||||
|
|
||||
ts := httptest.NewServer(r) |
|
||||
defer ts.Close() |
|
||||
|
|
||||
url, _ := url.Parse(ts.URL) |
|
||||
|
|
||||
{ |
|
||||
// First test with a verifier which hardly expires
|
|
||||
verifier := NewVerifierCache(url, time.Minute, 10*time.Minute) |
|
||||
|
|
||||
{ |
|
||||
// Unknown realm: can't get verifier
|
|
||||
_, err := verifier.GetOidcVerifier("unknown") |
|
||||
assert.NotNil(t, err) |
|
||||
} |
|
||||
|
|
||||
v1, e := verifier.GetOidcVerifier("realm1") |
|
||||
assert.Nil(t, e) |
|
||||
{ |
|
||||
// Ask for the same realm before its verifier expires
|
|
||||
v2, _ := verifier.GetOidcVerifier("realm1") |
|
||||
assert.Equal(t, v1, v2) |
|
||||
} |
|
||||
{ |
|
||||
// Ask for a different verifier
|
|
||||
v3, _ := verifier.GetOidcVerifier("realm2") |
|
||||
assert.NotEqual(t, v1, v3) |
|
||||
} |
|
||||
|
|
||||
time.Sleep(100 * time.Millisecond) |
|
||||
assert.NotNil(t, v1.Verify("abcdef")) |
|
||||
} |
|
||||
|
|
||||
{ |
|
||||
// Now, test with a verifier which quickly expires on error
|
|
||||
verifier := NewVerifierCache(url, time.Minute, time.Millisecond) |
|
||||
v1, _ := verifier.GetOidcVerifier("realm1") |
|
||||
time.Sleep(100 * time.Millisecond) |
|
||||
{ |
|
||||
// Ask for the same realm before its verifier expires
|
|
||||
v2, _ := verifier.GetOidcVerifier("realm1") |
|
||||
assert.Equal(t, v1, v2) |
|
||||
} |
|
||||
{ |
|
||||
// Verify an invalid token
|
|
||||
assert.NotNil(t, v1.Verify("abcdef")) |
|
||||
// Ask for the same realm before its verifier expires but after an error occured
|
|
||||
v2, _ := verifier.GetOidcVerifier("realm1") |
|
||||
assert.NotEqual(t, v1, v2) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
Loading…
Reference in new issue