Skip to content

Commit

Permalink
Merge branch 'master' into privileged_ldap
Browse files Browse the repository at this point in the history
  • Loading branch information
ja5087 authored Apr 21, 2019
2 parents 78f6518 + 4620511 commit 8b92ffc
Show file tree
Hide file tree
Showing 22 changed files with 705 additions and 309 deletions.
19 changes: 13 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
exclude: ^vendor/
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks.git
sha: v0.9.5
rev: v2.1.0
hooks:
- id: autopep8-wrapper
- id: check-added-large-files
- id: check-docstring-first
- id: check-executables-have-shebangs
Expand All @@ -17,22 +16,30 @@ repos:
- id: double-quote-string-fixer
- id: end-of-file-fixer
- id: file-contents-sorter
- id: flake8
- id: mixed-line-ending
- id: name-tests-test
- id: requirements-txt-fixer
- id: sort-simple-yaml
- id: trailing-whitespace
- repo: https://github.com/pre-commit/mirrors-autopep8
rev: v1.4.3
hooks:
- id: autopep8
- repo: https://gitlab.com/pycqa/flake8
rev: 3.7.7
hooks:
- id: flake8
language_version: python3.5
- repo: https://github.com/asottile/reorder_python_imports.git
sha: v0.3.5
rev: v1.4.0
hooks:
- id: reorder-python-imports
- repo: https://github.com/asottile/pyupgrade.git
sha: v1.2.0
rev: v1.12.0
hooks:
- id: pyupgrade
args: ['--py3-plus']
- repo: https://github.com/Lucas-C/pre-commit-hooks.git
sha: v1.1.1
rev: v1.1.6
hooks:
- id: remove-tabs
31 changes: 24 additions & 7 deletions ocflib/account/creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from ocflib.printing.quota import SEMESTERLY_QUOTA


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

# Inclusive endpoints
RESERVED_UID_RANGES = [
# 61184-65519 are the systemd dymanic users
# 65534 is the nobody user
# 65535 is the invalid user
# we reserve the gaps between these for extra safety
(61184, 65535),
]


def _get_first_available_uid(known_uid=_KNOWN_UID):
"""Return the first available UID number.
Expand All @@ -72,7 +81,14 @@ def _get_first_available_uid(known_uid=_KNOWN_UID):
else:
# If cached UID is later deleted, LDAP response will be empty.
max_uid = known_uid
return max_uid + 1

assert all(start <= end for start, end in RESERVED_UID_RANGES)
next_uid = max_uid + 1
for start, end in sorted(RESERVED_UID_RANGES):
if start <= next_uid <= end:
next_uid = end + 1

return next_uid


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

# TODO: Uncomment when we get privileged LDAP bind.
# check if user is eligible for an account
affiliations = attrs['berkeleyEduAffiliations']
if not eligible_for_account(affiliations):
raise ValidationWarning(
'Affiliate type not eligible for account: ' + str(affiliations))
# affiliations = attrs['berkeleyEduAffiliations']
# if not eligible_for_account(affiliations):
# raise ValidationWarning(
# 'Affiliate type not eligible for account: ' + str(affiliations))


def eligible_for_account(affiliations):
Expand Down Expand Up @@ -366,7 +383,7 @@ def similarity_heuristic(realname, username):
max_words = 8
max_iterations = math.factorial(max_words)

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

if len(words) > max_words:
Expand Down
5 changes: 2 additions & 3 deletions ocflib/account/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def users_by_callink_oid(callink_oid):
return users_by_filter('(callinkOid={})'.format(callink_oid))


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

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

def user_attrs_ucb_privileged(uid):
return user_attrs(uid, connection=ldap.ldap_ucb_privileged,
base=UCB_LDAP_PEOPLE)


def user_exists(account):
Expand Down
4 changes: 4 additions & 0 deletions ocflib/account/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
'email',
'epidemic',
'facebook',
'families',
'faq',
'ftp',
'games',
Expand All @@ -90,6 +91,7 @@
'https',
'icinga',
'info',
'infra',
'iodine',
'irc',
'jabber',
Expand Down Expand Up @@ -171,6 +173,7 @@
'rtkit',
'sales',
'saned',
'sdocs',
'secretary',
'senate-resolution',
'servers',
Expand Down Expand Up @@ -263,6 +266,7 @@
'announce',
'decal',
'decal-announce',
'extcomm',
'gm',
'jenkins',
'joinstaff',
Expand Down
11 changes: 11 additions & 0 deletions ocflib/infra/hosts.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ def hostname_from_domain(fqdn):
return fqdn.split('.')[0]


def domain_from_hostname(hostname):
"""Return the canonical domain from a hostname, and if it's already a hostname, just return itself.
>>> domain_from_hostname('tsunami')
'tsunami.ocf.berkeley.edu'
"""
if not hostname.endswith('.ocf.berkeley.edu'):
return hostname + '.ocf.berkeley.edu'
return hostname


def type_of_host(hostname):
"""Returns the type of a host as specified in LDAP.
Expand Down
83 changes: 83 additions & 0 deletions ocflib/infra/kanboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Print Kanboard task information."""
import json
from collections import namedtuple

import requests


KANBOARD_ROOT = 'https://kanboard.ocf.berkeley.edu'


def request(usr, api_key, method, params):
"""Sends a request to the Kanboard API in JSON-RPC 2.0 format."""
# The purpose of id is to be able to reorder responses to asynchronous,
# batched requests. Any valid integer is OK here, since we're just making
# one simple request.
payload = json.dumps({'jsonrpc': '2.0', 'method': method, 'id': 1, 'params': params})
return requests.post(
'{}/jsonrpc.php'.format(KANBOARD_ROOT),
data=payload,
auth=(usr, api_key),
timeout=10,
)


class KanboardError(ValueError):
pass


class KanboardTask(namedtuple('KanboardTask', ('number', 'title', 'creator', 'project'))):
"""A namedtuple representing a Kanboard task."""

def __str__(self):
return (
'k#{self.number}: "{self.title}" | '
'{self.project}, started by {self.creator} | '
'https://ocf.io/k/{self.number}'
).format(self=self)

@classmethod
def from_number(cls, usr, api_key, num):
"""Gets information about a Kanboard task based on its number.
Example usage:
KanboardTask.from_number('jsonrpc',
'19ffd9709d03ce50675c3a43d1c49c1ac207f4bc45f06c5b2701fbdf8929', 1)
:param usr: either the Kanboard username corresponding to the api key for a user
api key or 'jsonrpc' for the application api key
:param api_key: a user api key (which can be found under My profile -> Actions -> API) or the
application api key (which only admins can see)
"""

task_resp = request(usr, api_key, 'getTask', {'task_id': num})
if task_resp.status_code != 200:
raise KanboardError(
'Task request gave {}'.format(task_resp.status_code)
)

task = task_resp.json()['result']

users_resp = request(usr, api_key, 'getProjectUsers', {'project_id': task['project_id']})
if users_resp.status_code != 200:
raise KanboardError(
'Project request gave {}'.format(users_resp.status_code)
)

users = users_resp.json()['result']

proj_resp = request(usr, api_key, 'getProjectById', {'project_id': task['project_id']})
if proj_resp.status_code != 200:
raise KanboardError(
'Project request gave {}'.format(proj_resp.status_code)
)

proj = proj_resp.json()['result']

return cls(
number=task['id'],
title=task['title'],
creator=users[task['creator_id']],
project=proj['name'],
)
47 changes: 9 additions & 38 deletions ocflib/infra/ldap.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,11 @@
UCB_LDAP = 'ldap.berkeley.edu'
UCB_LDAP_URL = 'ldaps://' + UCB_LDAP
UCB_LDAP_PEOPLE = 'ou=People,dc=Berkeley,dc=EDU'
UCB_LDAP_DN = 'uid=ocf,ou=applications,dc=berkeley,dc=edu'
UCB_LDAP_PASSWORD_PATH = '/etc/ucbldap.passwd'


@contextmanager
def ldap_connection(host, dn=None, password=None):
"""Context manager that provides an ldap3 Connection. Also supports optional credentials for a privileged bind.
def ldap_connection(host):
"""Context manager that provides an ldap3 Connection.
Example usage:
Expand All @@ -39,47 +37,31 @@ def ldap_connection(host, dn=None, password=None):
:param host: server hostname
"""

server = ldap3.Server(host, use_ssl=True)
with ldap3.Connection(server, dn, password) as connection:
with ldap3.Connection(server) as connection:
yield connection


def ldap_ocf(dn=None, password=None):
"""Context manager that provides an ldap3 Connection to OCF's LDAP server. Accepts optional DN and password.
def ldap_ocf():
"""Context manager that provides an ldap3 Connection to OCF's LDAP server.
Example usage:
with ldap_ocf() as c:
c.search(OCF_LDAP_PEOPLE, '(uid=ckuehl)', attributes=['uidNumber'])
"""
return ldap_connection(OCF_LDAP, dn, password)
return ldap_connection(OCF_LDAP)


def ldap_ucb(dn=None, password=None):
"""Context manager that provides an ldap3 Connection to the campus LDAP. Accepts optional DN and password.
def ldap_ucb():
"""Context manager that provides an ldap3 Connection to the campus LDAP.
Example usage:
with ldap_ucb() as c:
c.search(UCB_LDAP_PEOPLE, '(uid=ckuehl)', attributes=['uidNumber'])
"""
return ldap_connection(UCB_LDAP, dn, password)


def ldap_ucb_privileged(dn=None, password=None):
"""Context manager that provides a privileged ldap3 Connection to the campus LDAP.
Note that this method will ignore all dn and password arguments,
which are being kept for compatibility with user_attrs().
Example usage:
with ldap_ucb_privileged() as c:
c.search(UCB_LDAP_PEOPLE, '(uid=ckuehl)', attributes=['uidNumber'])
"""
password = _read_ucb_password()
return ldap_ucb(UCB_LDAP_DN, password)
return ldap_connection(UCB_LDAP)


def _format_attr(key, values):
Expand Down Expand Up @@ -244,14 +226,3 @@ def format_timestamp(timestamp):
if timestamp.tzinfo is None or timestamp.tzinfo.utcoffset(timestamp) is None:
raise ValueError('Timestamp has no timezone info')
return timestamp.strftime('%Y%m%d%H%M%S%z')


def _read_ucb_password():
"""Returns a string of the current campus LDAP privileged bind password
found in UCB_LDAP_PASSWORD_PATH
:return: A string of the campus LDAP bind password
"""

with open(UCB_LDAP_PASSWORD_PATH, 'r') as passwordFile:
return passwordFile.read()
Loading

0 comments on commit 8b92ffc

Please sign in to comment.