This utility is intended to enable synchronization between GitHub and various LDAP and SAML providers. This is particularly useful for large organizations with many teams that either use GitHub Enterprise Cloud, do not use LDAP for authentication, or use a SAML provider other than what is natively supported. It supports both GitHub.com, GitHub Enterprise Server (GHES) and GitHub, but it will need to live in a location that can access your LDAP servers.
- LDAP
- Active Directory
- Azure AD
- Okta
- OneLogin
- Google Workspace
- Keycloak
This utility provides the following functionality:
| Feature | Supported | Description |
|---|---|---|
| Sync Users | Yes | Add or remove users from Teams in GitHub to keep in sync with Active Directory groups |
| Dynamic Config | Yes | Utilize a settings file to derive Active Directory and GitHub settings |
| LDAP SSL | Yes | SSL or TLS connections. |
| Failure notifications | Yes | Presently supports opening a GitHub issue when sync failed. The repo is configurable. |
| Sync on new team | Yes | Synchronize users when a new team is created |
| Sync on team edit | No | This event is not processed currently |
| Custom team/group maps | Yes | The team slug and group name will be matched automatically, unless you define a custom mapping with syncmap.yml |
| Force custom map | Yes | Sync only team defined in syncmap.yml |
| Dry run / Test mode | Yes | Run and print the differences, but make no changes |
| Nested teams/groups | No | Synchronize groups within groups. Presently, if a group is a member of another group, it is skipped |
- On your GitHub instance, visit the
settingspage on the organization that you want to own the GitHub App, and navigate to theGitHub Appssection.- You can access this page by visiting the following url:
https://<MY_GITHUB_HOSTNAME>/organizations/<MY_ORG_NAME>/settings/apps
- You can access this page by visiting the following url:
- Create a new GitHub App with the following settings:
- Webhook URL: URL of the machine on which this app has been deployed (Example:
http://ip.of.machine:3000) - Homepage URL: URL of the machine on which this app has been deployed (Example:
http://ip.of.machine:3000) - Webhook Secret: The webhook secret that will be or has been defined as an environment variable in your deployment environment as
WEBHOOK_SECRET - Permissions and Events: This application will need to be able to manage teams on GitHub, so the
eventsandpermissionslisted below will be required. For more information on how to create a GitHub App, please visit https://developer.github.com/apps/building-github-apps/creating-a-github-app
- Webhook URL: URL of the machine on which this app has been deployed (Example:
- Once these have been configured, select the
Create GitHub Appbutton at the bottom of the page to continue - Make a note of the
APP IDon your newly-created GitHub App. You will need to set this as an environment variable when configuring the app. - Generate and download a private key from the new App page, and store it in your deployment environment. You can either do this by saving the file directly in the environment and specifying its path with the environment variable
PRIVATE_KEY_PATH - After you have created the GitHub App, you will need to install it to the desired GitHub Organizations.
- Select
Install App - Select
All Repositoriesor the desired repositories you wish to watch
- Select
| Category | Attribute | Permission |
|---|---|---|
| Repository permissions | Issues | Read & write |
| Repository permissions | Metadata | Read-only |
| Organization permissions | Members | Read & write |
| User permissions | Email addresses | Read-only |
| Event | Required? | Description |
|---|---|---|
Team | Optional | Trigger when a new team is created, deleted, edited, renamed, etc. |
Authentication methods
- Username/Password
- Service Principal
- Certificate
- Device Auth
This app requires the following Azure permissions:
GroupMember.Read.AllUser.Read.All
If you have ADMIN_FINE_GRAINED_AUTHZ enabled, you only need the following permission for the user realm:
view-users
You must delegate domain-wide authority to the service account with the following scopes:
https://www.googleapis.com/auth/admin.directory.group.readonlyhttps://www.googleapis.com/auth/admin.directory.group.member.readonlyhttps://www.googleapis.com/auth/admin.directory.user.readonly
You must provide a Google Workspace Admin account for the service account to impersonate. It must have Admin API permissions greater or equal to the scopes listed above.
To get started, ensure that you are using Python 3.9 (or update your Pipfile to the version you're running, 3.4+). The following additional libraries are required:
- Flask
- github3.py
- python-ldap3
- APScheduler
- python-dotenv
- PyYAML
- msal
- asyncio
- okta
- onelogin
- python-keycloak
Install the required libraries.
pipenv installOnce you have all of the requirements installed, be sure to edit the .env to match your environment.
## GitHub App settingsWEBHOOK_SECRET=developmentAPP_ID=12345PRIVATE_KEY_PATH=.ssh/team-sync.pemGHE_HOST=github.example.com## AzureAD = AAD## AD/LDAP = LDAP## Okta = OKTA## OneLogin = ONELOGIN## Google Workspace = GOOGLE_WORKSPACEUSER_DIRECTORY=LDAP## Sync users on username or email attributeUSER_SYNC_ATTRIBUTE=usernameLDAP_SERVER_HOST=dc1.example.comLDAP_SERVER_PORT=389LDAP_BASE_DN="DC=example,DC=com"LDAP_USER_BASE_DN="CN=Users,DC=example,DC=example"LDAP_GROUP_BASE_DN="OU=Groups,DC=example,DC=example"LDAP_USER_FILTER="(objectClass=person)"LDAP_USER_ATTRIBUTE=sAMAccountNameLDAP_USER_MAIL_ATTRIBUTE=mailLDAP_GROUP_FILTER="(&(objectClass=group)(cn={group_name}))"LDAP_GROUP_MEMBER_ATTRIBUTE=memberLDAP_BIND_USER="bind-user@example.com"LDAP_BIND_PASSWORD="p4$$w0rd"LDAP_SEARCH_PAGE_SIZE=1000LDAP_SERVER_HOST=dc1.example.comLDAP_SERVER_PORT=389LDAP_BASE_DN="dc=example,dc=com"LDAP_USER_BASE_DN="ou=People,dc=example,dc=com"LDAP_GROUP_BASE_DN="ou=Groups,dc=example,dc=com"LDAP_USER_FILTER="(&(objectClass=person)({ldap_user_attribute}={username}))"LDAP_USER_ATTRIBUTE=uidLDAP_USER_MAIL_ATTRIBUTE=mailLDAP_GROUP_FILTER="(&(objectClass=posixGroup)(cn={group_name}))"LDAP_GROUP_MEMBER_ATTRIBUTE=memberUidLDAP_BIND_USER="cn=admin,dc=example,dc=com"LDAP_BIND_PASSWORD="p4$$w0rd"LDAP_SEARCH_PAGE_SIZE=1000AZURE_TENANT_ID="<tenant_id>"AZURE_CLIENT_ID="<client_id>"AZURE_CLIENT_SECRET="<client_secret>"AZURE_APP_SCOPE="default"AZURE_API_ENDPOINT="https://graph.microsoft.com/v1.0"# can also be an extensionAttributeAZURE_USERNAME_ATTRIBUTE=userPrincipalNameAZURE_USER_IS_UPN=true# use transitive members of a group instead of direct membersAZURE_USE_TRANSITIVE_GROUP_MEMBERS=falseOKTA_ORG_URL=https://example.okta.comOKTA_USERNAME_ATTRIBUTE=github_username# token loginOKTA_ACCESS_TOKEN=asdfghkjliptojkjsj00294759# OAuth loginOKTA_AUTH_METHOD=oauthOKTA_CLIENT_ID=abcdefghijklOKTA_SCOPES='okta.users.read okta.groups.read'OKTA_PRIVATE_KEY='{"kty": "RSA", ...}'KEYCLOAK_USERNAME=api-accountKEYCLOAK_PASSWORD=ExamplePasswordKEYCLOAK_REALM=ExampleCorpKEYCLOAK_ADMIN_REALM=masterKEYCLOAK_USE_GITHUB_IDP=trueONELOGIN_CLIENT_ID='asdafsflkjlk13q33433445wee'ONELOGIN_CLIENT_SECRET='ca3a86f982fjjkjjkfkhls'REGION=USGOOGLE_WORKSPACE_SA_CREDS_FILE=googleAuth.jsonGOOGLE_WORKSPACE_ADMIN_EMAIL=admin@example.comGOOGLE_WORKSPACE_USERNAME_CUSTOM_SCHEMA_NAME=schema-nameGOOGLE_WORKSPACE_USERNAME_FIELD=field-name## Additional settingsCHANGE_THRESHOLD=25OPEN_ISSUE_ON_FAILURE=trueREPO_FOR_ISSUES=github-demo/demo-repoISSUE_ASSIGNEE=githubberSYNC_SCHEDULE=0 * * * *TEST_MODE=falseSYNCMAP_ONLY=falseEMU_SHORTCODE=volcano### Automatically add users missing from the organizationADD_MEMBER=false## Automatically remove users from the organization that are not part of a teamREMOVE_ORG_MEMBERS_WITHOUT_TEAM=false###################### Flask Settings ######################## Default: app, comment out to run once as a scriptFLASK_APP=app## Default: productionFLASK_ENV=development## Default: 5000FLASK_RUN_PORT=5000## Default: 127.0.0.1FLASK_RUN_HOST=0.0.0.0---
mapping:
- github: demo-teamdirectory: ldap super usersorg: my github org
- github: demo-admin-2directory: some other groupThe custom map uses slugs that are lowercase. If you don't specify organization name, it will synchronize all teams with same name in any organization.
This example runs the app in a standard Flask environment.
pipenv run flask run --host=0.0.0.0 --port=5000Or you can run the app with Python directly.
pipenv run python app.pyThis project draws much from: