forked from public/pyceo
Compare commits
72 Commits
master
...
positions-
@ -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 |
@ -1,5 +1,7 @@ |
||||
/build-stamp |
||||
/build |
||||
__pycache__/ |
||||
*.pyc |
||||
/build-ceo |
||||
/build-ceod |
||||
/venv/ |
||||
.vscode/ |
||||
*.o |
||||
*.so |
||||
.idea/ |
||||
|
@ -0,0 +1,173 @@ |
||||
# pyceo |
||||
[](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 +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 = [ |
||||