diff --git a/.gitignore b/.gitignore index 81cf92f..d565b7e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ # Test binary, build with `go test -c` *.test +mock/ # Output of the go coverage tool, specifically when used with LiteIDE *.out @@ -18,4 +19,5 @@ vendor/* bin/* **/debug -**/.coverprofile \ No newline at end of file +**/.coverprofile +mock/ diff --git a/Gopkg.lock b/Gopkg.lock index 823360e..ed7b2c5 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,16 +2,12 @@ [[projects]] - branch = "master" - digest = "1:840df8ec522c8d8d5f2f6fe0cf507e2370dcd5137bc119c1a280211e0cae2b9a" + digest = "1:fcbbb02d897902b3460476fcc6723f21a1674c0b6bd4a5579de11d53983d3d70" name = "github.com/cloudtrust/common-service" - packages = [ - ".", - "errors", - "log", - ] + packages = ["errors"] pruneopts = "" - revision = "641630ce2eb40df09540f688a885ef450ea10d6b" + revision = "0dac96e146315562f548272f90e3f0ccf9ea7ddb" + version = "v2.2.0" [[projects]] digest = "1:bb7f91ab4d1c44a3bb2651c613463c134165bda0282fca891a63b88d1b501997" @@ -38,17 +34,17 @@ version = "v2.0.0" [[projects]] - digest = "1:48e65aaf8ce34ffb3e8d56daa9417826db162afbc2040705db331e9a2e9eebe3" + digest = "1:324437ddda77526fea02c29a2208ef1260e7a96ff0202965143215941564c400" name = "github.com/go-kit/kit" packages = [ "endpoint", "log", - "log/level", + "transport", "transport/http", ] pruneopts = "" - revision = "12210fb6ace19e0496167bb3e667dcd91fa9f69b" - version = "v0.8.0" + revision = "cc938d52e0cdf4c811ab203f428fcd06f9d9a148" + version = "v0.10.0" [[projects]] digest = "1:aa9a6ccd5fd7d29804a27cb0666bc4ac5eb4b73439b7edd54f4b377e5ef8bb47" @@ -59,12 +55,20 @@ version = "v0.5.0" [[projects]] - digest = "1:686f864c452288f97bb25a51da037cdad321b40e17a803aa7645f848c729874e" + digest = "1:03f8f40437ef8b7a1c7aed0ab000474245559153639ee44d6d43ec8eb9e24eda" + name = "github.com/golang/mock" + packages = ["gomock"] + pruneopts = "" + revision = "3a35fb6e3e18b9dbfee291262260dee7372d2a92" + version = "v1.4.3" + +[[projects]] + digest = "1:02c7a8570f619bdb5620e8f6a16407455c8e63433d8a9e829f618813dc7f89a5" name = "github.com/golang/protobuf" packages = ["proto"] pruneopts = "" - revision = "6c66de79d66478d166c7ea05f5d2ccaf016fbd6b" - version = "v1.4.1" + revision = "d04d7b157bb510b1e0c10132224b616ac0e26b17" + version = "v1.4.2" [[projects]] digest = "1:fbab76ba211c99fcd45a481a32530efc229f3510fd94889f361dcaf13ff05fe0" @@ -110,16 +114,16 @@ version = "v1.0.5" [[projects]] - digest = "1:cc4eb6813da8d08694e557fcafae8fcc24f47f61a0717f952da130ca9a486dfc" + digest = "1:9cc06d7468e429919ead45fc408b26838b4dfb12075f9756685ac6eae867b1a3" name = "github.com/stretchr/testify" packages = ["assert"] pruneopts = "" - revision = "3ebf1ddaeb260c4b1ae502a01c7844fa8c1fa0e9" - version = "v1.5.1" + revision = "004e3cb72213e5e727e4e08f668ee6c8f27e5d32" + version = "v1.6.0" [[projects]] branch = "master" - digest = "1:90f8aa620559abef3e8222064705e420dcb3498085b20782d128e5fa477b3a89" + digest = "1:90e0a5569023a45d6f5ba62cf7b250b89a4310107e42c45b76481f518bb09cc7" name = "golang.org/x/crypto" packages = [ "ed25519", @@ -127,11 +131,11 @@ "pbkdf2", ] pruneopts = "" - revision = "06a226fb4e3765ef3f48aa2852b401bc7b98e981" + revision = "279210d13fedf5be6d476bad5df6a015042bb905" [[projects]] branch = "master" - digest = "1:ea84836e35d7a66c9b8944796295912509c80c921244bc4e098c5417219895f2" + digest = "1:7254fd3f1a6e5ae98ab721ce6f3f09a79a59d3a1426e045fabc2548101e37333" name = "golang.org/x/net" packages = [ "context", @@ -140,7 +144,7 @@ "publicsuffix", ] pruneopts = "" - revision = "7e3656a0809f6f95abd88ac65313578f80b00df2" + revision = "627f9648deb96c27737b83199d44bb5c1010cbcf" [[projects]] branch = "master" @@ -195,7 +199,7 @@ version = "v1.6.6" [[projects]] - digest = "1:3f5031bb27e860d92d43004a88db1143b1c393c0430f68e562f1efd7e8d186ec" + digest = "1:7579d802036911f3a087652830860739fcd8a03967ad5a3531e190aefe647f9d" name = "google.golang.org/protobuf" packages = [ "encoding/prototext", @@ -227,8 +231,8 @@ "runtime/protoimpl", ] pruneopts = "" - revision = "8b0d71ac935315b10385aaaf30175de0c74bbba6" - version = "v1.22.0" + revision = "5c3dd7024aed895adfe053f26b5a479e991cbca9" + version = "v1.24.0" [[projects]] digest = "1:95fa5eae3b22887e8aea55ad4f93bc1374d586f7dd3504cf0010845ccc0a95a8" @@ -266,23 +270,22 @@ version = "v2.5.1" [[projects]] - digest = "1:2efc9662a6a1ff28c65c84fc2f9030f13d3afecdb2ecad445f3b0c80e75fc281" - name = "gopkg.in/yaml.v2" + branch = "v3" + digest = "1:4eec9bf48dd37d5c72af308fb8609bb61ed7c18dc2677848ff1da45edb1dbbfd" + name = "gopkg.in/yaml.v3" packages = ["."] pruneopts = "" - revision = "53403b58ad1b561927d19068c655246f2db79d48" - version = "v2.2.8" + revision = "e3079894b1e86c57cd48ff0d67dfd45de276d131" [solve-meta] analyzer-name = "dep" analyzer-version = 1 input-imports = [ - "github.com/cloudtrust/common-service", "github.com/cloudtrust/common-service/errors", - "github.com/cloudtrust/common-service/log", "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", diff --git a/Gopkg.toml b/Gopkg.toml index c6e8c4f..e03946d 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -22,7 +22,7 @@ [[constraint]] name = "github.com/cloudtrust/common-service" - branch = "master" + version = "v2.2.0" [[constraint]] name = "github.com/pkg/errors" @@ -32,7 +32,6 @@ name = "gopkg.in/h2non/gentleman.v2" version = "2.0.0" - [[constraint]] name = "github.com/gbrlsnchs/jwt" version = "2.0.0" diff --git a/Jenkinsfile b/Jenkinsfile index d6b8c7a..e19f26e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -31,6 +31,8 @@ pipeline { 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 diff --git a/api/account.go b/api/account.go index 2082bee..a9626b1 100644 --- a/api/account.go +++ b/api/account.go @@ -19,39 +19,44 @@ const ( 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), headers.Set("Accept", "application/json")) + 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), headers.Set("Accept", "application/json")) + 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), headers.Set("Accept", "application/json"), headers.Set("Content-Type", "text/plain")) + 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), headers.Set("Accept", "application/json")) + 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), headers.Set("Accept", "application/json")) + _, 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), headers.Set("Accept", "application/json")) + _, err := c.client.post(accessToken, nil, url.Path(accountMoveAfterPath), url.Param("realm", realmName), url.Param("credentialID", credentialID), url.Param("previousCredentialID", previousCredentialID), hdrAcceptJSON) return err } @@ -65,7 +70,7 @@ func (c *AccountClient) UpdatePassword(accessToken, realm, currentPassword, newP // 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), headers.Set("Accept", "application/json")) + var err = c.client.get(accessToken, &resp, url.Path(accountExtensionAPIPath), url.Param("realm", realm), hdrAcceptJSON) return resp, err } @@ -77,7 +82,7 @@ func (c *AccountClient) UpdateAccount(accessToken string, realm string, user key // DeleteAccount delete current user func (c *AccountClient) DeleteAccount(accessToken string, realmName string) error { - return c.client.delete(accessToken, url.Path(accountExtensionAPIPath), url.Param("realm", realmName), headers.Set("Accept", "application/json")) + return c.client.delete(accessToken, url.Path(accountExtensionAPIPath), url.Param("realm", realmName), hdrAcceptJSON) } // ExecuteActionsEmail send an email with required actions to the user diff --git a/api/authentication_management.go b/api/authentication_management.go index feb7f40..b51c04c 100644 --- a/api/authentication_management.go +++ b/api/authentication_management.go @@ -7,7 +7,10 @@ import ( ) const ( - authenticationManagementPath = "/auth/admin/realms/:realm/authentication" + authenticationManagementPath = "/auth/admin/realms/:realm/authentication" + authenticationConfigPath = authenticationManagementPath + "/config/:id" + authenticationRequiredActionsPath = authenticationManagementPath + "/required-actions" + authenticationRequiredActionPath = authenticationRequiredActionsPath + "/:alias" ) // GetAuthenticatorProviders returns a list of authenticator providers. @@ -34,18 +37,18 @@ func (c *Client) GetAuthenticatorProviderConfig(accessToken string, realmName, p // GetAuthenticatorConfig returns the authenticator configuration. func (c *Client) GetAuthenticatorConfig(accessToken string, realmName, configID string) (keycloak.AuthenticatorConfigRepresentation, error) { var resp = keycloak.AuthenticatorConfigRepresentation{} - var err = c.get(accessToken, &resp, url.Path(authenticationManagementPath+"/config/:id"), url.Param("realm", realmName), url.Param("id", configID)) + var err = c.get(accessToken, &resp, url.Path(authenticationConfigPath), url.Param("realm", realmName), url.Param("id", configID)) return resp, err } // UpdateAuthenticatorConfig updates the authenticator configuration. func (c *Client) UpdateAuthenticatorConfig(accessToken string, realmName, configID string, config keycloak.AuthenticatorConfigRepresentation) error { - return c.put(accessToken, url.Path(authenticationManagementPath+"/config/:id"), url.Param("realm", realmName), url.Param("id", configID), body.JSON(config)) + return c.put(accessToken, url.Path(authenticationConfigPath), url.Param("realm", realmName), url.Param("id", configID), body.JSON(config)) } // DeleteAuthenticatorConfig deletes the authenticator configuration. func (c *Client) DeleteAuthenticatorConfig(accessToken string, realmName, configID string) error { - return c.delete(accessToken, url.Path(authenticationManagementPath+"/config/:id"), url.Param("realm", realmName), url.Param("id", configID)) + return c.delete(accessToken, url.Path(authenticationConfigPath), url.Param("realm", realmName), url.Param("id", configID)) } // CreateAuthenticationExecution add new authentication execution @@ -167,25 +170,25 @@ func (c *Client) RegisterRequiredAction(accessToken string, realmName, providerI // GetRequiredActions returns a list of required actions. func (c *Client) GetRequiredActions(accessToken string, realmName string) ([]keycloak.RequiredActionProviderRepresentation, error) { var resp = []keycloak.RequiredActionProviderRepresentation{} - var err = c.get(accessToken, &resp, url.Path(authenticationManagementPath+"/required-actions"), url.Param("realm", realmName)) + var err = c.get(accessToken, &resp, url.Path(authenticationRequiredActionsPath), url.Param("realm", realmName)) return resp, err } // GetRequiredAction returns the required action for the alias. func (c *Client) GetRequiredAction(accessToken string, realmName, actionAlias string) (keycloak.RequiredActionProviderRepresentation, error) { var resp = keycloak.RequiredActionProviderRepresentation{} - var err = c.get(accessToken, &resp, url.Path(authenticationManagementPath+"/required-actions/:alias"), url.Param("realm", realmName), url.Param("alias", actionAlias)) + var err = c.get(accessToken, &resp, url.Path(authenticationRequiredActionPath), url.Param("realm", realmName), url.Param("alias", actionAlias)) return resp, err } // UpdateRequiredAction updates the required action. func (c *Client) UpdateRequiredAction(accessToken string, realmName, actionAlias string, action keycloak.RequiredActionProviderRepresentation) error { - return c.put(accessToken, url.Path(authenticationManagementPath+"/required-actions/:alias"), url.Param("realm", realmName), url.Param("alias", actionAlias), body.JSON(action)) + return c.put(accessToken, url.Path(authenticationRequiredActionPath), url.Param("realm", realmName), url.Param("alias", actionAlias), body.JSON(action)) } // DeleteRequiredAction deletes the required action. func (c *Client) DeleteRequiredAction(accessToken string, realmName, actionAlias string) error { - return c.delete(accessToken, url.Path(authenticationManagementPath+"/required-actions/:alias"), url.Param("realm", realmName), url.Param("alias", actionAlias)) + return c.delete(accessToken, url.Path(authenticationRequiredActionPath), url.Param("realm", realmName), url.Param("alias", actionAlias)) } // GetUnregisteredRequiredActions returns a list of unregistered required actions. diff --git a/api/credentials.go b/api/credentials.go index 7700e45..fa7a4bf 100644 --- a/api/credentials.go +++ b/api/credentials.go @@ -3,7 +3,6 @@ package api import ( "github.com/cloudtrust/keycloak-client" "gopkg.in/h2non/gentleman.v2/plugins/body" - "gopkg.in/h2non/gentleman.v2/plugins/headers" "gopkg.in/h2non/gentleman.v2/plugins/url" ) @@ -39,7 +38,7 @@ func (c *Client) GetCredentialTypes(accessToken string, realmName string) ([]str // UpdateLabelCredential updates the label of credential func (c *Client) UpdateLabelCredential(accessToken string, realmName string, userID string, credentialID string, label string) error { - return c.put(accessToken, url.Path(labelPath), url.Param("realm", realmName), url.Param("id", userID), url.Param("credentialID", credentialID), body.String(label), headers.Set("Accept", "application/json"), headers.Set("Content-Type", "text/plain")) + return c.put(accessToken, url.Path(labelPath), url.Param("realm", realmName), url.Param("id", userID), url.Param("credentialID", credentialID), body.String(label), hdrAcceptJSON, hdrContentTypeTextPlain) } // DeleteCredential deletes the credential diff --git a/api/keycloak_client.go b/api/keycloak_client.go index 9c545fd..02d8d37 100644 --- a/api/keycloak_client.go +++ b/api/keycloak_client.go @@ -35,11 +35,11 @@ type AccountClient struct { } // New returns a keycloak client. -func New(config keycloak.Config) (*Client, error) { +func New(config keycloak.Config, keyContextIssuerDomain interface{}) (*Client, error) { var issuerMgr toolbox.IssuerManager { var err error - issuerMgr, err = toolbox.NewIssuerManager(config) + issuerMgr, err = toolbox.NewIssuerManager(config, keyContextIssuerDomain) if err != nil { return nil, errors.Wrap(err, keycloak.MsgErrCannotParse+"."+keycloak.TokenProviderURL) } @@ -159,10 +159,7 @@ func (c *Client) get(accessToken string, data interface{}, plugins ...plugin.Plu switch { case resp.StatusCode == http.StatusUnauthorized: - return keycloak.HTTPError{ - HTTPStatus: resp.StatusCode, - Message: string(resp.Bytes()), - } + return keycloak.ClientDetailedError{HTTPStatus: http.StatusUnauthorized, Message: string(resp.Bytes())} case resp.StatusCode >= 400: return treatErrorStatus(resp) case resp.StatusCode >= 200: @@ -201,10 +198,7 @@ func (c *Client) post(accessToken string, data interface{}, plugins ...plugin.Pl switch { case resp.StatusCode == http.StatusUnauthorized: - return "", keycloak.HTTPError{ - HTTPStatus: resp.StatusCode, - Message: string(resp.Bytes()), - } + return "", keycloak.ClientDetailedError{HTTPStatus: http.StatusUnauthorized, Message: string(resp.Bytes())} case resp.StatusCode >= 400: return "", treatErrorStatus(resp) case resp.StatusCode >= 200: @@ -245,10 +239,7 @@ func (c *Client) delete(accessToken string, plugins ...plugin.Plugin) error { switch { case resp.StatusCode == http.StatusUnauthorized: - return keycloak.HTTPError{ - HTTPStatus: resp.StatusCode, - Message: string(resp.Bytes()), - } + return keycloak.ClientDetailedError{HTTPStatus: http.StatusUnauthorized, Message: string(resp.Bytes())} case resp.StatusCode >= 400: return treatErrorStatus(resp) case resp.StatusCode >= 200: @@ -282,10 +273,7 @@ func (c *Client) put(accessToken string, plugins ...plugin.Plugin) error { switch { case resp.StatusCode == http.StatusUnauthorized: - return keycloak.HTTPError{ - HTTPStatus: resp.StatusCode, - Message: string(resp.Bytes()), - } + return keycloak.ClientDetailedError{HTTPStatus: http.StatusUnauthorized, Message: string(resp.Bytes())} case resp.StatusCode >= 400: return treatErrorStatus(resp) case resp.StatusCode >= 200: diff --git a/api/realm.go b/api/realm.go index 3bc7256..23c5384 100644 --- a/api/realm.go +++ b/api/realm.go @@ -3,7 +3,6 @@ package api import ( "github.com/cloudtrust/keycloak-client" "gopkg.in/h2non/gentleman.v2/plugins/body" - "gopkg.in/h2non/gentleman.v2/plugins/headers" "gopkg.in/h2non/gentleman.v2/plugins/url" ) @@ -56,6 +55,6 @@ func (c *Client) ExportRealm(accessToken string, realmName string) (keycloak.Rea // GetRealmCredentialRegistrators returns list of credentials types available for the realm func (c *Client) GetRealmCredentialRegistrators(accessToken string, realmName string) ([]string, error) { var resp = []string{} - var err = c.get(accessToken, &resp, url.Path(realmCredentialRegistrators), url.Param("realm", realmName), headers.Set("Accept", "application/json")) + var err = c.get(accessToken, &resp, url.Path(realmCredentialRegistrators), url.Param("realm", realmName), hdrAcceptJSON) return resp, err } diff --git a/errormessages.go b/errormessages.go index 0c31356..bcf0189 100644 --- a/errormessages.go +++ b/errormessages.go @@ -1,6 +1,8 @@ package keycloak -import "strconv" +import ( + "fmt" +) // Constants for error management const ( @@ -35,5 +37,26 @@ type HTTPError struct { } func (e HTTPError) Error() string { - return strconv.Itoa(e.HTTPStatus) + ":" + e.Message + return fmt.Sprintf("%d:%s", e.HTTPStatus, e.Message) +} + +// ClientDetailedError struct +type ClientDetailedError struct { + HTTPStatus int + Message string +} + +// Error implements error +func (e ClientDetailedError) Error() string { + return fmt.Sprintf("%d:%s", e.HTTPStatus, e.Message) +} + +// Status implements common-service/errors/DetailedError +func (e ClientDetailedError) Status() int { + return e.HTTPStatus +} + +// ErrorMessage implements common-service/errors/DetailedError +func (e ClientDetailedError) ErrorMessage() string { + return e.Message } diff --git a/integration/integration_test.go b/integration/integration_test.go index fbed72d..26616a9 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -12,15 +12,19 @@ import ( "github.com/spf13/pflag" ) +type keyContext int + const ( tstRealm = "__internal" reqRealm = "master" user = "version" + + keyContextIssuerDomain keyContext = iota ) func main() { var conf = getKeycloakConfig() - var client, err = api.New(*conf) + var client, err = api.New(*conf, keyContextIssuerDomain) if err != nil { log.Fatalf("could not create keycloak client: %v", err) } diff --git a/model_toolbox.go b/model_toolbox.go index 861e5f4..addfa67 100644 --- a/model_toolbox.go +++ b/model_toolbox.go @@ -155,6 +155,15 @@ func (a Attributes) reformatDate(value *string, dateLayouts []string) *string { return &res } +// Merge current attributes with others (Values from others replace those with the same key in current attributes) +func (a Attributes) Merge(others *Attributes) { + if others != nil { + for key, attribute := range *others { + a[key] = attribute + } + } +} + // GetAttribute returns an attribute given its key func (u *UserRepresentation) GetAttribute(key AttributeKey) []string { if u.Attributes != nil { diff --git a/model_toolbox_test.go b/model_toolbox_test.go index fb5dadf..88a1252 100644 --- a/model_toolbox_test.go +++ b/model_toolbox_test.go @@ -102,3 +102,28 @@ func TestUserRepresentationAttributes(t *testing.T) { assert.True(t, *res) }) } + +func TestMergeAttributes(t *testing.T) { + var ( + currentAttributes = make(Attributes) + newAttributes = make(Attributes) + keyOne = AttributeKey("one") + keyTwo = AttributeKey("two") + keyThree = AttributeKey("three") + ) + + currentAttributes.SetString(keyOne, "abc") + currentAttributes.SetString(keyThree, "zyx") + + currentAttributes.Merge(nil) + assert.Len(t, currentAttributes, 2) + + newAttributes.SetString(keyTwo, "def") + newAttributes.SetString(keyThree, "ghi") + + currentAttributes.Merge(&newAttributes) + assert.Len(t, currentAttributes, 3) + assert.Equal(t, "abc", *currentAttributes.GetString(keyOne)) + assert.Equal(t, "def", *currentAttributes.GetString(keyTwo)) + assert.Equal(t, "ghi", *currentAttributes.GetString(keyThree)) +} diff --git a/toolbox/issuer.go b/toolbox/issuer.go index 4bc4db4..339f26e 100644 --- a/toolbox/issuer.go +++ b/toolbox/issuer.go @@ -8,7 +8,6 @@ import ( "strings" "time" - cs "github.com/cloudtrust/common-service" "github.com/cloudtrust/keycloak-client" ) @@ -18,7 +17,8 @@ type IssuerManager interface { } type issuerManager struct { - domainToIssuer map[string]OidcVerifierProvider + domainToIssuer map[string]OidcVerifierProvider + keyContextIssuerDomain interface{} } func getProtocolAndDomain(URL string) string { @@ -32,7 +32,7 @@ func getProtocolAndDomain(URL string) string { } // NewIssuerManager creates a new URLProvider -func NewIssuerManager(config keycloak.Config) (IssuerManager, error) { +func NewIssuerManager(config keycloak.Config, keyContextIssuerDomain interface{}) (IssuerManager, error) { URLs := config.AddrTokenProvider // Use default values when clients are not initializing these values cacheTTL := config.CacheTTL @@ -55,12 +55,13 @@ func NewIssuerManager(config keycloak.Config) (IssuerManager, error) { domainToIssuer[getProtocolAndDomain(value)] = issuer } return &issuerManager{ - domainToIssuer: domainToIssuer, + domainToIssuer: domainToIssuer, + keyContextIssuerDomain: keyContextIssuerDomain, }, nil } func (im *issuerManager) GetIssuer(ctx context.Context) (OidcVerifierProvider, error) { - if rawValue := ctx.Value(cs.CtContextIssuerDomain); rawValue != nil { + if rawValue := ctx.Value(im.keyContextIssuerDomain); rawValue != nil { // The issuer domain has been found in the context issuerDomain := getProtocolAndDomain(rawValue.(string)) if issuer, ok := im.domainToIssuer[issuerDomain]; ok { diff --git a/toolbox/issuer_test.go b/toolbox/issuer_test.go index d3d3ed7..cf60043 100644 --- a/toolbox/issuer_test.go +++ b/toolbox/issuer_test.go @@ -5,11 +5,16 @@ import ( "fmt" "testing" - cs "github.com/cloudtrust/common-service" "github.com/cloudtrust/keycloak-client" "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)) @@ -17,28 +22,28 @@ func TestGetProtocolAndDomain(t *testing.T) { } func TestNewIssuerManager(t *testing.T) { - { - _, err := NewIssuerManager(keycloak.Config{AddrTokenProvider: ":"}) + t.Run("Invalid URL", func(t *testing.T) { + _, err := NewIssuerManager(keycloak.Config{AddrTokenProvider: ":"}, keyContextIssuerDomain) 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}) + prov, err := NewIssuerManager(keycloak.Config{AddrTokenProvider: allDomains}, keyContextIssuerDomain) assert.Nil(t, err) assert.NotNil(t, prov) // No issuer provided with context issuerNoContext, _ := prov.GetIssuer(context.Background()) // Unrecognized issuer provided in context - issuerDefault, _ := prov.GetIssuer(context.WithValue(context.Background(), cs.CtContextIssuerDomain, "http://unknown.issuer.com/one/path")) + issuerDefault, _ := prov.GetIssuer(context.WithValue(context.Background(), keyContextIssuerDomain, "http://unknown.issuer.com/one/path")) // Case insensitive - issuerMyDomain, _ := prov.GetIssuer(context.WithValue(context.Background(), cs.CtContextIssuerDomain, "http://MY.DOMAIN.COM/issuer")) + issuerMyDomain, _ := prov.GetIssuer(context.WithValue(context.Background(), keyContextIssuerDomain, "http://MY.DOMAIN.COM/issuer")) // Other domain - issuerOtherDomain, _ := prov.GetIssuer(context.WithValue(context.Background(), cs.CtContextIssuerDomain, "http://other.domain.com:2120/any/thing/here")) + issuerOtherDomain, _ := prov.GetIssuer(context.WithValue(context.Background(), keyContextIssuerDomain, "http://other.domain.com:2120/any/thing/here")) assert.Equal(t, issuerNoContext, issuerDefault) assert.NotEqual(t, issuerNoContext, issuerMyDomain) diff --git a/toolbox/logger.go b/toolbox/logger.go new file mode 100644 index 0000000..470e777 --- /dev/null +++ b/toolbox/logger.go @@ -0,0 +1,8 @@ +package toolbox + +import "context" + +// Logger interface for logging with level +type Logger interface { + Warn(ctx context.Context, keyvals ...interface{}) +} diff --git a/toolbox/mock_test.go b/toolbox/mock_test.go new file mode 100644 index 0000000..0af1c5a --- /dev/null +++ b/toolbox/mock_test.go @@ -0,0 +1,3 @@ +package toolbox + +//go:generate mockgen -destination=./mock/logger.go -package=mock -mock_names=Logger=Logger github.com/cloudtrust/keycloak-client/toolbox Logger diff --git a/toolbox/oidc_connect.go b/toolbox/oidc_connect.go index fbe5a44..31c49a4 100644 --- a/toolbox/oidc_connect.go +++ b/toolbox/oidc_connect.go @@ -11,7 +11,6 @@ import ( "time" errorhandler "github.com/cloudtrust/common-service/errors" - "github.com/cloudtrust/common-service/log" "github.com/cloudtrust/keycloak-client" ) @@ -35,7 +34,7 @@ type oidcTokenProvider struct { timeout time.Duration tokenURL string reqBody string - logger log.Logger + logger Logger oidcToken oidcToken validUntil int64 } @@ -46,7 +45,7 @@ const ( ) // NewOidcTokenProvider creates an OidcTokenProvider -func NewOidcTokenProvider(config keycloak.Config, realm, username, password, clientID string, logger log.Logger) OidcTokenProvider { +func NewOidcTokenProvider(config keycloak.Config, realm, username, password, clientID string, logger Logger) OidcTokenProvider { var urls = strings.Split(config.AddrTokenProvider, " ") var keycloakPublicURL = urls[0] @@ -90,7 +89,7 @@ func (o *oidcTokenProvider) ProvideToken(ctx context.Context) (string, error) { } buf := new(bytes.Buffer) - buf.ReadFrom(resp.Body) + _, _ = buf.ReadFrom(resp.Body) err = json.Unmarshal(buf.Bytes(), &o.oidcToken) if err != nil { diff --git a/toolbox/oidc_connect_test.go b/toolbox/oidc_connect_test.go index 1a0d590..09cc4dd 100644 --- a/toolbox/oidc_connect_test.go +++ b/toolbox/oidc_connect_test.go @@ -9,8 +9,10 @@ import ( "testing" "time" - "github.com/cloudtrust/common-service/log" + "github.com/golang/mock/gomock" + "github.com/cloudtrust/keycloak-client" + "github.com/cloudtrust/keycloak-client/toolbox/mock" "github.com/gorilla/mux" "github.com/stretchr/testify/assert" ) @@ -30,6 +32,12 @@ func (t *TestResponse) ServeHTTP(w http.ResponseWriter, r *http.Request) { } 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, @@ -46,31 +54,31 @@ func TestCreateToken(t *testing.T) { 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", log.NewNopLogger()) + 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", log.NewNopLogger()) + 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", log.NewNopLogger()) + 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", log.NewNopLogger()) + 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", log.NewNopLogger()) + var p = NewOidcTokenProvider(keycloak.Config{AddrTokenProvider: ts.URL}, "valid", "user", "passwd", "clientID", mockLogger) var timeStart = time.Now() diff --git a/toolbox/oidc_verifier_test.go b/toolbox/oidc_verifier_test.go index b765334..a76f271 100644 --- a/toolbox/oidc_verifier_test.go +++ b/toolbox/oidc_verifier_test.go @@ -1,7 +1,5 @@ package toolbox -//go:generate mockgen -destination=./mock/authmanager.go -package=mock -mock_names=AuthorizationManager=AuthorizationManager github.com/cloudtrust/common-service/security AuthorizationManager - import ( "context" "encoding/json"