ldap-teamsync/app.py

299 строки
9.2 KiB
Python
Исходник Обычный вид История

2020-07-01 06:32:44 +03:00
import atexit
2020-06-18 23:48:01 +03:00
import os
2020-07-01 06:32:44 +03:00
import time
2021-03-25 06:36:24 +03:00
import json
import github3
2020-07-01 06:32:44 +03:00
from distutils.util import strtobool
2020-06-20 01:00:02 +03:00
from apscheduler.schedulers.background import BackgroundScheduler
2020-07-01 01:08:15 +03:00
from apscheduler.triggers.cron import CronTrigger
2020-07-01 06:32:44 +03:00
from flask import Flask
2021-03-22 07:13:27 +03:00
from githubapp import GitHubApp, DirectoryClient, CRON_INTERVAL, TEST_MODE
2020-06-17 17:43:47 +03:00
app = Flask(__name__)
github_app = GitHubApp(app)
2021-03-22 07:13:27 +03:00
directory = DirectoryClient()
2021-04-12 10:42:33 +03:00
addUserAsMember = os.getenv('ADD_MEMBER', 'False') == 'True'
2020-06-17 17:43:47 +03:00
2020-06-20 01:00:02 +03:00
# Schedule a full sync
scheduler = BackgroundScheduler(daemon=True)
scheduler.start()
atexit.register(lambda: scheduler.shutdown(wait=False))
2020-07-01 07:24:37 +03:00
2021-03-22 07:55:00 +03:00
@github_app.on("team.created")
2020-07-01 00:33:46 +03:00
def sync_new_team():
2020-06-30 22:30:09 +03:00
"""
2020-07-01 00:33:46 +03:00
Sync a new team when it is created
2020-06-30 22:30:09 +03:00
:return:
"""
2021-03-22 07:55:00 +03:00
owner = github_app.payload["organization"]["login"]
team_id = github_app.payload["team"]["id"]
2021-03-22 18:54:04 +03:00
if os.environ["USER_DIRECTORY"].upper() == "AAD":
2021-04-07 10:58:27 +03:00
# Azure APIs don't currently support case insensitive searching
2021-03-22 18:54:04 +03:00
slug = github_app.payload["team"]["name"].replace(" ", "-")
2021-03-22 18:53:35 +03:00
else:
slug = github_app.payload["team"]["slug"]
2020-07-01 00:33:46 +03:00
client = github_app.installation_client
2021-03-22 07:55:00 +03:00
sync_team(client=client, owner=owner, team_id=team_id, slug=slug)
2020-06-20 01:00:02 +03:00
2020-07-01 00:33:46 +03:00
def sync_team(client=None, owner=None, team_id=None, slug=None):
2020-06-19 21:38:37 +03:00
"""
2020-07-01 07:24:37 +03:00
Prepare the team sync
2020-07-01 00:33:46 +03:00
:param client:
:param owner:
:param team_id:
:param slug:
2020-06-19 21:38:37 +03:00
:return:
"""
2021-03-25 06:45:04 +03:00
print("-------------------------------")
print(f"Processing Team: {slug}")
2020-07-01 00:33:46 +03:00
org = client.organization(owner)
team = org.team(team_id)
2020-07-02 03:19:12 +03:00
custom_map = load_custom_map()
try:
directory_group = custom_map[slug] if slug in custom_map else slug
directory_members = directory_group_members(group=directory_group)
except Exception as e:
directory_members = []
print(e)
2020-07-02 03:19:12 +03:00
team_members = github_team_members(
2021-03-22 07:55:00 +03:00
client=client, owner=owner, team_id=team_id, attribute="username"
2020-06-17 23:31:54 +03:00
)
compare = compare_members(
2021-03-22 07:55:00 +03:00
group=directory_members, team=team_members, attribute="username"
2020-06-17 23:31:54 +03:00
)
2020-07-01 07:24:37 +03:00
if TEST_MODE:
2021-03-22 07:55:00 +03:00
print("Skipping execution due to TEST_MODE...")
2021-03-25 06:36:24 +03:00
print(json.dumps(compare, indent=2))
2020-07-01 07:24:37 +03:00
else:
2020-07-01 06:51:35 +03:00
try:
2021-03-22 07:55:00 +03:00
execute_sync(org=org, team=team, slug=slug, state=compare)
2020-07-01 06:51:35 +03:00
except ValueError as e:
2021-03-22 07:55:00 +03:00
if strtobool(os.environ["OPEN_ISSUE_ON_FAILURE"]):
2020-07-01 06:51:35 +03:00
open_issue(client=client, slug=slug, message=e)
except AssertionError as e:
2021-03-22 07:55:00 +03:00
if strtobool(os.environ["OPEN_ISSUE_ON_FAILURE"]):
2020-07-01 06:51:35 +03:00
open_issue(client=client, slug=slug, message=e)
2020-06-19 00:58:56 +03:00
2020-06-17 17:43:47 +03:00
2021-03-22 07:13:27 +03:00
def directory_group_members(group=None):
2020-06-17 17:43:47 +03:00
"""
2021-03-22 07:13:27 +03:00
Look up members of a group in your user directory
:param group: The name of the group to query in your directory server
2020-06-17 18:20:02 +03:00
:type group: str
2021-03-22 07:13:27 +03:00
:return: group_members
2020-06-17 18:20:02 +03:00
:rtype: list
2020-06-17 17:43:47 +03:00
"""
try:
members = directory.get_group_members(group_name=group)
group_members = [member for member in members]
except Exception as e:
group_members = []
print(e)
2021-03-22 07:13:27 +03:00
return group_members
2020-06-17 17:43:47 +03:00
2020-06-19 21:38:37 +03:00
2020-07-02 03:19:12 +03:00
def github_team_info(client=None, owner=None, team_id=None):
2020-06-19 21:38:37 +03:00
"""
Look up team info in GitHub
2020-07-01 00:33:46 +03:00
:param client:
:param owner:
2020-06-19 21:38:37 +03:00
:param team_id:
:return:
"""
2020-07-01 00:33:46 +03:00
org = client.organization(owner)
2020-06-19 00:58:56 +03:00
return org.team(team_id)
2020-06-17 17:43:47 +03:00
2020-06-19 21:38:37 +03:00
2021-03-22 07:55:00 +03:00
def github_team_members(client=None, owner=None, team_id=None, attribute="username"):
2020-06-17 17:43:47 +03:00
"""
Look up members of a give team in GitHub
2020-07-01 00:33:46 +03:00
:param client:
:param owner:
2020-06-17 17:43:47 +03:00
:param team_id:
2020-06-17 23:31:54 +03:00
:param attribute:
2020-07-01 00:33:46 +03:00
:type owner: str
2020-06-17 18:20:02 +03:00
:type team_id: int
2020-06-17 23:31:54 +03:00
:type attribute: str
2020-06-17 18:20:02 +03:00
:return: team_members
:rtype: list
2020-06-17 17:43:47 +03:00
"""
2020-06-17 23:31:54 +03:00
team_members = []
2020-07-02 03:19:12 +03:00
team = github_team_info(client=client, owner=owner, team_id=team_id)
2021-03-22 07:55:00 +03:00
if attribute == "email":
2020-06-17 23:31:54 +03:00
for m in team.members():
2020-07-01 00:33:46 +03:00
user = client.user(m.login)
2021-03-22 07:55:00 +03:00
team_members.append(
{
"username": str(user.login).casefold(),
"email": str(user.email).casefold(),
}
)
2020-06-17 23:31:54 +03:00
else:
for member in team.members():
2021-04-07 11:04:33 +03:00
team_members.append({"username": str(member).casefold(), "email": ""})
2020-06-17 17:43:47 +03:00
return team_members
2021-03-22 07:55:00 +03:00
def compare_members(group, team, attribute="username"):
2020-06-17 18:20:02 +03:00
"""
2021-03-22 07:13:27 +03:00
Compare users in GitHub and the User Directory to see which users need to be added or removed
2020-07-01 06:32:44 +03:00
:param group:
:param team:
2020-06-17 23:31:54 +03:00
:param attribute:
2020-06-17 18:20:02 +03:00
:return: sync_state
:rtype: dict
"""
2021-03-22 07:13:27 +03:00
directory_list = [x[attribute] for x in group]
2020-07-01 06:32:44 +03:00
github_list = [x[attribute] for x in team]
2021-03-22 18:53:35 +03:00
add_users = list(set(directory_list) - set(github_list))
2021-03-22 07:13:27 +03:00
remove_users = list(set(github_list) - set(directory_list))
2020-06-17 18:20:02 +03:00
sync_state = {
2021-03-22 07:55:00 +03:00
"directory": group,
"github": team,
"action": {"add": add_users, "remove": remove_users},
2020-06-17 18:20:02 +03:00
}
return sync_state
2020-06-17 17:43:47 +03:00
2020-06-19 22:58:55 +03:00
def execute_sync(org, team, slug, state):
2020-06-19 21:38:37 +03:00
"""
Perform the synchronization
:param org:
:param team:
2020-06-19 22:58:55 +03:00
:param slug:
2020-06-19 21:38:37 +03:00
:param state:
:return:
"""
2021-04-07 11:04:33 +03:00
total_changes = len(state["action"]["remove"]) + len(state["action"]["add"])
2021-03-22 07:55:00 +03:00
if len(state["directory"]) == 0:
2021-03-22 07:13:27 +03:00
message = f"{os.environ.get('USER_DIRECTORY', 'LDAP').upper()} group returned empty: {slug}"
2020-06-19 22:58:55 +03:00
raise ValueError(message)
2021-03-22 07:55:00 +03:00
elif int(total_changes) > int(os.environ.get("CHANGE_THRESHOLD", 25)):
2020-06-19 22:58:55 +03:00
message = "Skipping sync for {}.<br>".format(slug)
message += "Total number of changes ({}) would exceed the change threshold ({}).".format(
2021-03-22 07:55:00 +03:00
str(total_changes), str(os.environ.get("CHANGE_THRESHOLD", 25))
2020-06-19 22:58:55 +03:00
)
message += "<br>Please investigate this change and increase your threshold if this is accurate."
raise AssertionError(message)
else:
2021-03-22 07:55:00 +03:00
for user in state["action"]["add"]:
2020-06-19 22:58:55 +03:00
# Validate that user is in org
if org.is_member(user) or addUserAsMember:
try:
2021-04-07 10:59:53 +03:00
print(f"Adding {user} to {slug}")
team.add_or_update_membership(user)
except github3.exceptions.NotFoundError:
2021-04-07 10:59:53 +03:00
print(f"User: {user} not found")
pass
2020-06-19 22:58:55 +03:00
else:
2021-03-25 06:36:24 +03:00
print(f"Skipping {user} as they are not part of the org")
2020-06-19 22:58:55 +03:00
2021-03-22 07:55:00 +03:00
for user in state["action"]["remove"]:
2021-03-25 06:36:24 +03:00
print(f"Removing {user} from {slug}")
2020-06-19 22:58:55 +03:00
team.revoke_membership(user)
2020-07-01 00:33:46 +03:00
def open_issue(client, slug, message):
2020-07-01 07:24:37 +03:00
"""
Open an issue with the failed sync details
:param client: Our installation client
:param slug: Team slug
:param message: Error message to detail
:return:
"""
2021-03-22 07:55:00 +03:00
repo_for_issues = os.environ["REPO_FOR_ISSUES"]
owner = repo_for_issues.split("/")[0]
repository = repo_for_issues.split("/")[1]
assignee = os.environ["ISSUE_ASSIGNEE"]
2020-07-01 00:33:46 +03:00
client.create_issue(
2020-06-19 22:03:09 +03:00
owner=owner,
repository=repository,
assignee=assignee,
2020-06-19 22:58:55 +03:00
title="Team sync failed for @{}/{}".format(owner, slug),
2021-03-22 07:55:00 +03:00
body=str(message),
2020-06-19 22:03:09 +03:00
)
2020-06-30 22:30:09 +03:00
2020-07-01 07:24:37 +03:00
2021-03-22 07:55:00 +03:00
def load_custom_map(file="syncmap.yml"):
2020-07-02 03:19:12 +03:00
"""
Custom team synchronization
:param file:
:return:
"""
syncmap = {}
if os.path.isfile(file):
from yaml import load, Loader
2021-03-22 07:55:00 +03:00
with open(file, "r") as f:
2020-07-02 03:19:12 +03:00
data = load(f, Loader=Loader)
2021-03-22 07:55:00 +03:00
for d in data["mapping"]:
syncmap[d["github"]] = d["directory"]
2020-07-02 03:19:12 +03:00
return syncmap
def get_app_installations():
"""
Get a list of installations for this app
:return:
"""
with app.app_context() as ctx:
try:
2021-04-07 10:58:27 +03:00
c = ctx.push()
gh = GitHubApp(c)
installations = gh.app_client.app_installations
finally:
2021-04-07 10:58:27 +03:00
ctx.pop()
return installations
2021-03-22 07:55:00 +03:00
@scheduler.scheduled_job(
trigger=CronTrigger.from_crontab(CRON_INTERVAL), id="sync_all_teams"
)
2020-07-01 00:33:46 +03:00
def sync_all_teams():
"""
2021-03-22 07:13:27 +03:00
Lookup teams in a GitHub org and synchronize all teams with your user directory
2020-07-01 00:33:46 +03:00
:return:
"""
print(f'Syncing all teams: {time.strftime("%A, %d. %B %Y %I:%M:%S %p")}')
installations = get_app_installations()
for i in installations():
2021-03-25 06:45:04 +03:00
print("========================================================")
print(f"## Processing Organization: {i.account['login']}")
print("========================================================")
with app.app_context() as ctx:
try:
gh = GitHubApp(ctx.push())
client = gh.app_installation(installation_id=i.id)
org = client.organization(i.account["login"])
for team in org.teams():
try:
sync_team(
2021-04-07 11:04:33 +03:00
client=client,
owner=org.login,
team_id=team.id,
slug=team.slug,
)
except Exception as e:
print(f"Organization: {org.login}")
print(f"Unable to sync team: {team.slug}")
print(f"DEBUG: {e}")
except Exception as e:
print(f"DEBUG: {e}")
finally:
ctx.pop()
2021-04-07 11:16:53 +03:00
sync_all_teams()
2020-07-01 00:33:46 +03:00
2021-03-22 07:55:00 +03:00
if __name__ == "__main__":
2020-07-01 07:24:37 +03:00
sync_all_teams()
2020-07-01 06:32:44 +03:00
app.run(
2021-03-22 07:55:00 +03:00
host=os.environ.get("FLASK_RUN_HOST", "0.0.0.0"),
port=os.environ.get("FLASK_RUN_PORT", "5000"),
2020-07-01 07:24:37 +03:00
)