Commit 36cbcbf7 authored by Steve Borho's avatar Steve Borho
Browse files

sync: simplify authentication dialog, rename to 'Security'

This dialog configures both https connection properties and user authentication.
It no longer offers the full feature set of the [auth] section as the complexity
is much too great for what is a rare requirement.  Also the previous logic was
defaulting to allowing authentication over http, which was transmitting your
password in plaintext if you happened to accidentally push over http.

It also disables the password entry if mercurial_keyring is enabled, since it
completely destroys the usefulness of mercurial_keyring if you go and save your
password in plaintext for zero benefit.

More work remains to be done, we need to add --insecure to the command line if
the hostname is found in [insecurehosts] and we are using https.
parent 8a0c060855e0
...@@ -146,8 +146,8 @@ class SyncWidget(QWidget): ...@@ -146,8 +146,8 @@ class SyncWidget(QWidget):
self.pathentry.setAcceptDrops(False) self.pathentry.setAcceptDrops(False)
self.pathentry.textChanged.connect(self.refreshUrl) self.pathentry.textChanged.connect(self.refreshUrl)
hbox.addWidget(self.pathentry, 1) hbox.addWidget(self.pathentry, 1)
self.authbutton = QPushButton(_('Authentication')) self.securebutton = QPushButton(_('Security'))
hbox.addWidget(self.authbutton) hbox.addWidget(self.securebutton)
self.savebutton = QPushButton(_('Save')) self.savebutton = QPushButton(_('Save'))
hbox.addWidget(self.savebutton) hbox.addWidget(self.savebutton)
self.layout().addLayout(hbox) self.layout().addLayout(hbox)
...@@ -186,7 +186,7 @@ class SyncWidget(QWidget): ...@@ -186,7 +186,7 @@ class SyncWidget(QWidget):
self.layout().addLayout(hbox, 1) self.layout().addLayout(hbox, 1)
self.savebutton.clicked.connect(self.saveclicked) self.savebutton.clicked.connect(self.saveclicked)
self.authbutton.clicked.connect(self.authclicked) self.securebutton.clicked.connect(self.secureclicked)
self.postpullbutton.clicked.connect(self.postpullclicked) self.postpullbutton.clicked.connect(self.postpullclicked)
self.optionsbutton.pressed.connect(self.editOptions) self.optionsbutton.pressed.connect(self.editOptions)
...@@ -311,7 +311,7 @@ class SyncWidget(QWidget): ...@@ -311,7 +311,7 @@ class SyncWidget(QWidget):
schemeIndex = self.schemecombo.currentIndex() schemeIndex = self.schemecombo.currentIndex()
self.hostentry.setEnabled(schemeIndex != 0) self.hostentry.setEnabled(schemeIndex != 0)
self.portentry.setEnabled(schemeIndex != 0) self.portentry.setEnabled(schemeIndex != 0)
self.authbutton.setEnabled(schemeIndex > 1) self.securebutton.setEnabled(schemeIndex > 1)
def currentUrl(self, hidepw): def currentUrl(self, hidepw):
scheme = _schemes[self.schemecombo.currentIndex()] scheme = _schemes[self.schemecombo.currentIndex()]
...@@ -482,11 +482,11 @@ class SyncWidget(QWidget): ...@@ -482,11 +482,11 @@ class SyncWidget(QWidget):
if dlg.exec_() == QDialog.Accepted: if dlg.exec_() == QDialog.Accepted:
self.curalias = hglib.fromunicode(dlg.aliasentry.text()) self.curalias = hglib.fromunicode(dlg.aliasentry.text())
def authclicked(self): def secureclicked(self):
host = hglib.fromunicode(self.hostentry.text()) host = hglib.fromunicode(self.hostentry.text())
user = self.curuser or '' user = self.curuser or ''
pw = self.curpw or '' pw = self.curpw or ''
dlg = AuthDialog(self.repo, host, user, pw, self) dlg = SecureDialog(self.repo, host, user, pw, self)
dlg.setWindowFlags(Qt.Sheet) dlg.setWindowFlags(Qt.Sheet)
dlg.setWindowModality(Qt.WindowModal) dlg.setWindowModality(Qt.WindowModal)
if dlg.exec_() == QDialog.Accepted: if dlg.exec_() == QDialog.Accepted:
...@@ -941,37 +941,74 @@ class SaveDialog(QDialog): ...@@ -941,37 +941,74 @@ class SaveDialog(QDialog):
def reject(self): def reject(self):
super(SaveDialog, self).reject() super(SaveDialog, self).reject()
class AuthDialog(QDialog): class SecureDialog(QDialog):
def __init__(self, repo, host, user, pw, parent): def __init__(self, repo, host, user, pw, parent):
super(AuthDialog, self).__init__(parent) super(SecureDialog, self).__init__(parent)
def genfingerprint():
pem = ssl.get_server_certificate( (host, 443) )
der = ssl.PEM_cert_to_DER_cert(pem)
hash = util.sha1(der).hexdigest()
pretty = ":".join([hash[x:x + 2] for x in xrange(0, len(hash), 2)])
le.setText(pretty)
uhost = hglib.tounicode(host) uhost = hglib.tounicode(host)
self.setWindowTitle(_('Authentication: ') + uhost) self.setWindowTitle(_('Security: ') + uhost)
self.setWindowFlags(self.windowFlags() & \ self.setWindowFlags(self.windowFlags() & \
~Qt.WindowContextHelpButtonHint) ~Qt.WindowContextHelpButtonHint)
self.repo = repo self.repo = repo
self.host = host
self.alias = host.replace('.', '')
self.setLayout(QVBoxLayout()) self.setLayout(QVBoxLayout())
securebox = QGroupBox(_('Secure HTTPS Connection'))
self.layout().addWidget(securebox)
vbox = QVBoxLayout()
securebox.setLayout(vbox)
self.layout().addWidget(securebox)
self.cacertradio = QRadioButton(
_('Verify with Certificate Authority certificates (best)'))
self.fprintradio = QRadioButton(
_('Verify with stored host fingerprint (good)'))
self.insecureradio = QRadioButton(
_('No host validation, but still encrypted (bad)'))
hbox = QHBoxLayout()
fprint = repo.ui.config('hostfingerprints', host, '')
self.fprintentry = le = QLineEdit(fprint)
self.fprintradio.toggled.connect(self.fprintentry.setEnabled)
self.fprintentry.setEnabled(False)
if hasattr(le, 'setPlaceholderText'): # Qt >= 4.7
le.setPlaceholderText('### host certificate fingerprint ###')
hbox.addWidget(le)
try:
import ssl # Python 2.6 or backport for 2.5
qb = QPushButton(_('Query'))
qb.clicked.connect(genfingerprint)
qb.setEnabled(False)
self.fprintradio.toggled.connect(qb.setEnabled)
hbox.addWidget(qb)
except ImportError:
pass
vbox.addWidget(self.cacertradio)
vbox.addWidget(self.fprintradio)
vbox.addLayout(hbox)
vbox.addWidget(self.insecureradio)
self.cacertradio.setEnabled(bool(repo.ui.config('web', 'cacerts')))
self.cacertradio.setChecked(True) # default
if fprint:
self.fprintradio.setChecked(True)
elif repo.ui.config('insecurehosts', host):
self.insecureradio.setChecked(True)
authbox = QGroupBox(_('User Authentication')) authbox = QGroupBox(_('User Authentication'))
self.layout().addWidget(authbox)
form = QFormLayout() form = QFormLayout()
authbox.setLayout(form) authbox.setLayout(form)
self.aliasentry = QLineEdit(host.split('.', 1)[0]) self.layout().addWidget(authbox)
form.addRow(_('Site Alias'), self.aliasentry)
cfg = repo.ui.config('auth', self.alias+'.username', '')
self.schemes = QComboBox() self.userentry = QLineEdit(user or cfg)
self.schemes.addItems(('http https', 'http', 'https'))
form.addRow(_('Schemes'), self.schemes)
self.prefixentry = QLineEdit(host)
self.prefixentry.setToolTip(
_('''Either * or a URI prefix with or without the scheme part. The
authentication entry with the longest matching prefix is used (where * matches
everything and counts as a match of length 1). If the prefix doesn't include
a scheme, the match is performed against the URI with its scheme stripped as
well, and the schemes argument, q.v., is then subsequently consulted.'''))
form.addRow(_('Prefix'), self.prefixentry)
self.userentry = QLineEdit(user)
self.userentry.setToolTip( self.userentry.setToolTip(
_('''Optional. Username to authenticate with. If not given, and the remote _('''Optional. Username to authenticate with. If not given, and the remote
site requires basic or digest authentication, the user will be prompted for site requires basic or digest authentication, the user will be prompted for
...@@ -979,58 +1016,35 @@ it. Environment variables are expanded in the username letting you do ...@@ -979,58 +1016,35 @@ it. Environment variables are expanded in the username letting you do
foo.username = $USER.''')) foo.username = $USER.'''))
form.addRow(_('Username'), self.userentry) form.addRow(_('Username'), self.userentry)
self.pwentry = QLineEdit(pw) cfg = repo.ui.config('auth', self.alias+'.password', '')
self.pwentry = QLineEdit(pw or cfg)
self.pwentry.setEchoMode(QLineEdit.Password) self.pwentry.setEchoMode(QLineEdit.Password)
self.pwentry.setToolTip( self.pwentry.setToolTip(
_('''Optional. Password to authenticate with. If not given, and the remote _('''Optional. Password to authenticate with. If not given, and the remote
site requires basic or digest authentication, the user will be prompted for site requires basic or digest authentication, the user will be prompted for
it.''')) it.'''))
form.addRow(_('Password'), self.pwentry) form.addRow(_('Password'), self.pwentry)
if 'mercurial_keyring' in repo.extensions():
self.keyentry = QLineEdit() self.pwentry.clear()
self.pwentry.setEnabled(False)
self.pwentry.setToolTip(_('Mercurial keyring extension is enabled. '
'Passwords will be stored in platform native '
'cryptographically secure method.'))
cfg = repo.ui.config('auth', self.alias+'.key', '')
self.keyentry = QLineEdit(cfg)
self.keyentry.setToolTip( self.keyentry.setToolTip(
_('''Optional. PEM encoded client certificate key file. Environment variables _('''Optional. PEM encoded client certificate key file. Environment variables
are expanded in the filename.''')) are expanded in the filename.'''))
form.addRow(_('User Certificate Key File'), self.keyentry) form.addRow(_('User Certificate Key File'), self.keyentry)
self.chainentry = QLineEdit() cfg = repo.ui.config('auth', self.alias+'.cert', '')
self.chainentry = QLineEdit(cfg)
self.chainentry.setToolTip( self.chainentry.setToolTip(
_('''Optional. PEM encoded client certificate chain file. Environment variables _('''Optional. PEM encoded client certificate chain file. Environment variables
are expanded in the filename.''')) are expanded in the filename.'''))
form.addRow(_('User Certificate Chain File'), self.chainentry) form.addRow(_('User Certificate Chain File'), self.chainentry)
txt = _('Authentication section %smanual page%s') % (
'<a href="http://www.selenic.com/mercurial/hgrc.5.html#auth">',
'</a>')
self.lbl = QLabel(txt)
self.lbl.setOpenExternalLinks(True)
form.addRow(self.lbl, None)
def genfingerprint():
pem = ssl.get_server_certificate( (host, 443) )
der = ssl.PEM_cert_to_DER_cert(pem)
hash = util.sha1(der).hexdigest()
pretty = ":".join([hash[x:x + 2] for x in xrange(0, len(hash), 2)])
le.setText(pretty)
hostbox = QGroupBox(_('%s host certificate fingerprint') % uhost)
self.layout().addWidget(hostbox)
hbox = QHBoxLayout()
hostbox.setLayout(hbox)
self.host = host
cur = self.repo.ui.config('hostfingerprints', host, '')
self.fingerprintentry = le = QLineEdit(cur)
if hasattr(le, 'setPlaceholderText'): # Qt >= 4.7
le.setPlaceholderText('### host certificate fingerprint ###')
hbox.addWidget(le)
try:
import ssl # Python 2.6 or backport for 2.5
qb = QPushButton(_('Query'))
qb.clicked.connect(genfingerprint)
hbox.addWidget(qb)
except ImportError:
pass
BB = QDialogButtonBox BB = QDialogButtonBox
bb = QDialogButtonBox(BB.Help|BB.Save|BB.Cancel) bb = QDialogButtonBox(BB.Help|BB.Save|BB.Cancel)
bb.rejected.connect(self.reject) bb.rejected.connect(self.reject)
...@@ -1054,36 +1068,35 @@ are expanded in the filename.''')) ...@@ -1054,36 +1068,35 @@ are expanded in the filename.'''))
return return
if fn is None: if fn is None:
return return
schemes = hglib.fromunicode(self.schemes.currentText())
prefix = hglib.fromunicode(self.prefixentry.text()) def setorclear(section, item, value):
if value:
cfg.set(section, item, value)
elif not value and item in cfg[section]:
del cfg[section][item]
if self.cacertradio.isChecked():
fprint = None
insecure = None
elif self.fprintradio.isChecked():
fprint = hglib.fromunicode(self.fprintentry.text())
insecure = None
else:
fprint = None
insecure = '1'
setorclear('hostfingerprints', self.host, fprint)
setorclear('insecurehosts', self.host, insecure)
username = hglib.fromunicode(self.userentry.text()) username = hglib.fromunicode(self.userentry.text())
password = hglib.fromunicode(self.pwentry.text()) password = hglib.fromunicode(self.pwentry.text())
key = hglib.fromunicode(self.keyentry.text()) key = hglib.fromunicode(self.keyentry.text())
chain = hglib.fromunicode(self.chainentry.text()) chain = hglib.fromunicode(self.chainentry.text())
alias = hglib.fromunicode(self.aliasentry.text())
if alias+'.prefix' in cfg['auth']: cfg.set('auth', self.alias+'.prefix', self.host)
if not qtlib.QuestionMsgBox(_('Confirm authentication replace'), setorclear('auth', self.alias+'.username', username)
_('Authentication info for %s already ' setorclear('auth', self.alias+'.password', password)
'exists, replace?') % alias): setorclear('auth', self.alias+'.key', key)
return setorclear('auth', self.alias+'.cert', chain)
cfg.set('auth', alias+'.schemes', schemes)
if username:
cfg.set('auth', alias+'.username', username)
if prefix:
cfg.set('auth', alias+'.prefix', prefix)
def setorclear(item, value):
item = '.'.join([alias, item])
if value:
cfg.set('auth', item, value)
elif not value and item in cfg['auth']:
del cfg['auth'][item]
setorclear('password', password)
setorclear('key', key)
setorclear('cert', chain)
fprint = hglib.fromunicode(self.fingerprintentry.text())
if fprint:
cfg.set('hostfingerprints', self.host, fprint)
self.repo.incrementBusyCount() self.repo.incrementBusyCount()
try: try:
...@@ -1092,10 +1105,10 @@ are expanded in the filename.''')) ...@@ -1092,10 +1105,10 @@ are expanded in the filename.'''))
qtlib.WarningMsgBox(_('Unable to write configuration file'), qtlib.WarningMsgBox(_('Unable to write configuration file'),
hglib.tounicode(e), parent=self) hglib.tounicode(e), parent=self)
self.repo.decrementBusyCount() self.repo.decrementBusyCount()
super(AuthDialog, self).accept() super(SecureDialog, self).accept()
def reject(self): def reject(self):
super(AuthDialog, self).reject() super(SecureDialog, self).reject()
class PathsTree(QTreeView): class PathsTree(QTreeView):
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment