1
0
Fork 0
mirror of https://github.com/requarks/wiki.git synced 2026-03-02 22:06:55 -05:00

Sync groups and group membership through auth strategies #1517

Open
opened 2026-02-20 18:12:36 -05:00 by deekerman · 16 comments
Owner

Originally created by @signalkraft on GitHub (May 13, 2020).

Originally assigned to: @NGPixel on GitHub.

First off: It seems there is already a few feature requests on https://wiki.js.org/feedback/?search=group for this, so I added a ticket because I'm thinking about helping with the first implementation here.

Actual behavior

Currently authenticating with different strategies does not update group membership, even if the strategy supports that (i.e. roles in Keycloak, groups in LDAP). Manually adding users to groups is cumbersome and makes it difficult to use Wiki.js in larger teams where you want give some sub-teams their own private section.

Expected behavior

Signing in with a strategy that supports group / role memberships should create a group on Wiki.js, if it doesn't exist yet, and then add the user to the group during authentication. There should be settings in the admin UI of the different strategies that support groups, to control this behavior. My guess would be:

  • Toggle "Synchronize groups"
  • Toggle "Synchronize group membership"
  • Group search query for LDAP

You could get infinitely more complex with custom group mappings, background sync of groups from LDAP, nested groups, permission mapping, etc, but as a first version the above seems useful.

--

I'd be happy to dig into the code and try to contribute a PR for LDAP and/or Keycloak, if you agree that this is a useful feature @NGPixel - it seems widely requested on Canny.

Originally created by @signalkraft on GitHub (May 13, 2020). Originally assigned to: @NGPixel on GitHub. First off: It seems there is already a few feature requests on https://wiki.js.org/feedback/?search=group for this, so I added a ticket because I'm thinking about helping with the first implementation here. ### Actual behavior Currently authenticating with different strategies does not update group membership, even if the strategy supports that (i.e. roles in Keycloak, groups in LDAP). Manually adding users to groups is cumbersome and makes it difficult to use Wiki.js in larger teams where you want give some sub-teams their own private section. ### Expected behavior Signing in with a strategy that supports group / role memberships should create a group on Wiki.js, if it doesn't exist yet, and then add the user to the group during authentication. There should be settings in the admin UI of the different strategies that support groups, to control this behavior. My guess would be: * Toggle "Synchronize groups" * Toggle "Synchronize group membership" * Group search query for LDAP You could get infinitely more complex with custom group mappings, background sync of groups from LDAP, nested groups, permission mapping, etc, but as a first version the above seems useful. -- I'd be happy to dig into the code and try to contribute a PR for LDAP and/or Keycloak, if you agree that this is a useful feature @NGPixel - it seems widely requested on Canny.
Author
Owner

@NGPixel commented on GitHub (May 15, 2020):

Sounds good 👍

@NGPixel commented on GitHub (May 15, 2020): Sounds good 👍
Author
Owner

@baodrate commented on GitHub (Aug 17, 2020):

@signalkraft have you started on this? I'd like to give it a shot, but don't want to repeat any work you might've already done

@baodrate commented on GitHub (Aug 17, 2020): @signalkraft have you started on this? I'd like to give it a shot, but don't want to repeat any work you might've already done
Author
Owner

@signalkraft commented on GitHub (Aug 25, 2020):

@qubidt I looked into the underlying auth lib (passport) but couldn't figure out a good way to get groups out of it. Its main purpose is authentication, so maybe I also went at this from the wrong angle.

I fixed my own issue with groups by building a small python service that syncs users and groups back and forth over the (excellent) GraphQL API. So go for it!

@signalkraft commented on GitHub (Aug 25, 2020): @qubidt I looked into the underlying auth lib (passport) but couldn't figure out a good way to get groups out of it. Its main purpose is authentication, so maybe I also went at this from the wrong angle. I fixed my own issue with groups by building a small python service that syncs users and groups back and forth over the (excellent) GraphQL API. So go for it!
Author
Owner

@baodrate commented on GitHub (Sep 2, 2020):

@signalkraft thanks, I looked into it a bit and also came to a similar conclusion. Not sure I'm familiar enough with the codebase to make the wide changes this would require. Your solution sounds like it could work well for me so I'll give it a shot, thanks!

@baodrate commented on GitHub (Sep 2, 2020): @signalkraft thanks, I looked into it a bit and also came to a similar conclusion. Not sure I'm familiar enough with the codebase to make the wide changes this would require. Your solution sounds like it could work well for me so I'll give it a shot, thanks!
Author
Owner

