Skip to content

Commit 8b92ffc

Browse files
authored
Merge branch 'master' into privileged_ldap
2 parents 78f6518 + 4620511 commit 8b92ffc

22 files changed

+705
-309
lines changed

.pre-commit-config.yaml

+13-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
exclude: ^vendor/
22
repos:
33
- repo: https://github.com/pre-commit/pre-commit-hooks.git
4-
sha: v0.9.5
4+
rev: v2.1.0
55
hooks:
6-
- id: autopep8-wrapper
76
- id: check-added-large-files
87
- id: check-docstring-first
98
- id: check-executables-have-shebangs
@@ -17,22 +16,30 @@ repos:
1716
- id: double-quote-string-fixer
1817
- id: end-of-file-fixer
1918
- id: file-contents-sorter
20-
- id: flake8
2119
- id: mixed-line-ending
2220
- id: name-tests-test
2321
- id: requirements-txt-fixer
2422
- id: sort-simple-yaml
2523
- id: trailing-whitespace
24+
- repo: https://github.com/pre-commit/mirrors-autopep8
25+
rev: v1.4.3
26+
hooks:
27+
- id: autopep8
28+
- repo: https://gitlab.com/pycqa/flake8
29+
rev: 3.7.7
30+
hooks:
31+
- id: flake8
32+
language_version: python3.5
2633
- repo: https://github.com/asottile/reorder_python_imports.git
27-
sha: v0.3.5
34+
rev: v1.4.0
2835
hooks:
2936
- id: reorder-python-imports
3037
- repo: https://github.com/asottile/pyupgrade.git
31-
sha: v1.2.0
38+
rev: v1.12.0
3239
hooks:
3340
- id: pyupgrade
3441
args: ['--py3-plus']
3542
- repo: https://github.com/Lucas-C/pre-commit-hooks.git
36-
sha: v1.1.1
43+
rev: v1.1.6
3744
hooks:
3845
- id: remove-tabs

ocflib/account/creation.py

+24-7
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from ocflib.printing.quota import SEMESTERLY_QUOTA
2929

3030

31-
_KNOWN_UID = 56162
31+
_KNOWN_UID = 58676
3232
BAD_WORDS = frozenset((
3333
'anal', 'anus', 'arse', 'ass', 'bastard', 'bitch', 'biatch', 'bloody', 'blowjob', 'bollock',
3434
'bollok', 'boner', 'chink', 'clit', 'cock', 'coon', 'cunt', 'damn', 'dick', 'dildo', 'douche',
@@ -49,6 +49,15 @@
4949
zwIDAQAB
5050
-----END PUBLIC KEY-----'''
5151

52+
# Inclusive endpoints
53+
RESERVED_UID_RANGES = [
54+
# 61184-65519 are the systemd dymanic users
55+
# 65534 is the nobody user
56+
# 65535 is the invalid user
57+
# we reserve the gaps between these for extra safety
58+
(61184, 65535),
59+
]
60+
5261

5362
def _get_first_available_uid(known_uid=_KNOWN_UID):
5463
"""Return the first available UID number.
@@ -72,7 +81,14 @@ def _get_first_available_uid(known_uid=_KNOWN_UID):
7281
else:
7382
# If cached UID is later deleted, LDAP response will be empty.
7483
max_uid = known_uid
75-
return max_uid + 1
84+
85+
assert all(start <= end for start, end in RESERVED_UID_RANGES)
86+
next_uid = max_uid + 1
87+
for start, end in sorted(RESERVED_UID_RANGES):
88+
if start <= next_uid <= end:
89+
next_uid = end + 1
90+
91+
return next_uid
7692

7793

7894
def create_account(request, creds, report_status, known_uid=_KNOWN_UID):
@@ -263,11 +279,12 @@ def validate_calnet_uid(uid):
263279
if not attrs:
264280
raise ValidationError("CalNet UID can't be found in university LDAP.")
265281

282+
# TODO: Uncomment when we get privileged LDAP bind.
266283
# check if user is eligible for an account
267-
affiliations = attrs['berkeleyEduAffiliations']
268-
if not eligible_for_account(affiliations):
269-
raise ValidationWarning(
270-
'Affiliate type not eligible for account: ' + str(affiliations))
284+
# affiliations = attrs['berkeleyEduAffiliations']
285+
# if not eligible_for_account(affiliations):
286+
# raise ValidationWarning(
287+
# 'Affiliate type not eligible for account: ' + str(affiliations))
271288

272289

273290
def eligible_for_account(affiliations):
@@ -366,7 +383,7 @@ def similarity_heuristic(realname, username):
366383
max_words = 8
367384
max_iterations = math.factorial(max_words)
368385

369-
words = re.findall('\w+', realname)
386+
words = re.findall(r'\w+', realname)
370387
initials = [word[0] for word in words]
371388

372389
if len(words) > max_words:

ocflib/account/search.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def users_by_callink_oid(callink_oid):
3232
return users_by_filter('(callinkOid={})'.format(callink_oid))
3333

3434

35-
def user_attrs(uid, connection=ldap.ldap_ocf, base=OCF_LDAP_PEOPLE, dn=None, password=None):
35+
def user_attrs(uid, connection=ldap.ldap_ocf, base=OCF_LDAP_PEOPLE):
3636
"""Returns a dictionary of LDAP attributes for a given LDAP UID.
3737
3838
The returned dictionary looks like:
@@ -44,7 +44,7 @@ def user_attrs(uid, connection=ldap.ldap_ocf, base=OCF_LDAP_PEOPLE, dn=None, pas
4444
4545
Returns None if no account exists with uid=user_account.
4646
"""
47-
with connection(dn, password) as c:
47+
with connection() as c:
4848
c.search(base, '(uid={})'.format(uid), attributes=ldap3.ALL_ATTRIBUTES)
4949

5050
if len(c.response) > 0:
@@ -58,7 +58,6 @@ def user_attrs_ucb(uid):
5858

5959
def user_attrs_ucb_privileged(uid):
6060
return user_attrs(uid, connection=ldap.ldap_ucb_privileged,
61-
base=UCB_LDAP_PEOPLE)
6261

6362

6463
def user_exists(account):

ocflib/account/validators.py

+4
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
'email',
6969
'epidemic',
7070
'facebook',
71+
'families',
7172
'faq',
7273
'ftp',
7374
'games',
@@ -90,6 +91,7 @@
9091
'https',
9192
'icinga',
9293
'info',
94+
'infra',
9395
'iodine',
9496
'irc',
9597
'jabber',
@@ -171,6 +173,7 @@
171173
'rtkit',
172174
'sales',
173175
'saned',
176+
'sdocs',
174177
'secretary',
175178
'senate-resolution',
176179
'servers',
@@ -263,6 +266,7 @@
263266
'announce',
264267
'decal',
265268
'decal-announce',
269+
'extcomm',
266270
'gm',
267271
'jenkins',
268272
'joinstaff',

ocflib/infra/hosts.py

+11
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ def hostname_from_domain(fqdn):
4040
return fqdn.split('.')[0]
4141

4242

43+
def domain_from_hostname(hostname):
44+
"""Return the canonical domain from a hostname, and if it's already a hostname, just return itself.
45+
46+
>>> domain_from_hostname('tsunami')
47+
'tsunami.ocf.berkeley.edu'
48+
"""
49+
if not hostname.endswith('.ocf.berkeley.edu'):
50+
return hostname + '.ocf.berkeley.edu'
51+
return hostname
52+
53+
4354
def type_of_host(hostname):
4455
"""Returns the type of a host as specified in LDAP.
4556

ocflib/infra/kanboard.py

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""Print Kanboard task information."""
2+
import json
3+
from collections import namedtuple
4+
5+
import requests
6+
7+
8+
KANBOARD_ROOT = 'https://kanboard.ocf.berkeley.edu'
9+
10+
11+
def request(usr, api_key, method, params):
12+
"""Sends a request to the Kanboard API in JSON-RPC 2.0 format."""
13+
# The purpose of id is to be able to reorder responses to asynchronous,
14+
# batched requests. Any valid integer is OK here, since we're just making
15+
# one simple request.
16+
payload = json.dumps({'jsonrpc': '2.0', 'method': method, 'id': 1, 'params': params})
17+
return requests.post(
18+
'{}/jsonrpc.php'.format(KANBOARD_ROOT),
19+
data=payload,
20+
auth=(usr, api_key),
21+
timeout=10,
22+
)
23+
24+
25+
class KanboardError(ValueError):
26+
pass
27+
28+
29+
class KanboardTask(namedtuple('KanboardTask', ('number', 'title', 'creator', 'project'))):
30+
"""A namedtuple representing a Kanboard task."""
31+
32+
def __str__(self):
33+
return (
34+
'k#{self.number}: "{self.title}" | '
35+
'{self.project}, started by {self.creator} | '
36+
'https://ocf.io/k/{self.number}'
37+
).format(self=self)
38+
39+
@classmethod
40+
def from_number(cls, usr, api_key, num):
41+
"""Gets information about a Kanboard task based on its number.
42+
43+
Example usage:
44+
45+
KanboardTask.from_number('jsonrpc',
46+
'19ffd9709d03ce50675c3a43d1c49c1ac207f4bc45f06c5b2701fbdf8929', 1)
47+
48+
:param usr: either the Kanboard username corresponding to the api key for a user
49+
api key or 'jsonrpc' for the application api key
50+
:param api_key: a user api key (which can be found under My profile -> Actions -> API) or the
51+
application api key (which only admins can see)
52+
"""
53+
54+
task_resp = request(usr, api_key, 'getTask', {'task_id': num})
55+
if task_resp.status_code != 200:
56+
raise KanboardError(
57+
'Task request gave {}'.format(task_resp.status_code)
58+
)
59+
60+
task = task_resp.json()['result']
61+
62+
users_resp = request(usr, api_key, 'getProjectUsers', {'project_id': task['project_id']})
63+
if users_resp.status_code != 200:
64+
raise KanboardError(
65+
'Project request gave {}'.format(users_resp.status_code)
66+
)
67+
68+
users = users_resp.json()['result']
69+
70+
proj_resp = request(usr, api_key, 'getProjectById', {'project_id': task['project_id']})
71+
if proj_resp.status_code != 200:
72+
raise KanboardError(
73+
'Project request gave {}'.format(proj_resp.status_code)
74+
)
75+
76+
proj = proj_resp.json()['result']
77+
78+
return cls(
79+
number=task['id'],
80+
title=task['title'],
81+
creator=users[task['creator_id']],
82+
project=proj['name'],
83+
)

ocflib/infra/ldap.py

+9-38
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,11 @@
2121
UCB_LDAP = 'ldap.berkeley.edu'
2222
UCB_LDAP_URL = 'ldaps://' + UCB_LDAP
2323
UCB_LDAP_PEOPLE = 'ou=People,dc=Berkeley,dc=EDU'
24-
UCB_LDAP_DN = 'uid=ocf,ou=applications,dc=berkeley,dc=edu'
25-
UCB_LDAP_PASSWORD_PATH = '/etc/ucbldap.passwd'
2624

2725

2826
@contextmanager
29-
def ldap_connection(host, dn=None, password=None):
30-
"""Context manager that provides an ldap3 Connection. Also supports optional credentials for a privileged bind.
27+
def ldap_connection(host):
28+
"""Context manager that provides an ldap3 Connection.
3129
3230
Example usage:
3331
@@ -39,47 +37,31 @@ def ldap_connection(host, dn=None, password=None):
3937
4038
:param host: server hostname
4139
"""
42-
4340
server = ldap3.Server(host, use_ssl=True)
44-
with ldap3.Connection(server, dn, password) as connection:
41+
with ldap3.Connection(server) as connection:
4542
yield connection
4643

4744

48-
def ldap_ocf(dn=None, password=None):
49-
"""Context manager that provides an ldap3 Connection to OCF's LDAP server. Accepts optional DN and password.
45+
def ldap_ocf():
46+
"""Context manager that provides an ldap3 Connection to OCF's LDAP server.
5047
5148
Example usage:
5249
5350
with ldap_ocf() as c:
5451
c.search(OCF_LDAP_PEOPLE, '(uid=ckuehl)', attributes=['uidNumber'])
5552
"""
56-
return ldap_connection(OCF_LDAP, dn, password)
53+
return ldap_connection(OCF_LDAP)
5754

5855

59-
def ldap_ucb(dn=None, password=None):
60-
"""Context manager that provides an ldap3 Connection to the campus LDAP. Accepts optional DN and password.
56+
def ldap_ucb():
57+
"""Context manager that provides an ldap3 Connection to the campus LDAP.
6158
6259
Example usage:
6360
6461
with ldap_ucb() as c:
6562
c.search(UCB_LDAP_PEOPLE, '(uid=ckuehl)', attributes=['uidNumber'])
6663
"""
67-
return ldap_connection(UCB_LDAP, dn, password)
68-
69-
70-
def ldap_ucb_privileged(dn=None, password=None):
71-
"""Context manager that provides a privileged ldap3 Connection to the campus LDAP.
72-
73-
Note that this method will ignore all dn and password arguments,
74-
which are being kept for compatibility with user_attrs().
75-
76-
Example usage:
77-
78-
with ldap_ucb_privileged() as c:
79-
c.search(UCB_LDAP_PEOPLE, '(uid=ckuehl)', attributes=['uidNumber'])
80-
"""
81-
password = _read_ucb_password()
82-
return ldap_ucb(UCB_LDAP_DN, password)
64+
return ldap_connection(UCB_LDAP)
8365

8466

8567
def _format_attr(key, values):
@@ -244,14 +226,3 @@ def format_timestamp(timestamp):
244226
if timestamp.tzinfo is None or timestamp.tzinfo.utcoffset(timestamp) is None:
245227
raise ValueError('Timestamp has no timezone info')
246228
return timestamp.strftime('%Y%m%d%H%M%S%z')
247-
248-
249-
def _read_ucb_password():
250-
"""Returns a string of the current campus LDAP privileged bind password
251-
found in UCB_LDAP_PASSWORD_PATH
252-
253-
:return: A string of the campus LDAP bind password
254-
"""
255-
256-
with open(UCB_LDAP_PASSWORD_PATH, 'r') as passwordFile:
257-
return passwordFile.read()

0 commit comments

Comments
 (0)