diff --git a/fas/.gitignore b/fas/.gitignore
index dec3ed5..3021ee9 100644
--- a/fas/.gitignore
+++ b/fas/.gitignore
@@ -8,4 +8,5 @@ fas.log
*.pyc
*.pyo
*.swp
+*.mo
fas.egg-info
diff --git a/fas/README b/fas/README
index 4ef21fc..66fe76e 100644
--- a/fas/README
+++ b/fas/README
@@ -27,7 +27,7 @@ Before you can get started, make sure to have the following packages installed
yum install git-core postgresql-plpython postgresql-server postgresql-python \
python-TurboMail TurboGears pygpgme python-sqlalchemy python-genshi \
- python-psycopg2
+ python-psycopg2 pytz
# Note: on RHEL5 you need postgresql-pl instead of postgresql-plpython
@@ -123,4 +123,5 @@ To test, look and see if your groups or users show up with getent. For
example:
getent passwd
- getent group
\ No newline at end of file
+ getent group
+
diff --git a/fas/client/fasClient.py b/fas/client/fasClient.py
index db65ca3..bc1f33a 100755
--- a/fas/client/fasClient.py
+++ b/fas/client/fasClient.py
@@ -30,7 +30,7 @@ from optparse import OptionParser
from shutil import move, rmtree
from rhpl.translate import _
-FAS_URL = 'http://localhost:8080/fas/'
+FAS_URL = 'http://localhost:8088/fas/'
parser = OptionParser()
@@ -78,20 +78,15 @@ class MakeShellAccounts(BaseClient):
temp = None
def mk_tempdir(self):
- self.temp = tempfile.mkdtemp('-tmp', 'fas-')
- os.chmod(self.temp, 00400)
+ self.temp = tempfile.mkdtemp('-tmp', 'fas-', '/var/db')
def rm_tempdir(self):
rmtree(self.temp)
- def group_list(self, search='*'):
- params = {'search' : search}
- data = self.send_request('group/list', auth=True, input=params)
- return data
-
def shadow_text(self, people=None):
i = 0
file = open(self.temp + '/shadow.txt', 'w')
+ os.chmod(self.temp + '/shadow.txt', 00400)
if not people:
people = self.people_list()
for person in people:
@@ -103,8 +98,6 @@ class MakeShellAccounts(BaseClient):
file.write(".%s %s:%s:99999:0:99999:7:::\n" % (username, username, password))
i = i + 1
file.close()
- os.chmod(self.temp + '/shadow.txt', 00400)
-
def passwd_text(self, people=None):
i = 0
@@ -136,10 +129,10 @@ class MakeShellAccounts(BaseClient):
for person in people:
uid = person['id']
username = person['username']
- usernames['%s' % uid] = username
+ usernames[uid] = username
file.write("=%i %s:x:%i:\n" % (uid, username, uid))
- file.write( "0%i %s:x:%i:\n" % (i, username, uid))
- file.write( ".%s %s:x:%i:\n" % (username, username, uid))
+ file.write("0%i %s:x:%i:\n" % (i, username, uid))
+ file.write(".%s %s:x:%i:\n" % (username, username, uid))
i = i + 1
for group in groups['groups']:
@@ -149,18 +142,23 @@ class MakeShellAccounts(BaseClient):
try:
''' Shoot me now I know this isn't right '''
members = []
- for member in groups['memberships'][u'%s' % gid]:
- members.append(usernames['%s' % member['person_id']])
+ for member in groups['memberships'][name]:
+ members.append(usernames[member['person_id']])
memberships = ','.join(members)
except KeyError:
''' No users exist in the group '''
pass
- file.write( "=%i %s:x:%i:%s\n" % (gid, name, gid, memberships))
+ file.write("=%i %s:x:%i:%s\n" % (gid, name, gid, memberships))
file.write("0%i %s:x:%i:%s\n" % (i, name, gid, memberships))
file.write(".%s %s:x:%i:%s\n" % (name, name, gid, memberships))
i = i + 1
file.close()
+
+ def group_list(self, search='*'):
+ params = {'search' : search}
+ data = self.send_request('group/list', auth=True, input=params)
+ return data
def people_list(self, search='*'):
params = {'search' : search}
@@ -205,14 +203,11 @@ def enable():
if line.startswith('passwd') or line.startswith('shadow') or line.startswith('group'):
parts = line.split()
if 'db' in parts:
- new.write(line)
print "%s already has db enabled" % parts[0].split(':')[0]
else:
- tmp = line.strip('\n')
- tmp = tmp + ' db\n'
- new.write(tmp)
- else:
- new.write(line)
+ line = line.strip('\n')
+ line += ' db\n'
+ new.write(line)
new.close()
try:
move('/tmp/.fas.nsswitch.conf', '/etc/nsswitch.conf')
@@ -226,13 +221,10 @@ def disable():
if line.startswith('passwd') or line.startswith('shadow') or line.startswith('group'):
parts = line.split()
if 'db' in parts:
- tmp = line.replace(' db', '')
- new.write(tmp)
+ line = line.replace(' db', '')
else:
print "%s already has db disabled" % parts[0].split(':')[0]
- new.write(line)
- else:
- new.write(line)
+ new.write(line)
new.close()
try:
move('/tmp/.fas.nsswitch.conf', '/etc/nsswitch.conf')
@@ -240,13 +232,7 @@ def disable():
print "ERROR: Could not write nsswitch.conf - %s" % e
if __name__ == '__main__':
- if opts.enable:
- enable()
- sys.exit()
- elif opts.disable:
- disable()
- sys.exit()
- elif opts.install:
+ if opts.install:
try:
fas = MakeShellAccounts(FAS_URL, 'admin', 'admin', False)
except AuthError, e:
@@ -263,5 +249,9 @@ if __name__ == '__main__':
if not opts.no_shadow:
fas.install_shadow_db()
fas.rm_tempdir()
- else:
+ if opts.enable:
+ enable()
+ if opts.disable:
+ disable()
+ if not (opts.install or opts.enable or opts.disable):
parser.print_help()
diff --git a/fas/dev.cfg b/fas/dev.cfg
index 8346e6f..e266447 100644
--- a/fas/dev.cfg
+++ b/fas/dev.cfg
@@ -7,6 +7,7 @@
#mail.server = 'bastion.fedora.phx.redhat.com'
#base_url_filter.base_url = "http://192.168.2.101:8080"
+fas.url = 'http://localhost:8088/fas/'
mail.on = True
mail.server = 'bastion.fedora.phx.redhat.com'
mail.testmode = True
diff --git a/fas/fas/config/app.cfg b/fas/fas/config/app.cfg
index 2afa041..7bab914 100644
--- a/fas/fas/config/app.cfg
+++ b/fas/fas/config/app.cfg
@@ -59,6 +59,8 @@ genshi.encoding="utf-8"
# i18n
session_filter.on = True
i18n.run_template_filter = True
+i18n.domain = 'fas'
+i18n.locale_dir = 'po'
# VISIT TRACKING
# Each visit to your application will be assigned a unique visit ID tracked via
diff --git a/fas/fas/controllers.py b/fas/fas/controllers.py
index c102188..35e67a6 100644
--- a/fas/fas/controllers.py
+++ b/fas/fas/controllers.py
@@ -5,6 +5,7 @@ from cherrypy import request, response
from turbogears import exception_handler
import turbogears
+import cherrypy
import time
from fas.user import User
@@ -24,6 +25,17 @@ def add_custom_stdvars(vars):
turbogears.view.variable_providers.append(add_custom_stdvars)
+def get_locale(locale=None):
+ if locale:
+ return locale
+ if turbogears.identity.current.user_name:
+ person = People.by_username(turbogears.identity.current.user_name)
+ return person.locale
+ else:
+ return turbogears.i18n.utils._get_locale()
+
+config.update({'i18n.get_locale': get_locale})
+
# from fas import json
# import logging
# log = logging.getLogger("fas.controllers")
@@ -38,7 +50,7 @@ class Root(controllers.RootController):
cla = CLA()
json = JsonRequest()
help = Help()
-# openid = OpenID()
+ #openid = OpenID()
# TODO: Find a better place for this.
os.environ['GNUPGHOME'] = config.get('gpghome')
@@ -110,3 +122,11 @@ class Root(controllers.RootController):
# is better.
return dict(status=True)
raise redirect(request.headers.get("Referer", "/"))
+
+ @expose()
+ def language(self, locale):
+ locale_key = config.get("i18n.session_key", "locale")
+ cherrypy.session[locale_key] = locale
+ raise redirect(request.headers.get("Referer", "/"))
+
+
diff --git a/fas/fas/fasLDAP.py b/fas/fas/fasLDAP.py
deleted file mode 100644
index 0398046..0000000
--- a/fas/fas/fasLDAP.py
+++ /dev/null
@@ -1,553 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2007 Red Hat, Inc. All rights reserved.
-#
-# This copyrighted material is made available to anyone wishing to use, modify,
-# copy, or redistribute it subject to the terms and conditions of the GNU
-# General Public License v.2. This program is distributed in the hope that it
-# will be useful, but WITHOUT ANY WARRANTY expressed or implied, including the
-# implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-# See the GNU General Public License for more details. You should have
-# received a copy of the GNU General Public License along with this program;
-# if not, write to the Free Software Foundation, Inc., 51 Franklin Street,
-# Fifth Floor, Boston, MA 02110-1301, USA. Any Red Hat trademarks that are
-# incorporated in the source code or documentation are not subject to the GNU
-# General Public License and may only be used or replicated with the express
-# permission of Red Hat, Inc.
-#
-# Author(s): Mike McGrath
-# Toshio Kuratomi
-# Ricky Zhou
-#
-
-'''
-python-fedora, python module to interact with Fedora Infrastructure Services
-'''
-
-import ldap
-from ldap import modlist
-import time
-from random import Random
-import sha
-from base64 import b64encode
-import sys
-import os
-
-dbName = 'fastest'
-
-class AuthError(Exception):
- pass
-
-def retrieve_db_info(dbKey):
- '''Retrieve information to connect to the db from the filesystem.
-
- Arguments:
- :dbKey: The string identifying the database entry in the config file.
-
- Returns: A dictionary containing the values to use in connecting to the
- database.
-
- Exceptions:
- :IOError: Returned if the config file is not on the system.
- :AuthError: Returned if there is no record found for dbKey in the
- config file.
- '''
- # Open a filehandle to the config file
- if os.environ.has_key('HOME') and os.path.isfile(
- os.path.join(os.environ.get('HOME'), '.fedora-db-access')):
- fh = file(os.path.join(
- os.environ.get('HOME'), '.fedora-db-access'), 'r')
- elif os.path.isfile('/etc/sysconfig/fedora-db-access'):
- fh = file('/etc/sysconfig/fedora-db-access', 'r')
- else:
- raise IOError, 'fedora-db-access file does not exist.'
-
- # Read the file until we get the information for the requested db
- dbInfo = None
- for line in fh.readlines():
- if not line:
- break
- line = line.strip()
- if not line or line[0] == '#':
- continue
- pieces = line.split(None, 1)
- if len(pieces) < 2:
- continue
- if pieces[0] == dbKey:
- dbInfo = eval(pieces[1])
- break
-
- if fh:
- fh.close()
- if not dbInfo:
- raise AuthError, 'Authentication source "%s" not configured' % (dbKey,)
- return dbInfo
-
-class Server(object):
- def __init__(self, server=None, who=None, password=None):
- pass
- #try:
- #dbInfo = retrieve_db_info(dbName)
- #except IOError:
- #raise AuthError, 'Authentication config file fedora-db-access is' \
- #' not available'
- #server = server or dbInfo['host'] or 'localhost'
- #who = 'cn=%s' % (who or dbInfo['user'])
- ## Some db connections have no password
- #password = password or dbInfo.get('password')
-
- #self.ldapConn = ldap.open(server)
- #self.ldapConn.simple_bind_s(who, password)
-
- def add(self, base, attributes):
- ''' Add a new group record to LDAP instance '''
- self.ldapConn.add_s(base, attributes.items())
-
- def delete(self, base):
- ''' Delete target base '''
- self.ldapConn.delete_s(base)
-
- def modify(self, base, attribute, new, old=None):
- ''' Modify an attribute, requires write access '''
- if new is None:
- return None
- new = unicode(new).encode('utf-8')
- if new == old:
- return None
-
- #o = { attribute : old }
- #n = { attribute : new }
-
- ldif = []
- ldif.append((ldap.MOD_DELETE,attribute,None))
- ldif.append((ldap.MOD_ADD,attribute,new))
-
- #ldif = ldap.modlist.modifyModlist(o, n, ignore_oldexistent=1)
- # commit
- self.ldapConn.modify_s(base, ldif)
-
- def search(self, base, ldapFilter, attributes=None):
- ''' Basic search function '''
- scope = ldap.SCOPE_SUBTREE
- count = 0
- timeout = 2
- result_set = []
- try:
- result_id = self.ldapConn.search(base, scope, ldapFilter, attributes)
- while True:
- result_type, result_data = self.ldapConn.result(result_id, timeout)
- if (result_data == []):
- break
- else:
- if result_type == ldap.RES_SEARCH_ENTRY:
- result_set.append(result_data)
- if len(result_set) == 0:
- return
- except ldap.LDAPError, e:
- raise
-
- return result_set
-
-###############################################################################
-# Group - Contains information about a specific group, 'sysadmin' would be
-# an example of a Group
-###############################################################################
-
-class Group(object):
- ''' Group abstraction class '''
-
- __server = Server()
- __base = 'ou=FedoraGroups,dc=fedoraproject,dc=org'
-
- def __init__(self, cn, fedoraGroupDesc, fedoraGroupOwner, fedoraGroupType, fedoraGroupNeedsSponsor, fedoraGroupUserCanRemove, fedoraGroupRequires, fedoraGroupJoinMsg):
- self.cn = cn
- self.fedoraGroupDesc = fedoraGroupDesc
- self.fedoraGroupOwner = fedoraGroupOwner
- self.fedoraGroupType = fedoraGroupType
- self.fedoraGroupNeedsSponsor = fedoraGroupNeedsSponsor
- self.fedoraGroupUserCanRemove = fedoraGroupUserCanRemove
- self.fedoraGroupRequires = fedoraGroupRequires
- self.fedoraGroupJoinMsg = fedoraGroupJoinMsg
-
- def __json__(self):
- return {'cn': self.cn,
- 'fedoraGroupDesc': self.fedoraGroupDesc,
- 'fedoraGroupOwner': self.fedoraGroupOwner,
- 'fedoraGroupType': self.fedoraGroupType,
- 'fedoraGroupNeedsSponsor': self.fedoraGroupNeedsSponsor,
- 'fedoraGroupUserCanRemove': self.fedoraGroupUserCanRemove,
- 'fedoraGroupRequires': self.fedoraGroupRequires,
- 'fedoraGroupJoinMsg': self.fedoraGroupJoinMsg
- }
-
- @classmethod
- def newGroup(self, cn, fedoraGroupDesc, fedoraGroupOwner, fedoraGroupType, fedoraGroupNeedsSponsor, fedoraGroupUserCanRemove, fedoraGroupRequires, fedoraGroupJoinMsg):
- ''' Create a new group '''
- attributes = { 'cn' : cn.encode('utf-8'),
- 'objectClass' : ('fedoraGroup'),
- 'fedoraGroupDesc' : fedoraGroupDesc.encode('utf-8'),
- 'fedoraGroupOwner' : fedoraGroupOwner.encode('utf-8'),
- 'fedoraGroupType' : fedoraGroupType.encode('utf-8'),
- 'fedoraGroupNeedsSponsor' : fedoraGroupNeedsSponsor.encode('utf-8'),
- 'fedoraGroupUserCanRemove' : fedoraGroupUserCanRemove.encode('utf-8'),
- 'fedoraGroupRequires' : fedoraGroupRequires.encode('utf-8'),
- 'fedoraGroupJoinMsg' : fedoraGroupJoinMsg.encode('utf-8'),
- }
-
- self.__server.add('cn=%s,%s' % (cn, self.__base), attributes)
-# attributes = {
-# 'objectClass' : ('organizationalUnit', 'top'),
-# 'ou' : 'FedoraGroups'
-# }
-# self.__server.add('ou=FedoraGroups,cn=%s,%s' % (cn, self.__base), attributes)
- return 0
-
-
-###############################################################################
-# UserGroup - Determines information about a user in a group, when they joined
-# who their sponsor is and their approval status are examples of
-# things found in this group
-###############################################################################
-class UserGroup(object):
- ''' Individual User->Group abstraction class '''
- def __init__(self, fedoraRoleApprovalDate=None, fedoraRoleSponsor=None, cn=None, fedoraRoleCreationDate=None, objectClass=None, fedoraRoleType=None, fedoraRoleStatus='Not a Member', fedoraRoleDomain=None):
- self.fedoraRoleApprovalDate = fedoraRoleApprovalDate
- self.fedoraRoleSponsor = fedoraRoleSponsor
- self.cn = cn
- self.fedoraRoleCreationDate = fedoraRoleCreationDate
- self.objectClass = objectClass
- self.fedoraRoleType = fedoraRoleType
- self.fedoraRoleStatus = fedoraRoleStatus
- self.fedoraRoleDomain = fedoraRoleDomain
-
-
-###############################################################################
-# Groups - Returns actual information in a group. This class actual queries
-# the LDAP database.
-###############################################################################
-class Groups(object):
- ''' Class contains group information '''
- __server = Server()
-
- def __init__(self):
- ### FIXME: I don't think the username should be used this way.
- self.__userName = None
-
- @classmethod
- def byUserName(self, cn, includeUnapproved=None, unapprovedOnly=None):
- ''' Return list of groups a certain user is in. Default excludes all non-approved groups'''
- groups = {}
- if includeUnapproved:
- ldapFilter = 'objectClass=FedoraRole'
- elif unapprovedOnly:
- ldapFilter = '(&(!(fedoraRoleStatus=approved)) (objectClass=fedoraRole))'
- else:
- ldapFilter = '(&(fedoraRoleStatus=approved)(objectClass=FedoraRole))'
-
- base = 'ou=Roles,cn=%s,ou=People,dc=fedoraproject,dc=org' % cn
- try:
- groupsDict = self.__server.search(base, ldapFilter)
- except ldap.NO_SUCH_OBJECT:
- return dict()
- if not groupsDict:
- groupsDict = []
- for group in groupsDict:
- cn = group[0][1]['cn'][0]
- groups[cn] = UserGroup(
- fedoraRoleApprovalDate = group[0][1]['fedoraRoleApprovalDate'][0].decode('utf8'),
- fedoraRoleSponsor = group[0][1]['fedoraRoleSponsor'][0].decode('utf8'),
- cn = group[0][1]['cn'][0].decode('utf8'),
- fedoraRoleCreationDate = group[0][1]['fedoraRoleCreationDate'][0].decode('utf8'),
- objectClass = group[0][1]['objectClass'][0].decode('utf8'),
- fedoraRoleType = group[0][1]['fedoraRoleType'][0].decode('utf8'),
- fedoraRoleStatus = group[0][1]['fedoraRoleStatus'][0].decode('utf8'),
- fedoraRoleDomain = group[0][1]['fedoraRoleDomain'][0].decode('utf8'),
- )
- ### FIXME: userName shouldn't be shared this way
- self.__userName = cn
- return groups
-
- @classmethod
- def groups(self, searchExpression='*', attributes=[]):
- ''' Return a list of available groups '''
- groups = {}
- ldapFilter = 'cn=%s' % (searchExpression)
- base = 'ou=FedoraGroups,dc=fedoraproject,dc=org'
- groupsDict = self.__server.search(base, ldapFilter, attributes)
- if groupsDict:
- for group in groupsDict:
- name = group[0][1]['cn'][0].decode('utf8')
- groups[name] = Group(
- cn = group[0][1]['cn'][0].decode('utf8'),
- fedoraGroupDesc = group[0][1]['fedoraGroupDesc'][0].decode('utf8'),
- fedoraGroupOwner = group[0][1]['fedoraGroupOwner'][0].decode('utf8'),
- fedoraGroupType = group[0][1]['fedoraGroupType'][0].decode('utf8'),
- fedoraGroupNeedsSponsor = group[0][1]['fedoraGroupNeedsSponsor'][0].decode('utf8'),
- fedoraGroupUserCanRemove = group[0][1]['fedoraGroupUserCanRemove'][0].decode('utf8'),
- fedoraGroupRequires = group[0][1]['fedoraGroupRequires'][0].decode('utf8'),
- fedoraGroupJoinMsg = group[0][1]['fedoraGroupJoinMsg'][0].decode('utf8'))
- else:
- return None
- return groups
-
- @classmethod
- def remove(self, groupName, userName=None):
- ''' Remove user from a group '''
- ### FIXME: Should require the userName instead of sharing it this way
- if not userName:
- userName = self.__userName
- try:
- g = self.byUserName(userName, includeUnapproved=True)[groupName]
- except:
- raise TypeError, 'User not in group %s' % groupName
- try:
- self.__server.delete('cn=%s+fedoraRoleType=%s,ou=Roles,cn=%s,ou=People,dc=fedoraproject,dc=org' % (g.cn, g.fedoraRoleType, userName))
- except ldap.NO_SUCH_OBJECT:
- self.__server.delete('cn=%s,ou=Roles,cn=%s,ou=People,dc=fedoraproject,dc=org' % (g.cn, userName))
- except:
- raise TypeError, 'Could Not delete %s from %s' % (userName, g.cn)
-
- @classmethod
- def apply(self, groupName, userName=None):
- ''' Apply for a group '''
-
- if not userName:
- userName = self.__userName
-
- if groupName in self.byUserName(userName):
- # Probably shouldn't be 'TypeError'
- raise TypeError, 'Already in that group'
- try:
- self.byGroupName(groupName)
- except TypeError:
- raise TypeError, 'Group "%s" does not exist' % groupName
-
- now = time.time()
-
- attributes = { 'cn' : groupName.encode('utf-8'),
- 'fedoraRoleApprovaldate' : 'None',
- 'fedoraRoleCreationDate' : str(now),
- 'fedoraRoleDomain' : 'None',
- 'fedoraRoleSponsor' : 'None',
- 'fedoraRoleStatus' : 'unapproved',
- 'fedoraRoleType' : 'user',
- 'objectClass' : ('fedoraRole')}
- self.__server.add('cn=%s,ou=Roles,cn=%s,ou=People,dc=fedoraproject,dc=org' % (groupName, userName), attributes)
-
-
- @classmethod
- def byGroupName(cls, cn, includeUnapproved=None, unapprovedOnly=None):
- ''' List users in a group. Default does not show unapproved '''
- self = cls()
- users = {}
- if includeUnapproved:
- ldapFilter = 'cn=%s' % cn
- elif unapprovedOnly:
- ldapFilter = '(&(cn=%s) (objectClass=fedoraRole) (!(fedoraRoleStatus=approved)))' % cn
- else:
- ldapFilter = '(&(cn=%s) (objectClass=fedoraRole) (fedoraRoleStatus=approved))' % cn
- base = 'ou=People,dc=fedoraproject,dc=org'
- attributes = ['cn']
- usersDict = self.__server.search(base, ldapFilter)
- try:
- for user in usersDict:
- userName = user[0][0].split(',')[2].split('=')[1]
-
- users[userName] = UserGroup(
- fedoraRoleApprovalDate = user[0][1]['fedoraRoleApprovalDate'][0].decode('utf8'),
- fedoraRoleSponsor = user[0][1]['fedoraRoleSponsor'][0].decode('utf8'),
- cn = user[0][1]['cn'][0].decode('utf8'),
- fedoraRoleCreationDate = user[0][1]['fedoraRoleCreationDate'][0].decode('utf8'),
- objectClass = user[0][1]['objectClass'][0].decode('utf8'),
- fedoraRoleType = user[0][1]['fedoraRoleType'][0].decode('utf8'),
- fedoraRoleStatus = user[0][1]['fedoraRoleStatus'][0].decode('utf8'),
- fedoraRoleDomain = user[0][1]['fedoraRoleDomain'][0].decode('utf8'),
- )
- except TypeError:
- users = {}
- return users
-
-class Person(object):
- '''Information and attributes about users '''
- __base = 'ou=People,dc=fedoraproject,dc=org'
- __server = Server()
- def __init__(self):
- ### FIXME: Not sure what this is used for. It might be able to go
- # away. It might need to be made a public attribute.
- self.__filter = ''
-
- @classmethod
- def newPerson(self, cn, givenName, mail, telephoneNumber, postalAddress):
- ''' Create a new user '''
- now = time.time()
- attributes = { 'cn' : cn.encode('utf-8'),
- 'objectClass' : ('fedoraPerson', 'inetOrgPerson', 'organizationalPerson', 'person', 'top'),
- 'displayName' : cn.encode('utf-8'),
- 'sn' : cn.encode('utf-8'),
- 'cn' : cn.encode('utf-8'),
- 'fedoraPersonSshKey' : '',
- 'facsimileTelephoneNumber' : '',
- 'fedoraPersonApprovalStatus' : 'approved',
- 'givenName' : givenName.encode('utf-8'),
- 'mail' : mail.encode('utf-8'),
- 'fedoraPersonKeyId' : '',
- 'fedoraPersonCertSerial' : '-1',
- 'description' : '',
- 'fedoraPersonCreationDate' : str(now),
- 'telephoneNumber' : telephoneNumber.encode('utf-8'),
- 'fedoraPersonBugzillaMail' : mail.encode('utf-8'),
- 'postalAddress' : postalAddress.encode('utf-8'),
- 'fedoraPersonIrcNick' : '',
- 'userPassword' : 'Disabled',
- 'fedoraPersonTimeZone' : 'UTC',
- }
- self.__server.add('cn=%s,%s' % (cn, self.__base), attributes)
- attributes = {
- 'objectClass' : ('organizationalUnit', 'top'),
- 'ou' : 'Roles'
- }
- self.__server.add('ou=Roles,cn=%s,%s' % (cn, self.__base), attributes)
- return 0
-
- ### FIXME: Overriding __getattr__ and __setattr__ can be tricky and have
- # performance penalties. If that's okay, you may also want to consider
- # inheriting from dict as that might be a better access method.
- def __getattr__(self, attr):
- if attr == '__filter':
- return self.__filter
- if attr == 'userName':
- attr = 'cn'
- try:
- attributes = []
- attributes.append(attr)
- return self.__server.search(self.__base, self.__filter, attributes)[0][0][1][attr][0].decode('utf8')
- except:
- # Should probably raise here.
- return None
-
- def __setattr__(self, attr, value):
- if attr.startswith('_'):
- #return setattr(self.__class__, attr, value)
- self.__dict__[attr] = value
- return
- base = 'cn=%s,ou=People,dc=fedoraproject,dc=org' % self.__getattr__('cn')
-
- if self.__getattr__(attr):
- self.__server.modify(base, attr, value, self.__getattr__(attr))
- else:
- try:
- self.__server.modify(base, attr, value)
- except:
- self.__server.modify(base, attr, value, self.__getattr__(attr))
-
- @classmethod
- def users(self, searchExpression='*', findAttr='cn'):
- ''' Returns a list of users '''
- users = []
- ldapFilter = '(&(objectClass=top)(%s=%s))' % (findAttr, searchExpression)
- attributes = ['cn']
- usersDict = self.__server.search(self.__base, ldapFilter, attributes)
- if usersDict:
- for user in usersDict:
- users.append(user[0][1]['cn'][0].decode('utf8'))
- else:
- return None
- return users
-
- @classmethod
- def byFilter(cls, ldapFilter):
- ''' Returns only the first result in the search '''
- self = cls()
- self.__filter = ldapFilter
- return self
-
- @classmethod
- def byUserName(self, cn):
- '''Wrapper for byFilter - search by cn'''
- return self.byFilter('cn=%s' % cn)
-
- @classmethod
- def auth(self, who, password, ldapServer=None):
- ''' Basic Authentication Module '''
- if not password:
- raise AuthError
- if not ldapServer:
- s = Server()
- ldapServer = s.ldapConn
- who = 'cn=%s,ou=People,dc=fedoraproject,dc=org' % who
- try:
- ldapServer.simple_bind_s(who, password)
- except:
- raise AuthError
-
- def upgrade(self, group):
- ''' Upgrade user in group '''
- base = 'cn=%s,ou=Roles,cn=%s,ou=People,dc=fedoraproject,dc=org' % (group, self.cn)
- g = Groups.byGroupName(group, includeUnapproved=True)[self.cn]
- if not g.fedoraRoleStatus.lower() == 'approved':
- '''User not approved or sponsored'''
- raise TypeError, 'User is not approved'
- if g.fedoraRoleType.lower() == 'administrator':
- raise TypeError, 'User cannot be upgraded beyond administrator'
- elif g.fedoraRoleType.lower() == 'sponsor':
- self.__server.modify(base, 'fedoraRoleType', 'administrator', g.fedoraRoleType)
- elif g.fedoraRoleType.lower() == 'user':
- self.__server.modify(base, 'fedoraRoleType', 'sponsor', g.fedoraRoleType)
-
- def downgrade(self, group):
- ''' Downgrade user in group '''
- base = 'cn=%s,ou=Roles,cn=%s,ou=People,dc=fedoraproject,dc=org' % (group, self.cn)
- g = Groups.byGroupName(group, includeUnapproved=True)[self.cn]
- if not g.fedoraRoleStatus.lower() == 'approved':
- '''User not approved or sponsored'''
- raise TypeError, 'User is not approved'
- if g.fedoraRoleType.lower() == 'user':
- raise TypeError, 'User cannot be downgraded below user, did you mean remove?'
- elif g.fedoraRoleType.lower() == 'sponsor':
- self.__server.modify(base, 'fedoraRoleType', 'user', g.fedoraRoleType)
- elif g.fedoraRoleType.lower() == 'administrator':
- self.__server.modify(base, 'fedoraRoleType', 'sponsor', g.fedoraRoleType)
-
- def sponsor(self, groupName, sponsor):
- ''' Sponsor current user '''
- base = 'cn=%s,ou=Roles,cn=%s,ou=People,dc=fedoraproject,dc=org' % (groupName, self.cn)
- g = Groups.byGroupName(groupName, includeUnapproved=True)[self.cn]
- group = Groups.groups(groupName)[groupName]
- now = time.time()
- self.__server.modify(base, 'fedoraRoleApprovalDate', now)
- if group.fedoraGroupNeedsSponsor.lower() == 'true':
- self.__server.modify(base, 'fedoraRoleSponsor', sponsor)
- else:
- self.__server.modify(base, 'fedoraRoleSponsor', 'None')
- self.__server.modify(base, 'fedoraRoleStatus', 'approved')
-
- def generatePassword(self,password=None,length=14,salt=''):
- ''' Generate Password '''
- secret = {} # contains both hash and password
-
- if not password:
- rand = Random()
- password = ''
- # Exclude 0,O and l,1
- righthand = '23456qwertasdfgzxcvbQWERTASDFGZXCVB'
- lefthand = '789yuiophjknmYUIPHJKLNM'
- for i in range(length):
- if i%2:
- password = password + rand.choice(lefthand)
- else:
- password = password + rand.choice(righthand)
-
- ctx = sha.new(password)
- ctx.update(salt)
- secret['hash'] = "{SSHA}%s" % b64encode(ctx.digest() + salt)
- secret['pass'] = password
-
- return secret
-
-
-class UserAccount:
- def __init__(self):
- self.realName = ''
- self.userName = ''
- self.primaryEmail = ''
- self.groups = []
diff --git a/fas/fas/group.py b/fas/fas/group.py
index eb0b32d..8fc685b 100644
--- a/fas/fas/group.py
+++ b/fas/fas/group.py
@@ -8,8 +8,6 @@ from fas.auth import *
from fas.user import KnownUser
-from textwrap import dedent
-
import re
class KnownGroup(validators.FancyValidator):
@@ -152,7 +150,7 @@ class Group(controllers.Controller):
@expose(template="fas.templates.group.new")
def create(self, name, display_name, owner, group_type, needs_sponsor=0, user_can_remove=1, prerequisite='', joinmsg=''):
'''Create a group'''
-
+
groupname = name
person = People.by_username(turbogears.identity.current.user_name)
person_owner = People.by_username(owner)
@@ -247,14 +245,16 @@ class Group(controllers.Controller):
username = turbogears.identity.current.user_name
person = People.by_username(username)
- re_search = re.sub(r'\*', r'%', search).lower()
- groups = Groups.query.filter(Groups.name.like(re_search)).order_by('name')
- groups = filter(lambda group: canViewGroup(person, group), groups)
- if len(groups) <= 0:
- turbogears.flash(_("No Groups found matching '%s'") % search)
memberships = {}
- for group in groups:
- memberships[group.id] = group.approved_roles
+ groups = []
+ re_search = re.sub(r'\*', r'%', search).lower()
+ results = Groups.query.filter(Groups.name.like(re_search)).order_by('name').all()
+ for group in results:
+ if canViewGroup(person, group):
+ groups.append(group)
+ memberships[group.name] = group.approved_roles
+ if not len(groups):
+ turbogears.flash(_("No Groups found matching '%s'") % search)
return dict(groups=groups, search=search, memberships=memberships)
@identity.require(turbogears.identity.not_anonymous())
@@ -284,18 +284,19 @@ class Group(controllers.Controller):
{'user': target.username, 'group': group.name})
else:
import turbomail
-
+
+ # TODO: How do we handle gettext calls for these kinds of emails?
# TODO: CC to right place, put a bit more thought into how to most elegantly do this
message = turbomail.Message(config.get('accounts_mail'), '%s-sponsors@fedoraproject.org' % group.name, \
"Fedora '%(group)s' sponsor needed for %(user)s" % {'user': target.username, 'group': group.name})
url = config.get('base_url_filter.base_url') + turbogears.url('/group/edit/%s' % groupname)
- message.plain = dedent('''
- Fedora user %(user)s, aka %(name)s <%(email)s> has requested
- membership for %(applicant)s (%(applicant_name)s) in the %(group)s group and needs a sponsor.
-
- Please go to %(url)s to take action.
- ''' % {'user': person.username, 'name': person.human_name, 'applicant': target.username, 'applicant_name': target.human_name, 'email': person.emails['primary'].email, 'url': url, 'group': group.name} )
+ message.plain = _('''
+Fedora user %(user)s, aka %(name)s <%(email)s> has requested
+membership for %(applicant)s (%(applicant_name)s) in the %(group)s group and needs a sponsor.
+
+Please go to %(url)s to take action.
+''') % {'user': person.username, 'name': person.human_name, 'applicant': target.username, 'applicant_name': target.human_name, 'email': person.emails['primary'].email, 'url': url, 'group': group.name}
turbomail.enqueue(message)
turbogears.flash(_('%(user)s has applied to %(group)s!') % \
{'user': target.username, 'group': group.name})
@@ -326,13 +327,13 @@ class Group(controllers.Controller):
else:
import turbomail
message = turbomail.Message(config.get('accounts_mail'), target.emails['primary'].email, "Your Fedora '%s' membership has been sponsored" % group.name)
- message.plain = dedent('''
- %(name)s <%(email)s> has sponsored you for membership in the %(group)s
- group of the Fedora account system. If applicable, this change should
- propagate into the e-mail aliases and CVS repository within an hour.
+ message.plain = _('''
+%(name)s <%(email)s> has sponsored you for membership in the %(group)s
+group of the Fedora account system. If applicable, this change should
+propagate into the e-mail aliases and CVS repository within an hour.
- %(joinmsg)s
- ''') % {'group': group.name, 'name': person.human_name, 'email': person.emails['primary'].email, 'joinmsg': group.joinmsg}
+%(joinmsg)s
+''') % {'group': group.name, 'name': person.human_name, 'email': person.emails['primary'].email, 'joinmsg': group.joinmsg}
turbomail.enqueue(message)
turbogears.flash(_("'%s' has been sponsored!") % person.human_name)
turbogears.redirect('/group/view/%s' % group.name)
@@ -364,12 +365,12 @@ class Group(controllers.Controller):
else:
import turbomail
message = turbomail.Message(config.get('accounts_mail'), target.emails['primary'].email, "Your Fedora '%s' membership has been removed" % group.name)
- message.plain = dedent('''
- %(name)s <%(email)s> has removed you from the '%(group)s'
- group of the Fedora Accounts System This change is effective
- immediately for new operations, and should propagate into the e-mail
- aliases within an hour.
- ''') % {'group': group.name, 'name': person.human_name, 'email': person.emails['primary'].email}
+ message.plain = _('''
+%(name)s <%(email)s> has removed you from the '%(group)s'
+group of the Fedora Accounts System This change is effective
+immediately for new operations, and should propagate into the e-mail
+aliases within an hour.
+''') % {'group': group.name, 'name': person.human_name, 'email': person.emails['primary'].email}
turbomail.enqueue(message)
turbogears.flash(_('%(name)s has been removed from %(group)s!') % \
{'name': target.username, 'group': group.name})
@@ -406,12 +407,12 @@ class Group(controllers.Controller):
# Should we make person.upgrade return this?
role = PersonRoles.query.filter_by(group=group, member=target).one()
status = role.role_type
- message.plain = dedent('''
- %(name)s <%(email)s> has upgraded you to %(status)s status in the
- '%(group)s' group of the Fedora Accounts System This change is
- effective immediately for new operations, and should propagate
- into the e-mail aliases within an hour.
- ''') % {'group': group.name, 'name': person.human_name, 'email': person.emails['primary'].email, 'status': status}
+ message.plain = _('''
+%(name)s <%(email)s> has upgraded you to %(status)s status in the
+'%(group)s' group of the Fedora Accounts System This change is
+effective immediately for new operations, and should propagate
+into the e-mail aliases within an hour.
+''') % {'group': group.name, 'name': person.human_name, 'email': person.emails['primary'].email, 'status': status}
turbomail.enqueue(message)
turbogears.flash(_('%s has been upgraded!') % target.username)
turbogears.redirect('/group/view/%s' % group.name)
@@ -443,12 +444,12 @@ class Group(controllers.Controller):
message = turbomail.Message(config.get('accounts_mail'), target.emails['primary'].email, "Your Fedora '%s' membership has been downgraded" % group.name)
role = PersonRoles.query.filter_by(group=group, member=target).one()
status = role.role_type
- message.plain = dedent('''
- %(name)s <%(email)s> has downgraded you to %(status)s status in the
- '%(group)s' group of the Fedora Accounts System This change is
- effective immediately for new operations, and should propagate
- into the e-mail aliases within an hour.
- ''') % {'group': group.name, 'name': person.human_name, 'email': person.emails['primary'].email, 'status': status}
+ message.plain = _('''
+%(name)s <%(email)s> has downgraded you to %(status)s status in the
+'%(group)s' group of the Fedora Accounts System This change is
+effective immediately for new operations, and should propagate
+into the e-mail aliases within an hour.
+''') % {'group': group.name, 'name': person.human_name, 'email': person.emails['primary'].email, 'status': status}
turbomail.enqueue(message)
turbogears.flash(_('%s has been downgraded!') % target.username)
turbogears.redirect('/group/view/%s' % group.name)
@@ -493,22 +494,22 @@ class Group(controllers.Controller):
if isApproved(person, group):
message = turbomail.Message(person.emails['primary'].email, target, _('Come join The Fedora Project!'))
- message.plain = _(dedent('''
- %(name)s <%(email)s> has invited you to join the Fedora
- Project! We are a community of users and developers who produce a
- complete operating system from entirely free and open source software
- (FOSS). %(name)s thinks that you have knowledge and skills
- that make you a great fit for the Fedora community, and that you might
- be interested in contributing.
+ message.plain = _('''
+%(name)s <%(email)s> has invited you to join the Fedora
+Project! We are a community of users and developers who produce a
+complete operating system from entirely free and open source software
+(FOSS). %(name)s thinks that you have knowledge and skills
+that make you a great fit for the Fedora community, and that you might
+be interested in contributing.
- How could you team up with the Fedora community to use and develop your
- skills? Check out http://fedoraproject.org/join-fedora for some ideas.
- Our community is more than just software developers -- we also have a
- place for you whether you're an artist, a web site builder, a writer, or
- a people person. You'll grow and learn as you work on a team with other
- very smart and talented people.
+How could you team up with the Fedora community to use and develop your
+skills? Check out http://fedoraproject.org/join-fedora for some ideas.
+Our community is more than just software developers -- we also have a
+place for you whether you're an artist, a web site builder, a writer, or
+a people person. You'll grow and learn as you work on a team with other
+very smart and talented people.
- Fedora and FOSS are changing the world -- come be a part of it!''')) % {'name': person.human_name, 'email': person.emails['primary'].email}
+Fedora and FOSS are changing the world -- come be a part of it!''') % {'name': person.human_name, 'email': person.emails['primary'].email}
turbomail.enqueue(message)
turbogears.flash(_('Message sent to: %s') % target)
turbogears.redirect('/group/view/%s' % group.name)
diff --git a/fas/fas/help.py b/fas/fas/help.py
index d01fdbe..23dd5d6 100644
--- a/fas/fas/help.py
+++ b/fas/fas/help.py
@@ -9,8 +9,13 @@ class Help(controllers.Controller):
'user_ircnick' : ['IRC Nick (Optional)', 'IRC Nick is used to identify yourself on irc.freenode.net. Please register your nick on irc.freenode.net first, then fill this in so people can find you online when they need to
'],
'user_primary_email' : ['Primary Email (Required)', 'This email address should be your prefered email contact and will be used to send various official emails to. This is also where your @fedoraproject.org email will get forwarded
'],
'user_human_name' : ['Full Name (Required)', 'Your Human Name or "real life" name
'],
- 'user_gpg_keyid' : ['GPG Key', 'Only required for users signing the CLA. It is generally used to prove that a message or email came from you or to encrypt information so that only the recipients can read it. See http://fedoraproject.org/wiki/Infrastructure/AccountSystem/CLAHowTo for more information
'],
- 'user_telephone' : ['Telephone', 'Only required for users signing the CLA . Sometimes during a time of emergency someone from the Fedora Project may need to contact you. For more information see our Privacy Policy
'],
+ 'user_gpg_keyid' : ['GPG Key', 'Only required for users signing the CLA . It is generally used to prove that a message or email came from you or to encrypt information so that only the recipients can read it. See the CLAHowTo for more information
'],
+ 'user_telephone' : ['Telephone', 'Only required for users signing the CLA . Sometimes during a time of emergency someone from the Fedora Project may need to contact you. For more information see our Privacy Policy
'],
+ 'user_postal_address': ['Postal Address', 'Only required for users signing the CLA . This should be a mailing address where you can be contacted. See our Privacy Policy about any concerns.
'],
+ 'user_timezone': ['Timezone (Optional)', 'Please specify the time zone you are in.
'],
+ 'user_comments': ['Comments (Optional)', 'Misc comments about yourself.
'],
+ 'user_account_status': ['Account Status', 'Shows account status, possible values include
'],
+ 'user_cla' : ['CLA', 'In order to become a full Fedora contributor you must sign a Contributor License Agreement . This license is a legal agreement between you and Red Hat. Full status allows people to contribute content and code and is recommended for anyone interested in getting involved in the Fedora Project. To find out more, see the CLAHowTo .
'],
}
def __init__(self):
diff --git a/fas/fas/model.py b/fas/fas/model.py
index 3a7e6db..bd02b6b 100644
--- a/fas/fas/model.py
+++ b/fas/fas/model.py
@@ -22,6 +22,7 @@
Model for the Fedora Account System
'''
from datetime import datetime
+import pytz
from turbogears.database import metadata, mapper, get_engine
# import some basic SQLAlchemy classes for declaring the data model
# (see http://www.sqlalchemy.org/docs/04/ormtutorial.html)
@@ -80,7 +81,7 @@ UnApprovedRolesSelect = PersonRolesTable.select(and_(
visits_table = Table('visit', metadata,
Column('visit_key', String(40), primary_key=True),
- Column('created', DateTime, nullable=False, default=datetime.now),
+ Column('created', DateTime, nullable=False, default=datetime.now(pytz.utc)),
Column('expiry', DateTime)
)
@@ -173,7 +174,7 @@ class People(SABase):
role = PersonRoles.query.filter_by(member=cls, group=group).one()
role.role_status = 'approved'
role.sponsor_id = requester.id
- role.approval = datetime.now()
+ role.approval = datetime.now(pytz.utc)
def remove(cls, group, requester):
role = PersonRoles.query.filter_by(member=cls, group=group).one()
diff --git a/fas/fas/openid_fas.py b/fas/fas/openid_fas.py
index 2b5e43e..e3d6635 100644
--- a/fas/fas/openid_fas.py
+++ b/fas/fas/openid_fas.py
@@ -11,7 +11,10 @@ from openid.store.filestore import FileOpenIDStore
from fas.auth import *
-from fas.user import knownUser, userNameExists
+from fas.user import KnownUser
+
+class UserID(validators.Schema):
+ targetname = KnownUser
class OpenID(controllers.Controller):
@@ -28,8 +31,8 @@ class OpenID(controllers.Controller):
@expose(template="fas.templates.openid.about")
def about(self):
'''Display an explanatory message about the OpenID service'''
- userName = turbogears.identity.current.user_name
- return dict(userName=userName)
+ username = turbogears.identity.current.user_name
+ return dict(username=username)
@expose(template="genshi-text:fas.templates.openid.auth", format="text", content_type='text/plain; charset=utf-8')
def server(self, **query):
@@ -58,10 +61,10 @@ class OpenID(controllers.Controller):
else:
openid_response = None
if openid_request.mode in BROWSER_REQUEST_MODES:
- userName = turbogears.identity.current.user_name;
+ username = turbogears.identity.current.user_name;
url = None
- if userName is not None:
- url = config.get('base_url') + turbogears.url('/openid/id/%s' % userName)
+ if username is not None:
+ url = config.get('base_url') + turbogears.url('/openid/id/%s' % username)
if openid_request.identity == url:
if openid_request.trust_root in session['openid_trusted']:
openid_response = openid_request.answer(True)
@@ -95,16 +98,15 @@ class OpenID(controllers.Controller):
@expose()
def login(self):
'''This exists only to make the user login and then redirect to /openid/server'''
- userName = turbogears.identity.current.user_name;
turbogears.redirect('/openid/server')
return dict()
@expose(template="fas.templates.openid.id")
- @validate(validators=userNameExists())
- def id(self, userName):
+ @validate(validators=UserID())
+ def id(self, username):
'''The "real" OpenID URL'''
- user = Person.byUserName(userName)
+ person = Person.by_username(username)
server = config.get('base_url') + turbogears.url('/openid/server')
- return dict(user=user, server=server)
+ return dict(person=person, server=server)
diff --git a/fas/fas/safasprovider.py b/fas/fas/safasprovider.py
index c425096..ac0220e 100644
--- a/fas/fas/safasprovider.py
+++ b/fas/fas/safasprovider.py
@@ -126,10 +126,6 @@ class SaFasIdentityProvider(SqlAlchemyIdentityProvider):
log.info(_("Loading: %(visitmod)s") % \
{'visitmod': visit_identity_class_path})
visit_identity_class = load_class(visit_identity_class_path)
- # Default encryption algorithm is to use plain text passwords
- algorithm = config.get("identity.saprovider.encryption_algorithm", None)
- self.encrypt_password = lambda pw: \
- identity._encrypt_password(algorithm, pw)
def create_provider_model(self):
'''
@@ -190,7 +186,6 @@ class SaFasIdentityProvider(SqlAlchemyIdentityProvider):
'''
return user.password == crypt.crypt(password, user.password)
- return user.password == self.encrypt_password(password)
def load_identity(self, visit_key):
'''Lookup the principal represented by visit_key.
diff --git a/fas/fas/static/css/style.css b/fas/fas/static/css/style.css
index fa0f658..e861567 100644
--- a/fas/fas/static/css/style.css
+++ b/fas/fas/static/css/style.css
@@ -189,6 +189,21 @@ a
background: #082C59;
}
+#language
+{
+ padding: 1ex;
+}
+
+#language label
+{
+ color: #FFFFFF;
+}
+
+#language input
+{
+ width: 4ex;
+}
+
#content
{
margin-left: 22ex;
diff --git a/fas/fas/templates/group/view.html b/fas/fas/templates/group/view.html
index 85520f2..318eaa8 100644
--- a/fas/fas/templates/group/view.html
+++ b/fas/fas/templates/group/view.html
@@ -10,7 +10,9 @@
${group.display_name} (${group.name})
@@ -70,12 +72,8 @@
${role.member.username}
${role.member.username}
${_('None')}
-
- ${role.creation}
- ${role.approval}
+ ${role.creation.astimezone(timezone).strftime('%Y-%m-%d %H:%M:%S %Z')}
+ ${role.approval.astimezone(timezone).strftime('%Y-%m-%d %H:%M:%S %Z')}
${role.role_status}
${role.role_type}
diff --git a/fas/fas/templates/master.html b/fas/fas/templates/master.html
index 1aec2f0..421a82d 100644
--- a/fas/fas/templates/master.html
+++ b/fas/fas/templates/master.html
@@ -9,10 +9,10 @@
-
-
-
-
+
+
+
+
@@ -64,6 +64,13 @@
${_('Apply For a new Group')}
${_('News')}
+
+
+
diff --git a/fas/fas/templates/openid/about.html b/fas/fas/templates/openid/about.html
index 4ac41f7..2cbe67f 100644
--- a/fas/fas/templates/openid/about.html
+++ b/fas/fas/templates/openid/about.html
@@ -7,9 +7,9 @@
${_('Fedora Accounts System')}
-
${_{'Fedora Project OpenID Provider')}
+
${_('Fedora Project OpenID Provider')}
- ${Markup_('Description goes here, <a href="http://username.fedorapeople.org/">username.fedorapeople.org</a>'))}
+ ${Markup_('Description goes here, <a href="http://username.fedorapeople.org/">username.fedorapeople.org</a>')}