| |
@@ -0,0 +1,264 @@
|
| |
+ from __future__ import unicode_literals
|
| |
+
|
| |
+ import logging
|
| |
+ import smtplib
|
| |
+
|
| |
+ import fedora
|
| |
+ import flask
|
| |
+ from six.moves.email_mime_text import MIMEText
|
| |
+
|
| |
+ from hubs.database import Session
|
| |
+ from hubs.models import Hub, User, Association
|
| |
+ from hubs.utils import get_fedmsg_config
|
| |
+
|
| |
+
|
| |
+ log = logging.getLogger(__name__)
|
| |
+
|
| |
+
|
| |
+ class FASClient(object):
|
| |
+
|
| |
+ HANDLED_ROLES = ("owner", "member", "pending-owner", "pending-member")
|
| |
+
|
| |
+ def __init__(self, client=None):
|
| |
+ if client is None:
|
| |
+ fedmsg_config = get_fedmsg_config()
|
| |
+ client = fedora.client.fas2.AccountSystem(
|
| |
+ username=fedmsg_config["fas_credentials"]["username"],
|
| |
+ password=fedmsg_config["fas_credentials"]["password"],
|
| |
+ )
|
| |
+ self.client = client
|
| |
+ self.db = Session()
|
| |
+
|
| |
+ def person_by_username(self, fas_name):
|
| |
+ return self.client.person_by_username(fas_name)
|
| |
+
|
| |
+ def group_by_name(self, fas_name):
|
| |
+ return self.client.group_by_name(fas_name)
|
| |
+
|
| |
+ def sync_team_hub(self, hub):
|
| |
+ fas_group = self.client.group_by_name(hub.name)
|
| |
+ # Config
|
| |
+ attr_map = {
|
| |
+ "display_name": "summary",
|
| |
+ "irc_network": "chat_domain",
|
| |
+ "irc_channel": "chat_channel",
|
| |
+ "mailing_list": "mailing_list",
|
| |
+ "mailing_list_url": "mailing_list_url",
|
| |
+ }
|
| |
+ for fas_attr, hub_attr in attr_map.items():
|
| |
+ if fas_group[fas_attr] is None:
|
| |
+ continue
|
| |
+ hub.config[hub_attr] = fas_group[fas_attr]
|
| |
+ return fas_group
|
| |
+
|
| |
+ def sync_team_hub_roles(self, hub, fas_group=None):
|
| |
+ if fas_group is None:
|
| |
+ fas_group = self.client.group_by_name(hub.name)
|
| |
+ affected_users = set()
|
| |
+ # List roles in FAS
|
| |
+ fas_roles = []
|
| |
+ # According to https://pagure.io/fedora-hubs/issue/389#comment-481972
|
| |
+ # "unapproved" members are still members in Hubs.
|
| |
+ all_fas_group_roles = (
|
| |
+ fas_group["approved_roles"] + fas_group["unapproved_roles"]
|
| |
+ )
|
| |
+ for membership in all_fas_group_roles:
|
| |
+ if membership["role_type"] in ("administrator", "sponsor"):
|
| |
+ role = "owner"
|
| |
+ else:
|
| |
+ role = "member"
|
| |
+ if membership["role_status"] == "pending":
|
| |
+ role = "pending-{}".format(role)
|
| |
+ person = self.client.person_by_id(membership["person_id"])
|
| |
+ fas_roles.append((person["username"], role))
|
| |
+ # List roles in Hubs
|
| |
+ existing_associations = Association.query.filter(
|
| |
+ Association.hub == hub,
|
| |
+ Association.role.in_(self.HANDLED_ROLES),
|
| |
+ ).all()
|
| |
+ # Remove extra roles
|
| |
+ for assoc in existing_associations:
|
| |
+ if (assoc.user.username, assoc.role) not in fas_roles:
|
| |
+ log.debug("Removing role %s from user %s on hub %s",
|
| |
+ assoc.role, assoc.user.username, hub.name)
|
| |
+ self.db.delete(assoc)
|
| |
+ affected_users.add(assoc.user.username)
|
| |
+ # Add missing roles
|
| |
+ existing_associations = [
|
| |
+ (assoc.user.username, assoc.role)
|
| |
+ for assoc in existing_associations
|
| |
+ ]
|
| |
+ for username, role in fas_roles:
|
| |
+ if (username, role) in existing_associations:
|
| |
+ continue
|
| |
+ user = User.query.get(username)
|
| |
+ if user is None:
|
| |
+ continue
|
| |
+ log.debug("Adding role %s to user %s on hub %s",
|
| |
+ role, username, hub.name)
|
| |
+ hub.subscribe(user, role)
|
| |
+ affected_users.add(username)
|
| |
+ return list(affected_users)
|
| |
+
|
| |
+ def sync_user_roles(self, user, hub=None):
|
| |
+ fas_user = self.client.person_by_username(user.username)
|
| |
+ affected_hubs = set()
|
| |
+ # List roles in FAS
|
| |
+ fas_roles = []
|
| |
+ for group_name, fas_role in fas_user["group_roles"].items():
|
| |
+ if hub is not None and group_name != hub.name:
|
| |
+ continue
|
| |
+ # # See https://pagure.io/fedora-hubs/issue/389#comment-481972
|
| |
+ # # "unapproved" members are still members in Hubs.
|
| |
+ # if fas_role["role_status"] == "unapproved":
|
| |
+ # continue
|
| |
+ if fas_role["role_type"] in ("administrator", "sponsor"):
|
| |
+ role = "owner"
|
| |
+ else:
|
| |
+ role = "member"
|
| |
+ if fas_role["role_status"] == "pending":
|
| |
+ role = "pending-{}".format(role)
|
| |
+ fas_roles.append((group_name, role))
|
| |
+ # List roles in Hubs
|
| |
+ existing_associations = Association.query.filter(
|
| |
+ Association.user == user,
|
| |
+ Association.role.in_(self.HANDLED_ROLES),
|
| |
+ )
|
| |
+ if hub is None:
|
| |
+ existing_associations = existing_associations.join(Hub).filter(
|
| |
+ Hub.hub_type == "team",
|
| |
+ )
|
| |
+ else:
|
| |
+ existing_associations = existing_associations.filter(
|
| |
+ Association.hub == hub,
|
| |
+ )
|
| |
+ existing_associations = existing_associations.all()
|
| |
+ # Remove extra roles
|
| |
+ for assoc in existing_associations:
|
| |
+ if (assoc.hub.name, assoc.role) not in fas_roles:
|
| |
+ log.debug("Removing role %s from user %s on hub %s",
|
| |
+ assoc.role, user.username, assoc.hub.name)
|
| |
+ self.db.delete(assoc)
|
| |
+ affected_hubs.add(assoc.hub.id)
|
| |
+ # Add missing roles
|
| |
+ existing_associations = [
|
| |
+ (assoc.hub.name, assoc.role)
|
| |
+ for assoc in existing_associations
|
| |
+ ]
|
| |
+ for group_name, role in fas_roles:
|
| |
+ if (group_name, role) in existing_associations:
|
| |
+ continue
|
| |
+ hub = Hub.by_name(group_name, "team")
|
| |
+ if hub is None:
|
| |
+ continue
|
| |
+ log.debug("Adding role %s to user %s on hub %s",
|
| |
+ role, user.username, hub.name)
|
| |
+ hub.subscribe(user, role)
|
| |
+ affected_hubs.add(hub.id)
|
| |
+ return list(affected_hubs)
|
| |
+
|
| |
+
|
| |
+ # The functions below will be called by the backend (worker)
|
| |
+
|
| |
+
|
| |
+ def sync_team_hub(hub_id, created=False):
|
| |
+ hub = Hub.query.get(hub_id)
|
| |
+ if hub is None:
|
| |
+ return []
|
| |
+ log.debug("Syncing team hub %s with FAS", hub.name)
|
| |
+ fas_client = FASClient()
|
| |
+ try:
|
| |
+ # Sync group config and roles
|
| |
+ fas_group = fas_client.sync_team_hub(hub)
|
| |
+ if created:
|
| |
+ # Only set the description on creation because we want to allow hub
|
| |
+ # admins to later change it to a different content from what's in
|
| |
+ # FAS.
|
| |
+ hub.config["description"] = fas_group["apply_rules"]
|
| |
+ # Sync roles on creation
|
| |
+ affected_users = fas_client.sync_team_hub_roles(hub, fas_group)
|
| |
+ else:
|
| |
+ affected_users = []
|
| |
+ fas_client.db.commit()
|
| |
+ except Exception:
|
| |
+ fas_client.db.rollback()
|
| |
+ raise
|
| |
+ log.info("Synced team hub %s with FAS (%d affected users)",
|
| |
+ hub.name, len(affected_users))
|
| |
+ return affected_users
|
| |
+
|
| |
+
|
| |
+ def sync_team_hub_roles(hub_id):
|
| |
+ hub = Hub.query.get(hub_id)
|
| |
+ if hub is None:
|
| |
+ return []
|
| |
+ log.debug("Syncing team hub %s's roles with FAS", hub.name)
|
| |
+ fas_client = FASClient()
|
| |
+ try:
|
| |
+ # Sync group config and roles
|
| |
+ affected_users = fas_client.sync_team_hub_roles(hub)
|
| |
+ fas_client.db.commit()
|
| |
+ except Exception:
|
| |
+ fas_client.db.rollback()
|
| |
+ raise
|
| |
+ log.info("Synced team hub %s's roles with FAS (%d affected users)",
|
| |
+ hub.name, len(affected_users))
|
| |
+ return affected_users
|
| |
+
|
| |
+
|
| |
+ def sync_user_roles(username, hub_id):
|
| |
+ user = User.query.get(username)
|
| |
+ if user is None:
|
| |
+ return []
|
| |
+ hub = Hub.query.get(hub_id)
|
| |
+ if hub is None:
|
| |
+ return []
|
| |
+ log.debug("Syncing user %s's roles on hub %s with FAS", username, hub.name)
|
| |
+ fas_client = FASClient()
|
| |
+ try:
|
| |
+ # Sync user roles
|
| |
+ affected_hubs = fas_client.sync_user_roles(user, hub)
|
| |
+ fas_client.db.commit()
|
| |
+ except Exception:
|
| |
+ fas_client.db.rollback()
|
| |
+ raise
|
| |
+ log.info("Synced user %s's roles with FAS (%d affected hubs)",
|
| |
+ username, len(affected_hubs))
|
| |
+ return affected_hubs
|
| |
+
|
| |
+
|
| |
+ # Send an email when there's a new membership request
|
| |
+
|
| |
+ def notify_membership_request(user, hub, role):
|
| |
+ if not role.startswith("pending-"):
|
| |
+ return # Don't notify for subscriptions and stargazing.
|
| |
+ content = """
|
| |
+ Fedora user {username} ({realname}) would like to join {groupname}, a Fedora group that you admin.
|
| |
+ The request was made via Fedora Hubs.
|
| |
+
|
| |
+ To accept {username}'s membership request, please visit the following URL and enter
|
| |
+ their Fedora Account System ID (FAS ID) into the "Add User" box and submit it:
|
| |
+
|
| |
+ https://admin.fedoraproject.org/accounts/group/view/{groupname}
|
| |
+
|
| |
+ FAS ID: {username}
|
| |
+
|
| |
+ Thanks for serving as a group administrator in the Fedora community!
|
| |
+
|
| |
+ Cheers,
|
| |
+ The Fedora Hubs Team
|
| |
+ """.format( # noqa:E501
|
| |
+ username=user.username, realname=user.fullname, groupname=hub.name,
|
| |
+ )
|
| |
+ # Create a text/plain message
|
| |
+ msg = MIMEText(content)
|
| |
+ msg['Subject'] = "New Recruit for your Fedora team ({})!".format(hub.name)
|
| |
+ email_from = "hubs@fedoraproject.org"
|
| |
+ email_to = "{}-administrators@fedoraproject.org".format(hub.name)
|
| |
+ msg['From'] = email_from
|
| |
+ msg['To'] = email_to
|
| |
+ # Send the message via the local SMTP server.
|
| |
+ app_config = flask.current_app.config
|
| |
+ s = smtplib.SMTP(app_config["EMAIL_HOST"], app_config["EMAIL_PORT"])
|
| |
+ s.sendmail(email_from, [email_to], msg.as_string())
|
| |
+ s.quit()
|
| |
See #389.
Adds hook that syncs team hub properties and roles from FAS when they are created or when there's a change.
Also add two scripts to create team hubs and sync them, which can be run on-demand.
Very big PR, sorry I couldn't cut it into more understandable chunks.