Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sync groups and group membership through auth strategies #1874

Open
signalkraft opened this issue May 13, 2020 · 14 comments
Open

Sync groups and group membership through auth strategies #1874

signalkraft opened this issue May 13, 2020 · 14 comments
Assignees

Comments

@signalkraft
Copy link

signalkraft commented May 13, 2020

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.

@NGPixel
Copy link
Member

NGPixel commented May 15, 2020

Sounds good 👍

@NGPixel NGPixel self-assigned this May 15, 2020
@baodrate
Copy link

@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

@signalkraft
Copy link
Author

signalkraft commented 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!

@baodrate
Copy link

baodrate commented 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!

@signalkraft
Copy link
Author

signalkraft commented 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.

@drehelis
Copy link

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

@NGPixel
Copy link
Member

NGPixel commented Mar 28, 2021

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

It is planned for 3.x yes

@warthy
Copy link
Contributor

warthy commented 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 ?

@devksingh4
Copy link

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.

@uberspot
Copy link

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

@jtagcat
Copy link

jtagcat commented 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.

@fionera
Copy link
Contributor

fionera commented 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

@aelgasser
Copy link
Contributor

@NGPixel et al. I just submitted a PR to implement group sync with SAML: #6299

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

@fionera
Copy link
Contributor

fionera commented Mar 27, 2023

@aelgasser I already implemented assignment for it for oidc #5568

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants