diff --git a/roles/pkgdb2/files/fas2.py b/roles/pkgdb2/files/fas2.py new file mode 100644 index 0000000000..ab474797d8 --- /dev/null +++ b/roles/pkgdb2/files/fas2.py @@ -0,0 +1,935 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2008-2012 Ricky Zhou, Red Hat, Inc. +# This file is part of python-fedora +# +# python-fedora is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# python-fedora is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with python-fedora; if not, see +# +''' +Provide a client module for talking to the Fedora Account System. + + +.. moduleauthor:: Ricky Zhou +.. moduleauthor:: Toshio Kuratomi +.. moduleauthor:: Ralph Bean +''' +import itertools +import urllib +import warnings + +from bunch import Bunch +from kitchen.text.converters import to_bytes + +try: + import libravatar +except ImportError: + libravatar = None + +try: + from hashlib import md5 +except ImportError: + from md5 import new as md5 + +from fedora.client import ( + AppError, BaseClient, FasProxyClient, + FedoraClientError, FedoraServiceError +) + +from fedora import __version__ + +### FIXME: To merge: +# /usr/bin/fasClient from fas +# API from Will Woods +# API from MyFedora + + +class FASError(FedoraClientError): + '''FAS Error''' + pass + + +class CLAError(FASError): + '''CLA Error''' + pass + +USERFIELDS = [ + 'affiliation', 'bugzilla_email', 'certificate_serial', + 'comments', 'country_code', 'creation', 'email', 'emailtoken', + 'facsimile', 'gpg_keyid', 'human_name', 'id', 'internal_comments', + 'ircnick', 'latitude', 'last_seen', 'longitude', 'password', + 'password_changed', 'passwordtoken', 'postal_address', 'privacy', + 'locale', 'ssh_key', 'status', 'status_change', 'telephone', + 'unverified_email', 'timezone', 'username', 'security_question', + 'security_answer', ] + + +class AccountSystem(BaseClient): + '''An object for querying the Fedora Account System. + + The Account System object provides a python API for talking to the Fedora + Account System. It abstracts the http requests, cookie handling, and + other details so you can concentrate on the methods that are important to + your program. + + .. warning:: + + If your code is trying to use the AccountSystem object to + connect to fas for multiple users you probably want to use + :class:`~fedora.client.FasProxyClient` instead. If your code is + trying to reuse a single instance of AccountSystem for multiple users + you *definitely* want to use :class:`~fedora.client.FasProxyClient` + instead. Using AccountSystem in these cases may result in a user + being logged in as a different user. (This may be the case even if + you instantiate a new AccountSystem object for each user if + :attr:cache_session: is True since that creates a file on the file + system that can end up loading session credentials for the wrong + person. + + .. versionchanged:: 0.3.26 + Added :meth:`~fedora.client.AccountSystem.gravatar_url` that returns + a url to a gravatar for a user. + .. versionchanged:: 0.3.33 + Renamed :meth:`~fedora.client.AccountSystem.gravatar_url` to + :meth:`~fedora.client.AccountSystem.avatar_url`. + ''' + # proxy is a thread-safe connection to the fas server for verifying + # passwords of other users + proxy = None + + # size that we allow to request from remote avatar providers. + _valid_avatar_sizes = (32, 64, 140) + # URLs for remote avatar providers. + _valid_avatar_services = ['libravatar', 'gravatar'] + + def __init__(self, base_url='https://admin.fedoraproject.org/accounts/', + *args, **kwargs): + '''Create the AccountSystem client object. + + :kwargs base_url: Base of every URL used to contact the server. + Defaults to the Fedora Project FAS instance. + :kwargs useragent: useragent string to use. If not given, default to + "Fedora Account System Client/VERSION" + :kwargs debug: If True, log debug information + :kwargs username: username for establishing authenticated connections + :kwargs password: password to use with authenticated connections + :kwargs session_cookie: **Deprecated** Use session_id instead. + User's session_cookie to connect to the server + :kwargs session_id: user's session_id to connect to the server + :kwargs cache_session: if set to true, cache the user's session cookie + on the filesystem between runs. + ''' + if 'useragent' not in kwargs: + kwargs['useragent'] = \ + 'Fedora Account System Client/%s' % __version__ + + super(AccountSystem, self).__init__(base_url, *args, **kwargs) + # We need a single proxy for the class to verify username/passwords + # against. + if not self.proxy: + self.proxy = FasProxyClient(base_url, useragent=self.useragent, + session_as_cookie=False, + debug=self.debug, + insecure=self.insecure) + + # Preseed a list of FAS accounts with bugzilla addresses + # This allows us to specify a different email for bugzilla than is + # in the FAS db. It is a hack, however, until FAS has a field for the + # bugzilla address. + self.__bugzilla_email = { + # Konstantin Ryabitsev: mricon@gmail.com + 100029: 'icon@fedoraproject.org', + # Sean Reifschneider: jafo@tummy.com + 100488: 'jafo-redhat@tummy.com', + # Karen Pease: karen-pease@uiowa.edu + 100281: 'meme@daughtersoftiresias.org', + # Robert Scheck: redhat@linuxnetz.de + 100093: 'redhat-bugzilla@linuxnetz.de', + # Scott Bakers: bakers@web-ster.com + 100881: 'scott@perturb.org', + # Colin Charles: byte@aeon.com.my + 100014: 'byte@fedoraproject.org', + # W. Michael Petullo: mike@flyn.org + 100136: 'redhat@flyn.org', + # Elliot Lee: sopwith+fedora@gmail.com + 100060: 'sopwith@redhat.com', + # Control Center Team: Bugzilla user but email doesn't exist + 9908: 'control-center-maint@redhat.com', + # Máirín Duffy + 100548: 'duffy@redhat.com', + # Muray McAllister: murray.mcallister@gmail.com + 102321: 'mmcallis@redhat.com', + # William Jon McCann: mccann@jhu.edu + 102489: 'jmccann@redhat.com', + # Matt Domsch's rebuild script -- bz email goes to /dev/null + 103590: 'ftbfs@fedoraproject.org', + # Sindre Pedersen Bjørdal: foolish@guezz.net + 100460: 'sindrepb@fedoraproject.org', + # Jesus M. Rodriguez: jmrodri@gmail.com + 102180: 'jesusr@redhat.com', + # Roozbeh Pournader: roozbeh@farsiweb.info + 100350: 'roozbeh@gmail.com', + # Michael DeHaan: michael.dehaan@gmail.com + 100603: 'mdehaan@redhat.com', + # Sebastian Gosenheimer: sgosenheimer@googlemail.com + 103647: 'sebastian.gosenheimer@proio.com', + # Ben Konrath: bkonrath@redhat.com + 101156: 'ben@bagu.org', + # Kai Engert: kaie@redhat.com + 100399: 'kengert@redhat.com', + # William Jon McCann: william.jon.mccann@gmail.com + 102952: 'jmccann@redhat.com', + # Simon Wesp: simon@w3sp.de + 109464: 'cassmodiah@fedoraproject.org', + # Robert M. Albrecht: romal@gmx.de + 101475: 'mail@romal.de', + # Mathieu Bridon: mathieu.bridon@gmail.com + 100753: 'bochecha@fedoraproject.org', + # Davide Cescato: davide.cescato@iaeste.ch + 123204: 'ceski@fedoraproject.org', + # Nick Bebout: nick@bebout.net + 101458: 'nb@fedoraproject.org', + # Niels Haase: haase.niels@gmail.com + 126862: 'arxs@fedoraproject.org', + # Thomas Janssen: th.p.janssen@googlemail.com + 103110: 'thomasj@fedoraproject.org', + # Michael J Gruber: 'michaeljgruber+fedoraproject@gmail.com' + 105113: 'mjg@fedoraproject.org', + # Juan Manuel Rodriguez Moreno: 'nushio@gmail.com' + 101302: 'nushio@fedoraproject.org', + # Andrew Cagney: 'andrew.cagney@gmail.com' + 102169: 'cagney@fedoraproject.org', + # Jeremy Katz: 'jeremy@katzbox.net' + 100036: 'katzj@fedoraproject.org', + # Dominic Hopf: 'dmaphy@gmail.com' + 124904: 'dmaphy@fedoraproject.org', + # Christoph Wickert: 'christoph.wickert@googlemail.com': + 100271: 'cwickert@fedoraproject.org', + # Elliott Baron: 'elliottbaron@gmail.com' + 106760: 'ebaron@fedoraproject.org', + # Thomas Spura: 'spurath@students.uni-mainz.de' + 111433: 'tomspur@fedoraproject.org', + # Adam Miller: 'maxamillion@gmail.com' + 110673: 'admiller@redhat.com', + # Garrett Holmstrom: 'garrett.holmstrom@gmail.com' + 131739: 'gholms@fedoraproject.org', + # Tareq Al Jurf: taljurf.fedora@gmail.com + 109863: 'taljurf@fedoraproject.org', + # Josh Kayse: jokajak@gmail.com + 148243: 'jokajak@fedoraproject.org', + # Behdad Esfahbod: fedora@behdad.org + 100102: 'behdad@fedoraproject.org', + # Daniel Bruno: danielbrunos@gmail.com + 101608: 'dbruno@fedoraproject.org', + # Beth Lynn Eicher: bethlynneicher@gmail.com + 148706: 'bethlynn@fedoraproject.org', + # Andre Robatino: andre.robatino@verizon.net + 114970: 'robatino@fedoraproject.org', + # Jeff Sheltren: jeff@tag1consulting.com + 100058: 'sheltren@fedoraproject.org', + # Josh Boyer: jwboyer@gmail.com + 100115: 'jwboyer@redhat.com', + # Matthew Miller: mattdm@mattdm.org + 100042: 'mattdm@redhat.com', + # Jamie Nguyen: j@jamielinux.com + 160587: 'jamielinux@fedoraproject.org', + # Nikos Roussos: nikos@roussos.cc + 144436: 'comzeradd@fedoraproject.org', + # Benedikt Schäfer: benedikt@schaefer-flieden.de + 154726: 'ib54003@fedoraproject.org', + # Ricky Elrod: codeblock@elrod.me + 139137: 'relrod@redhat.com', + # David Xie: david.scriptfan@gmail.com + 167133: 'davidx@fedoraproject.org', + # Felix Schwarz: felix.schwarz@oss.schwarz.eu + 103551: 'fschwarz@fedoraproject.org', + # Martin Holec: martix@martix.names + 137561: 'mholec@redhat.com', + } + # A few people have an email account that is used in owners.list but + # have setup a bugzilla account for their primary account system email + # address now. Map these here. + self.__alternate_email = { + # Damien Durand: splinux25@gmail.com + 'splinux@fedoraproject.org': 100406, + # Kevin Fenzi: kevin@tummy.com + 'kevin-redhat-bugzilla@tummy.com': 100037, + } + for bugzilla_map in self.__bugzilla_email.items(): + self.__alternate_email[bugzilla_map[1]] = bugzilla_map[0] + + # We use the two mappings as follows:: + # When looking up a user by email, use __alternate_email. + # When looking up a bugzilla email address use __bugzilla_email. + # + # This allows us to parse in owners.list and have a value for all the + # emails in there while not using the alternate email unless it is + # the only option. + + # TODO: Use exceptions properly + + ### Set insecure properly ### + # When setting insecure, we have to set it both on ourselves and on + # self.proxy + def _get_insecure(self): + return self._insecure + + def _set_insecure(self, insecure): + self._insecure = insecure + self.proxy = FasProxyClient(self.base_url, useragent=self.useragent, + session_as_cookie=False, debug=self.debug, + insecure=insecure) + return insecure + #: If this attribute is set to True, do not check server certificates + #: against their CA's. This means that man-in-the-middle attacks are + #: possible. You might turn this option on for testing against a local + #: version of a server with a self-signed certificate but it should be off + #: in production. + insecure = property(_get_insecure, _set_insecure) + + ### Groups ### + + def create_group(self, name, display_name, owner, group_type, + invite_only=0, needs_sponsor=0, user_can_remove=1, + prerequisite='', joinmsg='', apply_rules='None'): + '''Creates a FAS group. + + :arg name: The short group name (alphanumeric only). + :arg display_name: A longer version of the group's name. + :arg owner: The username of the FAS account which owns the new group. + :arg group_type: The kind of group being created. Current valid options + are git, svn, hg, shell, and tracking. + :kwarg invite_only: Users must be invited to the group, they cannot + join on their own. + :kwarg needs_sponsor: Users must be sponsored into the group. + :kwarg user_can_remove: Users can remove themselves from the group. + :kwarg prerequisite: Users must be in the given group (string) before + they can join the new group. + :kwarg joinmsg: A message shown to users when they apply to the group. + :kwarg apply_rules: Rules for applying to the group, shown to users + before they apply. + :rtype: :obj:`bunch.Bunch` + :returns: A Bunch containing information about the group that was + created. + + .. versionadded:: 0.3.29 + ''' + req_params = { + 'invite_only': invite_only, + 'needs_sponsor': needs_sponsor, + 'user_can_remove': user_can_remove, + 'prerequisite': prerequisite, + 'joinmsg': joinmsg, + 'apply_rules': apply_rules + } + + request = self.send_request( + '/group/create/%s/%s/%s/%s' % ( + urllib.quote(name), + urllib.quote(display_name), + urllib.quote(owner), + urllib.quote(group_type)), + req_params=req_params, + auth=True + ) + return request + + def group_by_id(self, group_id): + '''Returns a group object based on its id''' + params = {'group_id': int(group_id)} + request = self.send_request( + 'json/group_by_id', + auth=True, + req_params=params + ) + if request['success']: + return request['group'] + else: + return dict() + + def group_by_name(self, groupname): + '''Returns a group object based on its name''' + params = {'groupname': groupname} + request = self.send_request( + 'json/group_by_name', + auth=True, + req_params=params + ) + if request['success']: + return request['group'] + else: + raise AppError( + message='FAS server unable to retrieve group' + ' %(group)s' % {'group': to_bytes(groupname)}, + name='FASError') + + def group_members(self, groupname): + '''Return a list of people approved for a group. + + This method returns a list of people who are in the requested group. + The people are all approved in the group. Unapproved people are not + shown. The format of data is:: + + \[{'username': 'person1', 'role_type': 'user'}, + \{'username': 'person2', 'role_type': 'sponsor'}] + + role_type can be one of 'user', 'sponsor', or 'administrator'. + + .. versionadded:: 0.3.2 + .. versionchanged:: 0.3.21 + Return a Bunch instead of a DictContainer + ''' + request = self.send_request('/group/dump/%s' % + urllib.quote(groupname), auth=True) + + return [Bunch(username=user[0], + role_type=user[3]) for user in request['people']] + + ### People ### + + def person_by_id(self, person_id): + '''Returns a person object based on its id''' + person_id = int(person_id) + params = {'person_id': person_id} + request = self.send_request('json/person_by_id', auth=True, + req_params=params) + + if request['success']: + if person_id in self.__bugzilla_email: + request['person']['bugzilla_email'] = \ + self.__bugzilla_email[person_id] + else: + request['person']['bugzilla_email'] = \ + request['person']['email'] + + # In a devel version of FAS, membership info was returned + # separately + # This was later corrected (can remove this code at some point) + if 'approved' in request: + request['person']['approved_memberships'] = request['approved'] + if 'unapproved' in request: + request['person']['unapproved_memberships'] = \ + request['unapproved'] + return request['person'] + else: + return dict() + + def person_by_username(self, username): + '''Returns a person object based on its username''' + params = {'username': username} + request = self.send_request( + 'json/person_by_username', + auth=True, + req_params=params) + + if request['success']: + person = request['person'] + if person['id'] in self.__bugzilla_email: + person['bugzilla_email'] = self.__bugzilla_email[person['id']] + else: + person['bugzilla_email'] = person['email'] + # In a devel version of FAS, membership info was returned + # separately + # This was later corrected (can remove this code at some point) + if 'approved' in request: + request['person']['approved_memberships'] = request['approved'] + if 'unapproved' in request: + request['person']['unapproved_memberships'] = \ + request['unapproved'] + return person + else: + return dict() + + def avatar_url(self, username, size=64, + default=None, lookup_email=True, + service=None): + ''' Returns a URL to an avatar for a given username. + + Avatars are drawn from third party services. + + :arg username: FAS username to construct a avatar url for + :kwarg size: size of the avatar. Allowed sizes are 32, 64, 140. + Default: 64 + :kwarg default: If the service does not have a avatar image for the + email address, this url is returned instead. Default: + the fedora logo at the specified size. + :kwarg lookup_email: If true, use the email from FAS for gravatar.com + lookups, otherwise just append @fedoraproject.org to the username. + For libravatar.org lookups, this is ignored. The openid identifier + of the user is used instead. + Note that gravatar.com lookups will be much slower if lookup_email + is set to True since we'd have to make a query against FAS itself. + :kwarg service: One of 'libravatar' or 'gravatar'. + Default: 'libravatar'. + :raises ValueError: if the size parameter is not allowed or if the + service is not one of 'libravatar' or 'gravatar' + :rtype: :obj:`str` + :returns: url of a avatar for the user + + If that user has no avatar entry, instruct the remote service to + redirect us to the Fedora logo. + + If that user has no email attribute, then make a fake request to + the third party service. + + .. versionadded:: 0.3.26 + .. versionchanged: 0.3.30 + Add lookup_email parameter to control whether we generate avatar + urls with the email in fas or username@fedoraproject.org + .. versionchanged: 0.3.33 + Renamed from `gravatar_url` to `avatar_url` + .. versionchanged: 0.3.34 + Updated libravatar to use the user's openid identifier. + ''' + + if size not in self._valid_avatar_sizes: + raise ValueError( + 'Size %(size)i disallowed. Must be in %(valid_sizes)r' % { + 'size': size, + 'valid_sizes': self._valid_avatar_sizes + } + ) + + # If our caller explicitly requested libravatar but they don't have + # it installed, then we need to raise a nice error and let them know. + if service == 'libravatar' and not libravatar: + raise ValueError("Install python-pylibravatar if you want to " + "use libravatar as an avatar provider.") + + # If our caller didn't specify a service, let's pick a one for them. + # If they have pylibravatar installed, then by all means let freedom + # ring! Otherwise, we'll use gravatar.com if we have to. + if not service: + if libravatar: + service = 'libravatar' + else: + service = 'gravatar' + + # Just double check to make sure they didn't pass us a bogus service. + if service not in self._valid_avatar_services: + raise ValueError( + 'Service %(service)r disallowed. ' + 'Must be in %(valid_services)r' % { + 'service': service, + 'valid_services': self._valid_avatar_services + } + ) + + if not default: + default = "http://fedoraproject.org/static/images/" + \ + "fedora_infinity_%ix%i.png" % (size, size) + + if service == 'libravatar': + openid = 'http://%s.id.fedoraproject.org/' % username + return libravatar.libravatar_url( + openid=openid, + size=size, + default=default, + ) + else: + if lookup_email: + person = self.person_by_username(username) + email = person.get('email', 'no_email') + else: + email = "%s@fedoraproject.org" % username + + query_string = urllib.urlencode({ + 's': size, + 'd': default, + }) + + hash = md5(email).hexdigest() + + return "http://www.gravatar.com/avatar/%s?%s" % ( + hash, query_string) + + def gravatar_url(self, *args, **kwargs): + """ *Deprecated* - Use avatar_url. + + .. versionadded:: 0.3.26 + .. versionchanged: 0.3.30 + Add lookup_email parameter to control whether we generate gravatar + urls with the email in fas or username@fedoraproject.org + .. versionchanged: 0.3.33 + Deprecated in favor of `avatar_url`. + """ + + warnings.warn( + "gravatar_url is deprecated and will be removed in" + " a future version. Please port your code to use avatar_url(...," + " service='libravatar', ...) instead", + DeprecationWarning, stacklevel=2) + + if 'service' in kwargs: + raise TypeError("'service' is an invalid keyword argument for" + " this function. Use avatar_url() instead)") + + return self.avatar_url(*args, service='gravatar', **kwargs) + + def user_id(self): + '''Returns a dict relating user IDs to usernames''' + request = self.send_request('json/user_id', auth=True) + people = {} + for person_id, username in request['people'].items(): + # change userids from string back to integer + people[int(person_id)] = username + return people + + def people_by_key(self, key=u'username', search=u'*', fields=None): + '''Return a dict of people + + :kwarg key: Key by this field. Valid values are 'id', 'username', or + 'email'. Default is 'username' + :kwarg search: Pattern to match usernames against. Defaults to the + '*' wildcard which matches everyone. + :kwarg fields: Limit the data returned to a specific list of fields. + The default is to retrieve all fields. + Valid fields are: + + * affiliation + * alias_enabled + * bugzilla_email + * certificate_serial + * comments + * country_code + * creation + * email + * emailtoken + * facsimile + * gpg_keyid + * group_roles + * human_name + * id + * internal_comments + * ircnick + * last_seen + * latitude + * locale + * longitude + * memberships + * old_password + * password + * password_changed + * passwordtoken + * postal_address + * privacy + * roles + * ssh_key + * status + * status_change + * telephone + * timezone + * unverified_email + * username + + Note that for most users who access this data, many of these + fields will be set to None due to security or privacy settings. + :returns: a dict relating the key value to the fields. + + .. versionchanged:: 0.3.21 + Return a Bunch instead of a DictContainer + .. versionchanged:: 0.3.26 + Fixed to return a list with both people who have signed the CLA + and have not + ''' + # Make sure we have a valid key value + if key not in ('id', 'username', 'email'): + raise KeyError('key must be one of "id", "username", or' + ' "email"') + + if fields: + fields = list(fields) + for field in fields: + if field not in USERFIELDS: + raise KeyError('%(field)s is not a valid field to' + ' filter' % {'field': to_bytes(field)}) + else: + fields = USERFIELDS + + # Make sure we retrieve the key value + unrequested_fields = [] + if key not in fields: + unrequested_fields.append(key) + fields.append(key) + if 'bugzilla_email' in fields: + # Need id and email for the bugzilla information + if 'id' not in fields: + unrequested_fields.append('id') + fields.append('id') + if 'email' not in fields: + unrequested_fields.append('email') + fields.append('email') + + request = self.send_request( + '/user/list', + req_params={ + 'search': search, + 'fields': [f for f in fields if f != 'bugzilla_email'] + }, + auth=True) + + people = Bunch() + for person in itertools.chain(request['people'], + request['unapproved_people']): + # Retrieve bugzilla_email from our list if necessary + if 'bugzilla_email' in fields: + if person['id'] in self.__bugzilla_email: + person['bugzilla_email'] = \ + self.__bugzilla_email[person['id']] + else: + person['bugzilla_email'] = person['email'] + + person_key = person[key] + # Remove any fields that weren't requested by the user + if unrequested_fields: + for field in unrequested_fields: + del person[field] + + # Add the person record to the people dict + people[person_key] = person + + return people + + def people_by_id(self): + '''*Deprecated* Use people_by_key() instead. + + Returns a dict relating user IDs to human_name, email, username, + and bugzilla email + + .. versionchanged:: 0.3.21 + Return a Bunch instead of a DictContainer + ''' + warnings.warn( + "people_by_id() is deprecated and will be removed in" + " 0.4. Please port your code to use people_by_key(key='id'," + " fields=['human_name', 'email', 'username', 'bugzilla_email'])" + " instead", DeprecationWarning, stacklevel=2) + + request = self.send_request('/json/user_id', auth=True) + user_to_id = {} + people = Bunch() + for person_id, username in request['people'].items(): + person_id = int(person_id) + # change userids from string back to integer + people[person_id] = {'username': username, 'id': person_id} + user_to_id[username] = person_id + + # Retrieve further useful information about the users + request = self.send_request('/group/dump', auth=True) + for user in request['people']: + userid = user_to_id[user[0]] + person = people[userid] + person['email'] = user[1] + person['human_name'] = user[2] + if userid in self.__bugzilla_email: + person['bugzilla_email'] = self.__bugzilla_email[userid] + else: + person['bugzilla_email'] = person['email'] + + return people + + ### Utils ### + + def people_by_groupname(self, groupname): + '''Return a list of persons for the given groupname. + + :arg groupname: Name of the group to look up + :returns: A list of person objects from the group. If the group + contains no entries, then an empty list is returned. + ''' + people = self.people_by_id() + group = dict(self.group_by_name(groupname)) + userids = [user[u'person_id'] for user in + group[u'approved_roles'] + group[u'unapproved_roles']] + return [people[userid] for userid in userids] + + ### Configs ### + + def get_config(self, username, application, attribute): + '''Return the config entry for the key values. + + :arg username: Username of the person + :arg application: Application for which the config is set + :arg attribute: Attribute key to lookup + :raises AppError: if the server returns an exception + :returns: The unicode string that describes the value. If no entry + matched the username, application, and attribute then None is + returned. + ''' + request = self.send_request('config/list/%s/%s/%s' % + (username, application, attribute), + auth=True) + if 'exc' in request: + raise AppError( + name=request['exc'], + message=request['tg_flash'] + ) + + # Return the value if it exists, else None. + if 'configs' in request and attribute in request['configs']: + return request['configs'][attribute] + return None + + def get_configs_like(self, username, application, pattern=u'*'): + '''Return the config entries that match the keys and the pattern. + + Note: authentication on the server will prevent anyone but the user + or a fas admin from viewing or changing their configs. + + :arg username: Username of the person + :arg application: Application for which the config is set + :kwarg pattern: A pattern to select values for. This accepts * as a + wildcard character. Default='*' + :raises AppError: if the server returns an exception + :returns: A dict mapping ``attribute`` to ``value``. + ''' + request = self.send_request( + 'config/list/%s/%s/%s' % + (username, application, pattern), + auth=True) + if 'exc' in request: + raise AppError( + name=request['exc'], + message=request['tg_flash']) + + return request['configs'] + + def set_config(self, username, application, attribute, value): + '''Set a config entry in FAS for the user. + + Note: authentication on the server will prevent anyone but the user + or a fas admin from viewing or changing their configs. + + :arg username: Username of the person + :arg application: Application for which the config is set + :arg attribute: The name of the config key that we're setting + :arg value: The value to set this to + :raises AppError: if the server returns an exception + ''' + request = self.send_request( + 'config/set/%s/%s/%s' % + (username, application, attribute), + req_params={'value': value}, auth=True) + + if 'exc' in request: + raise AppError( + name=request['exc'], + message=request['tg_flash']) + + def people_query(self, constraints=None, columns=None): + '''Returns a list of dicts representing database rows + + :arg constraints: A dictionary specifying WHERE constraints on columns + :arg columns: A list of columns to be selected in the query + :raises AppError: if the query failed on the server (most likely + because the server was given a bad query) + :returns: A list of dicts representing database rows (the keys of + the dict are the columns requested) + + .. versionadded:: 0.3.12.1 + ''' + if constraints is None: + constraints = {} + if columns is None: + columns = [] + + req_params = {} + req_params.update(constraints) + req_params['columns'] = ','.join(columns) + + try: + request = self.send_request( + 'json/people_query', + req_params=req_params, auth=True) + if request['success']: + return request['data'] + else: + raise AppError(message=request['error'], name='FASError') + except FedoraServiceError: + raise + + ### Certs ### + + def user_gencert(self): + '''Generate a cert for a user''' + try: + request = self.send_request('user/dogencert', auth=True) + except FedoraServiceError: + raise + if not request['cla']: + raise CLAError + return "%(cert)s\n%(key)s" % request + + ### Passwords ### + + def verify_password(self, username, password): + '''Return whether the username and password pair are valid. + + :arg username: username to try authenticating + :arg password: password for the user + :returns: True if the username/password are valid. False otherwise. + ''' + return self.proxy.verify_password(username, password) + + ### fasClient Special Methods ### + + def group_data(self, force_refresh=None): + '''Return administrators/sponsors/users and group type for all groups + + :arg force_refresh: If true, the returned data will be queried from the + database, as opposed to memcached. + :raises AppError: if the query failed on the server + :returns: A dict mapping group names to the group type and the + user IDs of the administrator, sponsors, and users of the group. + + .. versionadded:: 0.3.8 + ''' + params = {} + if force_refresh: + params['force_refresh'] = True + + try: + request = self.send_request( + 'json/fas_client/group_data', + req_params=params, auth=True) + if request['success']: + return request['data'] + else: + raise AppError( + message='FAS server unable to retrieve' + ' group members', name='FASError') + except FedoraServiceError: + raise + + def user_data(self): + '''Return user data for all users in FAS + + Note: If the user is not authorized to see password hashes, + '*' is returned for the hash. + + :raises AppError: if the query failed on the server + :returns: A dict mapping user IDs to a username, password hash, + SSH public key, email address, and status. + + .. versionadded:: 0.3.8 + ''' + try: + request = self.send_request('json/fas_client/user_data', auth=True) + if request['success']: + return request['data'] + else: + raise AppError( + message='FAS server unable to retrieve user' + ' information', name='FASError') + except FedoraServiceError: + raise diff --git a/roles/pkgdb2/tasks/main.yml b/roles/pkgdb2/tasks/main.yml index 0185c27846..7e2c3e0e94 100644 --- a/roles/pkgdb2/tasks/main.yml +++ b/roles/pkgdb2/tasks/main.yml @@ -19,6 +19,12 @@ tags: - packages +# HOTFIX: adjust bugzilla overrides +- name: HOTFIX: adjust bugzilla overrides + copy: src=fas2.py dest=/usr/lib/python2.7/site-packages/fedora/client/fas2.py + tags: + - config + - name: copy sundry pkgdb configuration template: src={{ item.file }} dest={{ item.location }}/{{ item.dest }}