Nerzal / gocloak

golang keycloak client
Apache License 2.0
1.01k stars 277 forks source link

example to create user federation with gocloak #400

Closed justmike1 closed 1 year ago

justmike1 commented 1 year ago

Describe the bug I am not sure it's a bug but more of a doc misunderstanding.

To Reproduce creation of user federation provider with gocloak in keycloak

Expected behavior clear docs or function type to create the provider, like in pulumi's package

Screenshots how can I know what to do from looking at this type? where do I set the azure active directoy, giving my credentials, etc.

image

Additional context I am trying to get this working for a few days, I would appreciate very much a code block example of creating a user federation profile, (I am choosing active directory)

r3st commented 1 year ago

A user federation is a type component: gocloak.Component

Here is an example function of how I create this component and sync them. I extract only needed parts from my code. Original Code is tested with keycloak v15.1.1, v20.0.x and v21.0.0.

I hope it will be helpful.

type Tkc struct {
    realm  string
    token  *gocloak.JWT
    client *gocloak.GoCloak
}

func (tkc *Tkc) getRealms(ctx context.Context) (realms map[string]gocloak.RealmRepresentation, getErr error) {
    var realmList []*gocloak.RealmRepresentation

    // init map
    realms = make(map[string]gocloak.RealmRepresentation)

    // get all realms
    if realmList, getErr = tkc.client.GetRealms(ctx, tkc.token.AccessToken); getErr != nil {
        getErr = fmt.Errorf("get realms failed (error: %s)", getErr.Error())
        return realms, getErr
    }

    // transform to map with realmID (meaning realm name) as map key
    for _, r := range realmList {
        realms[*r.Realm] = *r
    }

    return realms, nil
}

func (tkc *Tkc) newUserFederation(ctx context.Context) (userFederation gocloak.Component, newErr error) {
    var idRealm string

    // get realm ID, is needed as ParentID in gocloak.Component
    if realms, newErr := tkc.getRealms(ctx); newErr != nil {
        return userFederation, newErr
    } else {
        idRealm = *realms[tkc.realm].ID
    }

    userFederationConfig := make(map[string][]string)

    // keycloak self
    userFederationConfig["enabled"] = []string{"true"}
    userFederationConfig["priority"] = []string{"1"}

    // sync options
    userFederationConfig["fullSyncPeriod"] = []string{"-1"}
    userFederationConfig["changedSyncPeriod"] = []string{"300"}
    userFederationConfig["batchSizeForSync"] = []string{"1000"}

    // ldap connection
    userFederationConfig["editMode"] = []string{"READ_ONLY"}
    userFederationConfig["vendor"] = []string{"other"}
    userFederationConfig["connectionUrl"] = []string{"ldap://ldap"}
    userFederationConfig["bindDn"] = []string{"cn=XXX,dc=example,dc=com"}
    userFederationConfig["bindCredential"] = []string{"YYYYYY"}
    userFederationConfig["usersDn"] = []string{"ou=users,dc=example,dc=com"}
    userFederationConfig["usernameLDAPAttribute"] = []string{"uid"}
    userFederationConfig["uuidLDAPAttribute"] = []string{"entryUUID"}
    userFederationConfig["authType"] = []string{"simple"}
    userFederationConfig["userObjectClasses"] = []string{"person, uidObject"}
    userFederationConfig["rdnLDAPAttribute"] = []string{"cn"}
    userFederationConfig["searchScope"] = []string{"1"}
    userFederationConfig["pagination"] = []string{"true"}

    userFederation = gocloak.Component{
        Name:            gocloak.StringP("ldap"),
        ProviderID:      gocloak.StringP("ldap"),
        ProviderType:    gocloak.StringP("org.keycloak.storage.UserStorageProvider"),
        ParentID:        gocloak.StringP(idRealm),
        ComponentConfig: &userFederationConfig,
    }

    return userFederation, nil
}

func (tkc *Tkc) RunCreateUserFederation() error {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)

    defer func() {
        cancel()
    }()

    // user federation is a keycloak component with type 'org.keycloak.storage.UserStorageProvider'
    var userFederation gocloak.Component

    // get default new legacy user federation to create or update an existing one
    if newUserFederation, runErr := tkc.newUserFederation(ctx); runErr != nil {
        return runErr
    } else {
        userFederation = newUserFederation
    }

    // search parameter to get exactly legacy user federation which belongs to given realm (parent)
    ldapGetParams := gocloak.GetComponentsParams{
        Name:         userFederation.Name,
        ProviderType: userFederation.ProviderType,
        ParentID:     userFederation.ParentID,
    }

    if comps, getErr := tkc.client.GetComponentsWithParams(ctx, tkc.token.AccessToken, tkc.realm, ldapGetParams); getErr != nil {
        return getErr
    } else {
        // means user federation not found for given realm
        if len(comps) == 0 {
            if idUserFederation, createErr := tkc.client.CreateComponent(ctx, tkc.token.AccessToken, tkc.realm, userFederation); createErr != nil {
                return createErr
            } else {
                // do full sync after creating new one
                if syncErr := tkc.syncUserFederation(ctx, idUserFederation, true); syncErr != nil {
                    syncErr = fmt.Errorf("legacy user federation '%s' created (%s), but sync failed", *userFederation.Name, idUserFederation)
                    return syncErr
                }
            }
        } else {
            // set ID of user federation for update exactly existing user federation
            userFederation.ID = comps[0].ID
            if updateErr := tkc.client.UpdateComponent(ctx, tkc.token.AccessToken, tkc.baseConfig.Realm, userFederation); updateErr != nil {
                return updateErr
            } else {
                // do change sync only after update exiting one
                if syncErr := tkc.syncUserFederation(ctx, *userFederation.ID, false); syncErr != nil {
                    syncErr = fmt.Errorf("legacy user federation '%s' updated (%s), but sync failed", *userFederation.Name, *userFederation.ID)
                    return syncErr
                }
            }
        }
    }

    return nil
}

func (tkc *Tkc) syncUserFederation(ctx context.Context, idUserFederation string, fullSync bool) error {
    var url string

    url = tkc.baseConfig.Url + "/admin/realms/" + tkc.realm + "/user-storage/" + idUserFederation + "/sync"

    if fullSync {
        url += "?action=triggerFullSync"
    } else {
        url += "?action=triggerChangedUsersSync"
    }

    if response, postErr := tkc.client.RestyClient().NewRequest().SetAuthToken(tkc.token.AccessToken).Post(url); postErr != nil {
        return postErr
    } else {
        if response.StatusCode() != 200 {
            postErr = fmt.Errorf("got status code '%d' with response body '%s'", response.StatusCode(), response.String())
            return postErr
        } 
    }

    return nil
}
}
justmike1 commented 1 year ago

Thank you, thank you, it worked after minor modifications and needed to add

userFederationConfig["importUsers"] = []string{"true"}

appreciated!!

I want to implement also a mapper from ldap (to provide me the roles), is it also a component? do we have docs for it here? thats pretty much the only thing left for how to implement user federation with gocloak, I am down also to do a doc PR for this after I get this working 100% @r3st

r3st commented 1 year ago

@justmike1 I removed the part with mappers from my first example. So here we are with a code snippet for mappers.

Example for two diffrent provider group-ldap-mapper and user-attribute-ldap-mapper for providerType org.keycloak.storage.ldap.mappers.LDAPStorageMapper.

I have no dedicated example for role-ldap-mapper. But the config must be most identical to the group mapper.

I hope the snippte will help you to get it working!

type Tkc struct {
    realm  string
    token  *gocloak.JWT
    client *gocloak.GoCloak
}

// get all ldap mapper for given user federation
func (tk *Tkc) getLdapMapperOfUserFederation(ctx context.Context, idUserFederation string) (mappers map[string]gocloak.Component, getErr error) {
    var mapperList []*gocloak.Component
    mappers = make(map[string]gocloak.Component)

    // get only typed components for given id
    mapperGetParameter := gocloak.GetComponentsParams{
        ProviderType: gocloak.StringP("org.keycloak.storage.ldap.mappers.LDAPStorageMapper"),
        ParentID:     gocloak.StringP(idUserFederation),
    }

    if mapperList, getErr = tk.client.GetComponentsWithParams(ctx, tk.token.AccessToken, tk.realm, mapperGetParameter); getErr != nil {
        return mappers, getErr
    }

    // transform to map with mapper name as map key
    for _, mapper := range mapperList {
        mappers[*mapper.Name] = *mapper
    }

    return mappers, nil
}

// create given user ldap attribute mappers for given user federation
func (tk *Tkc) createUserModelAttributeMapper(ctx context.Context, idUserFederation string, mappers map[string]string) (createErr error) {
    var existingMappers map[string]gocloak.Component
    var mapper gocloak.Component
    var idMapper string

    if existingMappers, createErr = tk.getLdapMapperOfUserFederation(ctx, idUserFederation); createErr != nil {
        return createErr
    }

    for userModelAttribute, ldapAttribute := range mappers {
        mapper = prepareUserModelAttributeMapper(userModelAttribute, ldapAttribute, idUserFederation)
        if existingMapper, exists := existingMappers[userModelAttribute]; exists {
            mapper.ID = existingMapper.ID
            if createErr = tk.client.UpdateComponent(ctx, tk.token.AccessToken, tk.realm, mapper); createErr != nil {
                return createErr
            }
            log.Infof("user attribute ldap mapper '%s' updated (%s)", *mapper.Name, *mapper.ID)
        } else {
            if idMapper, createErr = tk.client.CreateComponent(ctx, tk.token.AccessToken, tk.realm, mapper); createErr != nil {
                return createErr
            }
            log.Infof("user attribute ldap mapper '%s' created (%s)", *mapper.Name, idMapper)
        }
    }

    return nil
}

// create given group ldap mapper for given user federation
func (tk *Tkc) createGroupMapper(ctx context.Context, idUserFederation string, group string) (createErr error) {
    var existingMappers map[string]gocloak.Component
    var mapper gocloak.Component
    var idMapper string

    if existingMappers, createErr = tk.getLdapMapperOfUserFederation(ctx, idUserFederation); createErr != nil {
        return createErr
    }

    mapper = prepareGroupLdapMapper(group, idUserFederation)
    if existingMapper, exists := existingMappers[group]; exists {
        mapper.ID = existingMapper.ID
        if createErr = tk.client.UpdateComponent(ctx, tk.token.AccessToken, tk.realm, mapper); createErr != nil {
            return createErr
        }
        log.Infof("group ldap mapper '%s' updated (%s)", *mapper.Name, *mapper.ID)
    } else {
        if idMapper, createErr = tk.client.CreateComponent(ctx, tk.token.AccessToken, tk.realm, mapper); createErr != nil {
            return createErr
        }
        log.Infof("group ldap mapper '%s' created (%s)", *mapper.Name, idMapper)
    }

    return nil
}

// prepare component object of type 'org.keycloak.storage.ldap.mappers.LDAPStorageMapper' and ProviderId 'user-attribute-ldap-mapper' with config
func prepareUserModelAttributeMapper(userModelAttribute, ldapAttribute, idUserFederation string) (mapper gocloak.Component) {
    mapperConfig := make(map[string][]string)

    mapperConfig["ldap.attribute"] = []string{ldapAttribute}
    mapperConfig["user.model.attribute"] = []string{userModelAttribute}
    mapperConfig["is.mandatory.in.ldap"] = []string{"false"}
    mapperConfig["always.read.value.from.ldap"] = []string{"false"}
    mapperConfig["read.only"] = []string{"true"}

    mapper = gocloak.Component{
        Name:            gocloak.StringP(userModelAttribute),
        ProviderID:      gocloak.StringP("user-attribute-ldap-mapper"),
        ProviderType:    gocloak.StringP("org.keycloak.storage.ldap.mappers.LDAPStorageMapper"),
        ParentID:        gocloak.StringP(idUserFederation),
        ComponentConfig: &mapperConfig,
    }

    return mapper
}

