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

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

# Copyright 2013 The Distro Tracker Developers 

# See the COPYRIGHT file at the top-level directory of this distribution and 

# at https://deb.li/DTAuthors 

# 

# This file is part of Distro Tracker. It is subject to the license terms 

# in the LICENSE file found in the top-level directory of this 

# distribution and at https://deb.li/DTLicense. No part of Distro Tracker, 

# including this file, may be copied, modified, propagated, or distributed 

# except according to the terms contained in the LICENSE file. 

"""Authentication with the Debian SSO service.""" 

 

import json 

import logging 

 

from django.contrib import auth 

from django.contrib.auth.backends import RemoteUserBackend 

from django.contrib.auth.middleware import RemoteUserMiddleware 

from django.core.exceptions import ImproperlyConfigured, ValidationError 

from django.utils.http import urlencode 

 

from distro_tracker.accounts.models import User, UserEmail 

from distro_tracker.core.utils.http import get_resource_content 

 

logger = logging.getLogger(__name__) 

 

 

class DebianSsoUserMiddleware(RemoteUserMiddleware): 

""" 

Middleware that initiates user authentication based on the REMOTE_USER 

field provided by Debian's SSO system, or based on the SSL_CLIENT_S_DN_CN 

field provided by the validation of the SSL client certificate generated 

by sso.debian.org. 

 

If the currently logged in user is a DD (as identified by having a 

@debian.org address), they are forcefully logged out if the header 

is no longer found or is invalid. 

""" 

dacs_header = 'REMOTE_USER' 

cert_header = 'SSL_CLIENT_S_DN_CN' 

 

@staticmethod 

def dacs_user_to_email(username): 

parts = [part for part in username.split(':') if part] 

federation, jurisdiction = parts[:2] 

if (federation, jurisdiction) != ('DEBIANORG', 'DEBIAN'): 

return 

username = parts[-1] 

if '@' in username: 

return username # Full email already 

return username + '@debian.org' 

 

@staticmethod 

def is_debian_member(user): 

54 ↛ exitline 55 didn't finish the generator expression on line 55 return any( 

email.email.endswith('@debian.org') 

for email in user.emails.all() 

) 

 

def process_request(self, request): 

# AuthenticationMiddleware is required so that request.user exists. 

61 ↛ 62line 61 didn't jump to line 62, because the condition on line 61 was never true if not hasattr(request, 'user'): 

raise ImproperlyConfigured( 

"The Django remote user auth middleware requires the" 

" authentication middleware to be installed. Edit your" 

" MIDDLEWARE setting to insert" 

" 'django.contrib.auth.middleware.AuthenticationMiddleware'" 

" before the DebianSsoUserMiddleware class.") 

 

dacs_user = request.META.get(self.dacs_header) 

cert_user = request.META.get(self.cert_header) 

if cert_user is not None: 

remote_user = cert_user 

elif dacs_user is not None: 

remote_user = self.dacs_user_to_email(dacs_user) 

else: 

# Debian developers can only authenticate via SSO/SSL certs 

# so log them out now if they no longer have the proper META 

# variable 

79 ↛ 82line 79 didn't jump to line 82, because the condition on line 79 was never false if request.user.is_authenticated: 

80 ↛ 82line 80 didn't jump to line 82, because the condition on line 80 was never false if self.is_debian_member(request.user): 

auth.logout(request) 

return 

 

84 ↛ 85line 84 didn't jump to line 85, because the condition on line 84 was never true if request.user.is_authenticated: 

if request.user.emails.filter(email=remote_user).exists(): 

# The currently logged in user matches the one given by the 

# headers. 

return 

 

if remote_user and remote_user.endswith('@users.alioth.debian.org'): 

# Disallow logins with Alioth certs 

return 

 

# This will create the user if it doesn't exist 

user = auth.authenticate(remote_user=remote_user) 

if user: 

# User is valid. Set request.user and persist user in the session 

# by logging the user in. 

request.user = user 

auth.login(request, user) 

 

 

class DebianSsoUserBackend(RemoteUserBackend): 

""" 

The authentication backend which authenticates the provided remote 

user (identified by their @debian.org email) in Distro Tracker. If 

a matching User model instance does not exist, one is 

automatically created. In that case the DDs first and last name 

are pulled from Debian's NM REST API. 

""" 

def authenticate(self, request=None, remote_user=None): 

if not remote_user: 

return 

 

email = remote_user 

 

try: 

user_email, _ = UserEmail.objects.get_or_create(email=email) 

except ValidationError: 

logger.error('remote_user="%s" is not a valid email.', 

remote_user) 

return 

 

if not user_email.user: 

kwargs = {} 

names = self.get_user_details(remote_user) 

127 ↛ 129line 127 didn't jump to line 129, because the condition on line 127 was never false if names: 

kwargs.update(names) 

user = User.objects.create_user(main_email=email, **kwargs) 

else: 

user = User.objects.get(pk=user_email.user.id) 

 

return user 

 

@staticmethod 

def get_uid(remote_user): 

# Strips off the @debian.org part of the email leaving the uid 

if remote_user.endswith('@debian.org'): 

return remote_user[:-11] 

return remote_user 

 

def get_user_details(self, remote_user): 

""" 

Gets the details of the given user from the Debian NM REST API. 

 

:return: Dict with the keys ``first_name``, ``last_name`` 

``None`` if the API lookup did not return anything. 

""" 

if not remote_user.endswith('@debian.org'): 

# We only know how to extract data for DD via NM API 

return None 

 

content = get_resource_content( 

'https://nm.debian.org/api/people?' + 

urlencode({'uid': self.get_uid(remote_user)})) 

if content: 

result = json.loads(content.decode('utf-8'))['r'] 

 

if not result: 

return None 

return { 

'first_name': result[0]['cn'], 

'last_name': result[0]['sn'], 

}