Compare commits

...

72 Commits

Author SHA1 Message Date
Rio Liu 6577fb3ea6 fix flake8 2 years ago
Rio Liu 36def99b28 add unit test for positions and replace provideUtility() with getGlobalSiteManager().registerUtility() in unit tests 2 years ago
Rio Liu 5bae89a9fd handle empty input in print_colon_kv 2 years ago
Rio Liu 8f5a2803a6 fix some pr feedbacks 2 years ago
Rio6 8c9ddd2d27 fix flake 2 years ago
Rio6 a08a28c98f positions cli 2 years ago
Andrew Wang eb5d632606 db-api (#10) 2 years ago
Max Erenberg 7d23fd690f store GSSAPI token in flask.g 2 years ago
Max Erenberg d8e5b1f1d4 update README 2 years ago
Max Erenberg 46881f7a1f update .drone.yml 2 years ago
Max Erenberg e011e98026 use GSSAPI delegation 2 years ago
Max Erenberg 95e167578f remove libsasl2-dev dependency 2 years ago
Max Erenberg 51737585bd add updateprograms CLI 2 years ago
Max Erenberg 831ebf17aa add groups CLI 2 years ago
Max Erenberg 45192d75bf update social media links in welcome message 2 years ago
Max Erenberg e851c77e74 include password in welcome email 2 years ago
Max Erenberg 08a3faaefc add unit tests for members CLI 2 years ago
Max Erenberg 7a8751fd8f Merge branch 'v1' of csclub.uwaterloo.ca:public/pyceo into v1 2 years ago
Max Erenberg 6917247fdd add members CLI 2 years ago
Rio Liu ad937eebeb Positions API (#7) 2 years ago
Max Erenberg 0974a7471b ignore UserAlreadySubscribedError 2 years ago
Max Erenberg 0783588323 announce new user to ceo mailing list 2 years ago
Max Erenberg 7142659a8c force delete Kerberos test principals 2 years ago
Max Erenberg 862dfc01b2 add trigger branches to drone.yml 2 years ago
Max Erenberg bb82945b41 remove hostname from /etc/hosts in auth1 2 years ago
Max Erenberg 38f354c106 add sasl-host to slapd.conf 2 years ago
Max Erenberg 95d083fca1 use our own SPNEGO implementation 2 years ago
Max Erenberg 89e6c541ab add hostname check 2 years ago
Max Erenberg c39eff6ca7 let service container sleep 2 years ago
Max Erenberg e4970bf008 remove search option from resolv.conf 2 years ago
Max Erenberg d11c6af2ec add tests to drone.yml 2 years ago
Max Erenberg 4783621d22 update CI badge 2 years ago
Max Erenberg 14273dcbe6 add drone.yml 2 years ago
Max Erenberg 14c058eb67 use socket.gethostname() in krb5 test 2 years ago
Max Erenberg dc09210d23 add documentation about architecture 2 years ago
Max Erenberg 583fcded9b add test for API request without KRB-CRED 2 years ago
Max Erenberg 46fd926acc add test for RemoteMailmanService 2 years ago
Max Erenberg 490abb302c add simple authz tests 2 years ago
Max Erenberg 26fd8f6f68 remove duplicate function definition 2 years ago
Max Erenberg 2a286579cb Merge branch 'v1' into uwldap_tests 2 years ago
Max Erenberg ecf089c261 Implement Groups API (#6) 2 years ago
Max Erenberg cc0bc4a638 add tests for Mailman API 2 years ago
Max Erenberg 2273ffa241 add test for krb5 2 years ago
Max Erenberg 12a83ce4c0 remove create_sync_response 2 years ago
Max Erenberg 28c55b2fed add tests for UWLDAP API 2 years ago
Max Erenberg 448692018a add test for group.to_dict() with one member 2 years ago
Max Erenberg 6bf4d75a60 log error message instead of traceback 2 years ago
Max Erenberg 5bda74eaf9 fix test_group_to_dict 2 years ago
Max Erenberg df5d9e5f14 Merge branch 'v1' into groups_api 2 years ago
Max Erenberg 57ab275634 implement /api/groups endpoints 2 years ago
Max Erenberg e370035b25 add cffi as dev dependency 2 years ago
Max Erenberg d78d31eec0 add Kerberos delegation (#5) 2 years ago
Max Erenberg dd59bea918 add Kerberos delegation 2 years ago
Max Erenberg d82b5a763b use ldap3 instead of python-ldap 2 years ago
Max Erenberg 6cdb41d47b move all tests to top-level folder 2 years ago
Max Erenberg cbf4aa43f8 add tests for uwldap 2 years ago
Max Erenberg 9e4d564a33 move INI file locations 2 years ago
Max Erenberg 3ecf43731f add tests for Group class 2 years ago
Max Erenberg e7bfe36c0b add tests for User class 2 years ago
Max Erenberg 87298e18b3 cast string values in Config 2 years ago
Max Erenberg baeb83b1e2 use ConfigParser 2 years ago
Max Erenberg 4a312378b7 remove mailman transactions 2 years ago
Max Erenberg 96cb2bc808 add updateprograms 2 years ago
Max Erenberg 7c67a07200 use create_sync_response 2 years ago
Max Erenberg c32e565f68 implement renewals and password resets 2 years ago
Max Erenberg da14764687 Merge branch 'v1' of https://git.csclub.uwaterloo.ca/public/pyceo into v1 2 years ago
Max Erenberg ff2ac95d5e add PATCH /api/members/:username endpoint 2 years ago
Max Erenberg 9227552b29 re-send EHLO after STARTTLS 2 years ago
Max Erenberg 7b749701f0 add README 2 years ago
Max Erenberg e966e3f307 add app factory 2 years ago
Max Erenberg 3b78b7ffb4 add MailService and MailmanService 2 years ago
Max Erenberg de0f473881 add base classes for users and groups 2 years ago
  1. 41
      .drone.yml
  2. 83
      .drone/auth1-setup.sh
  3. 48
      .drone/coffee-setup.sh
  4. 17
      .drone/common.sh
  5. 0
      .drone/csc.schema
  6. 125
      .drone/data.ldif
  7. 19
      .drone/kdc.conf
  8. 27
      .drone/krb5.conf
  9. 3
      .drone/ldap.conf
  10. 20
      .drone/nsswitch.conf
  11. 64
      .drone/phosphoric-acid-setup.sh
  12. 287
      .drone/rfc2307bis.schema
  13. 108
      .drone/slapd.conf
  14. 4
      .gbp.conf
  15. 10
      .gitignore
  16. 173
      README.md
  17. 70
      architecture.md
  18. 40
      bin/ceo
  19. 5
      build.sh
  20. 1
      ceo/.gitignore
  21. 1
      ceo/__init__.py
  22. 4
      ceo/__main__.py
  23. 1
      ceo/cli/__init__.py
  24. 53
      ceo/cli/entrypoint.py
  25. 148
      ceo/cli/groups.py
  26. 218
      ceo/cli/members.py
  27. 51
      ceo/cli/positions.py
  28. 35
      ceo/cli/updateprograms.py
  29. 124
      ceo/cli/utils.py
  30. 162
      ceo/conf.py
  31. 1
      ceo/console/__init__.py
  32. 40
      ceo/console/expiredaccounts.py
  33. 27
      ceo/console/inactive.py
  34. 49
      ceo/console/main.py
  35. 24
      ceo/console/memberlist.py
  36. 38
      ceo/console/mysql.py
  37. 49
      ceo/console/updateprograms.py
  38. 13
      ceo/excep.py
  39. 24
      ceo/krb_check.py
  40. 148
      ceo/ldapi.py
  41. 609
      ceo/members.py
  42. 54
      ceo/mysql.py
  43. 30
      ceo/operation_strings.py
  44. 24
      ceo/ops.py
  45. 155
      ceo/pymazon.py
  46. 18
      ceo/remote.py
  47. 254
      ceo/terms.py
  48. 42
      ceo/test.py
  49. 1
      ceo/urwid/__init__.py
  50. 84
      ceo/urwid/databases.py
  51. 135
      ceo/urwid/groups.py
  52. 47
      ceo/urwid/info.py
  53. 8
      ceo/urwid/library.py
  54. 192
      ceo/urwid/main.py
  55. 267
      ceo/urwid/newmember.py
  56. 97
      ceo/urwid/positions.py
  57. 240
      ceo/urwid/renew.py
  58. 83
      ceo/urwid/search.py
  59. 95
      ceo/urwid/shell.py
  60. 247
      ceo/urwid/widgets.py
  61. 80
      ceo/urwid/window.py
  62. 58
      ceo/utils.py
  63. 8
      ceo/uwldap.py
  64. 0
      ceo_common/__init__.py
  65. 66
      ceo_common/errors.py
  66. 8
      ceo_common/interfaces/IConfig.py
  67. 18
      ceo_common/interfaces/IDatabaseService.py
  68. 29
      ceo_common/interfaces/IFileService.py
  69. 30
      ceo_common/interfaces/IGroup.py
  70. 24
      ceo_common/interfaces/IHTTPClient.py
  71. 14
      ceo_common/interfaces/IKerberosService.py
  72. 89
      ceo_common/interfaces/ILDAPService.py
  73. 25
      ceo_common/interfaces/IMailService.py
  74. 11
      ceo_common/interfaces/IMailmanService.py
  75. 25
      ceo_common/interfaces/IUWLDAPService.py
  76. 82
      ceo_common/interfaces/IUser.py
  77. 11
      ceo_common/interfaces/__init__.py
  78. 16
      ceo_common/logger_factory.py
  79. 32
      ceo_common/model/Config.py
  80. 59
      ceo_common/model/HTTPClient.py
  81. 34
      ceo_common/model/RemoteMailmanService.py
  82. 67
      ceo_common/model/Term.py
  83. 4
      ceo_common/model/__init__.py
  84. 0
      ceod/__init__.py
  85. 1
      ceod/api/__init__.py
  86. 120
      ceod/api/app_factory.py
  87. 104
      ceod/api/database.py
  88. 30
      ceod/api/error_handlers.py
  89. 68
      ceod/api/groups.py
  90. 33
      ceod/api/mailman.py
  91. 97
      ceod/api/members.py
  92. 45
      ceod/api/positions.py
  93. 69
      ceod/api/spnego.py
  94. 132
      ceod/api/utils.py
  95. 35
      ceod/api/uwldap.py
  96. 88
      ceod/db/MySQLService.py
  97. 86
      ceod/db/PostgreSQLService.py
  98. 2
      ceod/db/__init__.py
  99. 5
      ceod/db/utils.py
  100. 79
      ceod/model/FileService.py
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1,41 @@
kind: pipeline
type: docker
name: default
steps:
# use the step name to mock out the gethostname() call in our tests
- name: phosphoric-acid
image: python:3.7-buster
# unfortunately we have to do everything in one step because there's no
# way to share system packages between steps
commands:
# install dependencies
- apt update && apt install -y libkrb5-dev libpq-dev python3-dev
- python3 -m venv venv
- . venv/bin/activate
- pip install -r dev-requirements.txt
- pip install -r requirements.txt
# lint
- flake8
# unit + integration tests
- .drone/phosphoric-acid-setup.sh
- pytest -v
services:
- name: auth1
image: debian:buster
commands:
- .drone/auth1-setup.sh
- sleep infinity
- name: coffee
image: debian:buster
commands:
- .drone/coffee-setup.sh
- sleep infinity
trigger:
branch:
- master
- v1

@ -0,0 +1,83 @@
#!/bin/bash
set -ex
. .drone/common.sh
# set FQDN in /etc/hosts
add_fqdn_to_hosts $(get_ip_addr $(hostname)) auth1
# I'm not sure why, but we also need to remove the hosts entry for the
# container's real hostname, otherwise slapd only looks for the principal
# ldap/<container hostname> (this is with the sasl-host option)
sed -E "/\\b$(hostname)\\b/d" /etc/hosts > /tmp/hosts
cat /tmp/hosts > /etc/hosts
rm /tmp/hosts
export DEBIAN_FRONTEND=noninteractive
apt update
apt install -y psmisc
# LDAP
apt install -y --no-install-recommends slapd ldap-utils libnss-ldapd sudo-ldap
# `service slapd stop` doesn't seem to work
killall slapd || true
service nslcd stop || true
rm -rf /etc/ldap/slapd.d
rm /var/lib/ldap/*
cp /usr/share/slapd/DB_CONFIG /var/lib/ldap/DB_CONFIG
cp .drone/slapd.conf /etc/ldap/slapd.conf
cp .drone/ldap.conf /etc/ldap/ldap.conf
cp /usr/share/doc/sudo-ldap/schema.OpenLDAP /etc/ldap/schema/sudo.schema
cp .drone/rfc2307bis.schema /etc/ldap/schema/
cp .drone/csc.schema /etc/ldap/schema/
chown -R openldap:openldap /etc/ldap/schema/ /var/lib/ldap/ /etc/ldap/
sleep 0.5 && service slapd start
grep -Eq '^map group member uniqueMember$' /etc/nslcd.conf || \
echo 'map group member uniqueMember' >> /etc/nslcd.conf
sed -E -i 's/^uri .*$/uri ldap:\/\/auth1.csclub.internal/' /etc/nslcd.conf
sed -E -i 's/^base .*$/base dc=csclub,dc=internal/' /etc/nslcd.conf
cp .drone/nsswitch.conf /etc/nsswitch.conf
service nslcd start
ldapadd -c -f .drone/data.ldif -Y EXTERNAL -H ldapi:///
# KERBEROS
apt install -y krb5-admin-server krb5-user libpam-krb5 libsasl2-modules-gssapi-mit sasl2-bin
service krb5-admin-server stop || true
service krb5-kdc stop || true
service saslauthd stop || true
cp .drone/krb5.conf /etc/krb5.conf
cp .drone/kdc.conf /etc/krb5kdc.conf
echo '*/admin *' > /etc/krb5kdc/kadm5.acl
rm -f /var/lib/krb5kdc/*
echo -e 'krb5\nkrb5' | krb5_newrealm
service krb5-kdc start
service krb5-admin-server start
rm -f /etc/krb5.keytab
cat <<EOF | kadmin.local
addpol -minlength 4 default
addprinc -pw krb5 sysadmin/admin
addprinc -pw krb5 ctdalek
addprinc -pw krb5 regular1
addprinc -randkey host/auth1.csclub.internal
addprinc -randkey ldap/auth1.csclub.internal
ktadd host/auth1.csclub.internal
ktadd ldap/auth1.csclub.internal
EOF
groupadd keytab || true
chgrp keytab /etc/krb5.keytab
chmod 640 /etc/krb5.keytab
usermod -a -G keytab openldap
usermod -a -G sasl openldap
cat <<EOF > /usr/lib/sasl2/slapd.conf
mech_list: plain login gssapi external
pwcheck_method: saslauthd
EOF
sed -E -i 's/^START=.*$/START=yes/' /etc/default/saslauthd
sed -E -i 's/^MECHANISMS=.*$/MECHANISMS="kerberos5"/' /etc/default/saslauthd
service saslauthd start
killall slapd && sleep 0.5 && service slapd start
# sync with phosphoric-acid
apt install -y netcat-openbsd
nc -l 0.0.0.0 9000

@ -0,0 +1,48 @@
#!/bin/bash
set -ex
. .drone/common.sh
# set FQDN in /etc/hosts
add_fqdn_to_hosts $(get_ip_addr $(hostname)) coffee
export DEBIAN_FRONTEND=noninteractive
apt update
apt install --no-install-recommends -y default-mysql-server postgresql
service mysql stop
sed -E -i 's/^(bind-address[[:space:]]+= 127.0.0.1)$/#\1/' /etc/mysql/mariadb.conf.d/50-server.cnf
service mysql start
cat <<EOF | mysql
CREATE USER 'mysql' IDENTIFIED BY 'mysql';
GRANT ALL PRIVILEGES ON *.* TO 'mysql' WITH GRANT OPTION;
EOF
service postgresql stop
POSTGRES_DIR=/etc/postgresql/11/main
cat <<EOF > $POSTGRES_DIR/pg_hba.conf
# TYPE DATABASE USER ADDRESS METHOD
local all postgres peer
host all postgres 0.0.0.0/0 md5
local all all peer
host all all localhost md5
local sameuser all md5
host sameuser all 0.0.0.0/0 md5
EOF
grep -Eq "^listen_addresses = '*'$" $POSTGRES_DIR/postgresql.conf || \
echo "listen_addresses = '*'" >> $POSTGRES_DIR/postgresql.conf
service postgresql start
su -c "
cat <<EOF | psql
ALTER USER postgres WITH PASSWORD 'postgres';
REVOKE ALL ON SCHEMA public FROM public;
GRANT ALL ON SCHEMA public TO postgres;
EOF" postgres
# sync with phosphoric-acid
apt install -y netcat-openbsd
nc -l 0.0.0.0 9000

@ -0,0 +1,17 @@
# don't resolve container names to *real* CSC machines
sed -E '/^(domain|search)[[:space:]]+csclub.uwaterloo.ca/d' /etc/resolv.conf > /tmp/resolv.conf
cp /tmp/resolv.conf /etc/resolv.conf
rm /tmp/resolv.conf
get_ip_addr() {
getent hosts $1 | cut -d' ' -f1
}
add_fqdn_to_hosts() {
ip_addr=$1
hostname=$2
sed -E "/${ip_addr}.*\\b${hostname}\\b/d" /etc/hosts > /tmp/hosts
cp /tmp/hosts /etc/hosts
rm /tmp/hosts
echo "$ip_addr $hostname.csclub.internal $hostname" >> /etc/hosts
}

@ -0,0 +1,125 @@
dn: dc=csclub,dc=internal
objectClass: top
objectClass: dcObject
objectClass: organization
dc: csclub
o: Computer Science Club
dn: ou=People,dc=csclub,dc=internal
objectClass: organizationalUnit
ou: People
dn: ou=Group,dc=csclub,dc=internal
objectClass: organizationalUnit
ou: Group
dn: ou=SUDOers,dc=csclub,dc=internal
objectClass: top
objectClass: organizationalUnit
ou: SUDOers
dn: cn=defaults,ou=SUDOers,dc=csclub,dc=internal
objectClass: top
objectClass: sudoRole
cn: defaults
sudoOption: !insults
sudoOption: !lecture
sudoOption: env_reset
sudoOption: listpw=never
sudoOption: shell_noargs
sudoOption: !mail_badpass
dn: cn=syscom,ou=Group,dc=csclub,dc=internal
objectClass: top
objectClass: group
objectClass: posixGroup
cn: syscom
gidNumber: 10001
uniqueMember: uid=ctdalek,ou=People,dc=csclub,dc=internal
dn: cn=%syscom,ou=SUDOers,dc=csclub,dc=internal
objectClass: top
objectClass: sudoRole
cn: %syscom
sudoUser: %syscom
sudoHost: ALL
sudoCommand: ALL
sudoRunAsUser: ALL
dn: cn=adm,ou=Group,dc=csclub,dc=internal
objectClass: top
objectClass: group
objectClass: posixGroup
gidNumber: 4
cn: adm
uniqueMember: uid=ctdalek,ou=People,dc=csclub,dc=internal
dn: cn=office,ou=Group,dc=csclub,dc=internal
objectClass: top
objectClass: group
objectClass: posixGroup
gidNumber: 10003
cn: office
uniqueMember: uid=ctdalek,ou=People,dc=csclub,dc=internal
dn: cn=src,ou=Group,dc=csclub,dc=internal
objectClass: top
objectClass: group
objectClass: posixGroup
gidNumber: 40
cn: src
uniqueMember: uid=ctdalek,ou=People,dc=csclub,dc=internal
dn: cn=staff,ou=Group,dc=csclub,dc=internal
objectClass: top
objectClass: group
objectClass: posixGroup
gidNumber: 50
cn: staff
uniqueMember: uid=ctdalek,ou=People,dc=csclub,dc=internal
dn: uid=ctdalek,ou=People,dc=csclub,dc=internal
cn: Calum Dalek
userPassword: {SASL}ctdalek@CSCLUB.INTERNAL
loginShell: /bin/bash
homeDirectory: /users/ctdalek
uid: ctdalek
uidNumber: 20001
gidNumber: 20001
objectClass: top
objectClass: account
objectClass: posixAccount
objectClass: shadowAccount
objectClass: member
program: MAT/Mathematics Computer Science
term: s2021
dn: cn=ctdalek,ou=Group,dc=csclub,dc=internal
objectClass: top
objectClass: group
objectClass: posixGroup
cn: ctdalek
gidNumber: 20001
dn: uid=regular1,ou=People,dc=csclub,dc=internal
cn: Regular One
userPassword: {SASL}regular1@CSCLUB.INTERNAL
loginShell: /bin/bash
homeDirectory: /users/regular1
uid: regular1
uidNumber: 20002
gidNumber: 20002
objectClass: top
objectClass: account
objectClass: posixAccount
objectClass: shadowAccount
objectClass: member
program: MAT/Mathematics Computer Science
term: s2021
dn: cn=regular1,ou=Group,dc=csclub,dc=internal
objectClass: top
objectClass: group
objectClass: posixGroup
cn: regular1
gidNumber: 20002

@ -0,0 +1,19 @@
[kdcdefaults]
kdc_ports = 88
[realms]
CSCLUB.INTERNAL = {
database_name = /var/lib/krb5kdc/principal
admin_keytab = FILE:/etc/krb5kdc/kadm5.keytab
acl_file = /etc/krb5kdc/kadm5.acl
key_stash_file = /etc/krb5kdc/stash
kdc_ports = 88
max_life = 10h 0m 0s
max_renewable_life = 7d 0h 0m 0s
master_key_type = des3-hmac-sha1
supported_enctypes = aes256-cts:normal arcfour-hmac:normal des3-hmac-sha1:normal des3-cbc-sha1:normal des-cbc-crc:normal des:normal des:v4 des:norealm des:onlyrealm des:afs3
default_principal_flags = +preauth
iprop_enable = true
iprop_slave_poll = 2m
iprop_port = 750
}

@ -0,0 +1,27 @@
[libdefaults]
default_realm = CSCLUB.INTERNAL
kdc_timesync = 1
ccache_type = 4
forwardable = true
proxiable = true
dns_lookup_kdc = false
dns_lookup_realm = false
allow_weak_crypto = true
[realms]
CSCLUB.INTERNAL = {
kdc = auth1.csclub.internal
admin_server = auth1.csclub.internal
}
[domain_realm]
.csclub.internal = CSCLUB.INTERNAL
csclub.internal = CSCLUB.INTERNAL
[logging]
kdc = SYSLOG:INFO:AUTH
admin_server = SYSLOG:INFO:AUTH
default = SYSLOG:INFO:AUTH

@ -0,0 +1,3 @@
BASE dc=csclub,dc=internal
URI ldap://auth1.csclub.internal
SUDOERS_BASE ou=SUDOers,dc=csclub,dc=internal

@ -0,0 +1,20 @@
# /etc/nsswitch.conf
#
# Example configuration of GNU Name Service Switch functionality.
# If you have the `glibc-doc-reference' and `info' packages installed, try:
# `info libc "Name Service Switch"' for information about this file.
passwd: files ldap
group: files ldap
shadow: files ldap
hosts: files dns
networks: files
protocols: db files
services: db files
ethers: db files
rpc: db files
netgroup: nis
sudoers: files ldap

@ -0,0 +1,64 @@
#!/bin/bash
set -ex
. .drone/common.sh
sync_with() {
host=$1
synced=false
# give it 5 minutes
for i in {1..60}; do
if nc -vz $host 9000 ; then
synced=true
break
fi
sleep 5
done
test $synced = true
}
# set FQDN in /etc/hosts
add_fqdn_to_hosts $(get_ip_addr $(hostname)) phosphoric-acid
add_fqdn_to_hosts $(get_ip_addr auth1) auth1
add_fqdn_to_hosts $(get_ip_addr coffee) coffee
export DEBIAN_FRONTEND=noninteractive
apt update
# LDAP
apt install -y --no-install-recommends libnss-ldapd
service nslcd stop || true
cp .drone/ldap.conf /etc/ldap/ldap.conf
grep -Eq '^map group member uniqueMember$' /etc/nslcd.conf || \
echo 'map group member uniqueMember' >> /etc/nslcd.conf
sed -E -i 's/^uri .*$/uri ldap:\/\/auth1.csclub.internal/' /etc/nslcd.conf
sed -E -i 's/^base .*$/base dc=csclub,dc=internal/' /etc/nslcd.conf
cp .drone/nsswitch.conf /etc/nsswitch.conf
# KERBEROS
apt install -y krb5-user libpam-krb5 libsasl2-modules-gssapi-mit
cp .drone/krb5.conf /etc/krb5.conf
apt install -y netcat-openbsd
sync_with auth1
rm -f /etc/krb5.keytab
cat <<EOF | kadmin -p sysadmin/admin
krb5
addprinc -randkey host/phosphoric-acid.csclub.internal
ktadd host/phosphoric-acid.csclub.internal
addprinc -randkey ceod/phosphoric-acid.csclub.internal
ktadd ceod/phosphoric-acid.csclub.internal
addprinc -randkey ceod/admin
ktadd ceod/admin
EOF
service nslcd start
sync_with coffee
# initialize the skel directory
shopt -s dotglob
mkdir -p /users/skel
cp /etc/skel/* /users/skel/

@ -0,0 +1,287 @@
# builtin
#attributetype ( 1.3.6.1.1.1.1.0 NAME 'uidNumber'
# DESC 'An integer uniquely identifying a user in an administrative domain'
# EQUALITY integerMatch
# SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
# SINGLE-VALUE )
#
# builtin
#attributetype ( 1.3.6.1.1.1.1.1 NAME 'gidNumber'
# DESC 'An integer uniquely identifying a group in an
# administrative domain'
# EQUALITY integerMatch
# SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
# SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.2 NAME 'gecos'
DESC 'The GECOS field; the common name'
EQUALITY caseIgnoreIA5Match
SUBSTR caseIgnoreIA5SubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.3 NAME 'homeDirectory'
DESC 'The absolute path to the home directory'
EQUALITY caseExactIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.4 NAME 'loginShell'
DESC 'The path to the login shell'
EQUALITY caseExactIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.5 NAME 'shadowLastChange'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.6 NAME 'shadowMin'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.7 NAME 'shadowMax'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.8 NAME 'shadowWarning'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.9 NAME 'shadowInactive'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.10 NAME 'shadowExpire'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.11 NAME 'shadowFlag'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.12 NAME 'memberUid'
EQUALITY caseExactIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
attributetype ( 1.3.6.1.1.1.1.13 NAME 'memberNisNetgroup'
EQUALITY caseExactIA5Match
SUBSTR caseExactIA5SubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
attributetype ( 1.3.6.1.1.1.1.14 NAME 'nisNetgroupTriple'
DESC 'Netgroup triple'
EQUALITY caseIgnoreIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
attributetype ( 1.3.6.1.1.1.1.15 NAME 'ipServicePort'
DESC 'Service port number'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.16 NAME 'ipServiceProtocol'
DESC 'Service protocol name'
SUP name )
attributetype ( 1.3.6.1.1.1.1.17 NAME 'ipProtocolNumber'
DESC 'IP protocol number'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.18 NAME 'oncRpcNumber'
DESC 'ONC RPC number'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.19 NAME 'ipHostNumber'
DESC 'IPv4 addresses as a dotted decimal omitting leading
zeros or IPv6 addresses as defined in RFC2373'
SUP name )
attributetype ( 1.3.6.1.1.1.1.20 NAME 'ipNetworkNumber'
DESC 'IP network as a dotted decimal, eg. 192.168,
omitting leading zeros'
SUP name
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.21 NAME 'ipNetmaskNumber'
DESC 'IP netmask as a dotted decimal, eg. 255.255.255.0,
omitting leading zeros'
EQUALITY caseIgnoreIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.22 NAME 'macAddress'
DESC 'MAC address in maximal, colon separated hex
notation, eg. 00:00:92:90:ee:e2'
EQUALITY caseIgnoreIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
attributetype ( 1.3.6.1.1.1.1.23 NAME 'bootParameter'
DESC 'rpc.bootparamd parameter'
EQUALITY caseExactIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
attributetype ( 1.3.6.1.1.1.1.24 NAME 'bootFile'
DESC 'Boot image name'
EQUALITY caseExactIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
attributetype ( 1.3.6.1.1.1.1.26 NAME 'nisMapName'
DESC 'Name of a A generic NIS map'
SUP name )
attributetype ( 1.3.6.1.1.1.1.27 NAME 'nisMapEntry'
DESC 'A generic NIS entry'
EQUALITY caseExactIA5Match
SUBSTR caseExactIA5SubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.28 NAME 'nisPublicKey'
DESC 'NIS public key'
EQUALITY octetStringMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.29 NAME 'nisSecretKey'
DESC 'NIS secret key'
EQUALITY octetStringMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.30 NAME 'nisDomain'
DESC 'NIS domain'
EQUALITY caseIgnoreIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26)
attributetype ( 1.3.6.1.1.1.1.31 NAME 'automountMapName'
DESC 'automount Map Name'
EQUALITY caseExactIA5Match
SUBSTR caseExactIA5SubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.32 NAME 'automountKey'
DESC 'Automount Key value'
EQUALITY caseExactIA5Match
SUBSTR caseExactIA5SubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.33 NAME 'automountInformation'
DESC 'Automount information'
EQUALITY caseExactIA5Match
SUBSTR caseExactIA5SubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
objectclass ( 1.3.6.1.1.1.2.0 NAME 'posixAccount' SUP top AUXILIARY
DESC 'Abstraction of an account with POSIX attributes'
MUST ( cn $ uid $ uidNumber $ gidNumber $ homeDirectory )
MAY ( userPassword $ loginShell $ gecos $
description ) )
objectclass ( 1.3.6.1.1.1.2.1 NAME 'shadowAccount' SUP top AUXILIARY
DESC 'Additional attributes for shadow passwords'
MUST uid
MAY ( userPassword $ description $
shadowLastChange $ shadowMin $ shadowMax $
shadowWarning $ shadowInactive $
shadowExpire $ shadowFlag ) )
objectclass ( 1.3.6.1.1.1.2.2 NAME 'posixGroup' SUP top AUXILIARY
DESC 'Abstraction of a group of accounts'
MUST gidNumber
MAY ( userPassword $ memberUid $
description ) )
objectclass ( 1.3.6.1.1.1.2.3 NAME 'ipService' SUP top STRUCTURAL
DESC 'Abstraction an Internet Protocol service.
Maps an IP port and protocol (such as tcp or udp)
to one or more names; the distinguished value of
the cn attribute denotes the services canonical
name'
MUST ( cn $ ipServicePort $ ipServiceProtocol )
MAY description )
objectclass ( 1.3.6.1.1.1.2.4 NAME 'ipProtocol' SUP top STRUCTURAL
DESC 'Abstraction of an IP protocol. Maps a protocol number
to one or more names. The distinguished value of the cn
attribute denotes the protocols canonical name'
MUST ( cn $ ipProtocolNumber )
MAY description )
objectclass ( 1.3.6.1.1.1.2.5 NAME 'oncRpc' SUP top STRUCTURAL
DESC 'Abstraction of an Open Network Computing (ONC)
[RFC1057] Remote Procedure Call (RPC) binding.
This class maps an ONC RPC number to a name.
The distinguished value of the cn attribute denotes
the RPC services canonical name'
MUST ( cn $ oncRpcNumber )
MAY description )
objectclass ( 1.3.6.1.1.1.2.6 NAME 'ipHost' SUP top AUXILIARY
DESC 'Abstraction of a host, an IP device. The distinguished
value of the cn attribute denotes the hosts canonical
name. Device SHOULD be used as a structural class'
MUST ( cn $ ipHostNumber )
MAY ( userPassword $ l $ description $ manager ) )
objectclass ( 1.3.6.1.1.1.2.7 NAME 'ipNetwork' SUP top STRUCTURAL
DESC 'Abstraction of a network. The distinguished value of
the cn attribute denotes the networks canonical name'
MUST ipNetworkNumber
MAY ( cn $ ipNetmaskNumber $ l $ description $ manager ) )
objectclass ( 1.3.6.1.1.1.2.8 NAME 'nisNetgroup' SUP top STRUCTURAL
DESC 'Abstraction of a netgroup. May refer to other netgroups'
MUST cn
MAY ( nisNetgroupTriple $ memberNisNetgroup $ description ) )
objectclass ( 1.3.6.1.1.1.2.9 NAME 'nisMap' SUP top STRUCTURAL
DESC 'A generic abstraction of a NIS map'
MUST nisMapName
MAY description )
objectclass ( 1.3.6.1.1.1.2.10 NAME 'nisObject' SUP top STRUCTURAL
DESC 'An entry in a NIS map'
MUST ( cn $ nisMapEntry $ nisMapName )
MAY description )
objectclass ( 1.3.6.1.1.1.2.11 NAME 'ieee802Device' SUP top AUXILIARY
DESC 'A device with a MAC address; device SHOULD be
used as a structural class'
MAY macAddress )
objectclass ( 1.3.6.1.1.1.2.12 NAME 'bootableDevice' SUP top AUXILIARY
DESC 'A device with boot parameters; device SHOULD be
used as a structural class'
MAY ( bootFile $ bootParameter ) )
objectclass ( 1.3.6.1.1.1.2.14 NAME 'nisKeyObject' SUP top AUXILIARY
DESC 'An object with a public and secret key'
MUST ( cn $ nisPublicKey $ nisSecretKey )
MAY ( uidNumber $ description ) )
objectclass ( 1.3.6.1.1.1.2.15 NAME 'nisDomainObject' SUP top AUXILIARY
DESC 'Associates a NIS domain with a naming context'
MUST nisDomain )
objectclass ( 1.3.6.1.1.1.2.16 NAME 'automountMap' SUP top STRUCTURAL
MUST ( automountMapName )
MAY description )
objectclass ( 1.3.6.1.1.1.2.17 NAME 'automount' SUP top STRUCTURAL
DESC 'Automount information'
MUST ( automountKey $ automountInformation )
MAY description )
## namedObject is needed for groups without members
objectclass ( 1.3.6.1.4.1.5322.13.1.1 NAME 'namedObject' SUP top
STRUCTURAL MAY cn )

@ -0,0 +1,108 @@
# This is the main slapd configuration file. See slapd.conf(5) for more
# info on the configuration options.
include /etc/ldap/schema/core.schema
include /etc/ldap/schema/cosine.schema
include /etc/ldap/schema/rfc2307bis.schema
include /etc/ldap/schema/inetorgperson.schema
include /etc/ldap/schema/sudo.schema
include /etc/ldap/schema/csc.schema
include /etc/ldap/schema/misc.schema
pidfile /var/run/slapd/slapd.pid
argsfile /var/run/slapd/slapd.args
#Warning: "stats" is *lots* of logging
loglevel sync
#loglevel stats config sync acl
modulepath /usr/lib/ldap
moduleload back_hdb
moduleload syncprov
moduleload auditlog
moduleload unique
sizelimit unlimited
timelimit unlimited
# consider local connections encrypted
localssf 128
# map kerberos users to ldap users
sasl-realm CSCLUB.INTERNAL
sasl-host auth1.csclub.internal
authz-regexp "uid=([^/=]*),cn=CSCLUB.INTERNAL,cn=GSSAPI,cn=auth"
"uid=$1,ou=people,dc=csclub,dc=internal"
authz-regexp "uid=ceod/admin,cn=CSCLUB.INTERNAL,cn=GSSAPI,cn=auth"
"cn=ceod,dc=csclub,dc=internal"
access to *
by dn="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" manage
by * break
# systems committee get full access
access to *
by dn="cn=ceod,dc=csclub,dc=internal" write
by group/group/uniqueMember="cn=syscom,ou=Group,dc=csclub,dc=internal" write
by * break
# allow office staff to add terms
# the renewal program may do the same
access to attrs=term
by group/group/uniqueMember="cn=office,ou=Group,dc=csclub,dc=internal" add
by dn="cn=renewal,dc=csclub,dc=internal" add
by * read
access to attrs=nonMemberTerm
by group/group/uniqueMember="cn=office,ou=Group,dc=csclub,dc=internal" add
by dn="cn=renewal,dc=csclub,dc=internal" add
by * read
# allow users to change their shells
access to attrs=loginShell
by self write
by * read
# allow simple authentication
access to attrs=userPassword
by anonymous auth
by * none
# allow access to attributes of top; they would otherwise be denied below
access to attrs=@top
by * read
# default permit
access to *
by * read
# main database options
# note: the mdb backend has a horrible bug in 2.4.31
# that causes indexing to destroy the database
database hdb
suffix "dc=csclub,dc=internal"
directory "/var/lib/ldap"
rootdn cn=root,dc=csclub,dc=internal
index default eq
index objectClass
index entryCSN,entryUUID
index uid,uidNumber
index cn,gidNumber
index uniqueMember,memberUid
index sudoUser,sudoHost pres,sub,eq
index term,nonMemberTerm
index mailLocalAddress
index modifyTimestamp,createTimestamp
# log all changes to the directory
overlay auditlog
auditlog /var/log/ldap/audit.log
# enforce uniqueness of usernames etc.
overlay unique
unique_uri ldap:///ou=People,dc=csclub,dc=internal?uid,uidNumber?sub
unique_uri ldap:///ou=Group,dc=csclub,dc=internal?cn,gidNumber?sub
# this is the master server
overlay syncprov
syncprov-checkpoint 100 10
syncprov-sessionlog 100

@ -1,4 +0,0 @@
[DEFAULT]
sign-tags = True
posttag = git push /users/git/public/pyceo.git --tags
debian-tag=v%(version)s

10
.gitignore vendored

@ -1,5 +1,7 @@
/build-stamp
/build
__pycache__/
*.pyc
/build-ceo
/build-ceod
/venv/
.vscode/
*.o
*.so
.idea/

@ -0,0 +1,173 @@
# pyceo
[![Build Status](https://ci.csclub.uwaterloo.ca/api/badges/public/pyceo/status.svg?ref=refs/heads/v1)](https://ci.csclub.uwaterloo.ca/public/pyceo)
CEO (**C**SC **E**lectronic **O**ffice) is the tool used by CSC to manage
club accounts and memberships. See [architecture.md](architecture.md) for an
overview of its architecture.
## Development
First, make sure that you have installed the
[syscom dev environment](https://git.uwaterloo.ca/csc/syscom-dev-environment).
This will setup all of the services needed for ceo to work. You should clone
this repo in the phosphoric-acid container under ctdalek's home directory; you
will then be able to access it from any container thanks to NFS.
### Environment setup
Once you have the dev environment setup, there are a few more steps you'll
need to do for ceo.
#### Kerberos principals
First, you'll need `ceod/<hostname>` principals for each of phosphoric-acid,
coffee and mail. (coffee is taking over the role of caffeine for the DB
endpoints). For example, in the phosphoric-acid container:
```sh
kadmin -p sysadmin/admin
<password is krb5>
addprinc -randkey ceod/phosphoric-acid.csclub.internal
ktadd ceod/phosphoric-acid.csclub.internal
```
Do this for coffee and mail as well. You need to actually be in the
appropriate container when running these commands, since the credentials
are being added to the local keytab.
On phosphoric-acid, you will additionally need to create a principal
called `ceod/admin` (remember to addprinc **and** ktadd).
#### Database
**Note**: The instructions below apply to the dev environment only; in
production, the DB superusers should be restricted to the host where
the DB is running.
Attach to the coffee container, run `mysql`, and run the following:
```
CREATE USER 'mysql' IDENTIFIED BY 'mysql';
GRANT ALL PRIVILEGES ON *.* TO 'mysql' WITH GRANT OPTION;
```
(In prod, the superuser should have '@localhost' appended to its name.)
Now open /etc/mysql/mariadb.conf.d/50-server.cnf and comment out the following line:
```
bind-address = 127.0.0.1
```
Then restart MariaDB:
```
systemctl restart mariadb
```
Install PostgreSQL in the container:
```
apt install -y postgresql
```
Modify the superuser `postgres` for password authentication and restrict new users:
```
su postgres
psql
ALTER USER postgres WITH PASSWORD 'postgres';
REVOKE ALL ON SCHEMA public FROM public;
GRANT ALL ON SCHEMA public TO postgres;
```
Create a new `pg_hba.conf`:
```
cd /etc/postgresql/<version>/<branch>/
mv pg_hba.conf pg_hba.conf.old
```
```
# new pg_hba.conf
# TYPE DATABASE USER ADDRESS METHOD
local all postgres peer
host all postgres 0.0.0.0/0 md5
local all all peer
host all all localhost md5
local sameuser all md5
host sameuser all 0.0.0.0/0 md5
```
**Warning**: in prod, the postgres user should only be allowed to connect locally,
so the relevant snippet in pg_hba.conf should look something like
```
local all postgres md5
host all postgres localhost md5
host all postgres 0.0.0.0/0 reject
host all postgres ::/0 reject
```
Add the following to postgresql.conf:
```
listen_addresses = '*'
```
Now restart PostgreSQL:
```
systemctl restart postgresql
```
**In prod**, users can login remotely but superusers (`postgres` and `mysql`) are only
allowed to login from the database host.
#### Mailman
You should create the following mailing lists from the mail container:
```sh
/opt/mailman3/bin/mailman create syscom@csclub.internal
/opt/mailman3/bin/mailman create syscom-alerts@csclub.internal
/opt/mailman3/bin/mailman create exec@csclub.internal
/opt/mailman3/bin/mailman create ceo@csclub.internal
```
See https://git.uwaterloo.ca/csc/syscom-dev-environment/-/tree/master/mail
for instructions on how to access the Mailman UI from your browser.
If you want to actually see the archived messages, you'll
need to tweak the settings for each list from the UI so that non-member
messages get accepted (by default they get held).
#### Dependencies
Next, install and activate a virtualenv:
```sh
sudo apt install libkrb5-dev libpq-dev python3-dev
python3 -m venv venv
. venv/bin/activate
pip install -r requirements.txt
pip install -r dev-requirements.txt
```
## Running the application
ceod is a distributed application, with instances on different hosts offering
different services.
Therefore, you will need to run ceod on multiple hosts. Currently, those are
phosphoric-acid, mail and caffeine (in the dev environment, caffeine is
replaced by coffee).
To run ceod on a single host (as root, since the app needs to read the keytab):
```sh
export FLASK_APP=ceod.api
export FLASK_ENV=development
flask run -h 0.0.0.0 -p 9987
```
Sometimes changes you make in the source code don't show up while Flask
is running. Stop the flask app (Ctrl-C), run `clear_cache.sh`, then
restart the app.
## Interacting with the application
The client part of ceo hasn't been written yet, so we'll use curl to
interact with ceod for now.
ceod uses [SPNEGO](https://en.wikipedia.org/wiki/SPNEGO) for authentication,
and TLS for confidentiality and integrity. In development mode, TLS can be
disabled.
First, make sure that your version of curl has been compiled with SPNEGO
support:
```sh
curl -V
```
Your should see 'SPNEGO' in the 'Features' section.
Here's an example of making a request to an endpoint which writes to LDAP:
```sh
# Get a Kerberos TGT first
kinit
# Make the request
curl --negotiate -u : --service-name ceod --delegation always \
-d '{"uid":"test_1","cn":"Test One","program":"Math","terms":["s2021"]}' \
-X POST http://phosphoric-acid:9987/api/members
```

@ -0,0 +1,70 @@
# Architecture
ceo is a distributed HTTP application running on three hosts. As of this
writing, those are phosphoric-acid, mail and caffeine (coffee in the dev
environment).
* The `mail` host provides the `/api/mailman` endpoints. This is because
the REST API for Mailman3 is currently configured to run on localhost.
* The `caffeine` host provides the `/api/db` endpoints. This is because
the root account of MySQL and PostgreSQL on caffeine can only be accessed
locally.
* All other endpoints are provided by `phosphoric-acid`. phosphoric-acid is the
only host with the `ceod/admin` Kerberos key which means it is the only host
which can create new principals and reset passwords.
Some endpoints can be accessed from multiple hosts. This is explained more in
[Security](#security).
Interestingly, ceod instances can actually make API calls to each other. For
example, when the instance on phosphoric-acid creates a new user, it will
make a call to the instance on mail to subscribe the user to the csc-general
mailing list.
## Security
In the old ceo, most LDAP modifications were performed on the client side,
using the client's Kerberos credentials to authenticate to LDAP via GSSAPI.
Using the client's credentials is desirable since we currently have custom
authz rules in our slapd.conf on auth1 and auth2. If we were to use the
server's credentials instead, this would result in two different sets of
authz rules - one at the API layer and one at the OpenLDAP layer - and
syscom members would very likely forget to update both at the same time.
So, we want a way for the server to use the client's credentials when
interacting with LDAP. The most secure way to do this is via a Kerberos
extension called "constrained delegation", or [S4U](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-sfu/1fb9caca-449f-4183-8f7a-1a5fc7e7290a).
While the MIT KDC, which we are currently using, does provide support for S4U,
this [requires using LDAP as a database backend](https://k5wiki.kerberos.org/wiki/Projects/ConstrainedDelegation#CHECK_ALLOWED_TO_DELEGATE),
which we are *not* using. While it is theoretically possible to migrate our
KDC databases to LDAP, this would be a very risky operation, and probably
not worth it if ceo is the only app which will use it.
Therefore, we will use unconstrained delegation. The client essentially
forwards their TGT to ceod, which uses it to access other services over GSSAPI
on the client's behalf. We accomplish this using GSSAPI delegation (i.e. set
the GSS_C_DELEG_FLAG when creating a security context).
Since the client's credentials are used when interacting with LDAP, this means
that most LDAP-related endpoints can actually be accessed from any host.
Only the Kerberos-specific endpoints (e.g. resetting a password) truly need
to be on phosphoric-acid.
### Authentication
The REST API uses SPNEGO for authetication via the HTTP Negotiate
Authentication scheme (https://www.ietf.org/rfc/rfc4559.txt). The API
does not verify that the user actually knows the key for the service ticket;
therefore, TLS is necessary to prevent MITM attacks. (TLS is also necessary
to protect the KRB-CRED message, which is unencrypted.)
SPNEGO is pretty awkward, to be honest, as it completely breaks the stateless
nature of HTTP. If we decide that SPNEGO is too much trouble, we should switch
to plain HTTP cookies instead, and cache them somewhere in the client's home
directory.
## Web UI
For future contributors: if you wish to make ceo accessible from the browser,
you will need to add some kind of "Kerberos gateway" logic to the API such
that the user's password can be used to obtain Kerberos tickets. One possible
implementation would be to prompt the user for a password, obtain a TGT,
then encrypt the TGT and store it as a JWT in the user's browser. The API
can decrypt the JWT later and use it as long as the ticket has not expired;
otherwise, the user will be re-prompted for their password.

@ -1,40 +0,0 @@
#!/usr/bin/python
import sys, ldap
from getpass import getpass
import ceo.urwid.main
import ceo.console.main
from ceo import ldapi, members
def start():
try:
if len(sys.argv) == 1:
print "Reading config file...",
members.configure()
print "Connecting to LDAP..."
members.connect(AuthCallback())
ceo.urwid.main.start()
else:
members.configure()
members.connect(AuthCallback())
ceo.console.main.start()
except ldap.LOCAL_ERROR, e:
print ldapi.format_ldaperror(e)
except ldap.INSUFFICIENT_ACCESS, e:
print ldapi.format_ldaperror(e)
print "You probably aren't permitted to do whatever you just tried."
print "Admittedly, ceo probably shouldn't have crashed either."
class AuthCallback:
def callback(self, error):
try:
print "Password: ",
return getpass("")
except KeyboardInterrupt:
print ""
sys.exit(1)
if __name__ == '__main__':
start()

@ -1,5 +0,0 @@
if test -e .git; then
git-buildpackage --git-ignore-new -us -uc
else
debuild -us -uc
fi

1
ceo/.gitignore vendored

@ -1 +0,0 @@
/ceo_pb2.py

@ -1 +0,0 @@
"""CSC Electronic Office"""

@ -0,0 +1,4 @@
from .cli import cli
if __name__ == '__main__':
cli(obj={})

@ -0,0 +1 @@
from .entrypoint import cli

@ -0,0 +1,53 @@
import importlib.resources
import os
import socket
import click
from zope import component
from ..krb_check import krb_check
from .members import members
from .groups import groups
from .positions import positions
from .updateprograms import updateprograms
from ceo_common.interfaces import IConfig, IHTTPClient
from ceo_common.model import Config, HTTPClient
@click.group()
@click.pass_context
def cli(ctx):
# ensure ctx exists and is a dict
ctx.ensure_object(dict)
princ = krb_check()
user = princ[:princ.index('@')]
ctx.obj['user'] = user
if os.environ.get('PYTEST') != '1':
register_services()
cli.add_command(members)
cli.add_command(groups)
cli.add_command(positions)
cli.add_command(updateprograms)
def register_services():
# Using base component directly so events get triggered
baseComponent = component.getGlobalSiteManager()
# Config
# This is a hack to determine if we're in the dev env or not
if socket.getfqdn().endswith('.csclub.internal'):
with importlib.resources.path('tests', 'ceo_dev.ini') as p:
config_file = p.__fspath__()
else:
config_file = os.environ.get('CEO_CONFIG', '/etc/csc/ceo.ini')
cfg = Config(config_file)
baseComponent.registerUtility(cfg, IConfig)
# HTTPService
http_client = HTTPClient()
baseComponent.registerUtility(http_client, IHTTPClient)

@ -0,0 +1,148 @@
from typing import Dict
import click
from zope import component
from ..utils import http_post, http_get, http_delete
from .utils import handle_stream_response, handle_sync_response, print_colon_kv, \
check_if_in_development
from ceo_common.interfaces import IConfig
from ceod.transactions.groups import (
AddGroupTransaction,
AddMemberToGroupTransaction,
RemoveMemberFromGroupTransaction,
DeleteGroupTransaction,
)
@click.group(short_help='Perform operations on CSC groups/clubs')
def groups():
pass
@groups.command(short_help='Add a new group')
@click.argument('group_name')
@click.option('-d', '--description', help='Group description', prompt=True)
def add(group_name, description):
click.echo('The following group will be created:')
lines = [
('cn', group_name),
('description', description),
]
print_colon_kv(lines)
click.confirm('Do you want to continue?', abort=True)
body = {
'cn': group_name,
'description': description,
}
operations = AddGroupTransaction.operations
resp = http_post('/api/groups', json=body)
data = handle_stream_response(resp, operations)
result = data[-1]['result']
print_group_lines(result)
def print_group_lines(result: Dict):
"""Pretty-print a group JSON response."""
lines = [