// prepare component object of type 'org.keycloak.storage.ldap.mappers.LDAPStorageMapper' with config
func prepareGroupLdapMapper(name, idUserFederation string) (mapper gocloak.Component) {
    mapperConfig := make(map[string][]string)

    mapperConfig["mode"] = []string{"LDAP_ONLY"}
    mapperConfig["membership.attribute.type"] = []string{"DN"}
    mapperConfig["user.roles.retrieve.strategy"] = []string{"LOAD_GROUPS_BY_MEMBER_ATTRIBUTE"}
    mapperConfig["group.name.ldap.attribute"] = []string{"cn"}
    mapperConfig["membership.ldap.attribute"] = []string{"member"}
    mapperConfig["preserve.group.inheritance"] = []string{"true"}
    mapperConfig["membership.user.ldap.attribute"] = []string{"uid"}
    mapperConfig["ignore.missing.group"] = []string{"false"}
    mapperConfig["memberof.ldap.attribute"] = []string{"memberOf"}
    mapperConfig["groups.dn"] = []string{"ou=groups,dc=example,dc=com"}
    mapperConfig["group.object.classes"] = []string{"groupOfNames"}
    mapperConfig["drop.non.existing.groups.during.sync"] = []string{"false"}

    mapper = gocloak.Component{
        Name:            gocloak.StringP(name),
        ProviderID:      gocloak.StringP("group-ldap-mapper"),
        ProviderType:    gocloak.StringP("org.keycloak.storage.ldap.mappers.LDAPStorageMapper"),
        ParentID:        gocloak.StringP(idUserFederation),
        ComponentConfig: &mapperConfig,
    }

    return mapper
}

// From last example: But with Mappers
func (tkc *Tkc) newUserFederation(ctx context.Context) (userFederation gocloak.Component, newErr error) {
    var idRealm string

    // get realm ID, is needed as ParentID in gocloak.Component
    if realms, newErr := tkc.getRealms(ctx); newErr != nil {
        return userFederation, newErr
    } else {
        idRealm = *realms[tkc.realm].ID
    }

    userFederationConfig := make(map[string][]string)

    // keycloak self
    userFederationConfig["enabled"] = []string{"true"}
    userFederationConfig["priority"] = []string{"1"}

    // sync options
    userFederationConfig["fullSyncPeriod"] = []string{"-1"}
    userFederationConfig["changedSyncPeriod"] = []string{"300"}
    userFederationConfig["batchSizeForSync"] = []string{"1000"}
    userFederationConfig["importUsers"] = []string{"true"}

    // ldap connection
    userFederationConfig["editMode"] = []string{"READ_ONLY"}
    userFederationConfig["vendor"] = []string{"other"}
    userFederationConfig["connectionUrl"] = []string{"ldap://ldap"}
    userFederationConfig["bindDn"] = []string{"cn=XXX,dc=example,dc=com"}
    userFederationConfig["bindCredential"] = []string{"YYYYYY"}
    userFederationConfig["usersDn"] = []string{"ou=users,dc=example,dc=com"}
    userFederationConfig["usernameLDAPAttribute"] = []string{"uid"}
    userFederationConfig["uuidLDAPAttribute"] = []string{"entryUUID"}
    userFederationConfig["authType"] = []string{"simple"}
    userFederationConfig["userObjectClasses"] = []string{"person, uidObject"}
    userFederationConfig["rdnLDAPAttribute"] = []string{"cn"}
    userFederationConfig["searchScope"] = []string{"1"}
    userFederationConfig["pagination"] = []string{"true"}

    userFederation = gocloak.Component{
        Name:            gocloak.StringP("ldap"),
        ProviderID:      gocloak.StringP("ldap"),
        ProviderType:    gocloak.StringP("org.keycloak.storage.UserStorageProvider"),
        ParentID:        gocloak.StringP(idRealm),
        ComponentConfig: &userFederationConfig,
    }

    return userFederation, nil
}