@signalkraft commented on GitHub (Sep 2, 2020):

@qubidt Here's my two scripts as a starting point for anyone in a similar position. get_ipa_client is just a thin wrapper around FreeIPAs Python SDK.


import os

import requests


def run_query(query, variables=None):
    headers = {"Authorization": f"Bearer {os.environ.get('WIKIJS_API_KEY')}"}
    json = {'query': query}
    if variables:
        json['variables'] = variables
    request = requests.post(os.environ.get('WIKIJS_API'), json=json, headers=headers)
    if request.status_code == 200:
        return request.json()
    else:
        raise Exception("Query failed to run by returning code of {}. {}".format(request.status_code, query))


def get_groups():
    query = """
    {
      groups {
        list {
          id
          name
          isSystem
          userCount
        }
      }
    }
    """
    return run_query(query)['data']['groups']['list']


def get_users():
    query = """
    {
      users {
        list {
          id
          name
          email
          providerKey
          isSystem
          createdAt
          __typename
        }
        __typename
      }
    }
    """
    return run_query(query)['data']['users']['list']


def create_group(name: str):
    query = """
    mutation($name: String!) {
      groups {
        create(name: $name) {
          responseResult {
            succeeded
            errorCode
            slug
            message
            __typename
          }
          group {
            id
            name
            createdAt
            updatedAt
            __typename
          }
          __typename
        }
        __typename
      }
    }

    """
    return run_query(query, {'name': name})['data']['groups']['create']['group']


def assign_user(group_id: int, user_id: int):
    query = """
    mutation($groupId: Int!, $userId: Int!) {
      groups {
        assignUser(groupId: $groupId, userId: $userId) {
          responseResult {
            succeeded
            errorCode
            slug
            message
            __typename
          }
          __typename
        }
        __typename
      }
    }
    """
    return run_query(query, {'groupId': group_id, 'userId': user_id})
#!/usr/bin/env python3
import logging
import os
from typing import List

from ipa import get_ipa_client
from wiki import get_groups, get_users, create_group, assign_user


# Set up logging to console
logger = logging.getLogger(__name__)
logger.setLevel(os.environ.get('LOG_LEVEL', 'INFO'))
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)


if __name__ == '__main__':
    ipa_client = get_ipa_client()
    wikijs_groups = get_groups()  # type: List
    wikijs_users = get_users()  # type: List

    for ipa_group in ipa_client.list_groups():
        group_name = ipa_group['cn'][0]
        logger.info("Synchronizing group %s...", group_name)
        if group_name not in [g['name'] for g in wikijs_groups]:
            # Create group on Wiki.js
            wikijs_group = create_group(group_name)
            wikijs_groups.append(wikijs_group)
            logger.info("Created new Wiki.js group %s", group_name)
        else:
            # Get group by name
            wikijs_group = [g for g in wikijs_groups if g['name'] == group_name][0]
            logger.debug("Using existing group %s", group_name)

        for ipa_user in ipa_group.get('member_user', []):
            if ipa_user in [u['name'] for u in wikijs_users]:
                # Try to assign user to Wiki.js group
                wikijs_user = [u for u in wikijs_users if u['name'] == ipa_user][0]
                result = assign_user(wikijs_group['id'], wikijs_user['id'])
                if 'errors' in result and 'already assigned' in result['errors'][0]['message']:
                    # Ignore already exists errors
                    logger.debug("IPA User %s already in group %s", ipa_user, group_name)
                else:
                    logger.info("Added IPA user %s to group %s", ipa_user, group_name)
            else:
                logger.warning("Skipping IPA user %s, because it wasn't found on Wiki.js", ipa_user)

Bunch of ways to improve this still (nested loops, referencing nested dicts without null checks) but it runs reliably for me.

