pkg/client/keycloak/adapter/gocloak_adapter.go (1,033 lines of code) (raw):
package adapter
import (
"context"
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"reflect"
"strconv"
"strings"
"sync"
"time"
"github.com/Nerzal/gocloak/v12"
"github.com/go-logr/logr"
"github.com/go-resty/resty/v2"
"github.com/pkg/errors"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"golang.org/x/sync/errgroup"
ctrl "sigs.k8s.io/controller-runtime"
"github.com/epam/edp-keycloak-operator/pkg/client/keycloak/dto"
)
const (
authPath = "/auth"
idPResource = "/admin/realms/{realm}/identity-provider/instances"
idPMapperResource = "/admin/realms/{realm}/identity-provider/instances/{alias}/mappers"
getOneIdP = idPResource + "/{alias}"
openIdConfig = "/realms/{realm}/.well-known/openid-configuration"
authExecutions = "/admin/realms/{realm}/authentication/flows/browser/executions"
authExecutionConfig = "/admin/realms/{realm}/authentication/executions/{id}/config"
postClientScopeMapper = "/admin/realms/{realm}/client-scopes/{scopeId}/protocol-mappers/models"
getRealmClientScopes = "/admin/realms/{realm}/client-scopes"
postClientScope = "/admin/realms/{realm}/client-scopes"
putClientScope = "/admin/realms/{realm}/client-scopes/{id}"
getClientProtocolMappers = "/admin/realms/{realm}/clients/{id}/protocol-mappers/models"
mapperToIdentityProvider = "/admin/realms/{realm}/identity-provider/instances/{alias}/mappers"
updateMapperToIdentityProvider = "/admin/realms/{realm}/identity-provider/instances/{alias}/mappers/{id}"
authFlows = "/admin/realms/{realm}/authentication/flows"
authFlow = "/admin/realms/{realm}/authentication/flows/{id}"
authFlowExecutionCreate = "/admin/realms/{realm}/authentication/executions"
authFlowExecutionGetUpdate = "/admin/realms/{realm}/authentication/flows/{alias}/executions"
authFlowExecutionDelete = "/admin/realms/{realm}/authentication/executions/{id}"
raiseExecutionPriority = "/admin/realms/{realm}/authentication/executions/{id}/raise-priority"
lowerExecutionPriority = "/admin/realms/{realm}/authentication/executions/{id}/lower-priority"
authFlowExecutionConfig = "/admin/realms/{realm}/authentication/executions/{id}/config"
authFlowConfig = "/admin/realms/{realm}/authentication/config/{id}"
deleteClientScopeProtocolMapper = "/admin/realms/{realm}/client-scopes/{clientScopeID}/protocol-mappers/models/{protocolMapperID}"
createClientScopeProtocolMapper = "/admin/realms/{realm}/client-scopes/{clientScopeID}/protocol-mappers/models"
putDefaultClientScope = "/admin/realms/{realm}/default-default-client-scopes/{clientScopeID}"
deleteDefaultClientScope = "/admin/realms/{realm}/default-default-client-scopes/{clientScopeID}"
getDefaultClientScopes = "/admin/realms/{realm}/default-default-client-scopes"
realmEventConfigPut = "/admin/realms/{realm}/events/config"
realmComponent = "/admin/realms/{realm}/components"
realmComponentEntity = "/admin/realms/{realm}/components/{id}"
identityProviderEntity = "/admin/realms/{realm}/identity-provider/instances/{alias}"
identityProviderCreateList = "/admin/realms/{realm}/identity-provider/instances"
idpMapperCreateList = "/admin/realms/{realm}/identity-provider/instances/{alias}/mappers"
idpMapperEntity = "/admin/realms/{realm}/identity-provider/instances/{alias}/mappers/{id}"
deleteRealmUser = "/admin/realms/{realm}/users/{id}"
setRealmUserPassword = "/admin/realms/{realm}/users/{id}/reset-password"
getUserRealmRoleMappings = "/admin/realms/{realm}/users/{id}/role-mappings/realm"
getUserGroupMappings = "/admin/realms/{realm}/users/{id}/groups"
manageUserGroups = "/admin/realms/{realm}/users/{userID}/groups/{groupID}"
getChildGroups = "/admin/realms/{realm}/groups/{groupID}/children"
getGroup = "/admin/realms/{realm}/groups/{groupID}"
logClientDTO = "client dto"
)
const (
keycloakApiParamId = "id"
keycloakApiParamRole = "role"
keycloakApiParamRealm = "realm"
keycloakApiParamAlias = "alias"
keycloakApiParamClientScopeId = "clientScopeID"
)
const (
logKeyUser = "user dto"
logKeyRealm = "realm"
)
type TokenExpiredError string
func (e TokenExpiredError) Error() string {
return string(e)
}
func IsErrTokenExpired(err error) bool {
errTokenExpired := TokenExpiredError("")
return errors.As(err, &errTokenExpired)
}
type GoCloakAdapter struct {
client GoCloak
token *gocloak.JWT
log logr.Logger
basePath string
legacyMode bool
}
type JWTPayload struct {
Exp int64 `json:"exp"`
}
type GoCloakConfig struct {
Url string
User string
Password string
RootCertificate string
InsecureSkipVerify bool
}
func (a GoCloakAdapter) GetGoCloak() GoCloak {
return a.client
}
func MakeFromToken(conf GoCloakConfig, tokenData []byte, log logr.Logger) (*GoCloakAdapter, error) {
var token gocloak.JWT
if err := json.Unmarshal(tokenData, &token); err != nil {
return nil, errors.Wrapf(err, "unable decode json data")
}
const requiredTokenParts = 3
tokenParts := strings.Split(token.AccessToken, ".")
if len(tokenParts) < requiredTokenParts {
return nil, errors.New("wrong JWT token structure")
}
tokenPayload, err := base64.RawURLEncoding.DecodeString(tokenParts[1])
if err != nil {
return nil, errors.Wrap(err, "wrong JWT token base64 encoding")
}
var tokenPayloadDecoded JWTPayload
if err = json.Unmarshal(tokenPayload, &tokenPayloadDecoded); err != nil {
return nil, errors.Wrap(err, "unable to decode JWT payload json")
}
if tokenPayloadDecoded.Exp < time.Now().Unix() {
return nil, TokenExpiredError("token is expired")
}
kcCl, legacyMode, err := makeClientFromToken(conf, token.AccessToken)
if err != nil {
return nil, fmt.Errorf("failed to make new keycloak client: %w", err)
}
return &GoCloakAdapter{
client: kcCl,
token: &token,
log: log,
basePath: conf.Url,
legacyMode: legacyMode,
}, nil
}
// makeClientFromToken returns Keycloak client, a bool flag indicating whether it was created in legacy mode and an error.
func makeClientFromToken(conf GoCloakConfig, token string) (*gocloak.GoCloak, bool, error) {
restyClient := resty.New()
if conf.InsecureSkipVerify {
restyClient.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})
}
if conf.RootCertificate != "" {
restyClient.SetRootCertificateFromString(conf.RootCertificate)
}
kcCl := gocloak.NewClient(conf.Url)
kcCl.SetRestyClient(restyClient)
_, err := kcCl.GetRealms(context.Background(), token)
if err == nil {
return kcCl, false, nil
}
if isNotLegacyResponseCode(err) {
return nil, false, fmt.Errorf("unexpected error received while trying to get realms using the modern client: %w", err)
}
kcCl = gocloak.NewClient(conf.Url, gocloak.SetLegacyWildFlySupport())
kcCl.SetRestyClient(restyClient)
if _, err := kcCl.GetRealms(context.Background(), token); err != nil {
return nil, false, fmt.Errorf("failed to create both current and legacy clients: %w", err)
}
return kcCl, true, nil
}
func MakeFromServiceAccount(ctx context.Context,
conf GoCloakConfig,
realm string,
log logr.Logger,
restyClient *resty.Client,
) (*GoCloakAdapter, error) {
if restyClient == nil {
restyClient = resty.New()
}
if conf.InsecureSkipVerify {
restyClient.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})
}
if conf.RootCertificate != "" {
restyClient.SetRootCertificateFromString(conf.RootCertificate)
}
kcCl := gocloak.NewClient(conf.Url)
kcCl.SetRestyClient(restyClient)
token, err := kcCl.LoginClient(ctx, conf.User, conf.Password, realm)
if err == nil {
return &GoCloakAdapter{
client: kcCl,
token: token,
log: log,
basePath: conf.Url,
legacyMode: false,
}, nil
}
if isNotLegacyResponseCode(err) {
return nil, fmt.Errorf("unexpected error received while trying to get realms using the modern client: %w", err)
}
kcCl = gocloak.NewClient(conf.Url, gocloak.SetLegacyWildFlySupport())
kcCl.SetRestyClient(restyClient)
token, err = kcCl.LoginClient(ctx, conf.User, conf.Password, realm)
if err != nil {
return nil, fmt.Errorf("failed to login with client creds on both current and legacy clients - "+
"clientID: %s, realm: %s: %w", conf.User, realm, err)
}
return &GoCloakAdapter{
client: kcCl,
token: token,
log: log,
basePath: conf.Url,
legacyMode: true,
}, nil
}
func isNotLegacyResponseCode(err error) bool {
apiErr := new(gocloak.APIError)
ok := errors.As(err, &apiErr)
return !ok || (apiErr.Code != http.StatusNotFound && apiErr.Code != http.StatusServiceUnavailable)
}
func Make(ctx context.Context, conf GoCloakConfig, log logr.Logger, restyClient *resty.Client) (*GoCloakAdapter, error) {
if restyClient == nil {
restyClient = resty.New()
}
if conf.InsecureSkipVerify {
restyClient.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})
}
if conf.RootCertificate != "" {
restyClient.SetRootCertificateFromString(conf.RootCertificate)
}
kcCl := gocloak.NewClient(conf.Url)
kcCl.SetRestyClient(restyClient)
token, err := kcCl.LoginAdmin(ctx, conf.User, conf.Password, "master")
if err == nil {
return &GoCloakAdapter{
client: kcCl,
token: token,
log: log,
basePath: conf.Url,
legacyMode: false,
}, nil
}
if isNotLegacyResponseCode(err) {
return nil, fmt.Errorf("unexpected error received while trying to get realms using the modern client: %w", err)
}
kcCl = gocloak.NewClient(conf.Url, gocloak.SetLegacyWildFlySupport())
kcCl.SetRestyClient(restyClient)
token, err = kcCl.LoginAdmin(ctx, conf.User, conf.Password, "master")
if err != nil {
return nil, errors.Wrapf(err, "cannot login to keycloak server with user: %s", conf.User)
}
return &GoCloakAdapter{
client: kcCl,
token: token,
log: log,
basePath: conf.Url,
legacyMode: true,
}, nil
}
func (a GoCloakAdapter) ExportToken() ([]byte, error) {
tokenData, err := json.Marshal(a.token)
if err != nil {
return nil, errors.Wrap(err, "unable to json encode token")
}
return tokenData, nil
}
// buildPath returns request path corresponding with the mode the client is operating in.
func (a GoCloakAdapter) buildPath(endpoint string) string {
if a.legacyMode {
return a.basePath + authPath + endpoint
}
return a.basePath + endpoint
}
func (a GoCloakAdapter) ExistClient(clientID, realm string) (bool, error) {
log := a.log.WithValues("clientID", clientID, logKeyRealm, realm)
log.Info("Start check client in Keycloak...")
clns, err := a.client.GetClients(context.Background(), a.token.AccessToken, realm, gocloak.GetClientsParams{
ClientID: &clientID,
})
if err != nil {
return false, fmt.Errorf("failed to get clients for realm %s: %w", realm, err)
}
res := checkFullNameMatch(clientID, clns)
log.Info("End check client in Keycloak")
return res, nil
}
func (a GoCloakAdapter) ExistClientRole(client *dto.Client, clientRole string) (bool, error) {
log := a.log.WithValues(logClientDTO, client, "client role", clientRole)
log.Info("Start check client role in Keycloak...")
id, err := a.GetClientID(client.ClientId, client.RealmName)
if err != nil {
return false, err
}
clientRoles, err := a.client.GetClientRoles(context.Background(), a.token.AccessToken, client.RealmName, id, gocloak.GetRoleParams{})
_, err = strip404(err)
if err != nil {
return false, err
}
clientRoleExists := false
for _, cl := range clientRoles {
if cl.Name != nil && *cl.Name == clientRole {
clientRoleExists = true
break
}
}
log.Info("End check client role in Keycloak", "clientRoleExists", clientRoleExists)
return clientRoleExists, nil
}
func (a GoCloakAdapter) CreateClientRole(client *dto.Client, clientRole string) error {
log := a.log.WithValues(logClientDTO, client, "client role", clientRole)
log.Info("Start create client role in Keycloak...")
id, err := a.GetClientID(client.ClientId, client.RealmName)
if err != nil {
return err
}
if _, err = a.client.CreateClientRole(context.Background(), a.token.AccessToken, client.RealmName, id, gocloak.Role{
Name: &clientRole,
ClientRole: gocloak.BoolP(true),
}); err != nil {
return errors.Wrap(err, "unable to create client role")
}
log.Info("Keycloak client role has been created")
return nil
}
func (a GoCloakAdapter) GetRealmRoles(ctx context.Context, realm string) (map[string]gocloak.Role, error) {
roles, err := a.client.GetRealmRoles(
ctx,
a.token.AccessToken,
realm,
gocloak.GetRoleParams{
Max: gocloak.IntP(100),
},
)
if err != nil {
return nil, fmt.Errorf("failed to get realm roles: %w", err)
}
rolesMap := make(map[string]gocloak.Role, len(roles))
for _, r := range roles {
if r != nil && r.Name != nil {
rolesMap[*r.Name] = *r
}
}
return rolesMap, nil
}
func checkFullRoleNameMatch(role string, roles *[]gocloak.Role) bool {
if roles == nil {
return false
}
for _, cl := range *roles {
if cl.Name != nil && *cl.Name == role {
return true
}
}
return false
}
func checkFullUsernameMatch(userName string, users []*gocloak.User) (*gocloak.User, bool) {
if users == nil {
return nil, false
}
for _, el := range users {
if el.Username != nil && *el.Username == userName {
return el, true
}
}
return nil, false
}
func checkFullNameMatch(clientID string, clients []*gocloak.Client) bool {
if clients == nil {
return false
}
for _, el := range clients {
if el.ClientID != nil && *el.ClientID == clientID {
return true
}
}
return false
}
func (a GoCloakAdapter) DeleteClient(ctx context.Context, kcClientID, realmName string) error {
log := a.log.WithValues("client id", kcClientID)
log.Info("Start delete client in Keycloak...")
if err := a.client.DeleteClient(ctx, a.token.AccessToken, realmName, kcClientID); err != nil {
return errors.Wrap(err, "unable to delete client")
}
log.Info("Keycloak client has been deleted")
return nil
}
func (a GoCloakAdapter) UpdateClient(ctx context.Context, client *dto.Client) error {
log := a.log.WithValues(logClientDTO, client)
log.Info("Start update client in Keycloak...")
if err := a.client.UpdateClient(ctx, a.token.AccessToken, client.RealmName, getGclCln(client)); err != nil {
return fmt.Errorf("unable to update keycloak client: %w", err)
}
log.Info("Keycloak client has been updated")
return nil
}
func (a GoCloakAdapter) CreateClient(ctx context.Context, client *dto.Client) error {
log := a.log.WithValues(logClientDTO, client)
log.Info("Start create client in Keycloak...")
_, err := a.client.CreateClient(ctx, a.token.AccessToken, client.RealmName, getGclCln(client))
if err != nil {
return fmt.Errorf("failed to create keycloak client: %w", err)
}
log.Info("Keycloak client has been created")
return nil
}
func getGclCln(client *dto.Client) gocloak.Client {
//TODO: check collision with protocol mappers list in spec
protocolMappers := getProtocolMappers(client.AdvancedProtocolMappers)
cl := gocloak.Client{
Attributes: &client.Attributes,
AuthorizationServicesEnabled: &client.AuthorizationServicesEnabled,
BearerOnly: &client.BearerOnly,
ClientAuthenticatorType: &client.ClientAuthenticatorType,
ClientID: &client.ClientId,
ConsentRequired: &client.ConsentRequired,
Description: &client.Description,
DirectAccessGrantsEnabled: &client.DirectAccess,
Enabled: &client.Enabled,
FrontChannelLogout: &client.FrontChannelLogout,
FullScopeAllowed: &client.FullScopeAllowed,
ImplicitFlowEnabled: &client.ImplicitFlowEnabled,
Name: &client.Name,
Origin: &client.Origin,
Protocol: &client.Protocol,
ProtocolMappers: &protocolMappers,
PublicClient: &client.PublicClient,
RedirectURIs: &[]string{
client.WebUrl + "/*",
},
RegistrationAccessToken: &client.RegistrationAccessToken,
RootURL: &client.WebUrl,
AdminURL: &client.AdminUrl,
BaseURL: &client.HomeUrl,
Secret: &client.ClientSecret,
ServiceAccountsEnabled: &client.ServiceAccountEnabled,
StandardFlowEnabled: &client.StandardFlowEnabled,
SurrogateAuthRequired: &client.SurrogateAuthRequired,
WebOrigins: &client.WebOrigins,
AuthenticationFlowBindingOverrides: &client.AuthenticationFlowBindingOverrides,
}
// Set the admin URL to the web URL for backwards compatibility.
// Before adding the admin URL field, the admin URL was the same as the web URL.
if client.AdminUrl == "" {
cl.AdminURL = &client.WebUrl
}
if len(client.RedirectUris) > 0 {
cl.RedirectURIs = &client.RedirectUris
}
if client.ID != "" {
cl.ID = &client.ID
}
return cl
}
func getProtocolMappers(need bool) []gocloak.ProtocolMapperRepresentation {
if !need {
return nil
}
return []gocloak.ProtocolMapperRepresentation{
{
Name: gocloak.StringP("username"),
Protocol: gocloak.StringP("openid-connect"),
ProtocolMapper: gocloak.StringP("oidc-usermodel-property-mapper"),
Config: &map[string]string{
"userinfo.token.claim": "true",
"user.attribute": "username",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "preferred_username",
"jsonType.label": "String",
},
},
{
Name: gocloak.StringP("realm roles"),
Protocol: gocloak.StringP("openid-connect"),
ProtocolMapper: gocloak.StringP("oidc-usermodel-realm-role-mapper"),
Config: &map[string]string{
"userinfo.token.claim": strconv.FormatBool(true),
"multivalued": strconv.FormatBool(true),
"id.token.claim": strconv.FormatBool(true),
"access.token.claim": strconv.FormatBool(false),
"claim.name": "roles",
"jsonType.label": "String",
},
},
}
}
func (a GoCloakAdapter) GetClientID(clientID, realm string) (string, error) {
clients, err := a.client.GetClients(context.Background(), a.token.AccessToken, realm,
gocloak.GetClientsParams{
ClientID: &clientID,
})
if err != nil {
return "", errors.Wrap(err, "unable to get realm clients")
}
for _, item := range clients {
if item.ClientID != nil && *item.ClientID == clientID {
return *item.ID, nil
}
}
return "", NotFoundError(fmt.Sprintf("unable to get Client ID. Client %v doesn't exist", clientID))
}
func (a GoCloakAdapter) GetClients(ctx context.Context, realm string) (map[string]*gocloak.Client, error) {
clients, err := a.client.GetClients(ctx, a.token.AccessToken, realm, gocloak.GetClientsParams{
Max: gocloak.IntP(100),
})
if err != nil {
return nil, fmt.Errorf("failed to get clients for realm %s: %w", realm, err)
}
cl := make(map[string]*gocloak.Client, len(clients))
for _, c := range clients {
if c.ClientID != nil {
cl[*c.ClientID] = c
}
}
return cl, nil
}
func (a GoCloakAdapter) GetClient(ctx context.Context, realm, client string) (*gocloak.Client, error) {
cl, err := a.client.GetClients(ctx, a.token.AccessToken, realm, gocloak.GetClientsParams{
ClientID: gocloak.StringP(client),
})
if err != nil {
return nil, fmt.Errorf("failed to get clients for realm %s: %w", realm, err)
}
if len(cl) == 0 {
return nil, NotFoundError(fmt.Sprintf("client %s doesn't exist", client))
}
if len(cl) > 1 {
return nil, fmt.Errorf("more than one client with ID %s found", client)
}
return cl[0], nil
}
func (a GoCloakAdapter) CreateRealmUser(realmName string, user *dto.User) error {
log := a.log.WithValues(logKeyUser, user, logKeyRealm, realmName)
log.Info("Start create realm user in Keycloak...")
userDto := gocloak.User{
Username: &user.Username,
Email: &user.Username,
Enabled: gocloak.BoolP(true),
}
_, err := a.client.CreateUser(context.Background(), a.token.AccessToken, realmName, userDto)
if err != nil {
return fmt.Errorf("failed to create user in realm %s: %w", realmName, err)
}
log.Info("Keycloak realm user has been created")
return nil
}
func (a GoCloakAdapter) ExistRealmUser(realmName string, user *dto.User) (bool, error) {
log := a.log.WithValues(logKeyUser, user, logKeyRealm, realmName)
log.Info("Start check user in Keycloak realm...")
usr, err := a.client.GetUsers(context.Background(), a.token.AccessToken, realmName, gocloak.GetUsersParams{
Username: &user.Username,
})
_, err = strip404(err)
if err != nil {
return false, err
}
_, userExists := checkFullUsernameMatch(user.Username, usr)
log.Info("End check user in Keycloak", "userExists", userExists)
return userExists, nil
}
// GetUsersByNames returns a map of users by their names.
// The function use goroutines to get users in parallel because the Keycloak API doesn't support getting users by names.
func (a GoCloakAdapter) GetUsersByNames(ctx context.Context, realm string, names []string) (map[string]gocloak.User, error) {
namesChan := make(chan string)
go func() {
defer close(namesChan)
for _, name := range names {
namesChan <- name
}
}()
const workersCount = 10
var wg sync.WaitGroup
wg.Add(workersCount)
results := make(chan *gocloak.User)
errc := make(chan error, workersCount)
for i := 0; i < workersCount; i++ {
go func(ctx context.Context, realm string, names <-chan string, results chan<- *gocloak.User, errc chan<- error) {
defer wg.Done()
for userName := range names {
users, err := a.client.GetUsers(ctx, a.token.AccessToken, realm, gocloak.GetUsersParams{
Max: gocloak.IntP(100),
BriefRepresentation: gocloak.BoolP(true),
Username: gocloak.StringP(userName),
})
if err != nil {
errc <- fmt.Errorf("failed to get user %s from realm %s: %w", userName, realm, err)
return
}
user, _ := checkFullUsernameMatch(userName, users)
results <- user
}
}(ctx, realm, namesChan, results, errc)
}
go func() {
wg.Wait()
close(results)
close(errc)
}()
users := make(map[string]gocloak.User, len(names))
for user := range results {
if user != nil && user.Username != nil {
users[*user.Username] = *user
}
}
if err := <-errc; err != nil {
return nil, err
}
return users, nil
}
func (a GoCloakAdapter) DeleteRealmUser(ctx context.Context, realmName, username string) error {
usrs, err := a.client.GetUsers(ctx, a.token.AccessToken, realmName, gocloak.GetUsersParams{
Username: &username,
})
if err != nil {
return errors.Wrap(err, "unable to get users")
}
usr, exists := checkFullUsernameMatch(username, usrs)
if !exists {
return NotFoundError("user not found")
}
rsp, err := a.startRestyRequest().
SetPathParams(map[string]string{
keycloakApiParamRealm: realmName,
keycloakApiParamId: *usr.ID,
}).
Delete(a.buildPath(deleteRealmUser))
if err = a.checkError(err, rsp); err != nil {
return errors.Wrap(err, "unable to delete user")
}
return nil
}
func (a GoCloakAdapter) HasUserRealmRole(realmName string, user *dto.User, role string) (bool, error) {
log := a.log.WithValues(keycloakApiParamRole, role, logKeyRealm, realmName, logKeyUser, user)
log.Info("Start check user roles in Keycloak realm...")
users, err := a.client.GetUsers(context.Background(), a.token.AccessToken, realmName, gocloak.GetUsersParams{
Username: &user.Username,
})
if err != nil {
return false, errors.Wrap(err, "unable to get users from keycloak")
}
if len(users) == 0 {
return false, fmt.Errorf("no such user %v has been found", user.Username)
}
rolesMapping, err := a.client.GetRoleMappingByUserID(context.Background(), a.token.AccessToken, realmName,
*users[0].ID)
if err != nil {
return false, errors.Wrap(err, "unable to GetRoleMappingByUserID")
}
hasRealmRole := checkFullRoleNameMatch(role, rolesMapping.RealmMappings)
log.Info("End check user role in Keycloak", "hasRealmRole", hasRealmRole)
return hasRealmRole, nil
}
func (a GoCloakAdapter) HasUserClientRole(realmName string, clientId string, user *dto.User, role string) (bool, error) {
log := a.log.WithValues(keycloakApiParamRole, role, "client", clientId, logKeyRealm, realmName, logKeyUser, user)
log.Info("Start check user roles in Keycloak realm...")
users, err := a.client.GetUsers(context.Background(), a.token.AccessToken, realmName, gocloak.GetUsersParams{
Username: &user.Username,
})
if err != nil {
return false, fmt.Errorf("failed to get user %s: %w", user.Username, err)
}
if len(users) == 0 {
return false, errors.Errorf("no such user %v has been found", user.Username)
}
rolesMapping, err := a.client.GetRoleMappingByUserID(context.Background(), a.token.AccessToken, realmName,
*users[0].ID)
if err != nil {
return false, fmt.Errorf("failed to get role mapping by user id %s: %w", *users[0].ID, err)
}
hasClientRole := false
if clientMap, ok := rolesMapping.ClientMappings[clientId]; ok && clientMap != nil && clientMap.Mappings != nil {
hasClientRole = checkFullRoleNameMatch(role, clientMap.Mappings)
}
log.Info("End check user role in Keycloak", "hasClientRole", hasClientRole)
return hasClientRole, nil
}
func (a GoCloakAdapter) AddRealmRoleToUser(ctx context.Context, realmName, username, roleName string) error {
users, err := a.client.GetUsers(ctx, a.token.AccessToken, realmName, gocloak.GetUsersParams{
Username: &username,
})
if err != nil {
return errors.Wrap(err, "error during get kc users")
}
if len(users) == 0 {
return errors.Errorf("no users with username %s found", username)
}
rl, err := a.client.GetRealmRole(ctx, a.token.AccessToken, realmName, roleName)
if err != nil {
return errors.Wrap(err, "unable to get realm role from keycloak")
}
if err := a.client.AddRealmRoleToUser(ctx, a.token.AccessToken, realmName, *users[0].ID,
[]gocloak.Role{
*rl,
}); err != nil {
return errors.Wrap(err, "unable to add realm role to user")
}
return nil
}
func (a GoCloakAdapter) AddClientRoleToUser(realmName string, clientId string, user *dto.User, roleName string) error {
log := a.log.WithValues(keycloakApiParamRole, roleName, logKeyRealm, realmName, "user", user.Username)
log.Info("Start mapping realm role to user in Keycloak...")
client, err := a.client.GetClients(context.Background(), a.token.AccessToken, realmName, gocloak.GetClientsParams{
ClientID: &clientId,
})
if err != nil {
return fmt.Errorf("failed to get client %s: %w", clientId, err)
}
if len(client) == 0 {
return fmt.Errorf("no such client %v has been found", clientId)
}
role, err := a.client.GetClientRole(context.Background(), a.token.AccessToken, realmName, *client[0].ID, roleName)
if err != nil {
return errors.Wrap(err, "error during GetClientRole")
}
if role == nil {
return errors.Errorf("no such client role %v has been found", roleName)
}
users, err := a.client.GetUsers(context.Background(), a.token.AccessToken, realmName, gocloak.GetUsersParams{
Username: &user.Username,
})
if err != nil {
return fmt.Errorf("failed to get user %s: %w", user.Username, err)
}
if len(users) == 0 {
return fmt.Errorf("no such user %v has been found", user.Username)
}
err = a.addClientRoleToUser(realmName, *users[0].ID, []gocloak.Role{*role})
if err != nil {
return err
}
log.Info("Role to user has been added")
return nil
}
func (a GoCloakAdapter) addClientRoleToUser(realmName string, userId string, roles []gocloak.Role) error {
if err := a.client.AddClientRoleToUser(
context.Background(),
a.token.AccessToken, realmName,
*roles[0].ContainerID,
userId,
roles,
); err != nil {
return fmt.Errorf("failed to add client role to user %s: %w", userId, err)
}
return nil
}
func getDefaultRealm(realm *dto.Realm) gocloak.RealmRepresentation {
return gocloak.RealmRepresentation{
Realm: &realm.Name,
Enabled: gocloak.BoolP(true),
ID: realm.ID,
}
}
func strip404(in error) (bool, error) {
if in == nil {
return true, nil
}
if is404(in) {
return false, nil
}
return false, in
}
func is404(e error) bool {
return strings.Contains(e.Error(), "404")
}
func (a GoCloakAdapter) CreateIncludedRealmRole(realmName string, role *dto.IncludedRealmRole) error {
log := a.log.WithValues(logKeyRealm, realmName, keycloakApiParamRole, role)
log.Info("Start create realm roles in Keycloak...")
realmRole := gocloak.Role{
Name: &role.Name,
}
_, err := a.client.CreateRealmRole(context.Background(), a.token.AccessToken, realmName, realmRole)
if err != nil {
return fmt.Errorf("failed to create realm role %s: %w", role.Name, err)
}
persRole, err := a.client.GetRealmRole(context.Background(), a.token.AccessToken, realmName, role.Name)
if err != nil {
return fmt.Errorf("failed to get realm role %s: %w", role.Name, err)
}
err = a.client.AddRealmRoleComposite(context.Background(), a.token.AccessToken, realmName, role.Composite, []gocloak.Role{*persRole})
if err != nil {
return fmt.Errorf("failed to add realm role composite: %w", err)
}
log.Info("Keycloak roles has been created")
return nil
}
func (a GoCloakAdapter) CreatePrimaryRealmRole(ctx context.Context, realmName string, role *dto.PrimaryRealmRole) (string, error) {
log := ctrl.LoggerFrom(ctx).WithValues("realm_name", realmName, keycloakApiParamRole, role)
log.Info("Start create realm roles in Keycloak.")
realmRole := gocloak.Role{
Name: &role.Name,
Description: &role.Description,
Attributes: &role.Attributes,
Composite: &role.IsComposite,
}
if _, err := a.client.CreateRealmRole(ctx, a.token.AccessToken, realmName, realmRole); err != nil {
return "", fmt.Errorf("failed to create realm role %s: %w", role.Name, err)
}
currentRealmRole, err := a.client.GetRealmRole(ctx, a.token.AccessToken, realmName, role.Name)
if err != nil {
return "", fmt.Errorf("failed to get created realm role %s: %w", role.Name, err)
}
role.ID = currentRealmRole.ID
log.Info("Keycloak roles has been created.")
return *role.ID, nil
}
func (a GoCloakAdapter) syncRoleComposites(ctx context.Context, realmName string, role *dto.PrimaryRealmRole) error {
associatedRoles, err := a.getRolesAssociatedRoles(ctx, realmName, role)
if err != nil {
return err
}
realmRolesToAdd, err := a.processAssociatedRealmRoles(ctx, realmName, role, associatedRoles)
if err != nil {
return err
}
clientRolesToAdd, err := a.processAssociatedClientRoles(ctx, realmName, role, associatedRoles)
if err != nil {
return err
}
rolesToAdd := slices.Clone(realmRolesToAdd)
rolesToAdd = append(rolesToAdd, clientRolesToAdd...)
if len(rolesToAdd) > 0 {
if err = a.client.AddRealmRoleComposite(ctx, a.token.AccessToken, realmName, role.Name, rolesToAdd); err != nil {
return fmt.Errorf("unable to add realm role composite roles: %w", err)
}
}
if len(associatedRoles) > 0 {
if err = a.client.DeleteRealmRoleComposite(ctx, a.token.AccessToken, realmName, role.Name, maps.Values(associatedRoles)); err != nil {
return fmt.Errorf("unable to delete realm role composite roles: %w", err)
}
}
return nil
}
// processAssociatedRealmRoles returns realm roles to add to the role.
// It also removes roles from associatedRoles map that are already associated with the role.
func (a GoCloakAdapter) processAssociatedRealmRoles(ctx context.Context, realmName string, role *dto.PrimaryRealmRole, associatedRoles map[string]gocloak.Role) ([]gocloak.Role, error) {
rolesToAdd := make([]gocloak.Role, 0, len(role.Composites))
group := errgroup.Group{}
m := sync.Mutex{}
for _, composite := range role.Composites {
roleName := composite
if _, ok := associatedRoles[roleName]; ok {
delete(associatedRoles, roleName)
continue
}
group.Go(func() error {
compositeRole, err := a.client.GetRealmRole(ctx, a.token.AccessToken, realmName, roleName)
if err != nil {
return fmt.Errorf("unable to get realm role %s: %w", roleName, err)
}
m.Lock()
rolesToAdd = append(rolesToAdd, *compositeRole)
m.Unlock()
return nil
})
}
if err := group.Wait(); err != nil {
return nil, fmt.Errorf("unable to get realm roles: %w", err)
}
return rolesToAdd, nil
}
// processAssociatedClientRoles returns client roles to add to the role.
// It also removes roles from associatedRoles map that are already associated with the role.
func (a GoCloakAdapter) processAssociatedClientRoles(ctx context.Context, realmName string, role *dto.PrimaryRealmRole, associatedRoles map[string]gocloak.Role) ([]gocloak.Role, error) {
rolesToAdd := make([]gocloak.Role, 0)
group := errgroup.Group{}
m := sync.Mutex{}
for cl, composite := range role.CompositesClientRoles {
roles := composite
client, err := a.GetClient(ctx, realmName, cl)
if err != nil {
return nil, fmt.Errorf("unable to get client %s: %w", cl, err)
}
for _, r := range roles {
roleName := r
clientID := *client.ID
mapKey := fmt.Sprintf("%s-%s", clientID, roleName)
if _, ok := associatedRoles[mapKey]; ok {
delete(associatedRoles, mapKey)
continue
}
group.Go(func() error {
compositeRole, err := a.client.GetClientRole(ctx, a.token.AccessToken, realmName, clientID, roleName)
if err != nil {
return fmt.Errorf("unable to get client role %s: %w", roleName, err)
}
m.Lock()
rolesToAdd = append(rolesToAdd, *compositeRole)
m.Unlock()
return nil
})
}
}
if err := group.Wait(); err != nil {
return nil, fmt.Errorf("unable to get realm roles: %w", err)
}
return rolesToAdd, nil
}
// getRolesAssociatedRoles returns map of roles associated with role.
// Key is role name. If role is client role, key is client name + "-" + role name.
func (a GoCloakAdapter) getRolesAssociatedRoles(ctx context.Context, realmName string, role *dto.PrimaryRealmRole) (map[string]gocloak.Role, error) {
currentAssociatedRoles, err := a.client.GetCompositeRolesByRoleID(ctx, a.token.AccessToken, realmName, *role.ID)
if err != nil {
return nil, fmt.Errorf("unable to get composite realm roles: %w", err)
}
currentAssociatedRolesMap := make(map[string]gocloak.Role, len(currentAssociatedRoles))
for _, r := range currentAssociatedRoles {
mapKey := *r.Name
if r.ClientRole != nil && *r.ClientRole {
mapKey = fmt.Sprintf("%s-%s", *r.ContainerID, *r.Name)
}
currentAssociatedRolesMap[mapKey] = *r
}
return currentAssociatedRolesMap, nil
}
func (a GoCloakAdapter) GetOpenIdConfig(realm *dto.Realm) (string, error) {
log := a.log.WithValues("realm dto", realm)
log.Info("Start get openid configuration...")
resp, err := a.client.RestyClient().R().
SetPathParams(map[string]string{
keycloakApiParamRealm: realm.Name,
}).
Get(a.buildPath(openIdConfig))
if err != nil {
return "", fmt.Errorf("request get open id config failed: %w", err)
}
res := resp.String()
log.Info("End get openid configuration", "openIdConfig", res)
return res, nil
}
func (a GoCloakAdapter) prepareProtocolMapperMaps(
client *dto.Client,
clientID string,
claimedMappers []gocloak.ProtocolMapperRepresentation,
) (
currentMappersMap,
claimedMappersMap map[string]gocloak.ProtocolMapperRepresentation,
resultErr error,
) {
currentMappers, err := a.GetClientProtocolMappers(client, clientID)
if err != nil {
resultErr = errors.Wrap(err, "unable to get client protocol mappers")
return
}
currentMappersMap = make(map[string]gocloak.ProtocolMapperRepresentation)
claimedMappersMap = make(map[string]gocloak.ProtocolMapperRepresentation)
// build maps to optimize comparing loops
for i, m := range currentMappers {
currentMappersMap[*m.Name] = currentMappers[i]
}
for i, m := range claimedMappers {
// this block needed to fix 500 error response from server and for proper work of DeepEqual
if m.Config == nil || *m.Config == nil {
claimedMappers[i].Config = &map[string]string{}
}
claimedMappersMap[*m.Name] = claimedMappers[i]
}
return
}
func (a GoCloakAdapter) mapperNeedsToBeCreated(
claimed *gocloak.ProtocolMapperRepresentation,
currentMappersMap map[string]gocloak.ProtocolMapperRepresentation,
realmName,
clientID string,
) error {
if _, ok := currentMappersMap[*claimed.Name]; !ok { // not exists in kc, must be created
if _, err := a.client.CreateClientProtocolMapper(context.Background(), a.token.AccessToken,
realmName, clientID, *claimed); err != nil {
return errors.Wrap(err, "unable to client create protocol mapper")
}
}
return nil
}
func (a GoCloakAdapter) mapperNeedsToBeUpdated(
claimed *gocloak.ProtocolMapperRepresentation,
currentMappersMap map[string]gocloak.ProtocolMapperRepresentation,
realmName,
clientID string,
) error {
if current, ok := currentMappersMap[*claimed.Name]; ok { // claimed exists in current state, must be checked for update
claimed.ID = current.ID // set id from current entity to claimed for proper DeepEqual comparison
if !reflect.DeepEqual(claimed, current) { // mappers is not equal, needs to update
if err := a.client.UpdateClientProtocolMapper(context.Background(), a.token.AccessToken,
realmName, clientID, *claimed.ID, *claimed); err != nil {
return errors.Wrap(err, "unable to update client protocol mapper")
}
}
}
return nil
}
func (a GoCloakAdapter) SyncClientProtocolMapper(
client *dto.Client, claimedMappers []gocloak.ProtocolMapperRepresentation, addOnly bool) error {
log := a.log.WithValues("clientId", client.ClientId)
log.Info("Start put Client protocol mappers...")
clientID, err := a.GetClientID(client.ClientId, client.RealmName)
if err != nil {
return errors.Wrap(err, "unable to get client id")
}
// prepare mapper entity maps for simplifying comparison procedure
currentMappersMap, claimedMappersMap, err := a.prepareProtocolMapperMaps(client, clientID, claimedMappers)
if err != nil {
return errors.Wrap(err, "unable to prepare protocol mapper maps")
}
// compare actual client protocol mappers from keycloak to desired mappers, and sync them
for _, claimed := range claimedMappers {
if err := a.mapperNeedsToBeCreated(&claimed, currentMappersMap, client.RealmName, clientID); err != nil {
return errors.Wrap(err, "error during mapperNeedsToBeCreated")
}
if err := a.mapperNeedsToBeUpdated(&claimed, currentMappersMap, client.RealmName, clientID); err != nil {
return errors.Wrap(err, "error during mapperNeedsToBeUpdated")
}
}
if !addOnly {
for _, kc := range currentMappersMap {
if _, ok := claimedMappersMap[*kc.Name]; !ok { // current mapper not exists in claimed, must be deleted
if err := a.client.DeleteClientProtocolMapper(context.Background(), a.token.AccessToken, client.RealmName,
clientID, *kc.ID); err != nil {
return errors.Wrap(err, "unable to delete client protocol mapper")
}
}
}
}
log.Info("Client protocol mapper was successfully configured!")
return nil
}
func (a GoCloakAdapter) GetClientProtocolMappers(client *dto.Client,
clientID string) ([]gocloak.ProtocolMapperRepresentation, error) {
var mappers []gocloak.ProtocolMapperRepresentation
resp, err := a.client.RestyClient().R().
SetAuthToken(a.token.AccessToken).
SetHeader(contentTypeHeader, contentTypeJson).
SetPathParams(map[string]string{
keycloakApiParamRealm: client.RealmName,
keycloakApiParamId: clientID,
}).
SetResult(&mappers).Get(a.buildPath(getClientProtocolMappers))
if err != nil {
return nil, errors.Wrap(err, "failed to get client protocol mappers")
}
if resp.IsError() {
return nil, errors.New(resp.String())
}
return mappers, nil
}
func (a GoCloakAdapter) checkError(err error, response *resty.Response) error {
if err != nil {
return errors.Wrap(err, "response error")
}
if response == nil {
return errors.New("empty response")
}
if response.IsError() {
return errors.Errorf("status: %s, body: %s", response.Status(), response.String())
}
return nil
}