func (tkc *Tkc) RunCreateUserFederation() error {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)

    defer func() {
        cancel()
    }()

    // user federation is a keycloak component with type 'org.keycloak.storage.UserStorageProvider'
    var userFederation gocloak.Component

    // ldap mappers for legacy user federation
    userModelAttributeMappers := make(map[string]string)
    userModelAttributeMappers["office"] = "physicalDeliveryOfficeName"
    userModelAttributeMappers["department"] = "destinationIndicator"
    userModelAttributeMappers["address"] = "postalAddress"
    userModelAttributeMappers["telephone"] = "telephoneNumber"
    userModelAttributeMappers["fax"] = "facsimileTelephoneNumber"
    userModelAttributeMappers["title"] = "title"
    userModelAttributeMappers["firstName"] = "cn"

    // ldap group mapper name
    groupMapper := "group"

    // get default new legacy user federation to create or update an existing one
    if newUserFederation, runErr := tkc.newUserFederation(ctx); runErr != nil {
        return runErr
    } else {
        userFederation = newUserFederation
    }

    // search parameter to get exactly legacy user federation which belongs to given realm (parent)
    ldapGetParams := gocloak.GetComponentsParams{
        Name:         userFederation.Name,
        ProviderType: userFederation.ProviderType,
        ParentID:     userFederation.ParentID,
    }

    if comps, getErr := tkc.client.GetComponentsWithParams(ctx, tkc.token.AccessToken, tkc.realm, ldapGetParams); getErr != nil {
        return getErr
    } else {
        // means user federation not found for given realm
        if len(comps) == 0 {
            if idUserFederation, createErr := tkc.client.CreateComponent(ctx, tkc.token.AccessToken, tkc.realm, userFederation); createErr != nil {
                return createErr
            } else {
                // do create federation user attribute ldap mappers
                if createErr := tkc.createUserModelAttributeMapper(ctx, idUserFederation, userModelAttributeMappers); createErr != nil {
                    return createErr
                }
                // do create federation group ldap mappers
                if createErr := tkc.createGroupMapper(ctx, idUserFederation, groupMapper); createErr != nil {
                    return createErr
                }
                // do full sync after creating new one
                if syncErr := tkc.syncUserFederation(ctx, idUserFederation, true); syncErr != nil {
                    syncErr = fmt.Errorf("legacy user federation '%s' created (%s), but sync failed", *userFederation.Name, idUserFederation)
                    return syncErr
                }
            }
        } else {
            // set ID of user federation for update exactly existing user federation
            userFederation.ID = comps[0].ID
            if updateErr := tkc.client.UpdateComponent(ctx, tkc.token.AccessToken, tkc.baseConfig.Realm, userFederation); updateErr != nil {
                return updateErr
            } else {
                // do change sync only after update exiting one
                if syncErr := tkc.syncUserFederation(ctx, *userFederation.ID, false); syncErr != nil {
                    syncErr = fmt.Errorf("legacy user federation '%s' updated (%s), but sync failed", *userFederation.Name, *userFederation.ID)
                    return syncErr
                }
            }
        }
    }

    return nil
}
justmike1 commented 1 year ago

role-mapper-ldap worked flawlessly using your examples.

Thank you so much! helped me a lot, hopefully will continue to help others. https://github.com/Nerzal/gocloak/pull/407