1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
|
# -*- coding: utf-8 -*-
#
# Copyright (C) 2010 Chris Shenton <chris@koansys.com>
# Copyright (C) 2015 Steffen Hoffmann <hoff.st@web.de>
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution.
#
# Author: Chris Shenton <chris@koansys.com>
from io import StringIO
from trac.config import IntOption, Option
from trac.core import Component, implements
from trac.util.text import unicode_passwd
from ..api import IPasswordStore
DICTIONARY = u"""
ATTRIBUTE User-Name 1 string
ATTRIBUTE User-Password 2 string encrypt=1
"""
class RadiusAuthStore(Component):
"""[extra] Provides RADIUS authentication support.
Custom configuration is mandatory.
Provide IP address and authentication port of your RADIUS server. RADIUS
uses UDP port 1812 for authentication as per IETF RFC2865, but old servers
may still use 1645. You must also supply a shared secret, which the RADIUS
server admin must disclose to you.
"""
implements(IPasswordStore)
radius_server = Option('account-manager', 'radius_server',
doc="RADIUS server IP address, required.")
radius_authport = IntOption('account-manager', 'radius_authport', 1812,
doc="RADIUS server authentication port, defaults to 1812.")
# Conceal shared secret.
radius_secret = unicode_passwd(Option('account-manager', 'radius_secret',
doc="RADIUS server shared secret, required."))
def get_users(self):
"""Returns an iterable of the known usernames."""
return []
def has_user(self, user):
"""Returns whether the user account exists."""
# DEVEL: Shall we really deny knowing a specified user?
return False
def check_password(self, username, password):
"""Checks if the password is valid for the user."""
# Handle pyrad lib absence and upstream incompatibilities gracefully.
try:
import pyrad.packet
from pyrad.client import Client, Timeout
from pyrad.dictionary import Dictionary
except ImportError as e:
self.log.error("RADIUS auth store could not import pyrad, "
"need to install the egg: %s", e)
return
self.log.debug("RADIUS server=%s:%s (authport), secret='%s'",
self.radius_server, self.radius_authport,
self.radius_secret)
self.log.debug("RADIUS auth callenge for username=%s password=%s",
username, unicode_passwd(password))
client = Client(server=self.radius_server,
authport=self.radius_authport,
secret=self.radius_secret.encode('utf-8'),
dict=Dictionary(StringIO(DICTIONARY)),
)
req = client.CreateAuthPacket(code=pyrad.packet.AccessRequest,
User_Name=username.encode('utf-8'))
req['User-Password'] = req.PwCrypt(password)
self.log.debug("RADIUS auth sending packet req=%s", req)
try:
reply = client.SendPacket(req)
except Timeout as e:
self.log.error("RADIUS timeout contacting server=%s:%s (%s)",
self.radius_server, self.radius_authport, e)
return
# DEVEL: Too broad, narrow down that exception handler scope.
except Exception as e:
self.log.error("RADIUS error while using server=%s:%s: (%s)",
self.radius_server, self.radius_authport, e)
return
self.log.debug("RADIUS authentication reply code=%s", reply.code)
if pyrad.packet.AccessAccept == reply.code:
self.log.debug("RADIUS Accept for username=%s", username)
return True
# Rejection of login attempt, stopping further auth store interaction.
elif pyrad.packet.AccessReject == reply.code:
self.log.debug("RADIUS Reject for username=%s", username)
return False
# DEVEL: Any way to alert users that RSA token is in 'Next Token' mode
# so they know to fix it?
elif pyrad.packet.AccessChallenge == reply.code:
self.log.info("RADIUS returned Challenge for username=%s; "
"on RSA servers this indicates 'Next Token' mode.",
username)
return
else:
self.log.warning("RADIUS Unknown reply code (%s) for username=%s",
reply.code, username)
return
|