@signalkraft commented on GitHub (Sep 2, 2020): @qubidt Here's my two scripts as a starting point for anyone in a similar position. `get_ipa_client` is just a thin wrapper around FreeIPAs Python SDK. ```python import os import requests def run_query(query, variables=None): headers = {"Authorization": f"Bearer {os.environ.get('WIKIJS_API_KEY')}"} json = {'query': query} if variables: json['variables'] = variables request = requests.post(os.environ.get('WIKIJS_API'), json=json, headers=headers) if request.status_code == 200: return request.json() else: raise Exception("Query failed to run by returning code of {}. {}".format(request.status_code, query)) def get_groups(): query = """ { groups { list { id name isSystem userCount } } } """ return run_query(query)['data']['groups']['list'] def get_users(): query = """ { users { list { id name email providerKey isSystem createdAt __typename } __typename } } """ return run_query(query)['data']['users']['list'] def create_group(name: str): query = """ mutation($name: String!) { groups { create(name: $name) { responseResult { succeeded errorCode slug message __typename } group { id name createdAt updatedAt __typename } __typename } __typename } } """ return run_query(query, {'name': name})['data']['groups']['create']['group'] def assign_user(group_id: int, user_id: int): query = """ mutation($groupId: Int!, $userId: Int!) { groups { assignUser(groupId: $groupId, userId: $userId) { responseResult { succeeded errorCode slug message __typename } __typename } __typename } } """ return run_query(query, {'groupId': group_id, 'userId': user_id}) ``` ```python #!/usr/bin/env python3 import logging import os from typing import List from ipa import get_ipa_client from wiki import get_groups, get_users, create_group, assign_user # Set up logging to console logger = logging.getLogger(__name__) logger.setLevel(os.environ.get('LOG_LEVEL', 'INFO')) ch = logging.StreamHandler() ch.setLevel(logging.DEBUG) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') ch.setFormatter(formatter) logger.addHandler(ch) if __name__ == '__main__': ipa_client = get_ipa_client() wikijs_groups = get_groups() # type: List wikijs_users = get_users() # type: List for ipa_group in ipa_client.list_groups(): group_name = ipa_group['cn'][0] logger.info("Synchronizing group %s...", group_name) if group_name not in [g['name'] for g in wikijs_groups]: # Create group on Wiki.js wikijs_group = create_group(group_name) wikijs_groups.append(wikijs_group) logger.info("Created new Wiki.js group %s", group_name) else: # Get group by name wikijs_group = [g for g in wikijs_groups if g['name'] == group_name][0] logger.debug("Using existing group %s", group_name) for ipa_user in ipa_group.get('member_user', []): if ipa_user in [u['name'] for u in wikijs_users]: # Try to assign user to Wiki.js group wikijs_user = [u for u in wikijs_users if u['name'] == ipa_user][0] result = assign_user(wikijs_group['id'], wikijs_user['id']) if 'errors' in result and 'already assigned' in result['errors'][0]['message']: # Ignore already exists errors logger.debug("IPA User %s already in group %s", ipa_user, group_name) else: logger.info("Added IPA user %s to group %s", ipa_user, group_name) else: logger.warning("Skipping IPA user %s, because it wasn't found on Wiki.js", ipa_user) ``` Bunch of ways to improve this still (nested loops, referencing nested dicts without null checks) but it runs reliably for me.
Author
Owner

@drehelis commented on GitHub (Mar 27, 2021):

@NGPixel is this something that is being looked into for the upcoming major releases?

@drehelis commented on GitHub (Mar 27, 2021): @NGPixel is this something that is being looked into for the upcoming major releases?
Author
Owner

@NGPixel commented on GitHub (Mar 28, 2021):

@NGPixel is this something that is being looked into for the upcoming major releases?

It is planned for 3.x yes

@NGPixel commented on GitHub (Mar 28, 2021): > @NGPixel is this something that is being looked into for the upcoming major releases? It is planned for 3.x yes
Author
Owner

@warthy commented on GitHub (May 20, 2021):

It is planned for 3.x yes

is it worth it to open an MR with a potential "quick" implementation or V3 we be release soon, so there wouldn't any point ?

@warthy commented on GitHub (May 20, 2021): > It is planned for 3.x yes is it worth it to open an MR with a potential "quick" implementation or V3 we be release soon, so there wouldn't any point ?
Author
Owner

@devksingh4 commented on GitHub (Feb 20, 2022):

Hello,
Has there been any progress on implementing this for v3.x? I am hoping to use it with Azure AD. I didn't see any posted updates here or on the feedback page within the last year.

@devksingh4 commented on GitHub (Feb 20, 2022): Hello, Has there been any progress on implementing this for v3.x? I am hoping to use it with Azure AD. I didn't see any posted updates here or on the feedback page within the last year.
Author
Owner

@uberspot commented on GitHub (Apr 11, 2022):

+1 on this issue. Reviving this thread in the hope that someone would implement this.
It's a very useful feature.

@uberspot commented on GitHub (Apr 11, 2022): +1 on this issue. Reviving this thread in the hope that someone would implement this. It's a very useful feature.
Author
Owner

@jtagcat commented on GitHub (Apr 11, 2022):

GitHub Etiquette

  • Please use the 👍 reaction to show that you are affected by the same issue.
  • Please don't comment if you have no relevant information to add. It's just extra noise for everyone subscribed to this issue.
  • Subscribe to receive notifications on status change and new comments.
@jtagcat commented on GitHub (Apr 11, 2022): ### GitHub Etiquette * Please use the 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to show that you are affected by the same issue. * Please don't comment if you have no relevant information to add. It's just extra noise for everyone subscribed to this issue. * Subscribe to receive notifications on status change and new comments.
Author
Owner

@fionera commented on GitHub (May 5, 2022):

I would love tho see this happen. In the best case with a mapping of provider role name => group. I sadly dont know how the wikijs Codebase works and how much work implementing this would be

@fionera commented on GitHub (May 5, 2022): I would love tho see this happen. In the best case with a mapping of provider role name => group. I sadly dont know how the wikijs Codebase works and how much work implementing this would be
Author
Owner

@aelgasser commented on GitHub (Mar 27, 2023):

@NGPixel et al. I just submitted a PR to implement group sync with SAML: https://github.com/requarks/wiki/pull/6299

@fionera if you're still interested, this can be a starting point for you to implement it in another strategy.

@aelgasser commented on GitHub (Mar 27, 2023): @NGPixel et al. I just submitted a PR to implement group sync with SAML: https://github.com/requarks/wiki/pull/6299 @fionera if you're still interested, this can be a starting point for you to implement it in another strategy.
Author
Owner

@fionera commented on GitHub (Mar 27, 2023):

@aelgasser I already implemented assignment for it for oidc https://github.com/requarks/wiki/pull/5568

@fionera commented on GitHub (Mar 27, 2023): @aelgasser I already implemented assignment for it for oidc https://github.com/requarks/wiki/pull/5568
Author
Owner

@ValentinKolb commented on GitHub (Jan 26, 2026):

Nested LDAP Groups via memberOf Fallback

Hey, I ran into a related issue with nested LDAP groups in FreeIPA that might be worth addressing here.

The Problem

The current group search filter (member=uid={{username}},...) only finds direct memberships. With nested groups like:

alice → member of → dev-frontend → member of → developers → member of → staff

WikiJS only sees dev-frontend, not developers or staff.

Simple Fix

LDAP servers already resolve this via the memberOf attribute on users - it contains ALL groups (direct + inherited). We just need a fallback when group search returns empty:

if (conf.mapGroups) {
  let ldapGroups = _.get(profile, '_groups')

  // Fallback: If no groups from group search, try memberOf attribute
  if (!ldapGroups || !_.isArray(ldapGroups) || ldapGroups.length === 0) {
    const memberOf = _.get(profile, 'memberOf', [])
    const memberOfArray = _.isArray(memberOf) ? memberOf : [memberOf]

    ldapGroups = memberOfArray
      .filter(dn => dn && typeof dn === 'string')
      .map(dn => {
        const match = dn.match(/^[^=]+=([^,]+),/i)
        return match ? { [conf.groupNameField]: match[1] } : null
      })
      .filter(Boolean)
  }

  if (ldapGroups && _.isArray(ldapGroups) && ldapGroups.length > 0) {
    const groups = ldapGroups.map(g => g[conf.groupNameField])
    const currentGroups = (await user.$relatedQuery('groups').select('groups.id')).map(g => g.id)
    const expectedGroups = Object.values(WIKI.auth.groups).filter(g => groups.includes(g.name)).map(g => g.id)
    for (const groupId of _.difference(expectedGroups, currentGroups)) {
      await user.$relatedQuery('groups').relate(groupId)
    }
    for (const groupId of _.difference(currentGroups, expectedGroups)) {
      await user.$relatedQuery('groups').unrelate().where('groupId', groupId)
    }
  }
}

This is backwards compatible - existing setups keep working, nested group users get proper mappings.

Only requirement: memberOf needs to be in the user search attributes.

Happy to submit a PR if there's interest!

@ValentinKolb commented on GitHub (Jan 26, 2026): ## Nested LDAP Groups via `memberOf` Fallback Hey, I ran into a related issue with **nested LDAP groups** in FreeIPA that might be worth addressing here. ### The Problem The current group search filter `(member=uid={{username}},...)` only finds direct memberships. With nested groups like: ``` alice → member of → dev-frontend → member of → developers → member of → staff ``` WikiJS only sees `dev-frontend`, not `developers` or `staff`. ### Simple Fix LDAP servers already resolve this via the `memberOf` attribute on users - it contains ALL groups (direct + inherited). We just need a fallback when group search returns empty: ```javascript if (conf.mapGroups) { let ldapGroups = _.get(profile, '_groups') // Fallback: If no groups from group search, try memberOf attribute if (!ldapGroups || !_.isArray(ldapGroups) || ldapGroups.length === 0) { const memberOf = _.get(profile, 'memberOf', []) const memberOfArray = _.isArray(memberOf) ? memberOf : [memberOf] ldapGroups = memberOfArray .filter(dn => dn && typeof dn === 'string') .map(dn => { const match = dn.match(/^[^=]+=([^,]+),/i) return match ? { [conf.groupNameField]: match[1] } : null }) .filter(Boolean) } if (ldapGroups && _.isArray(ldapGroups) && ldapGroups.length > 0) { const groups = ldapGroups.map(g => g[conf.groupNameField]) const currentGroups = (await user.$relatedQuery('groups').select('groups.id')).map(g => g.id) const expectedGroups = Object.values(WIKI.auth.groups).filter(g => groups.includes(g.name)).map(g => g.id) for (const groupId of _.difference(expectedGroups, currentGroups)) { await user.$relatedQuery('groups').relate(groupId) } for (const groupId of _.difference(currentGroups, expectedGroups)) { await user.$relatedQuery('groups').unrelate().where('groupId', groupId) } } } ``` This is backwards compatible - existing setups keep working, nested group users get proper mappings. Only requirement: `memberOf` needs to be in the user search attributes. Happy to submit a PR if there's interest!
Author
Owner

@drumadrian commented on GitHub (Jan 26, 2026):

hi @ValentineKolb

it sounds like the limitation preventing visibility of all relationships,
specifically memberOf

is this correct?

would we also need to test these use cases?

Things to Consider

• Data Structure: The code snippet uses _.get(profile, '_groups'). You
would need to ensure your specific LDAP configuration is actually pulling
the memberOf attribute from the server during the initial bind/search.

• Regex Parsing: The snippet shows a regex to extract the group name from a
DN (e.g., turning cn=developers,ou=groups,dc=example into just developers).
You'll want to make sure this matches your naming convention.
thanks for your input and suggestion 🤓

On Mon, Jan 26, 2026 at 12:05 PM Valentin Kolb @.***>
wrote:

ValentinKolb left a comment (requarks/wiki#1874)
https://github.com/requarks/wiki/issues/1874#issuecomment-3800672401
Nested LDAP Groups via memberOf Fallback

Hey, I ran into a related issue with nested LDAP groups in FreeIPA that
might be worth addressing here.
The Problem

The current group search filter (member=uid={{username}},...) only finds
direct memberships. With nested groups like:

alice → member of → dev-frontend → member of → developers → member of → staff

WikiJS only sees dev-frontend, not developers or staff.
Simple Fix

LDAP servers already resolve this via the memberOf attribute on users -
it contains ALL groups (direct + inherited). We just need a fallback when
group search returns empty:

if (conf.mapGroups) {
let ldapGroups = _.get(profile, '_groups')

// Fallback: If no groups from group search, try memberOf attribute
if (!ldapGroups || !_.isArray(ldapGroups) || ldapGroups.length === 0) {
const memberOf = _.get(profile, 'memberOf', [])
const memberOfArray = _.isArray(memberOf) ? memberOf : [memberOf]

ldapGroups = memberOfArray
  .filter(dn => dn && typeof dn === 'string')
  .map(dn => {
    const match = dn.match(/^[^=]+=([^,]+),/i)
    return match ? { [conf.groupNameField]: match[1] } : null
  })
  .filter(Boolean)

}

if (ldapGroups && _.isArray(ldapGroups) && ldapGroups.length > 0) {
const groups = ldapGroups.map(g => g[conf.groupNameField])
const currentGroups = (await user.$relatedQuery('groups').select('groups.id')).map(g => g.id)
const expectedGroups = Object.values(WIKI.auth.groups).filter(g => groups.includes(g.name)).map(g => g.id)
for (const groupId of _.difference(expectedGroups, currentGroups)) {
await user.$relatedQuery('groups').relate(groupId)
}
for (const groupId of _.difference(currentGroups, expectedGroups)) {
await user.$relatedQuery('groups').unrelate().where('groupId', groupId)
}
}}

This is backwards compatible - existing setups keep working, nested group
users get proper mappings.

Only requirement: memberOf needs to be in the user search attributes.

Happy to submit a PR if there's interest!


Reply to this email directly, view it on GitHub
https://github.com/requarks/wiki/issues/1874#issuecomment-3800672401,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/ABSE2RBIFKCX6JWJQUBBYFD4IZCMPAVCNFSM6AAAAABSSH3UWOVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTQMBQGY3TENBQGE
.
You are receiving this because you are subscribed to this thread.Message
ID: @.***>

@drumadrian commented on GitHub (Jan 26, 2026): hi @ValentineKolb it sounds like the limitation preventing visibility of all relationships, specifically memberOf is this correct? would we also need to test these use cases? Things to Consider • Data Structure: The code snippet uses _.get(profile, '_groups'). You would need to ensure your specific LDAP configuration is actually pulling the memberOf attribute from the server during the initial bind/search. • Regex Parsing: The snippet shows a regex to extract the group name from a DN (e.g., turning cn=developers,ou=groups,dc=example into just developers). You'll want to make sure this matches your naming convention. thanks for your input and suggestion 🤓 On Mon, Jan 26, 2026 at 12:05 PM Valentin Kolb ***@***.***> wrote: > *ValentinKolb* left a comment (requarks/wiki#1874) > <https://github.com/requarks/wiki/issues/1874#issuecomment-3800672401> > Nested LDAP Groups via memberOf Fallback > > Hey, I ran into a related issue with *nested LDAP groups* in FreeIPA that > might be worth addressing here. > The Problem > > The current group search filter (member=uid={{username}},...) only finds > direct memberships. With nested groups like: > > alice → member of → dev-frontend → member of → developers → member of → staff > > WikiJS only sees dev-frontend, not developers or staff. > Simple Fix > > LDAP servers already resolve this via the memberOf attribute on users - > it contains ALL groups (direct + inherited). We just need a fallback when > group search returns empty: > > if (conf.mapGroups) { > let ldapGroups = _.get(profile, '_groups') > > // Fallback: If no groups from group search, try memberOf attribute > if (!ldapGroups || !_.isArray(ldapGroups) || ldapGroups.length === 0) { > const memberOf = _.get(profile, 'memberOf', []) > const memberOfArray = _.isArray(memberOf) ? memberOf : [memberOf] > > ldapGroups = memberOfArray > .filter(dn => dn && typeof dn === 'string') > .map(dn => { > const match = dn.match(/^[^=]+=([^,]+),/i) > return match ? { [conf.groupNameField]: match[1] } : null > }) > .filter(Boolean) > } > > if (ldapGroups && _.isArray(ldapGroups) && ldapGroups.length > 0) { > const groups = ldapGroups.map(g => g[conf.groupNameField]) > const currentGroups = (await user.$relatedQuery('groups').select('groups.id')).map(g => g.id) > const expectedGroups = Object.values(WIKI.auth.groups).filter(g => groups.includes(g.name)).map(g => g.id) > for (const groupId of _.difference(expectedGroups, currentGroups)) { > await user.$relatedQuery('groups').relate(groupId) > } > for (const groupId of _.difference(currentGroups, expectedGroups)) { > await user.$relatedQuery('groups').unrelate().where('groupId', groupId) > } > }} > > This is backwards compatible - existing setups keep working, nested group > users get proper mappings. > > Only requirement: memberOf needs to be in the user search attributes. > > Happy to submit a PR if there's interest! > > — > Reply to this email directly, view it on GitHub > <https://github.com/requarks/wiki/issues/1874#issuecomment-3800672401>, > or unsubscribe > <https://github.com/notifications/unsubscribe-auth/ABSE2RBIFKCX6JWJQUBBYFD4IZCMPAVCNFSM6AAAAABSSH3UWOVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTQMBQGY3TENBQGE> > . > You are receiving this because you are subscribed to this thread.Message > ID: ***@***.***> >
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/wiki-requarks#1517
No description provided.