Compare commits

...

563 Commits

Author SHA1 Message Date
Max Erenberg cf42e49ae6 fix home links
continuous-integration/drone/push Build is passing Details
2024-03-24 18:11:48 -04:00
Max Erenberg bf2afd1195 Remove FCGI support
continuous-integration/drone/push Build is passing Details
Apache's ProxyPass directive doesn't seem to be passing the URI for
FCGI. There's probably a way to configure this, but it's easier to just
use HTTP instead.
2024-03-24 17:54:13 -04:00
Max Erenberg 5f8de94393 restrict app.sock access to www-data
continuous-integration/drone/push Build is passing Details
2024-03-23 23:00:37 -04:00
Max Erenberg 2164ceddf0 fix compilation error in api.go
continuous-integration/drone/push Build is passing Details
2024-03-23 19:32:23 -04:00
Max Erenberg 7716f7bd10 Add web UI for password resets (#123)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #123
2024-03-23 19:26:30 -04:00
Nathan Chung 32cb22665a
packaging: system update commands
continuous-integration/drone/push Build is passing Details
Signed-off-by: Nathan13888 <29968201+Nathan13888@users.noreply.github.com>
2024-02-21 02:24:41 -05:00
Nathan Chung 56a59186e0
extra packaging instructions and notes
continuous-integration/drone/push Build is passing Details
Signed-off-by: Nathan13888 <29968201+Nathan13888@users.noreply.github.com>
2024-02-21 02:20:58 -05:00
Nathan Chung 5ecec2b54c
1.0.31: changelog
Signed-off-by: Nathan13888 <29968201+Nathan13888@users.noreply.github.com>
2024-02-21 02:20:42 -05:00
Nathan Chung f584a89cec
update pgp packaging info
continuous-integration/drone/push Build is passing Details
Signed-off-by: Nathan13888 <29968201+Nathan13888@users.noreply.github.com>
2024-02-21 01:53:10 -05:00
Nathan Chung 7d9ec99f8f
release 1.0.31
continuous-integration/drone/push Build is passing Details
Signed-off-by: Nathan13888 <29968201+Nathan13888@users.noreply.github.com>
2024-02-21 01:40:22 -05:00
Nathan Chung 28adf6e13d
Merge branch '1.0.30-update-packaging'
continuous-integration/drone/push Build is passing Details
Signed-off-by: Nathan13888 <29968201+Nathan13888@users.noreply.github.com>
2024-02-21 01:39:50 -05:00
Max Erenberg 9c51ad3a01 Allow offsck to add members to the office group (#126)
continuous-integration/drone/push Build is passing Details
Closes #62.

Reviewed-on: #126
2024-02-17 19:31:03 -05:00
Nathan Chung bf7f1c7724
update debian packaging instructions
continuous-integration/drone/push Build is passing Details
Signed-off-by: Nathan13888 <29968201+Nathan13888@users.noreply.github.com>
2024-02-14 14:16:40 -05:00
Leon Zhang 194b5ec4a6 See #125 (#127)
continuous-integration/drone/push Build is passing Details
I couldn't figure out how to reopen a PR after merge, so I'm opening another one.

Co-authored-by: Leon <lzhang219@gmail.com>
Reviewed-on: #127
Reviewed-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Reviewed-by: Nathan Chung <n4chung@csclub.uwaterloo.ca>
2024-02-11 16:33:39 -05:00
Nathan Chung c83bbe2563 update packaging version and instructions (#122)
continuous-integration/drone/push Build is passing Details
- push version to release version `1.0.30`
- add additional instructions for packaging eg. pgp, additional build dependencies
- git ignore gpg private keys

Reviewed-on: #122
Reviewed-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Co-authored-by: Nathan13888 <29968201+Nathan13888@users.noreply.github.com>
Co-committed-by: Nathan13888 <29968201+Nathan13888@users.noreply.github.com>
2024-02-11 12:20:30 -05:00
Max Erenberg 32709ad401 Revert "Fix for regression in issue #124 (#125)"
continuous-integration/drone/push Build is passing Details
This reverts commit 3780662ba4.
2024-02-10 15:37:06 -05:00
Leon Zhang 3780662ba4 Fix for regression in issue #124 (#125)
continuous-integration/drone/push Build is passing Details
Co-authored-by: Leon <lzhang219@gmail.com>
Reviewed-on: #125
Reviewed-by: Nathan Chung <n4chung@csclub.uwaterloo.ca>
2024-02-10 15:32:52 -05:00
Nathan Chung b1dac8ce07
1.0.30: update changelog and package uploaders
continuous-integration/drone/pr Build is passing Details
Signed-off-by: Nathan13888 <29968201+Nathan13888@users.noreply.github.com>
2024-02-04 14:47:10 -05:00
Nathan Chung c5edf5ea48
update packaging version and instructions
continuous-integration/drone/pr Build is passing Details
Signed-off-by: Nathan13888 <29968201+Nathan13888@users.noreply.github.com>
2024-02-04 14:24:22 -05:00
Max Erenberg a4a4ef089c Query Active Directory LDAP for alumni (#120)
continuous-integration/drone/push Build is passing Details
Closes #116.

UWLDAP has program information for current students, so we should continue using it by default.
If the sn attribute (last name) is missing from the entry, then we query ADLDAP instead.

Reviewed-on: #120
2024-02-01 23:57:53 -05:00
Max Erenberg bd1da799c6 Allow ceod/* principals for all requests (#121)
continuous-integration/drone/push Build is passing Details
Allow the ceod/\* principals (which should only be used by the ceod daemons) to make requests to all API endpoints.

Reviewed-on: #121
2024-01-28 21:37:34 -05:00
Leon Zhang 25994af312 Update available positions in configuration files, pertaining to issue #63 (#117)
continuous-integration/drone/push Build is passing Details
Updated available positions adding new positions and removing 2 unused positions in ceo. Passed Drone CI.

Co-authored-by: Leon <lzhang219@gmail.com>
Reviewed-on: #117
Reviewed-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
2024-01-22 22:00:30 -05:00
Ohm Patel de23296413 Validate usernames across tui & for create_user on cli/api (#115)
continuous-integration/drone/push Build is passing Details
Current changes should address issues raised by @merenber in #114 excluding #114 (comment) (both CLI and TUI validation)

* Unit test for invalid name was added but needs to be modified as regex should be changed to disallow underscores eventually.

Reviewed-on: #115
Reviewed-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Co-authored-by: o32patel <ohm.patel@uwaterloo.ca>
Co-committed-by: o32patel <ohm.patel@uwaterloo.ca>
2024-01-22 13:15:40 -05:00
Ohm Patel f06ccdc3f9 Add username verification (#114)
continuous-integration/drone/push Build is passing Details
Move and add validation to Controller.get_username_from_view(). This addresses #101
+ Add tests for username validator

Reviewed-on: #114
Reviewed-by: Nathan Chung <n4chung@csclub.uwaterloo.ca>
Co-authored-by: o32patel <ohm.patel@uwaterloo.ca>
Co-committed-by: o32patel <ohm.patel@uwaterloo.ca>
2024-01-15 20:01:44 -05:00
Max Erenberg 5332259731 Use persistent Docker images for development (#113)
continuous-integration/drone/push Build is passing Details
This PR adds support for building Docker images during development which can be re-used multiple times. This allows us to easily run `docker-compose up` and `docker-compose down` many times without having to reinstall packages from scratch every time.

Reviewed-on: #113
2023-12-03 23:29:11 -05:00
Max Erenberg 392ec153d0 release 1.0.29
continuous-integration/drone/push Build is passing Details
2023-07-31 21:29:56 -04:00
Max Erenberg 36bf340385 remove ceod.postinst 2023-07-31 20:06:32 -04:00
Max Erenberg 7e851daa8f update deps 2023-07-31 19:27:45 -04:00
Max Erenberg e0ed4fa23a check that forwarding_addresses is a list 2023-07-31 18:26:06 -04:00
Max Erenberg 6786c8e44e shorten tests for group search API 2023-07-31 18:24:34 -04:00
Max Erenberg 337c05c511 release 1.0.27
continuous-integration/drone/push Build is passing Details
2023-06-09 02:51:35 -04:00
Max Erenberg 65688c72da make forwarding_addresses mandatory when creating member (#97)
continuous-integration/drone/push Build is passing Details
Closes #96.

Reviewed-on: #97
2023-06-09 02:39:50 -04:00
Justin Chung 968f0815c7 Add linting pre-commit hook and hook install script (#86)
continuous-integration/drone/push Build is passing Details
Co-authored-by: Justin Chung <20733699+justin13888@users.noreply.github.com>
Co-authored-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Reviewed-on: #86
Reviewed-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Co-authored-by: Justin Chung <j24chung@csclub.uwaterloo.ca>
Co-committed-by: Justin Chung <j24chung@csclub.uwaterloo.ca>
2023-05-29 00:14:05 -04:00
Daniel Sun 010937ea17 Add group lookup functionality (#88)
continuous-integration/drone/push Build is passing Details
note: **I am unaware of best practices** but I tried my best to keep changes consistent with the codebase

feedback would be much appreciated

notable changes:
**new api endpoint**: `/groups/search` -- I moved searching into the api so it could be used in tui and cli, also seemed like a good idea to keep the json response as small as possible
**tui searching** -- at first I wanted to make this realtime interactable, but the work required seemed inappropriate to a feature I am assuming will only be used sparingly

Co-authored-by: Daniel Sun <dandancool@github.com>
Co-authored-by: Daniel Sun <d6sun@uwaterloo.ca>
Reviewed-on: #88
Reviewed-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Co-authored-by: Daniel Sun <d6sun@csclub.uwaterloo.ca>
Co-committed-by: Daniel Sun <d6sun@csclub.uwaterloo.ca>
2023-03-04 01:21:04 -05:00
Max Erenberg 234ab62f27 add accounttype=0 to CloudStack listAccounts query
continuous-integration/drone/push Build is passing Details
2023-02-18 11:16:36 -05:00
Max Erenberg f9bda2f724 release 1.0.26
continuous-integration/drone/push Build is passing Details
2023-02-13 17:43:41 -05:00
Max Erenberg 239b992107 reduce UWLDAP batch size to 10
continuous-integration/drone/push Build is passing Details
2023-02-13 17:34:49 -05:00
Max Erenberg 754731ba5f upgrade Debian dependencies
continuous-integration/drone/push Build is passing Details
2023-02-06 02:16:17 -05:00
Max Erenberg b33339817f fix logging messages for renewing a member
continuous-integration/drone/push Build is passing Details
2023-02-06 00:29:45 -05:00
Max Erenberg 6dccd8b659 release 1.0.25 2023-02-06 00:12:22 -05:00
Max Erenberg 3f58d1aff5 print warning message when cloud account is created
continuous-integration/drone/push Build is passing Details
2023-02-05 23:48:38 -05:00
Justin Chung 5e8f1b5ba5 Implement TUI support for multiple users in each position (#80)
continuous-integration/drone/push Build is passing Details
Co-authored-by: Justin Chung <20733699+justin13888@users.noreply.github.com>
Co-authored-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Reviewed-on: #80
Co-authored-by: Justin Chung <j24chung@csclub.uwaterloo.ca>
Co-committed-by: Justin Chung <j24chung@csclub.uwaterloo.ca>
2023-01-23 02:26:13 -05:00
Max Erenberg f84965c8e1 reload all NGINX servers after adding a vhost (#90)
continuous-integration/drone/push Build is passing Details
Currently, only the NGINX server on biloba is reloaded after adding a new vhost or renewing an SSL certificate. The NGINX server on chamomile should also be reloaded, since chamomile is a warm standby for biloba.

This PR adds a new config option in ceod.ini to specify the shell command to reload the web servers.

Reviewed-on: #90
Co-authored-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Co-committed-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
2023-01-22 17:20:55 -05:00
Max Erenberg 4394c4e277 use bullseye for base container (#91)
continuous-integration/drone/push Build is passing Details
All of the machines running ceod are on bullseye, so we don't need to support buster anymore.

Reviewed-on: #91
Co-authored-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Co-committed-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
2023-01-22 17:15:40 -05:00
Jonathan Leung b507c56136 Show groups in member for API, CLI and TUI (#82)
continuous-integration/drone/push Build is passing Details
Closes #69.

Tests are failing locally with many `assert os.geteuid() == 0` errors even on the master branch. I will add tests after I figure this out.

Reviewed-on: #82
Co-authored-by: Jonathan Leung <j23leung@csclub.uwaterloo.ca>
Co-committed-by: Jonathan Leung <j23leung@csclub.uwaterloo.ca>
2022-11-26 20:09:05 -05:00
Max Erenberg c0c9736593 Use the admin creds in the HTTPClient when necessary (#85)
continuous-integration/drone/push Build is passing Details
Currently, ceod uses the Kerberos credentials of the client when making requests to other services. This requires the client to send delegated credentials. Unfortunately the NPM krb5 package appears to be unable to perform delegation. So we will use the admin credentials instead (when appropriate).

Reviewed-on: #85
Reviewed-by: Raymond Li <raymo@csclub.uwaterloo.ca>
Co-authored-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Co-committed-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
2022-11-06 15:23:27 -05:00
Max Erenberg 1e452d10ce Assume program is Alumni if UWLDAP is missing data (#84)
continuous-integration/drone/push Build is passing Details
This PR sets 'program=Alumni' for members who either do not have an 'ou' attribute in UWLDAP, or who do not have a UWLDAP entry at all.

Reviewed-on: #84
Reviewed-by: Raymond Li <raymo@csclub.uwaterloo.ca>
2022-11-01 21:02:05 -04:00
Max Erenberg 6a1fa81b82 merenber signs the packages
continuous-integration/drone/push Build is passing Details
Something went wrong when e42zhang tried to upload the packages to the
mirror. reprepro kept on complaining that no distribution would accept
the new package. I modified the changelog, re-signed and re-uploaded
the packages, and that worked, so I'm still not sure what the problem
was.
2022-10-23 22:04:32 -04:00
Max Erenberg 6df1f4d459 Revert "Simplify packaging"
This reverts commit b4a1373559.
2022-10-23 22:00:48 -04:00
Edwin 2cf9e25b59 More fixes
continuous-integration/drone/push Build is passing Details
2022-10-23 21:16:58 -04:00
Edwin 9ff3d850c9 Release 1.0.24 2022-10-23 19:50:38 -04:00
Edwin b4a1373559 Simplify packaging 2022-10-23 19:50:06 -04:00
Raymond Li dceb5d6572
Revert "#63: Add positions to CEO (#79)"
continuous-integration/drone/push Build is passing Details
This reverts commit 3b7c89c925.
2022-10-13 18:12:36 -04:00
Jonathan Leung c30ca54752 Sort group member listing by WatIAM ID (#78)
continuous-integration/drone/push Build is failing Details
Closes #74.

Co-authored-by: Jono <jowonowo@gmail.com>
Reviewed-on: #78
Reviewed-by: Raymond Li <raymo@csclub.uwaterloo.ca>
Co-authored-by: Jonathan Leung <j23leung@csclub.uwaterloo.ca>
Co-committed-by: Jonathan Leung <j23leung@csclub.uwaterloo.ca>
2022-10-13 14:58:50 -04:00
Nathan Chung 3b7c89c925 #63: Add positions to CEO (#79)
continuous-integration/drone/push Build is failing Details
#63

Added the following positions:
* ext affairs lead
* marketing lead
* design lead
* events lead
* reps lead
* mods lead
* photography lead
* other

Signed-off-by: n4chung <n4chung@csclub.uwaterloo.ca>

Co-authored-by: n4chung <n4chung@csclub.uwaterloo.ca>
Reviewed-on: #79
Reviewed-by: Raymond Li <raymo@csclub.uwaterloo.ca>
Co-authored-by: Nathan Chung <n4chung@csclub.uwaterloo.ca>
Co-committed-by: Nathan Chung <n4chung@csclub.uwaterloo.ca>
2022-10-13 14:58:34 -04:00
Max Erenberg a2324090f3 update README instructions for curl
continuous-integration/drone/push Build is passing Details
2022-10-12 01:56:54 -04:00
Max Erenberg f66f7c8f5a remove override_dh_systemd_start
continuous-integration/drone/push Build is passing Details
2022-10-09 17:52:37 -04:00
Max Erenberg 3e5b829085 check if mail_local_addresses exists in UWLDAP entry
continuous-integration/drone/push Build is passing Details
2022-10-07 08:13:59 -04:00
Rio Liu 57ba72ef26 Add support for using number in member terms renwewal API (#77)
continuous-integration/drone/push Build is passing Details
Closed #75

Co-authored-by: Rio6 <rio.liu@r26.me>
Co-authored-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Reviewed-on: #77
Co-authored-by: Rio Liu <r345liu@csclub.uwaterloo.ca>
Co-committed-by: Rio Liu <r345liu@csclub.uwaterloo.ca>
2022-10-07 07:58:23 -04:00
Max Erenberg 779e35a08e fix shadowExpire deserialization
continuous-integration/drone/push Build is passing Details
2022-09-10 16:01:40 -04:00
Raymond Li 3cc9b011c3 Use HTTPS in sample
continuous-integration/drone/push Build is passing Details
2022-09-10 14:18:17 -04:00
Max Erenberg 2739c45aff use LDAP instead of NSS for authz (#73)
continuous-integration/drone/push Build is passing Details
Closes #71.

Reviewed-on: #73
2022-09-09 17:26:54 -04:00
Max Erenberg 651f4fb702 add more logging (#72)
continuous-integration/drone/push Build is passing Details
Closes #70.

Reviewed-on: #72
2022-09-09 14:42:43 -04:00
Max Erenberg 953bee549e fix tests
continuous-integration/drone/push Build is passing Details
2022-09-05 17:34:28 -04:00
Max Erenberg 0334e7e667 fix email formatting bug in ClubWebHostingService
continuous-integration/drone/push Build is failing Details
2022-09-05 15:37:35 -04:00
Max Erenberg 8decd3bc30 packaging for bullseye
continuous-integration/drone/push Build is passing Details
2022-09-05 00:36:50 -04:00
Max Erenberg 8ad8271db1 Fix some bugs in ClubWebHostingService
continuous-integration/drone/push Build is passing Details
* Don't read the value of an Apache directive unless we are sure
  it can only accept one argument
* Handle the case where a club's www directory is not readable
2022-09-04 23:22:28 -04:00
Raymond Li 4ebb9bb0a8
Release v1.0.22
continuous-integration/drone/push Build is passing Details
2022-08-06 02:20:46 +00:00
Max Erenberg cfb5f77711 Disable inactive club sites (#68)
continuous-integration/drone/push Build is passing Details
Closes #51.

An API argument called `remove_inactive_club_reps` was added so that we can dynamically control whether we want to remove inactive club reps or not. The default action is only to disable club websites without changing group membership.

Reviewed-on: #68
Co-authored-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Co-committed-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
2022-07-22 23:51:59 -04:00
Max Erenberg 32b2dbb307 Allow addmember and removemember to accept multiple usernames (#67)
continuous-integration/drone/push Build is passing Details
Closes #57.

Reviewed-on: #67
2022-07-02 11:26:38 -04:00
Max Erenberg dc412ef5cb implement renewal reminders (#61)
continuous-integration/drone/push Build is passing Details
Closes #55.

Once this is merged and deployed, a cron job will be used to automatically run `ceo members remindexpire` at the beginning of every term.

Reviewed-on: #61
2022-06-30 20:02:06 -04:00
Max Erenberg dbb5bf1c8d packaging for bullseye
continuous-integration/drone/push Build is passing Details
2022-06-09 10:10:31 -04:00
Max Erenberg 00c7d562ad fix URL bug in ContainerRegistryService
continuous-integration/drone/push Build is passing Details
2022-06-09 09:07:12 -04:00
Max Erenberg 6fae2e4115 use quote_plus for signing CloudStack API requests
continuous-integration/drone/push Build is passing Details
2022-06-09 01:23:04 -04:00
Max Erenberg 1fc432bb0f add hint to GetGroupResponseView
continuous-integration/drone/push Build is passing Details
2022-06-05 10:20:22 -04:00
Max Erenberg 2678bdf16e packaging for bullseye and focal
continuous-integration/drone/push Build is passing Details
2022-06-04 22:47:05 -04:00
Rio Liu 55c4b2151d Unsubscribe/resubscribe members when they're expired and renewed (#53)
continuous-integration/drone/push Build is passing Details
Co-authored-by: Rio Liu <rio.liu@r26.me>
Co-authored-by: Rio6 <rio.liu@r26.me>
Co-authored-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Reviewed-on: #53
Co-authored-by: Rio <r345liu@localhost>
Co-committed-by: Rio <r345liu@localhost>
2022-06-02 02:06:49 -04:00
Max Erenberg 87470e1f3b don't reset password for local MySQL users
continuous-integration/drone/push Build is passing Details
2022-05-23 23:01:09 -04:00
Max Erenberg b543f0eb0c Rewrite TUI (#52)
continuous-integration/drone/push Build is passing Details
Closes #44.
Closes #47.
Closes #49.
Closes #50.

The TUI has been rewritten using urwid instead of asciimatics. The MVC pattern was also used to help increase organization and readability.

The mouse has been disabled, which allows users to easily copy text from the terminal.

Terms are now sorted when displayed, for both the CLI and the TUI.

Reviewed-on: #52
2022-05-22 14:09:46 -04:00
Max Erenberg 19496b4568 use mailman for mailman_host
continuous-integration/drone/push Build is failing Details
2022-05-01 03:39:17 -04:00
Raymond Li 8da700472f Update 'ceod/model/templates/welcome_message.j2'
continuous-integration/drone/push Build is passing Details
2022-03-18 21:35:32 -04:00
Max Erenberg 5197228d68 packaging for bullseye
continuous-integration/drone/push Build is passing Details
2022-03-13 08:03:23 -04:00
Max Erenberg 9b8425f30e bump version 2022-03-12 21:26:55 -05:00
Max Erenberg f3c542208a send cloud warning emails to root
continuous-integration/drone/push Build is passing Details
2022-03-12 16:09:19 -05:00
Max Erenberg 2487ab3668 update security section of docs 2022-03-12 15:50:42 -05:00
Max Erenberg 539de01c4d use admin GSSAPI creds for some API endpoints (#45)
continuous-integration/drone/push Build is passing Details
Office staff currently can't sign up new members because ceod uses their GSSAPI credentials to authenticate to LDAP, and those credentials are insufficient.

This PR uses the ceod/admin credentials instead for signing up new members and for renewing existing memberships.

Reviewed-on: #45
2022-03-12 15:19:14 -05:00
Max Erenberg af4e342f3c Add ':z' to Docker volume mounts
continuous-integration/drone/push Build is failing Details
On systems with SELinux enabled (e.g. Fedora), Docker/Podman cannot
access directories on the host by default.
2022-03-12 14:31:12 -05:00
Max Erenberg 00ced22950 add script to extend a term
continuous-integration/drone/push Build is passing Details
2022-01-15 23:44:23 -05:00
Max Erenberg 5200259cfa allow loginShell to be optional
continuous-integration/drone/push Build is passing Details
2022-01-10 01:32:26 -05:00
Max Erenberg 7d3e03e7fd increase retries for Postfix
continuous-integration/drone/push Build is passing Details
2022-01-07 23:06:58 -05:00
Max Erenberg 71e6b474a4 packaging for bullseye
continuous-integration/drone/push Build is passing Details
2022-01-05 01:32:58 -05:00
Max Erenberg 5351cf8aee Revert "don't subscribe club reps to csc-general"
This reverts commit fa05c4ad4a.
2022-01-05 01:28:05 -05:00
Max Erenberg 2ee9511337 set HOME environment variable in /etc/default/ceod
continuous-integration/drone/push Build is passing Details
2022-01-05 00:54:40 -05:00
Max Erenberg feb16ee625 packaging for bullseye
continuous-integration/drone/push Build is passing Details
2022-01-05 00:26:41 -05:00
Max Erenberg 0c166f93ad bump version
continuous-integration/drone/push Build is passing Details
2022-01-05 00:17:51 -05:00
Max Erenberg ef45344724 Revert "use Kubernetes runner"
This reverts commit 28b5000e89.
2022-01-05 00:17:04 -05:00
Max Erenberg 28b5000e89 use Kubernetes runner
continuous-integration/drone/push Build was killed Details
2022-01-04 23:54:51 -05:00
Max Erenberg 7908d49840 include ACME challenge location snippet in NGINX template 2022-01-04 23:53:44 -05:00
Max Erenberg 41d293ee3b add retry mechanism when sending email to new user
continuous-integration/drone/push Build is passing Details
2022-01-04 23:45:04 -05:00
Max Erenberg fa05c4ad4a don't subscribe club reps to csc-general
continuous-integration/drone/push Build is passing Details
2022-01-03 20:43:58 -05:00
Max Erenberg 02598fa3bc allow ignored Harbor projects to be configurable
continuous-integration/drone/push Build is passing Details
2022-01-02 22:43:22 -05:00
Max Erenberg 7ec17b2b4d chmod 600 the kubeconfig 2022-01-02 18:49:11 -05:00
Raymond Li 5f93b0e912 Update 'README.md'
continuous-integration/drone/push Build is passing Details
2022-01-02 16:06:37 -05:00
Raymond Li 7cb07547fa Update 'README.md'
continuous-integration/drone/push Build is passing Details
2022-01-02 16:06:03 -05:00
Raymond Li f45efefaca Autofill set positions (#41)
continuous-integration/drone/push Build is passing Details
Implements #40

Co-authored-by: Raymond Li <hi@raymond.li>
Reviewed-on: #41
Co-authored-by: Raymond Li <raymo@csclub.uwaterloo.ca>
Co-committed-by: Raymond Li <raymo@csclub.uwaterloo.ca>
2022-01-02 02:41:28 -05:00
Raymond Li d7b6ac2307 Update 'PACKAGING.md'
continuous-integration/drone/push Build is passing Details
2022-01-01 22:36:43 -05:00
Raymond Li 70d27c5817 Update 'PACKAGING.md'
continuous-integration/drone/push Build is passing Details
2022-01-01 22:34:39 -05:00
Max Erenberg 88b40b79cc Don't expire syscom members (#43)
continuous-integration/drone/push Build is passing Details
Closes #37.

Co-authored-by: Max Erenberg <>
Reviewed-on: #43
Co-authored-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Co-committed-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
2022-01-01 12:15:32 -05:00
Max Erenberg 1e94132e97 Add container registry API (#42)
continuous-integration/drone/push Build is passing Details
Add an API for members to create a project on Harbor.

Co-authored-by: Max Erenberg <>
Reviewed-on: #42
Co-authored-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Co-committed-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
2022-01-01 00:49:05 -05:00
Max Erenberg d200d3d6cf add packaging for bullseye and move Packaging documentation
continuous-integration/drone/push Build is passing Details
2021-12-25 12:43:05 -05:00
Max Erenberg 5e03ff932f bump version 2021-12-25 11:52:44 -05:00
Max Erenberg 0422e4487b fix flaky test for vhost rate limiting
continuous-integration/drone/push Build is passing Details
2021-12-25 11:31:54 -05:00
Max Erenberg 6e96e409be add (objectClass=member) filter for expired members 2021-12-25 11:23:06 -05:00
d278liu 250d24ae37 use binary search when finding new uid (#39)
continuous-integration/drone/push Build is passing Details
closes #36

Co-authored-by: Daniel Liu <mr.picklepinosaur@gmail.com>
Reviewed-on: #39
Co-authored-by: d278liu <d278liu@localhost>
Co-committed-by: d278liu <d278liu@localhost>
2021-12-23 17:00:27 -05:00
Max Erenberg 0640337564 Add ROOT environment variable to /etc/default/ceod
continuous-integration/drone/push Build is passing Details
kubectl was failing because it couldn't find the kubeconfig in
/root/.kube/config
2021-12-18 17:45:20 -05:00
Max Erenberg afb63f44dc add packaging for buster and bullseye 2021-12-18 16:59:13 -05:00
Max Erenberg 19c860b4ed bump version 2021-12-18 16:42:26 -05:00
Max Erenberg f08f4872cf Add Kubernetes API endpoint (#38)
continuous-integration/drone/push Build is passing Details
Add an API for members to create their own Kubernetes namespace.

Co-authored-by: Max Erenberg <>
Reviewed-on: #38
Co-authored-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Co-committed-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
2021-12-18 16:35:05 -05:00
Rio Liu b4110d887d Expire member cli and api (#33)
continuous-integration/drone/push Build is passing Details
continuous-integration/drone Build is passing Details
Closes #23

Co-authored-by: Rio Liu <rio.liu@r26.me>
Co-authored-by: Max Erenberg <>
Reviewed-on: #33
Co-authored-by: Rio <r345liu@localhost>
Co-committed-by: Rio <r345liu@localhost>
2021-12-11 16:30:18 -05:00
Max Erenberg f1c0ce3dd6 packaging for buster and bullseye 2021-11-28 23:22:16 -05:00
Max Erenberg 1338825c5d use NGINX with acme.sh 2021-11-28 22:35:46 -05:00
Max Erenberg 3a30f45672 add packaging for buster and bullseye 2021-11-28 15:42:59 -05:00
Max Erenberg bd50f4142f use Caddy instead of NGINX for vhosts 2021-11-28 15:21:48 -05:00
Max Erenberg 0d55f01bfc packaging for buster 2021-11-27 18:23:32 -05:00
Max Erenberg e71d9b7d30 packaging for bullseye 2021-11-27 18:10:52 -05:00
Max Erenberg aa2efcb26a use master branch in CI badge 2021-11-27 18:01:18 -05:00
Max Erenberg a7c5098b67 Add cloud vhost API (#35)
continuous-integration/drone/push Build is passing Details
Add an API for members to create their own virtual hosts.

Co-authored-by: Max Erenberg <>
Reviewed-on: #35
Co-authored-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Co-committed-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
2021-11-27 17:59:21 -05:00
Max Erenberg 0798419e34 packaging for buster
continuous-integration/drone/push Build is passing Details
2021-11-21 12:31:11 -05:00
Max Erenberg 7306241a78 packaging for bullseye 2021-11-21 12:06:07 -05:00
Max Erenberg eda5ca576a add cloud API to docs 2021-11-21 11:53:25 -05:00
Max Erenberg ac98aaf38d Add API to manage cloud accounts (#34)
continuous-integration/drone/push Build is passing Details
This PR adds API endpoints and a CLI to create cloud accounts and to purge accounts of expired members.

Co-authored-by: Max Erenberg <>
Reviewed-on: #34
Co-authored-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Co-committed-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
2021-11-21 11:11:20 -05:00
Max Erenberg 798510511f fix first/last name script
continuous-integration/drone/push Build is passing Details
2021-11-07 01:12:49 -04:00
Max Erenberg ed9893604f fix lint error
continuous-integration/drone/push Build is passing Details
2021-11-03 21:23:16 -04:00
Max Erenberg 52db130ef8 update docs for packaging in a container
continuous-integration/drone/push Build is failing Details
2021-11-02 20:34:44 -04:00
Max Erenberg 99819ce4fe go back to symlinks 2021-11-02 20:30:56 -04:00
Max Erenberg 89febf0400 use python binaries in virtualenv when packaging 2021-11-02 20:11:42 -04:00
Max Erenberg 620ef8ef8e fix UWLDAP multiple-UID bug 2021-11-02 19:02:07 -04:00
Max Erenberg ae48bcd98a fix bug in email sender address
continuous-integration/drone/push Build is passing Details
2021-11-02 03:05:46 -04:00
Max Erenberg 1f107b0614 use port 465 for smtps
continuous-integration/drone/push Build is passing Details
2021-11-02 02:46:05 -04:00
Max Erenberg f9f5d70ad3 use pip install . instead of python setup.py install 2021-11-02 02:07:15 -04:00
Max Erenberg bdc2f9b31b include Jinja templates in MANIFEST.in 2021-11-02 01:53:53 -04:00
Max Erenberg 729f443e72 move term check to LDAPService instead of User constructor 2021-11-02 01:30:01 -04:00
Max Erenberg dbbc533111 add cryptography package to requirements.txt 2021-11-02 00:35:26 -04:00
Max Erenberg 57787c170a remove v1 from drone.yml
continuous-integration/drone/push Build is passing Details
2021-11-01 22:11:16 -04:00
Max Erenberg beec46a951 re-run CI
continuous-integration/drone/push Build is passing Details
2021-10-29 00:18:16 -04:00
Max Erenberg 3218938909 add suggestions from ehashman
continuous-integration/drone/push Build is failing Details
2021-10-28 22:27:03 -04:00
Max Erenberg 2493bb1a6b modify CSC schema instead of using inetOrgPerson
continuous-integration/drone/push Build is passing Details
2021-10-28 21:33:32 -04:00
Max Erenberg 02aff43e7f Add debian packaging (#32)
continuous-integration/drone/push Build is failing Details
Closes #31.

Co-authored-by: Max Erenberg <>
Reviewed-on: #32
Co-authored-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Co-committed-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
2021-10-28 20:52:19 -04:00
Max Erenberg 2970736105 place CI fix into common.sh
continuous-integration/drone/push Build is passing Details
2021-10-25 08:18:25 -04:00
Max Erenberg a0cc29738b change perms on /tmp in CI container
continuous-integration/drone/push Build is passing Details
2021-10-25 08:09:07 -04:00
Max Erenberg 3e1af74f0c re-run CI
continuous-integration/drone/push Build is failing Details
2021-10-25 07:42:04 -04:00
Max Erenberg a5bab379e2 Merge branch 'v1' of csclub.uwaterloo.ca:public/pyceo into v1 2021-10-25 07:41:52 -04:00
Max Erenberg ac573039da add mailLocalAddress to each record (#30)
continuous-integration/drone/push Build is failing Details
Closes #26.

Co-authored-by: Max Erenberg <>
Reviewed-on: #30
Co-authored-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Co-committed-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
2021-10-25 07:36:25 -04:00
Max Erenberg 1374ff95aa Merge branch 'v1' of csclub.uwaterloo.ca:public/pyceo into v1 2021-10-24 07:20:10 -04:00
Max Erenberg 23f40c74f9 Use inetOrgPerson instead of account (#29)
continuous-integration/drone/push Build is passing Details
Closes #25.

Co-authored-by: Max Erenberg <>
Reviewed-on: #29
Co-authored-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Co-committed-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
2021-10-23 23:21:09 -04:00
Max Erenberg ac7f41801b Merge branch 'v1' of csclub.uwaterloo.ca:public/pyceo into v1 2021-10-23 10:24:44 -04:00
Max Erenberg e3c50d867a Add isClubRep attribute (#27)
continuous-integration/drone/push Build is passing Details
Closes #24.

Co-authored-by: Max Erenberg <>
Reviewed-on: #27
Co-authored-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Co-committed-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
2021-10-23 10:23:43 -04:00
Max Erenberg 1fbc068f3b update README 2021-10-05 00:10:15 -04:00
Max Erenberg 1fcc49ef12 add documentation (#22)
continuous-integration/drone/push Build is passing Details
Add OpenAPI spec and man pages

Co-authored-by: Max Erenberg <>
Co-authored-by: Rio Liu <rio.liu@r26.me>
Co-authored-by: Andrew Wang <a268wang@csclub.uwaterloo.ca>
Reviewed-on: #22
Co-authored-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Co-committed-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
2021-10-05 00:07:10 -04:00
Max Erenberg 3e1131c4e4 no-resizing (#21)
continuous-integration/drone/push Build is passing Details
This PR disables resizing the TUI.
Unfortunately this is a regression from the old ceo. But trying to preserve state when destroying and creating new views in asciimatics proved to be very difficult.

Co-authored-by: Max Erenberg <>
Reviewed-on: #21
Co-authored-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Co-committed-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
2021-10-04 20:04:05 -04:00
Rio Liu 7edc01e42b Positions TUI (#20)
continuous-integration/drone/push Build is passing Details
Closes #17

Co-authored-by: Rio Liu <rio.liu@r26.me>
Co-authored-by: Max Erenberg <>
Reviewed-on: #20
Co-authored-by: Rio <r345liu@localhost>
Co-committed-by: Rio <r345liu@localhost>
2021-09-26 15:23:47 -04:00
Max Erenberg 2a5d903eba add mailman CLI command
continuous-integration/drone/push Build is passing Details
2021-09-25 15:16:22 -04:00
Max Erenberg 3cba9680f5 test that email is sent when user is created 2021-09-25 13:56:23 -04:00
Max Erenberg 749ca41080 use custom TransactionView for AddUser
continuous-integration/drone/push Build is passing Details
2021-09-17 23:46:12 -04:00
Max Erenberg de18a9f293 add option to use Docker instead of VM (#16)
continuous-integration/drone/push Build is passing Details
Co-authored-by: Max Erenberg <>
Co-authored-by: Rio <r345liu@localhost>
Reviewed-on: #16
Co-authored-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Co-committed-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
2021-09-17 22:39:27 -04:00
Max Erenberg 652620a7c5 fix lint errors
continuous-integration/drone/push Build is passing Details
2021-09-12 09:36:54 -04:00
Max Erenberg 155c96c500 implement Database views in TUI
continuous-integration/drone/push Build is failing Details
2021-09-12 02:08:15 -04:00
Max Erenberg ad38588141 use single ListBox in WelcomeView
continuous-integration/drone/push Build is passing Details
2021-09-11 17:24:23 -04:00
Andrew Wang 33323fd112 Add database CLI (#15)
continuous-integration/drone/push Build is passing Details
Closes #12

Co-authored-by: Andrew Wang <a268wang@csclub.uwaterloo.ca>
Co-authored-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Reviewed-on: #15
Co-authored-by: Andrew Wang <a268wang@localhost>
Co-committed-by: Andrew Wang <a268wang@localhost>
2021-09-11 13:33:43 -04:00
Max Erenberg cb6243c3e2 remove unused handler
continuous-integration/drone/push Build is passing Details
2021-09-08 22:29:36 +00:00
Neil Parikh 6e2b9dee24 Update discord link (#13)
continuous-integration/drone/push Build is passing Details
Co-authored-by: n3parikh <n3parikh@localhost>
Reviewed-on: #13
Co-authored-by: n3parikh <n3parikh@csclub.uwaterloo.ca>
Co-committed-by: n3parikh <n3parikh@csclub.uwaterloo.ca>
2021-09-08 18:23:29 -04:00
Rio Liu 651988bb08 Positions CLI (#11)
continuous-integration/drone/push Build is passing Details
Closes #9

Co-authored-by: Rio6 <rio.liu@r26.me>
Co-authored-by: Rio Liu <rio.liu@r26.me>
Co-authored-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Reviewed-on: #11
Co-authored-by: Rio <r345liu@localhost>
Co-committed-by: Rio <r345liu@localhost>
2021-09-08 09:32:34 -04:00
Max Erenberg 82b7b2c015 fix lint errors
continuous-integration/drone/push Build is passing Details
2021-09-08 04:11:03 +00:00
Max Erenberg 0bf24230a0 add global quit button
continuous-integration/drone/push Build is failing Details
2021-09-08 04:10:21 +00:00
Max Erenberg 4aaf10b687 add Databases and Positions menus 2021-09-08 03:38:12 +00:00
Max Erenberg df7148940a implement RemoveMemberFromGroupView
continuous-integration/drone/push Build is passing Details
2021-09-08 03:20:51 +00:00
Max Erenberg 21173d1b8c implement AddMemberToGroupView 2021-09-08 02:59:56 +00:00
Max Erenberg beb16b1740 implement GetGroupView 2021-09-07 05:22:20 +00:00
Max Erenberg 6b3ad28e89 implement AddGroupView
continuous-integration/drone/push Build is passing Details
2021-09-07 05:02:34 +00:00
Max Erenberg ebaeeaaf13 implement ChanageLoginShellView and SetForwardingAddressesView
continuous-integration/drone/push Build is failing Details
2021-09-07 04:16:29 +00:00
Max Erenberg a08fca4c60 fix lint errors
continuous-integration/drone/push Build is passing Details
2021-09-07 03:05:13 +00:00
Max Erenberg 1406899ea2 implement ResetPasswordView
continuous-integration/drone/push Build is failing Details
2021-09-07 03:03:30 +00:00
Max Erenberg d3c98e418a implement GetUser in TUI 2021-09-07 02:29:53 +00:00
Max Erenberg af73dd713d add flash_message 2021-09-06 20:16:45 +00:00
Max Erenberg 39158676ae use CeoFrame as parent class for TransactionView
continuous-integration/drone/push Build is passing Details
2021-09-06 16:48:07 +00:00
Max Erenberg 40ee927b91 Merge branch 'v1' of csclub.uwaterloo.ca:public/pyceo into v1 2021-09-06 16:42:13 +00:00
Max Erenberg ee21873ad7 implement membership renewals in TUI 2021-09-06 16:40:05 +00:00
Max Erenberg cce920d6ba save view state in model 2021-09-05 22:48:20 +00:00
Andrew Wang c6c01d8720 allow mysql connections from unix socket (#14)
continuous-integration/drone/push Build is passing Details
Co-authored-by: Andrew Wang <someone.zip@gmail.com>
Co-authored-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Reviewed-on: #14
Co-authored-by: Andrew Wang <a268wang@localhost>
Co-committed-by: Andrew Wang <a268wang@localhost>
2021-09-04 22:25:37 -04:00
Max Erenberg 6f1851fc19 Merge branch 'v1' into tui
continuous-integration/drone/push Build is failing Details
2021-09-04 23:57:50 +00:00
Max Erenberg bb56870652 add skeleton for TUI 2021-09-04 23:05:19 +00:00
Andrew Wang eb5d632606 db-api (#10)
continuous-integration/drone/push Build is passing Details
Implement DB endpoints

Co-authored-by: Andrew Wang <someone.zip@gmail.com>
Co-authored-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Reviewed-on: #10
Co-authored-by: Andrew Wang <a268wang@localhost>
Co-committed-by: Andrew Wang <a268wang@localhost>
2021-08-29 13:08:35 -04:00
Max Erenberg 7d23fd690f store GSSAPI token in flask.g
continuous-integration/drone/push Build is passing Details
2021-08-28 05:51:48 +00:00
Max Erenberg d8e5b1f1d4 update README
continuous-integration/drone/push Build is passing Details
2021-08-26 02:26:56 +00:00
Max Erenberg 46881f7a1f update .drone.yml
continuous-integration/drone/push Build is passing Details
2021-08-26 02:20:24 +00:00
Max Erenberg e011e98026 use GSSAPI delegation
continuous-integration/drone/push Build was killed Details
2021-08-26 02:19:18 +00:00
Max Erenberg 95e167578f remove libsasl2-dev dependency 2021-08-24 20:50:34 +00:00
Max Erenberg 51737585bd add updateprograms CLI
continuous-integration/drone/push Build is passing Details
2021-08-24 19:37:05 +00:00
Max Erenberg 831ebf17aa add groups CLI
continuous-integration/drone/push Build is passing Details
2021-08-24 05:48:55 +00:00
Max Erenberg 45192d75bf update social media links in welcome message
continuous-integration/drone/push Build is passing Details
2021-08-23 23:40:52 +00:00
Max Erenberg e851c77e74 include password in welcome email 2021-08-23 23:36:49 +00:00
Max Erenberg 08a3faaefc add unit tests for members CLI
continuous-integration/drone/push Build is passing Details
2021-08-23 23:01:24 +00:00
Max Erenberg 7a8751fd8f Merge branch 'v1' of csclub.uwaterloo.ca:public/pyceo into v1 2021-08-23 13:59:24 +00:00
Max Erenberg 6917247fdd add members CLI 2021-08-23 13:59:01 +00:00
Rio Liu ad937eebeb Positions API (#7)
continuous-integration/drone/push Build is passing Details
Co-authored-by: Max Erenberg <merenber@csclub.uwaterloo.ca>
Co-authored-by: Rio Liu <r345liu@csclub.uwaterloo.ca>
Co-authored-by: Rio6 <rio.liu@r26.me>
Reviewed-on: #7
Co-authored-by: Rio <r345liu@localhost>
Co-committed-by: Rio <r345liu@localhost>
2021-08-22 17:57:36 -04:00
Max Erenberg 0974a7471b ignore UserAlreadySubscribedError
continuous-integration/drone/push Build is passing Details
2021-08-22 06:06:11 +00:00
Max Erenberg 0783588323 announce new user to ceo mailing list
continuous-integration/drone/push Build is passing Details
2021-08-22 05:44:41 +00:00
Max Erenberg 7142659a8c force delete Kerberos test principals
continuous-integration/drone/push Build is passing Details
2021-08-22 04:36:19 +00:00
Max Erenberg 862dfc01b2 add trigger branches to drone.yml
continuous-integration/drone/push Build is passing Details
2021-08-21 07:20:40 +00:00
Max Erenberg bb82945b41 remove hostname from /etc/hosts in auth1
continuous-integration/drone/push Build is passing Details
2021-08-21 07:13:36 +00:00
Max Erenberg 38f354c106 add sasl-host to slapd.conf
continuous-integration/drone/push Build is failing Details
2021-08-21 06:54:59 +00:00
Max Erenberg 95d083fca1 use our own SPNEGO implementation
continuous-integration/drone/push Build is failing Details
2021-08-21 06:27:33 +00:00
Max Erenberg 89e6c541ab add hostname check
continuous-integration/drone/push Build is failing Details
2021-08-20 18:46:36 +00:00
Max Erenberg c39eff6ca7 let service container sleep
continuous-integration/drone/push Build is failing Details
2021-08-20 18:39:31 +00:00
Max Erenberg e4970bf008 remove search option from resolv.conf
continuous-integration/drone/push Build is failing Details
2021-08-20 18:34:29 +00:00
Max Erenberg d11c6af2ec add tests to drone.yml
continuous-integration/drone/push Build is failing Details
2021-08-20 18:17:00 +00:00
Max Erenberg 4783621d22 update CI badge
continuous-integration/drone/push Build is passing Details
2021-08-20 02:29:25 +00:00
Max Erenberg 14273dcbe6 add drone.yml
continuous-integration/drone/push Build is passing Details
2021-08-20 02:24:55 +00:00
Max Erenberg 14c058eb67 use socket.gethostname() in krb5 test 2021-08-20 01:57:53 +00:00
Max Erenberg dc09210d23 add documentation about architecture 2021-08-20 01:41:50 +00:00
Max Erenberg 583fcded9b add test for API request without KRB-CRED 2021-08-19 23:53:13 +00:00
Max Erenberg 46fd926acc add test for RemoteMailmanService 2021-08-19 22:08:48 +00:00
Max Erenberg 490abb302c add simple authz tests 2021-08-19 20:33:44 +00:00
Max Erenberg 26fd8f6f68 remove duplicate function definition 2021-08-19 17:22:34 +00:00
Max Erenberg 2a286579cb Merge branch 'v1' into uwldap_tests 2021-08-19 17:20:47 +00:00
Max Erenberg ecf089c261 Implement Groups API (#6)
This PR implements the /api/groups endpoints.

Closes #2.

Reviewed-on: #6
Co-authored-by: Max Erenberg <merenber@localhost>
Co-committed-by: Max Erenberg <merenber@localhost>
2021-08-19 12:58:59 -04:00
Max Erenberg cc0bc4a638 add tests for Mailman API 2021-08-19 16:14:41 +00:00
Max Erenberg 2273ffa241 add test for krb5 2021-08-19 06:21:30 +00:00
Max Erenberg 12a83ce4c0 remove create_sync_response 2021-08-19 05:11:22 +00:00
Max Erenberg 28c55b2fed add tests for UWLDAP API 2021-08-19 04:56:25 +00:00
Max Erenberg 448692018a add test for group.to_dict() with one member 2021-08-19 00:23:55 +00:00
Max Erenberg 6bf4d75a60 log error message instead of traceback 2021-08-19 00:19:57 +00:00
Max Erenberg 5bda74eaf9 fix test_group_to_dict 2021-08-19 00:05:44 +00:00
Max Erenberg df5d9e5f14 Merge branch 'v1' into groups_api 2021-08-19 00:02:09 +00:00
Max Erenberg 57ab275634 implement /api/groups endpoints 2021-08-18 23:48:17 +00:00
Max Erenberg e370035b25 add cffi as dev dependency 2021-08-18 19:53:30 +00:00
Max Erenberg d78d31eec0 add Kerberos delegation (#5)
This PR adds unconstrained Kerberos delegation to the API.

The client obtains a forwarded TGT and sends it, base64-encoded, in an HTTP header named 'X-KRB5-CRED'. The server reads this credential, creates a new credentials cache for the user, and stores the credential into the new cache. The server can now authenticate to other services (e.g. LDAP) over GSSAPI using the forwarded client's credentials.

Reviewed-on: #5
Co-authored-by: Max Erenberg <merenber@localhost>
Co-committed-by: Max Erenberg <merenber@localhost>
2021-08-18 15:39:14 -04:00
Max Erenberg dd59bea918 add Kerberos delegation 2021-08-18 01:59:24 +00:00
Max Erenberg d82b5a763b use ldap3 instead of python-ldap 2021-08-15 05:04:49 +00:00
Max Erenberg 6cdb41d47b move all tests to top-level folder 2021-08-14 00:11:56 +00:00
Max Erenberg cbf4aa43f8 add tests for uwldap 2021-08-04 20:59:36 +00:00
Max Erenberg 9e4d564a33 move INI file locations 2021-08-04 17:15:06 +00:00
Max Erenberg 3ecf43731f add tests for Group class 2021-08-04 06:33:50 +00:00
Max Erenberg e7bfe36c0b add tests for User class 2021-08-04 05:54:21 +00:00
Max Erenberg 87298e18b3 cast string values in Config 2021-08-04 03:30:19 +00:00
Max Erenberg baeb83b1e2 use ConfigParser 2021-08-03 23:19:33 +00:00
Max Erenberg 4a312378b7 remove mailman transactions 2021-08-03 20:11:13 +00:00
Max Erenberg 96cb2bc808 add updateprograms 2021-08-03 14:09:07 +00:00
Max Erenberg 7c67a07200 use create_sync_response 2021-08-03 03:20:11 +00:00
Max Erenberg c32e565f68 implement renewals and password resets 2021-08-02 08:01:13 +00:00
Max Erenberg da14764687 Merge branch 'v1' of https://git.csclub.uwaterloo.ca/public/pyceo into v1 2021-08-02 07:21:20 +00:00
Max Erenberg ff2ac95d5e add PATCH /api/members/:username endpoint 2021-08-02 07:19:29 +00:00
Max Erenberg 9227552b29 re-send EHLO after STARTTLS 2021-07-31 08:34:06 -04:00
Max Erenberg 7b749701f0 add README 2021-07-24 21:35:09 +00:00
Max Erenberg e966e3f307 add app factory 2021-07-24 21:09:10 +00:00
Max Erenberg 3b78b7ffb4 add MailService and MailmanService 2021-07-24 00:08:22 +00:00
Max Erenberg de0f473881 add base classes for users and groups 2021-07-19 05:47:39 +00:00
Max Erenberg 0c6dc18085 update mailman path to use virtualenv 2021-05-18 01:52:09 -04:00
Zachary Seguin 2a7777b59e Set userPassword field for SASL authentication 2021-05-07 21:54:49 -04:00
Max Erenberg b5dda1df3d decrease minimum username length from 3 to 2 2021-05-02 18:12:44 -04:00
Max Erenberg 176e863f9b rename package version to buster 2021-04-11 22:34:38 -04:00
Max Erenberg 7223ac6278 update debian changelog 2021-04-11 21:56:34 -04:00
Max Erenberg eaff8d779b use Mailman 3 to subscribe new members to csc-general 2021-04-11 16:10:56 -04:00
Zachary Seguin 1a3244e039 Move adduser and mail operations to phosphoric-acid due to decommissioning of aspartame
Build for buster and stretch
2021-03-21 23:19:07 -04:00
Jennifer Zhou e73c7aae39 Merge branch 'master' of /users/git/public/pyceo 2018-10-21 22:41:58 -04:00
Jennifer Zhou 723f7ecfe7 Packaging for bionic 2018-10-21 22:40:04 -04:00
Jennifer Zhou d82fa5780b Packaging for bionic 2018-10-21 22:31:28 -04:00
Zachary Seguin 82cb7b1e64 Package for buster 2018-04-15 14:33:23 -04:00
Zachary Seguin 3976b1b17e Build ceo for Jessie and Xenial 2017-05-02 00:27:59 -04:00
Zachary Seguin c3ac824d67 Add python-dnspython as a dependency 2017-05-02 00:27:33 -04:00
Zachary Seguin df5f61ffd0 Update check_email to handle hosts without an A record (either AAAA only or MX only) 2017-05-01 17:36:05 -04:00
Zachary Seguin 466af409b4 Build for stretch 2017-01-20 23:08:35 -05:00
Zachary Seguin 548ee6de58 Re-build so debian.csclub include library as a dependency 2016-02-20 16:12:32 -05:00
Zachary Seguin 44db85fe2f Resolves issue where CEO would not start 2016-02-20 00:10:06 -05:00
Felix Bauckholt 4b0db35899 Replace the stub of the library with a system call to "librarian" 2016-02-19 21:57:54 -05:00
Zachary Seguin db90d4ce00 Update CEO for latest package versions on Jessie 2015-11-11 22:45:42 -05:00
Sean Hunt 5cf982e8d5 Remind users that club accounts are free. 2014-07-22 14:25:33 -04:00
Sean Hunt a23cd81cf7 Package for saucy. 2013-12-05 16:24:13 -05:00
Luqman Aden 0bb49fe405 Packaging for jessie. 2013-10-10 22:03:59 -04:00
Jeremy Roman 359c1f14eb squeeze build :( 2013-09-16 08:34:37 -04:00
Jeremy Roman 0352e952b8 changelog 0.5.24 2013-09-16 08:33:55 -04:00
Jeremy Roman 7a16c2d57c fix ceo_add_user_call in addclub 2013-09-16 08:28:46 -04:00
Jeremy Roman beef8f4abd update changelog (twice, because I forgot to update the reprepro config first) 2013-09-07 12:07:45 -04:00
Jeremy Roman 59194475a6 don't attempt to store Kerberos principal data in LDAP; this is not the current Kerberos backend used 2013-09-07 11:44:44 -04:00
Marc Burns a4ebb86d18 Fix build for squeeze. 2013-05-28 11:17:35 -04:00
Marc Burns ece1a2f92b Work around bug in libgssapi 2.0.25 present in wheezy. 2013-05-28 10:48:05 -04:00
Owen Smith b1ee751fa1 packaging for quantal 2013-05-25 20:00:11 -04:00
Sarah Harvey 703519acbf updated configs to change ceod to run on aspartame instead of ginseng 2013-02-07 00:20:21 -05:00
Sarah Harvey 5a78e55e96 Updated changelog with new version(s) 2012-09-12 08:43:00 -04:00
Sarah Harvey d8e9225ac9 changed mailman host to mail instead of caffeine 2012-09-10 19:08:46 -04:00
Jeremy Roman 69fe79fe35 build for precise 2012-04-26 15:21:08 -04:00
Jeremy Roman a81a97e296 Merge branch 'master' of /users/git/public/pyceo
Conflicts:
	debian/changelog
2012-03-19 16:29:54 -04:00
Jeremy Roman 3bed2ad5c0 build for oneiric 2012-03-19 16:29:26 -04:00
Marc Burns 494ec5106a Change ceo version. 2012-03-16 15:36:52 -04:00
Marc Burns 094efa9d93 Commit modified changelog. 2012-03-16 15:30:09 -04:00
Marc Burns b7b4105e51 This modification causes ceod to add the Kerberos principal.
It returns an error code to ceoc, which I will now fix.
2012-03-16 15:15:59 -04:00
Michael Spang dcc2222816 Fix default selection of next button
This used to work until urwid tried to start becoming smart about
which widget gets focus. Revert back to the dumb behavior.
2011-09-27 09:38:03 -04:00
Michael Spang ca7bf4d752 Update changelog for 0.5.16 2011-09-17 16:36:45 -04:00
Michael Spang 37a662540e Allow CM club to disable mailman subscription
This features breaks CMC ceo.
2011-09-17 16:28:49 -04:00
Elana Hashman dd98f24e17 Added progcom contact to welcome message 2011-09-03 11:18:26 -04:00
Jeremy Roman 0b3c9d835b tell ceod when it is a club rep; club reps don't need the new member email 2011-08-26 03:13:14 -04:00
Jeremy Roman e468fdcdbd welcome message 2011-08-26 01:04:21 -04:00
Jeremy Roman 5d71c4f7e8 merge in mspang's change which he didn't push to public/pyceo.git 2011-05-09 19:20:46 -04:00
Jeremy Roman 91cd9c7f24 Mailman moved 2011-05-09 19:14:16 -04:00
Michael Spang d925158e25 Update changelog 2011-03-13 03:24:54 -04:00
Michael Spang 8c61a6b918 sudoRunAs is deprecated 2011-03-13 03:23:17 -04:00
Michael Spang 1781e9bb83 Merge branch 'master' of /users/git/public/pyceo 2011-03-09 04:04:44 -05:00
Marc Burns 3c61210328 Fix library check in and search bug. 2011-03-04 16:54:48 -05:00
Michael Spang d866c4ce66 Refix post-tag 2011-03-04 00:53:03 -05:00
Michael Spang a54bf66fea Fix post-tag 2011-03-04 00:49:49 -05:00
Michael Spang aa9e963e76 Update changelog 2011-03-04 00:48:11 -05:00
Michael Spang ccd25d0eda Add m4burns to debian/control
The only point of this is to get rid of the annoying "non-maintainer upload"
behaviors of the debian tools.
2011-03-04 00:45:54 -05:00
Michael Spang c7170da126 Fix squeeze build warnings 2011-03-04 00:43:50 -05:00
Marc Burns 9b1e068894 Modified ceo/urwid/library.py to display message when book search yields no results. 2011-02-28 13:18:01 -05:00
Michael Spang c28889b7f9 Update changelog 2010-10-14 14:28:04 -04:00
Michael Spang a904bc57c9 Add jbroman to uploaders
This stop it from complaining about "non-maintainer uploads".
2010-10-14 14:28:04 -04:00
Michael Spang e607fa6c0c Make ceod build with kerberos 1.8 2010-10-14 14:28:02 -04:00
Michael Spang f1bce25d0c Fix freopen properly 2010-10-14 14:00:07 -04:00
Jeremy Roman 8b006491a7 fixed changelog 2010-09-26 22:36:05 -04:00
Jeremy Roman 5dfb716d11 fixed bug reported by jdonland 2010-09-26 22:34:08 -04:00
Jeremy Roman 066ef693eb adjust changelog to make debuild happy 2010-09-25 01:10:09 -04:00
Jeremy Roman 2caeaf536a update changelog for 0.5.8 2010-09-25 01:07:03 -04:00
Jeremy Roman 9ebd8d910f adding users to csc-general 2010-09-25 01:00:27 -04:00
Jeremy Roman e33c483d3a squeeze support 2010-09-24 21:19:36 -04:00
Jeremy Roman 6193168b2d add new members for multiple terms 2010-09-24 21:11:36 -04:00
Jeremy Roman 046c0aa4f5 tab support finally lands in ceo 2010-09-24 20:31:00 -04:00
Jeremy Roman c31e7167f2 updating the changelog 2010-09-14 18:56:04 -04:00
Jeremy Roman 28ca744cff add Office Manager position to positions list 2010-09-14 17:56:52 -04:00
Michael Ellis 1d0dbe3774 Added phpmyadmin URL to mysql info file 2010-08-19 14:24:05 -04:00
Michael Ellis 31720e3d4c fixing my email in changelog 2010-06-18 21:31:53 -04:00
Michael Ellis 1d73841693 No more office/syscom entries. Check if group is valid 2010-06-18 21:03:17 -04:00
Michael Ellis 67b56af44c Don't use uwdir emails for expired accounts since we ask for ~/.forward adress now. 2010-06-18 18:54:34 -04:00
Michael Spang fd65717c57 Bump version to 0.5.7 2010-05-09 02:11:11 -04:00
Michael Spang 0e504612c8 Readd quota support 2010-05-09 02:10:31 -04:00
Michael Ellis 396140ef6b Reworded expired account email. Club rep accounts can be renewed for free (as usual). 2010-02-01 10:10:09 -05:00
Michael Spang d02ee89728 Fix expiredaccounts
Also make it only send to accounts expired within the last 3 terms (a year).
2010-01-19 01:10:41 -05:00
Michael Spang bc55a2cb58 Bump version to 0.5.6 2009-12-20 13:49:14 -05:00
Michael Spang 1d647d49b3 Remove ternary operators
This removes complains by python-support on lenny.
2009-12-14 19:34:16 -05:00
Jeremy Roman 6f0c920435 added ability to use first letter of menu items 2009-11-24 12:42:49 -05:00
Michael Spang 60ead6d1e8 Fix auth for mysql database creation 2009-11-15 14:21:04 -05:00
Michael Spang e99a863c20 Fix use of freopen 2009-11-02 15:41:29 -05:00
Michael Spang db6fb3aeaf Bump version to 0.5.5 2009-11-02 20:35:09 +00:00
Michael Spang c0d9e7f3c7 Add CLI version of mysql thing 2009-11-02 20:18:55 +00:00
Michael Spang 6f4f0e6621 Add missing dependency on python-mysql 2009-11-02 14:49:32 -05:00
Michael Spang 01d97d489e Bump version to 0.5.4 2009-11-02 03:05:07 +00:00
Michael Spang d45fc419fa Switch from SCTP to TCP
Turns out SCTP doesn't work inside a container.
2009-11-01 16:05:56 -05:00
Michael Spang 11f432734c Bump version to 0.5.3 2009-10-24 14:50:23 -04:00
Michael Spang d37c8beac5 Improve error handling when writing 2009-10-24 14:47:44 -04:00
Michael Spang 0d42df6a85 Encrypt all post-auth ceoc<->ceod communication 2009-10-24 12:59:03 -04:00
Michael Spang 235681263d Fail fast if not authenticated 2009-10-24 12:24:07 -04:00
Michael Spang c00668b914 Clarify email forwarding upon renewal 2009-10-24 11:45:18 -04:00
Michael Spang c2b05b3d0f Fix gss error reporting bug 2009-09-20 20:10:27 -04:00
Michael Spang 655daaff8a Bump version 2009-09-16 18:33:50 -04:00
Michael Spang 5c3d5e861c Force redraw after status thing 2009-09-16 18:30:35 -04:00
Michael Spang 5606ef01e5 Add status thing 2009-09-16 17:32:11 -04:00
Michael Spang 2d023e6ec4 Blacklist orphaned/expired from updateprograms 2009-09-10 19:11:14 -04:00
Michael Spang 15bbfd0e07 Kill mathsoclist
We can't reliably filter the membership list, because we don't know who has
paid the MathSOC fee. Better to leave MathSOC to do this. In the case that they
to not verify the list then mathsoclist puts us at a disadvantage, as other
clubs likely do not filter their lists.
2009-09-10 18:59:22 -04:00
Michael Spang c51107ae2b Write mysql file to ~club 2009-09-10 15:56:52 -04:00
Michael Spang c7bd720124 Fix segfault 2009-09-10 14:41:42 -04:00
Michael Spang 8c1eb0a911 Move some code 2009-09-10 14:40:17 -04:00
Michael Spang 8ebe625e5f Clarify search operation in menu 2009-09-10 14:12:50 -04:00
Michael Spang 910de689cb Update changelog and fix lintian warnings about it 2009-09-10 07:36:49 -04:00
Michael Spang 2552bc2243 Add mysql database stuff 2009-09-10 07:33:32 -04:00
Michael Spang 827c17b107 Add manpage for ceod 2009-09-10 07:30:51 -04:00
Michael Spang 47a2e5e689 Rename ceo-gui to ceo-python 2009-09-10 07:30:49 -04:00
Michael Spang db6b95a7cf Add build.sh 2009-09-09 17:47:32 -04:00
Michael Spang ac79cd6e64 Kill Bartle's hybrid main.py 2009-09-09 06:58:56 -04:00
Michael Spang 5d8d866fca Fix deadlock bug when daemonizing
Closing stdin et. al breaks the assuption in spawnvem() that a newly
opened pipe is not one of the standard file descriptors. This lead
to stdout being closed in the child and so we got no output.
2009-09-08 18:50:34 -04:00
Michael Spang 80ac98531f Add init script for ceod 2009-09-08 17:50:31 -04:00
Michael Ellis c931a6bedb Fixed Library: Added signing to AWS requests. 2009-09-08 17:04:44 -04:00
Michael Spang 0413dcaaa4 Add labels to main menu
Most of CEO is restricted to office staff or worse, but anyone can run
it. This add labels to make necessary privileges clear to the user.
2009-08-23 15:40:22 -04:00
Michael Spang 5dc46021c5 Allow install if we have python-pyscopg2
Dunno if it works, but if not we need to make it as the old version is
obsolete.
2009-08-23 13:40:11 -04:00
Michael Spang 35179ec978 Add UI for email forwarding
We nag users to update their forwarding address every time they renew
membership.
2009-08-23 13:40:08 -04:00
Michael Spang 9eefe615c5 Add mail changing
This allows office staff to update people's .forward files via ceod.
2009-08-23 13:30:24 -04:00
Jacob Parker 4c1a7f8ee4 Merge branch 'master' of caffeine:/users/git/public/pyceo 2009-08-22 16:16:21 -04:00
Jacob Parker dd895884a9 Creates a .forward file for users if they enter an email. 2009-08-22 16:09:12 -04:00
Michael Spang c4cb1a3b29 Clean up password prompt 2009-08-22 15:05:56 -04:00
David Bartley fe9af9994e Depend on krb5 >= 1.7 2009-08-06 06:01:46 -04:00
David Bartley f309b9133b Add python-protobuf dependency 2009-08-06 05:58:22 -04:00
Michael Spang b1e054e5b9 Add some reminders
Note that the list of members we send to MathSoc to determine our
budget uses data from UW ldap. Thus we should be doubly sure people
sign up using their UW userid.
2009-08-06 02:35:45 -04:00
Michael Spang 5c88b54f78 Fix dependency of python protobuf 2009-08-06 01:43:17 -04:00
Michael Spang 4ee16577aa Disallow realname = username in GUI 2009-08-06 01:41:02 -04:00
Michael Spang e6e673447e Use ceoc directly in the gui 2009-08-06 01:39:33 -04:00
Michael Spang b348f5d5bd Always log to stderr in ceoc
The python thing will read errors from stderr, we need to log there
even if it's not a tty.
2009-08-06 01:00:10 -04:00
Michael Spang e3555e5b74 Build python protobuf 2009-08-06 00:46:18 -04:00
Michael Spang 17eb4d40b9 Track errors 2009-08-06 00:45:47 -04:00
Michael Spang 628c9076fe Change directory only when detaching 2009-07-31 04:29:07 -04:00
Michael Spang 43319f134b Use ldap_sasl_mech 2009-07-31 01:55:47 -04:00
Michael Spang b3face5de9 Cleanup Makefile 2009-07-31 01:34:38 -04:00
Michael Spang de59ad2755 Use LOG_PID everywhere 2009-07-31 01:20:59 -04:00
Michael Spang 2f6b0bd6e1 Dunno how I ever thought this would work
It worked for as long as it only used one out-of-scope array. Now
we're using two.
2009-07-30 22:50:00 -04:00
David Bartley a7961f1b9f Set acl's for club home directories. 2009-07-30 04:44:26 -04:00
Michael Spang 64f6eb6c8c Half way to 1.0! 2009-07-30 00:20:51 -04:00
Michael Spang 7f3f4c3a48 Install ceo to /usr not /usr/local 2009-07-30 00:07:01 -04:00
Michael Spang 873f7ac9a6 Merge commit 'ceod' 2009-07-29 23:49:26 -04:00
Michael Spang 778efc71aa Updates for LDAP-backed Kerberos
Principals are now created implicitly when the LDAP entry for a user
is added. We add the keys by "changing" the password from nonexistent
to existent.
2009-07-29 16:47:11 -04:00
Michael Spang 1c8e247732 Allow digits in variable names 2009-07-29 16:14:52 -04:00
Michael Spang c4b021b4f6 Fix arguments mismatch insanity
My reverting some of dtbartle's zfs-related stuff left us with a
hybrid zfsaddhomedir that was insane. This became a simpleaddhomedir
that was almost as insane.
2009-07-29 14:07:23 -04:00
Michael Spang b523fbf206 Fix getgroups call error
Made long ago. Jaunty's compiler detects this interestingly.
2009-07-29 14:07:18 -04:00
Michael Spang c0b87dbc98 Add some commas to dependencies 2009-07-29 13:35:12 -04:00
Michael Spang 5ce11709ff Merge commit 'public/master' into ceod 2009-07-29 13:29:42 -04:00
Michael Spang ad30f9c47a Insanify configuration files 2009-07-29 13:08:53 -04:00
Michael Spang 92652a3af0 Install ceo daemon
We need to split into different packages for the daemon and clients.
2009-07-29 11:35:32 -04:00
Michael Spang 23c6c89237 Fix redundant arguments to linker 2009-07-29 09:58:11 -04:00
Michael Spang e0332eecac Fix home directory error handling 2009-07-29 09:47:04 -04:00
Michael Spang aaeca32107 Make connection failure message more clear 2009-07-29 09:28:39 -04:00
Michael Spang bfff0e8e87 Use --as-needed when linking
This avoids complaints by dpkg due to unnecessary links.
2009-07-29 09:22:38 -04:00
Michael Spang e210f7d38b Remove unused vars 2009-07-29 09:00:55 -04:00
Michael Spang 37eb3e6465 Resurrect linux homedir support 2009-07-29 08:56:27 -04:00
Michael Spang 0d52c0475b Kill zfsaddhomedir 2009-07-29 08:30:08 -04:00
Michael Spang 6b83cc5c05 Abort on unexpected SCTP errors 2009-07-29 08:18:35 -04:00
Michael Spang 99ce020ba0 Fix changelog version 2009-07-29 07:31:34 -04:00
Michael Spang a838e28a2c Ignore debhelper log 2009-07-29 07:29:31 -04:00
Michael Spang 49bdd24661 Remove Solaris LDFLAGS 2009-07-29 07:28:53 -04:00
mgregson d818a687fc Moving from zfsaddhomedir to simpleaddhomedir. 2009-07-28 16:03:36 -06:00
mgregson 597c2180b9 Added simpleaddhomedir to makefile. 2009-07-28 16:02:59 -06:00
mgregson 283cfd1f49 Fixed stupid shit with incorrect arg counting. 2009-07-28 16:02:28 -06:00
mgregson c57902dfd0 Modifying zfsaddhomedir to operate on not-zfs stuff. Ignores quotas and ACLs. 2009-07-28 15:38:19 -06:00
Michael Spang 47b601d224 Update call to kadm5_init_with_skey 2009-07-25 05:53:54 -04:00
Michael Spang 25646ac593 Remove kadmin headers 2009-07-25 05:46:09 -04:00
Michael Spang 4ede8212d5 Require TGT in ldap_init 2009-07-25 05:34:08 -04:00
Michael Spang bac4db4f4d Fail op handling if unathenticated 2009-07-25 05:29:37 -04:00
Michael Spang 0c828122ac Make kerberos code more verbose 2009-07-25 05:29:21 -04:00
Michael Spang e75390b7de Fix networking bugs 2009-07-25 05:29:05 -04:00
Michael Spang 60e272e8c6 Remove keytab configuration
Instead we'll always use the default keytab, which is /etc/krb5.keytab
or the KRB5_KTNAME environment variable.
2009-07-25 04:24:44 -04:00
Michael Spang d6e6b2bc63 Add python-psycopg to depends 2009-07-25 02:28:30 -04:00
Michael Spang 7c2f6459e6 Add python-sqlobject to depends 2009-07-25 02:19:31 -04:00
Michael Spang e5394d7729 Make lintian happy 2009-07-24 18:48:12 -04:00
Michael Spang 5ef116c456 Add libsctp-dev to build dependencies 2009-07-23 20:43:13 -04:00
Michael Spang 5f99987916 Remove pointless indentation 2009-07-20 00:13:37 -04:00
Michael Spang 4ebea28c59 Revert "I bet this speeds up the compilation"
This reverts commit 6055aecb27.
2009-07-20 00:13:17 -04:00
Michael Spang 6977d1efd2 Revert "Use rsync in zfsaddhomedir"
This reverts commit 88952ae56a.
2009-07-20 00:12:59 -04:00
David Bartley 7766bddccb Fix typo in debian/control 2009-06-26 00:49:50 -04:00
Anthony Brennan 70ee21540b Updated the dependencies list to include all necessary python packages. 2009-06-25 20:36:41 -04:00
Michael Gregson 4720fcd252 Added comments containing code to add new members to a mailing list using listadmin.
Left to do:
  - create mailing list
  - create and publish listadmin config file
  - update code to use listadmin config file
  - uncomment code
  - ponder implications of listadmin config file (security)
2009-06-17 20:33:42 -06:00
David Bartley 4e1bc7fc41 Get rid of compile warning 2009-06-12 19:16:37 -04:00
David Bartley 1394f9a1c8 A bit better error handling 2009-06-12 18:49:52 -04:00
Michael Gregson da850543e5 Wee! New version of CEO 2009-03-11 03:30:34 -04:00
Michael Gregson 8805756a5e Fixing overdue check. 2009-03-11 03:20:12 -04:00
Michael Gregson 1f9607b3a0 Fixing library search shit. 2009-03-11 03:15:48 -04:00
Michael Gregson 9da9dbc920 Ooops 2009-03-11 02:26:05 -04:00
Michael Gregson 370b446414 Window now goes away, hopefully. 2009-03-11 02:24:57 -04:00
Michael Gregson d230578ff9 Updating changelog. 2009-03-11 02:10:29 -04:00
Michael Gregson 29913099b8 Magic! Shit works. Books can be added. 2009-03-11 02:08:25 -04:00
Michael Gregson 1d7f739631 Correct book counting. 2009-03-11 02:02:17 -04:00
Michael Gregson bf98adc034 Wee! Conf should work? 2009-03-11 01:57:04 -04:00
Michael Gregson 4aab446858 Maybe? 2009-03-11 01:53:48 -04:00
Michael Gregson a213655bd5 Maybe now? 2009-03-11 01:50:09 -04:00
Michael Gregson 256d897e7e Now have uncomment add book menu item. 2009-03-11 01:47:54 -04:00
Michael Gregson 7354142bad Fixing imports. 2009-03-11 01:41:27 -04:00
Michael Gregson 70916783d0 Adding pymazon. 2009-03-11 01:40:20 -04:00
Michael Gregson a609eb0798 Patches to library for adding books.
.cf are ignored now too.
2009-03-11 01:33:25 -04:00
David Bartley a5451d8e4a Release 0.4.20 2009-02-24 16:08:55 -05:00
David Bartley 1b04da2d15 Update kadmin headers 2009-02-24 16:02:06 -05:00
Michael Spang 19dd9bd764 Build for lenny 2009-02-17 22:25:33 -05:00
Michael Spang 170fe854aa Fix lintian warnings 2009-02-17 22:25:27 -05:00
Michael Spang 27be3e67d9 Remove pointless indentation 2009-01-31 18:44:01 -05:00
Michael Spang 8254cea7bf Revert "I bet this speeds up the compilation"
This reverts commit 6055aecb27.
2009-01-31 18:43:19 -05:00
Michael Spang a26181a278 Revert "Use rsync in zfsaddhomedir"
This reverts commit 88952ae56a.
2009-01-31 18:10:39 -05:00
Michael Spang 49004af3ca Free everything before exiting
This cleans up valgrind --show-reachable.
2009-01-31 17:39:37 -05:00
Michael Spang 2806b4a15e Nothing to see here 2009-01-31 16:32:23 -05:00
Michael Spang 87a353db17 Don't install op-adduser to /usr/bin 2009-01-31 02:05:41 -05:00
Michael Spang b2e745bffa Fix clean 2009-01-31 02:04:23 -05:00
Michael Spang 99aa8e0fca Fix clean 2009-01-31 02:03:47 -05:00
Michael Spang 6752ed1bc4 Remove obsolete code 2009-01-31 01:57:08 -05:00
Michael Spang 64f10c6009 Adjust Makefile 2009-01-31 01:57:07 -05:00
Michael Spang 7a7c1fcc41 Update .gitignore 2009-01-31 01:57:07 -05:00
Michael Spang a39d2b8485 Call ceod in addmember and addclub 2009-01-31 01:57:07 -05:00
Michael Spang 0edfa120eb Add op-adduser 2009-01-31 01:57:07 -05:00
Michael Spang 7de577ab32 Remove some unused config vars 2009-01-31 01:57:06 -05:00
Michael Spang 57b6e12476 Add ceoc 2009-01-31 01:57:06 -05:00
Michael Spang 39a3bda6b3 Make more noise in config parser 2009-01-31 01:57:06 -05:00
Michael Spang ef6b18c7bb Add ceod 2009-01-31 01:57:06 -05:00
Michael Spang e3f0ed509d Fix fallout from format magic 2009-01-31 01:57:05 -05:00
Michael Spang ddecf4a4a0 Use __attribute__(format) magic 2009-01-31 01:57:05 -05:00
Michael Spang 54658af34a Don't forget to flush 2009-01-31 01:57:05 -05:00
Michael Spang 3476038435 Convert logging to strbufs 2009-01-31 01:57:04 -05:00
Michael Spang f5a71b6b32 Add strbuf API
Shamelessly stolen from Git.
2009-01-31 01:57:04 -05:00
Michael Spang 235b223e4b LDAP tweaks 2009-01-31 01:57:04 -05:00
Michael Spang 8948b29cd1 Log Kerberos errors consistently 2009-01-31 01:56:54 -05:00
Michael Spang 3358c617ad Disable logging to stderr if it is not a tty 2009-01-30 23:40:22 -05:00
Michael Spang 597d6c5908 Rejigger the Makefile
It just wasn't complicated enough.
2009-01-30 22:45:10 -05:00
Michael Spang f56e928ed7 Fix build on Solaris 2009-01-30 22:45:10 -05:00
Michael Spang 13876b123a Use different CFLAGS for debug & package builds 2009-01-30 22:45:09 -05:00
Michael Spang cb8dd43d1d Set configuration directory from the environment 2009-01-30 22:45:09 -05:00
Michael Spang 3c6c173424 Ignore swap files and deleted NFS files 2009-01-30 22:45:09 -05:00
Michael Spang cd84888b1f Forbid adding users who have a group's name 2009-01-30 22:45:09 -05:00
Michael Spang 0ab9df26ef Update notify-hook from /etc 2009-01-30 22:45:09 -05:00
David Bartley 6055aecb27 I bet this speeds up the compilation 2009-01-30 01:46:54 -05:00
David Bartley 7407c9b8d7 Make mspang happy 2009-01-30 01:38:53 -05:00
David Bartley 88952ae56a Use rsync in zfsaddhomedir 2009-01-30 01:20:32 -05:00
Michael Gregson 58cd47a1b3 Patching 2009-01-28 01:07:58 -05:00
Michael Gregson ac196a1f31 Releasing changes. 2009-01-28 01:04:51 -05:00
Michael Gregson ab4cfdc17c Merge branch 'master' of caffeine.uwaterloo.ca:/srv/git/public/pyceo 2009-01-28 00:48:18 -05:00
Michael Gregson 4c4cf2b411 Added search for books that are signed out.
Book signouts now display due dates.
2009-01-28 00:47:45 -05:00
David Bartley 1b582abbf5 One of those LdapWordEdit was not meant to be 2009-01-23 00:53:28 -05:00
David Bartley 9d3aa35790 Add username autocomplete to library 2009-01-23 00:47:36 -05:00
Michael Spang e2bf0997e2 Make C configuration even more insane
David thought we weren't using enough void pointers.
2009-01-17 20:23:44 -05:00
Michael Spang 6aec4b3c25 Remove unused vars 2009-01-17 20:08:45 -05:00
Michael Spang 2608a4d913 Update config.o deps 2009-01-17 20:08:10 -05:00
Michael Spang a7433ec4a7 Make C configuration more insane 2009-01-17 19:17:47 -05:00
Michael Gregson 62171f0c26 Pushing out new version. 2009-01-15 23:43:13 -05:00
Michael Gregson fbfee2913a Backporting to older sqlobject 2009-01-15 23:28:13 -05:00
Michael Gregson 37e62fd2f4 Updating install process. 2009-01-15 22:47:47 -05:00
Michael Gregson ca94e2ed90 Pushing working version. 2009-01-15 22:43:32 -05:00
Michael Gregson 2d599b60e9 Debugging 2009-01-15 19:18:04 -05:00
Michael Gregson 0628f6edee Releasing with working library code and required changes to members code. 2009-01-15 19:10:21 -05:00
Michael Gregson 745ee4bc4a Releasing with working library code and required changes to members code. 2009-01-15 19:01:00 -05:00
Michael Gregson 5c139384b6 Releasing with working library code and required changes to members code. 2009-01-15 18:44:19 -05:00
Michael Gregson cec1cb1025 Releasing with working library code and required changes to members code. 2009-01-15 18:43:38 -05:00
Michael Gregson 3666c41b57 Releasing with working library code and required changes to members code. 2009-01-15 18:41:16 -05:00
Michael Gregson 033a15c9a5 Debugging 2009-01-15 18:37:19 -05:00
Michael Gregson c27529feaa Debugging 2009-01-15 18:34:23 -05:00
Michael Gregson 9484da3902 Added sig thingy. 2009-01-15 17:02:44 -05:00
Michael Gregson da534a2e8e Added sig thingy. 2009-01-15 17:02:20 -05:00
Michael Gregson 7e5b75c9f2 Added sig thingy. 2009-01-15 17:01:26 -05:00
Michael Gregson aed5f5007d Testing version for validation and overdue checks. 2009-01-15 16:55:21 -05:00
Michael Gregson c4b8499ad7 Adding user validation and overdue search. 2009-01-15 16:43:26 -05:00
Michael Gregson dc61d054e5 Fixed 2009-01-14 19:39:59 -05:00
Michael Gregson 591c82767f Broken 2009-01-14 19:38:50 -05:00
Michael Gregson 4f30556fa6 Broken 2009-01-14 19:38:15 -05:00
Michael Gregson 8d98eb0bf0 Forgot this last time 2009-01-14 19:35:38 -05:00
Michael Gregson e7974c3015 Things should work. 2009-01-14 18:57:31 -05:00
Michael Gregson ffe4056089 Fixed config path 2009-01-10 19:42:10 -05:00
Michael Gregson e3035e1b9a Checkout and check-in works! 2009-01-10 19:41:09 -05:00
Michael Gregson e334437d6d connect_sasl no longer causes entire program to die on error condition.
However, accessing LDAP beyond this point is probably a really bad idea
as we certainly do not do anything sane to handle the error.
2008-12-20 18:49:02 -05:00
Michael Spang 868b4b681b Sort term list 2008-09-10 17:27:41 -04:00
Michael Spang dd3880fb30 Merge branch 'master' of caffeine:/srv/git/public/pyceo 2008-09-10 17:13:04 -04:00
Michael Gregson c38f07131b Merge branch 'master' of ssh+git://caffeine.uwaterloo.ca/srv/git/public/pyceo 2008-07-24 22:08:58 -06:00
Michael Gregson 6f7cf8c5f8 Fixing bookpage of checked out books 2008-07-24 22:05:55 -06:00
Michael Gregson 75e02de4fc Pretty sure I fixed the check-in bug. 2008-07-24 21:43:52 -06:00
Michael Spang eeff9868d4 Fix build on Solaris 2008-06-27 19:36:46 -04:00
Michael Spang a4447e38bb Run as root:root not root:$LOGNAME 2008-06-27 19:35:49 -04:00
David Bartley e97203c36b Merge branch 'master' of /users/git/public/pyceo 2008-06-09 23:45:04 -04:00
David Bartley 17e06b4c55 Add database stubs 2008-06-09 23:45:01 -04:00
Michael Spang c7f9893fd4 Set $MAINTAINER to Systems Committee 2008-06-06 23:32:47 -04:00
Nick Guenther 2890a04f71 Made configuring slightly saner (it's now called directly from main, instead of surprisingly via connect()) 2008-06-05 09:03:45 -04:00
Nick Guenther 82be7b1020 abstracting BookPage -> BookPageBase 2008-06-04 05:24:00 -04:00
Nick Guenther 4581f0c6da Appearently I committed a bytecode file by accident. oops. 2008-06-04 03:18:12 -04:00
David Bartley 8ff408ed76 Release 0.4.11 2008-06-02 23:49:32 -04:00
David Bartley 8f2bea8540 Add library path to config 2008-06-02 23:43:13 -04:00
Nick Guenther 899791fb4e We've gone from not having a library, to having a basic library that almost works! There's kinks and the code could be cleaner in places, but it's a really decent start for only a day's work. yayyyy python 2008-06-02 23:21:25 -04:00
Nick Guenther 0241e3b0eb Search works whoooo 2008-06-02 20:40:25 -04:00
Nick Guenther 943079fb68 CEO notifies of it's connect attempt (since if LDAP is being sad then CEO hangs without any indication of why) 2008-06-02 20:17:27 -04:00
Nick Guenther 32004be45f Library GUI is coming, but awkwardsadface 2008-06-02 18:32:24 -04:00
Nick Guenther 9ac0c98ea6 library backend, initial version 2008-06-02 11:32:33 -04:00
David Bartley 671f0d95a8 Release v0.4.10 2008-05-28 02:02:59 -04:00
David Bartley f6565eb6b9 Add configurable refquota support 2008-05-28 02:01:24 -04:00
David Bartley 2820f4d824 Always call deauth 2008-05-20 18:59:05 -04:00
Michael Spang ede20cf95e Auth as ceo/admin for zfsaddhomedir 2008-05-20 18:54:40 -04:00
David Bartley c4e12ae227 Release 0.4.9 2008-05-15 22:15:17 -04:00
David Bartley 7172ecc756 Use refquota instead of quota 2008-05-15 22:14:24 -04:00
David Bartley 6135db3fcd Fix help text 2008-05-13 23:51:27 -04:00
David Bartley 3cfcdbee62 Import sys 2008-05-13 23:20:52 -04:00
David Bartley 181419ac7a Move mathsoc regex and exception userid's into config 2008-05-10 16:57:46 -04:00
David Bartley 0a35be2386 Release 0.4.8 2008-04-24 19:57:35 -04:00
David Bartley b0dc96ff75 Add term argument to mathsoclist 2008-04-08 19:02:00 -04:00
David Bartley fdcff72d83 Add mathsoclist command 2008-04-01 22:12:00 -04:00
David Bartley 755d835eec Improve help message 2008-04-01 22:11:17 -04:00
David Bartley 85d38fc879 Simplify help 2008-04-01 22:09:03 -04:00
David Bartley 8e4f11b47b Drop memberUid support; all groups use uniqueMember now 2008-04-01 21:30:58 -04:00
David Bartley 0cade22049 Add help for command-line ceo 2008-03-28 15:46:32 -04:00
David Bartley 93310af3c7 No point in recommending quota anymore 2008-03-25 14:16:43 -04:00
420 changed files with 23983 additions and 6342 deletions

40
.drone.yml Normal file
View File

@ -0,0 +1,40 @@
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.9-slim-bullseye
# 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 --no-install-recommends -y gcc libkrb5-dev libaugeas0
- python3 -m venv venv
- . venv/bin/activate
- venv/bin/pip install -r dev-requirements.txt -r requirements.txt
# lint
- flake8
# unit + integration tests
- bash -c ". .drone/phosphoric-acid-setup.sh && IMAGE__setup && CONTAINER__setup"
- pytest -v
services:
- name: auth1
image: debian:bullseye-slim
commands:
- bash -c ". .drone/auth1-setup.sh && IMAGE__setup && CONTAINER__setup"
- sleep infinity
- name: coffee
image: debian:bullseye-slim
commands:
- bash -c ". .drone/coffee-setup.sh && IMAGE__setup && CONTAINER__setup"
- sleep infinity
trigger:
branch:
- master

17
.drone/adldap_data.ldif Normal file
View File

@ -0,0 +1,17 @@
dn: ou=ADLDAP,dc=csclub,dc=internal
objectClass: organizationalUnit
ou: ADLDAP
dn: cn=alumni1,ou=ADLDAP,dc=csclub,dc=internal
description: One, Alumni
givenName: Alumni
sn: *One
cn: alumni1
sAMAccountName: alumni1
displayName: alumni1
mail: alumni1@alumni.uwaterloo.internal
objectClass: mockADUser
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top

131
.drone/auth1-setup.sh Executable file
View File

@ -0,0 +1,131 @@
#!/bin/bash
set -ex
. .drone/common.sh
# If we don't do this then OpenLDAP uses a lot of RAM
ulimit -n 1024
CONTAINER__fix_hosts() {
add_fqdn_to_hosts $(get_ip_addr $(hostname)) auth1
if [ -n "$CI" ]; then
# 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
fi
}
IMAGE__setup_ldap() {
# In the "slim" Docker images, /usr/share/doc/* is excluded by default
echo 'path-include /usr/share/doc/sudo-ldap/schema.OpenLDAP' > /etc/dpkg/dpkg.cfg.d/zz-ceo
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 .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,csc,mock_ad}.schema /etc/ldap/schema/
chown -R openldap:openldap /etc/ldap/schema/ /var/lib/ldap/ /etc/ldap/
sleep 0.5 && service slapd start
ldapadd -c -f .drone/data.ldif -Y EXTERNAL -H ldapi:///
if [ -z "$CI" ]; then
ldapadd -c -f .drone/uwldap_data.ldif -Y EXTERNAL -H ldapi:/// || true
ldapadd -c -f .drone/adldap_data.ldif -Y EXTERNAL -H ldapi:/// || true
# setup ldapvi for convenience
apt install -y --no-install-recommends vim ldapvi
grep -q 'export EDITOR' /root/.bashrc || \
echo 'export EDITOR=vim' >> /root/.bashrc
grep -q 'alias ldapvi' /root/.bashrc || \
echo 'alias ldapvi="ldapvi -Y EXTERNAL -h ldapi:///"' >> /root/.bashrc
fi
}
IMAGE__setup_krb5() {
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 -randkey ceod/admin
addprinc -randkey host/auth1.csclub.internal
addprinc -randkey ldap/auth1.csclub.internal
ktadd host/auth1.csclub.internal
ktadd ldap/auth1.csclub.internal
EOF
# Add all of the people defined in data.ldif
for princ in ctdalek exec1 regular1 office1; do
echo "addprinc -pw krb5 $princ" | kadmin.local
done
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
}
IMAGE__setup() {
# slapd needs /etc/hosts to be setup properly
CONTAINER__fix_resolv_conf
CONTAINER__fix_hosts
apt update
# for the 'killall' command
apt install -y psmisc
IMAGE__setup_ldap
IMAGE__setup_krb5
IMAGE__common_setup
service slapd stop || true
killall slapd || true
service krb5-admin-server stop || true
service krb5-kdc stop || true
service saslauthd stop || true
}
CONTAINER__setup() {
CONTAINER__fix_resolv_conf
CONTAINER__fix_hosts
local started_slapd=false
for i in {1..5}; do
if service slapd start; then
started_slapd=true
break
fi
sleep 1
done
if [ $started_slapd != "true" ]; then
echo "Failed to start slapd" >&2
return 1
fi
service krb5-admin-server start
service krb5-kdc start
service saslauthd start
service nslcd start
# Let other containers know that we're ready
nc -l -k 0.0.0.0 9000 &
}

67
.drone/coffee-setup.sh Executable file
View File

@ -0,0 +1,67 @@
#!/bin/bash
set -ex
. .drone/common.sh
CONTAINER__fix_hosts() {
add_fqdn_to_hosts $(get_ip_addr $(hostname)) coffee
add_fqdn_to_hosts $(get_ip_addr auth1) auth1
}
IMAGE__setup() {
IMAGE__ceod_setup
apt install --no-install-recommends -y default-mysql-server postgresql
# MYSQL
service mariadb stop
sed -E -i 's/^(bind-address[[:space:]]+= 127\.0\.0\.1)$/#\1/' /etc/mysql/mariadb.conf.d/50-server.cnf
service mariadb start
cat <<EOF | mysql
CREATE USER IF NOT EXISTS 'mysql' IDENTIFIED BY 'mysql';
GRANT ALL PRIVILEGES ON *.* TO 'mysql' WITH GRANT OPTION;
EOF
# POSTGRESQL
service postgresql stop
local POSTGRES_DIR=/etc/postgresql/*/main
cat <<EOF > $POSTGRES_DIR/pg_hba.conf
# TYPE DATABASE USER ADDRESS METHOD
local all postgres peer
host all postgres localhost md5
host all postgres 0.0.0.0/0 md5
host all postgres ::/0 md5
local all all peer
host all all localhost md5
local sameuser all peer
host sameuser all 0.0.0.0/0 md5
host sameuser all ::/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
service mariadb stop || true
service postgresql stop || true
}
CONTAINER__setup() {
CONTAINER__fix_resolv_conf
CONTAINER__fix_hosts
CONTAINER__ceod_setup
if [ -z "$CI" ]; then
CONTAINER__auth_setup coffee
fi
service mariadb start
service postgresql start
# sync with phosphoric-acid
nc -l -k 0.0.0.0 9000 &
}

116
.drone/common.sh Normal file
View File

@ -0,0 +1,116 @@
export DEBIAN_FRONTEND=noninteractive
# The IMAGE__ functions should be called when building the image.
# The CONTAINER__ functions should be called when running an instance of the
# image in a container.
IMAGE__auth_setup() {
# LDAP
apt install -y --no-install-recommends libnss-ldapd
service nslcd stop || true
mkdir -p /etc/ldap
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
}
IMAGE__common_setup() {
apt update
# netcat is used for synchronization between the containers
apt install -y netcat-openbsd
IMAGE__auth_setup
}
IMAGE__ceod_setup() {
IMAGE__common_setup
# ceod uses Augeas, which is not installed by default in the Python
# Docker container
apt install -y libaugeas0
}
CONTAINER__fix_resolv_conf() {
# don't resolve container names to *real* CSC machines
sed -E 's/([[:alnum:]-]+\.)*uwaterloo\.ca//g' /etc/resolv.conf > /tmp/resolv.conf
# remove empty 'search' lines, if we created them
sed -E -i '/^search[[:space:]]*$/d' /tmp/resolv.conf
# also remove the 'rotate' option, since this can cause the Docker DNS server
# to be circumvented
sed -E -i '/^options.*\brotate/d' /tmp/resolv.conf
# we can't replace /etc/resolv.conf using 'mv' because it's mounted into the container
cp /tmp/resolv.conf /etc/resolv.conf
rm /tmp/resolv.conf
}
CONTAINER__auth_setup() {
local hostname=$1
sync_with auth1
service nslcd start
rm -f /etc/krb5.keytab
cat <<EOF | kadmin -p sysadmin/admin -w krb5
addprinc -randkey host/$hostname.csclub.internal
addprinc -randkey ceod/$hostname.csclub.internal
ktadd host/$hostname.csclub.internal
ktadd ceod/$hostname.csclub.internal
EOF
}
CONTAINER__ceod_setup() {
# normally systemd creates /run/ceod for us
mkdir -p /run/ceod
# mock out systemctl
ln -sf /bin/true /usr/local/bin/systemctl
# mock out acme.sh
mkdir -p /root/.acme.sh
ln -sf /bin/true /root/.acme.sh/acme.sh
# mock out kubectl
cp .drone/mock_kubectl /usr/local/bin/kubectl
chmod +x /usr/local/bin/kubectl
# add k8s authority certificate
mkdir -p /etc/csc
cp .drone/k8s-authority.crt /etc/csc/k8s-authority.crt
# openssl is actually already present in the python Docker image,
# so we don't need to mock it out
}
# Common utility functions
get_ip_addr() {
# There appears to be a bug in newer versions of Podman where using both
# --name and --hostname causes a container to have two identical DNS
# entries, which causes `getent hosts` to print two lines.
# So we use `head -n 1` to select just the first line.
getent hosts $1 | head -n 1 | cut -d' ' -f1
}
add_fqdn_to_hosts() {
local ip_addr=$1
local hostname=$2
sed -E "/${ip_addr}.*\\b${hostname}\\b/d" /etc/hosts > /tmp/hosts
# we can't replace /etc/hosts using 'mv' because it's mounted into the container
cp /tmp/hosts /etc/hosts
rm /tmp/hosts
echo "$ip_addr $hostname.csclub.internal $hostname" >> /etc/hosts
}
sync_with() {
local host=$1
local port=9000
local synced=false
# give it 20 minutes (can be slow if you're using e.g. NFS or Ceph)
for i in {1..240}; do
if nc -vz $host $port ; then
synced=true
break
fi
sleep 5
done
test $synced = true
}

View File

@ -20,10 +20,15 @@ attributetype ( 1.3.6.1.4.1.27934.1.1.5 NAME 'nonMemberTerm'
EQUALITY caseIgnoreIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{5} )
attributetype ( 1.3.6.1.4.1.27934.1.1.6 NAME 'isClubRep'
EQUALITY booleanMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 )
objectclass ( 1.3.6.1.4.1.27934.1.2.1 NAME 'member'
SUP top AUXILIARY
MUST ( cn $ uid )
MAY ( studentid $ program $ term $ nonMemberTerm $ description $ position ) )
MAY ( studentid $ program $ term $ nonMemberTerm $ description $ position $
isClubRep $ sn $ givenName ) )
objectclass ( 1.3.6.1.4.1.27934.1.2.2 NAME 'club'
SUP top AUXILIARY

213
.drone/data.ldif Normal file
View File

@ -0,0 +1,213 @@
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
uniqueMember: uid=office1,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
givenName: Calum
sn: 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: f2021
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
givenName: Regular
sn: 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: f2021
dn: cn=regular1,ou=Group,dc=csclub,dc=internal
objectClass: top
objectClass: group
objectClass: posixGroup
cn: regular1
gidNumber: 20002
dn: uid=exec1,ou=People,dc=csclub,dc=internal
cn: Exec One
givenName: Exec
sn: One
userPassword: {SASL}exec1@CSCLUB.INTERNAL
loginShell: /bin/bash
homeDirectory: /users/exec1
uid: exec1
uidNumber: 20003
gidNumber: 20003
objectClass: top
objectClass: account
objectClass: posixAccount
objectClass: shadowAccount
objectClass: member
program: MAT/Mathematics Computer Science
term: f2021
dn: cn=exec1,ou=Group,dc=csclub,dc=internal
objectClass: top
objectClass: group
objectClass: posixGroup
cn: exec1
gidNumber: 20003
dn: cn=exec,ou=Group,dc=csclub,dc=internal
objectClass: top
objectClass: group
objectClass: posixGroup
cn: exec
gidNumber: 10013
uniqueMember: uid=exec1,ou=People,dc=csclub,dc=internal
dn: uid=office1,ou=People,dc=csclub,dc=internal
cn: Office One
givenName: Office
sn: One
userPassword: {SASL}office1@CSCLUB.INTERNAL
loginShell: /bin/bash
homeDirectory: /users/office1
uid: office1
uidNumber: 20004
gidNumber: 20004
objectClass: top
objectClass: account
objectClass: posixAccount
objectClass: shadowAccount
objectClass: member
program: MAT/Mathematics Computer Science
term: f2021
dn: cn=office1,ou=Group,dc=csclub,dc=internal
objectClass: top
objectClass: group
objectClass: posixGroup
cn: office1
gidNumber: 20004
dn: uid=alumni1,ou=People,dc=csclub,dc=internal
cn: Alumni One
givenName: Alumni
sn: One
userPassword: {SASL}alumni1@CSCLUB.INTERNAL
loginShell: /bin/bash
homeDirectory: /users/alumni1
uid: alumni1
uidNumber: 20005
gidNumber: 20005
objectClass: top
objectClass: account
objectClass: posixAccount
objectClass: shadowAccount
objectClass: member
program: Alumni
term: w2024
dn: cn=alumni1,ou=Group,dc=csclub,dc=internal
objectClass: top
objectClass: group
objectClass: posixGroup
cn: alumni1
gidNumber: 20005

19
.drone/k8s-authority.crt Normal file
View File

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIC/jCCAeagAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJl
cm5ldGVzMB4XDTIxMTIwNDIxNDcxOVoXDTMxMTIwMjIxNDcxOVowFTETMBEGA1UE
AxMKa3ViZXJuZXRlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN50
H4RcrV5ZDDqT5XMfN1ml8MalyMDAG8mE+lNT1rsUGBUp2jhNfG0OpFUm55yGarI9
2BrNGXLyFGm3yy6MWJorSUqaSBzt9+JHtBDVQwCgTX9PYSX1X/kFNQFLZkNrMtO4
417WELlkl9miCWWmTPOZAMYZWbnRKrndd3MsrhOcuDwqT5rX+LLl6VktWx5+qmuc
49sd3fWJ1MxLZ+Q6/Eo5jPuPVOPl8wLcwf9MD0rgRMVU+XycwDKr/3vmBbs22hiw
PcWIPHugAy4PRbiWfHOymO+c4WSCCS7nre3mIAyXuT0EEPDnEnrkbYoSuwIJ0tLp
N8/6vaLbBfO5ckAU2VUCAwEAAaNZMFcwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB
/wQFMAMBAf8wHQYDVR0OBBYEFNqlikMIHwY+A1/PHzwPB0CtSLX+MBUGA1UdEQQO
MAyCCmt1YmVybmV0ZXMwDQYJKoZIhvcNAQELBQADggEBAJ2j87US8VTVTFoayNSk
mzip60VzgKxawi/lP1F0/JqCHtdcaA/JmlN8FggzaSxS6AA/gxNTriLNLedhqgNF
f5F5Lq0bQAebzbijsEMr+wGE6zYBgg2L0u55jqSSU1Quhay83eCD0b0O3XHGdzg0
29jC+r8pOYWuwCBaIU8NN8EouHbQ25jqJAPLCIjuqPSEPfxjZla9f2ZO7Zpx+Yud
jDYHz9ZwBYmeR7Z74/oStJ+eIFfwlJKIQL0QFzKgw2KUHmmzHVxpx60rajiGNAb8
7FNPWTjIYX11Hy56jZAUirfwCak1IxfI8O0/X1LzVPCs7uaE1SG8TCsJgjrD2Nwm
2w4=
-----END CERTIFICATE-----

19
.drone/kdc.conf Normal file
View File

@ -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
}

27
.drone/krb5.conf Normal file
View File

@ -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

3
.drone/ldap.conf Normal file
View File

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

30
.drone/mail-setup.sh Executable file
View File

@ -0,0 +1,30 @@
#!/bin/bash
set -ex
. .drone/common.sh
CONTAINER__fix_hosts() {
add_fqdn_to_hosts $(get_ip_addr $(hostname)) mail
add_fqdn_to_hosts $(get_ip_addr auth1) auth1
}
IMAGE__setup() {
IMAGE__ceod_setup
}
CONTAINER__setup() {
CONTAINER__fix_resolv_conf
CONTAINER__fix_hosts
CONTAINER__ceod_setup
CONTAINER__auth_setup mail
# for the VHostManager
mkdir -p /run/ceod/member-vhosts
# mock services
venv/bin/python -m tests.MockMailmanServer &
venv/bin/python -m tests.MockSMTPServer &
venv/bin/python -m tests.MockCloudStackServer &
venv/bin/python -m tests.MockHarborServer &
# sync with phosphoric-acid
nc -l -k 0.0.0.0 9000 &
}

10
.drone/mock_ad.schema Normal file
View File

@ -0,0 +1,10 @@
# Mock Active Directory Schema
attributetype ( 1.3.6.1.4.1.70000.1.1.1 NAME 'sAMAccountName'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )
objectclass ( 1.3.6.1.4.1.70000.1.2.1 NAME 'mockADUser'
SUP top AUXILIARY
MUST ( sAMAccountName ) )

19
.drone/mock_kubectl Normal file
View File

@ -0,0 +1,19 @@
#!/bin/bash
if [ "$1" = apply ]; then
exit
elif [ "$1" = delete ]; then
exit
elif [ "$1" = certificate ]; then
exit
elif [ "$1" = get ]; then
if [ "$2" = csr -a "$4" = "-o" -a "$5" = 'jsonpath={.status.certificate}' ]; then
echo -n 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMxekNDQWI4Q0ZHeVk0ZVpVMnAvTjMzU0pCTlptMm1vSlE5TXFNQTBHQ1NxR1NJYjNEUUVCQ3dVQU1DZ3gKRURBT0JnTlZCQU1NQjJOMFpHRnNaV3N4RkRBU0JnTlZCQW9NQzJOell5MXRaVzFpWlhKek1CNFhEVEl4TVRJeApNekEwTWpJek4xb1hEVEl5TURFeE1qQTBNakl6TjFvd0tERVFNQTRHQTFVRUF3d0hZM1JrWVd4bGF6RVVNQklHCkExVUVDZ3dMWTNOakxXMWxiV0psY25Nd2dnRWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUIKQVFEV09vaTd6ejE0c3VBZ0V2QkgrSHFHSzlCUUlQTm5QQ0llVkxXenlFRTNxUWZRV2YvcWNzeGNST2pSKzVCTgpKSXBaQlNZdjRmNE52WFZqaHlQendoWUd0bXJRYksyT3RCTDlqMDJMWjhMVHp2TnE0MW9CYVdXUFhhaVdIVys2CjkzQnlBdXFPMmdnSEt0elNkV09TcTZpeFBXMVNGUzJRMkFWaXdZUEg3b1pQYnZacUZvMzdhbVdwd1pWUHVuVi8KV2tFRUttNUVqV05DSVUzVWpPdS9HeEJOT1g0WEpqWld4bFcwQUVROVp3K2ZSazBkdU5ScVVyUDQxbDZvcG4rKwpLRkE5NFg2NUlzcUMvMlJ4OWgrNkZFRHhIcjJPcjhOcGFuMXRjZEZHQlFyMGMxV1JxRkNHTytIM0VTeUNya1BjCmdnRDlVN3c0TmdGYkQyaVU0QXc3ZkhwakFnTUJBQUV3RFFZSktvWklodmNOQVFFTEJRQURnZ0VCQUY3VWUwc3YKcFhSUzN1TFl1Y0k3UkRNRGpOZnFpZ0R3NnorbzZxVmxTdGpZTGpDNjFXRyswZ0g4TDJIbm5jZVYyelhjNDkrQQp6TjFna0lWT3JlRUQvRitKbGRPUGgySUpOY1pGYTBsckFFV0dNNWRRR3pDSUM0cEtmSGxOMTZ0c0w2bGdqWTYzCmUvZlhMTFdLdktDR2lRMUlBUTh4KzYyaTVvSmU3aDBlQ1Q0aEEyM0JTRnRNelo2aEdGUURNNGxxaWhHQjEyT2UKZE5yYStsNVdLemNFR21aVFBYTXNudEZVVndPejhaNld2eGo0UW1zL1dQUElKWDdLM2NiRUo4L1RQWG1tUzJrQwowNUtueUxVQzltYnR2TGZoYldhbFZVVlJVUkYwT1RaVk5mNkt6MDJWYlRqQjRJQXdyWGZKZC9lMkMvNFpGWlJTCjVWMnlJSnBJeVJGWTdQST0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo='
exit
elif [ "$2" = namespaces ]; then
echo '{"items":[{"metadata":{"name":"default"}}]}'
exit
fi
fi
echo 'Unrecognized command'
exit 1

20
.drone/nsswitch.conf Normal file
View File

@ -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

47
.drone/phosphoric-acid-setup.sh Executable file
View File

@ -0,0 +1,47 @@
#!/bin/bash
set -ex
. .drone/common.sh
CONTAINER__fix_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
# mail container doesn't run in CI
if [ -z "$CI" ]; then
add_fqdn_to_hosts $(get_ip_addr mail) mail
fi
}
CONTAINER__setup_userdirs() {
# initialize the skel directory
shopt -s dotglob
mkdir -p /users/skel
cp /etc/skel/* /users/skel/
# create directories for users
for user in ctdalek regular1 exec1; do
mkdir -p /users/$user
chown $user:$user /users/$user
done
}
IMAGE__setup() {
IMAGE__ceod_setup
# git is required by the ClubWebHostingService
apt install --no-install-recommends -y git
}
CONTAINER__setup() {
CONTAINER__fix_resolv_conf
CONTAINER__fix_hosts
CONTAINER__ceod_setup
CONTAINER__auth_setup phosphoric-acid
CONTAINER__setup_userdirs
echo "ktadd ceod/admin" | kadmin -p sysadmin/admin -w krb5
sync_with coffee
if [ -z "$CI" ]; then
sync_with mail
fi
}

287
.drone/rfc2307bis.schema Normal file
View File

@ -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 )

114
.drone/slapd.conf Normal file
View File

@ -0,0 +1,114 @@
# 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/mock_ad.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
# hide most attributes for alumni in mock UWLDAP
access to attrs=cn,sn,givenName,displayName,ou,mail
dn.regex="^uid=alumni[^,]+,ou=(Test)?UWLDAP,dc=csclub,dc=internal$"
by * none
# 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

17
.drone/supervise.sh Executable file
View File

@ -0,0 +1,17 @@
#!/bin/sh
# A script that supervises a program. The program is restarted TIMEOUT second after it exits.
# SIGHUP restarts the program
# SIGTERM and SIGINT stops the program
TIMEOUT=1
running=1
trap 'kill -TERM $! 2>/dev/null' HUP
trap 'running=0; kill -TERM $! 2>/dev/null' TERM INT
trap 'running=0; kill -KILL $! 2>/dev/null' EXIT
while [ "$running" = 1 ]; do
"$@" &
wait
sleep "$TIMEOUT"
done

123
.drone/uwldap_data.ldif Normal file
View File

@ -0,0 +1,123 @@
dn: ou=UWLDAP,dc=csclub,dc=internal
objectClass: organizationalUnit
ou: UWLDAP
dn: uid=ctdalek,ou=UWLDAP,dc=csclub,dc=internal
displayName: Calum Dalek
givenName: Calum
sn: Dalek
cn: Calum Dalek
ou: MAT/Mathematics Computer Science
mailLocalAddress: ctdalek@uwaterloo.internal
objectClass: inetLocalMailRecipient
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: ctdalek
mail: ctdalek@uwaterloo.internal
dn: uid=regular1,ou=UWLDAP,dc=csclub,dc=internal
displayName: Regular One
givenName: Regular
sn: One
cn: Regular One
ou: MAT/Mathematics Computer Science
mailLocalAddress: regular1@uwaterloo.internal
objectClass: inetLocalMailRecipient
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: regular1
mail: regular1@uwaterloo.internal
dn: uid=regular2,ou=UWLDAP,dc=csclub,dc=internal
displayName: Regular Two
givenName: Regular
sn: Two
cn: Regular Two
ou: MAT/Mathematics Computer Science
mailLocalAddress: regular2@uwaterloo.internal
objectClass: inetLocalMailRecipient
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: regular2
mail: regular2@uwaterloo.internal
dn: uid=regular3,ou=UWLDAP,dc=csclub,dc=internal
displayName: Regular Three
givenName: Regular
sn: Three
cn: Regular Three
ou: MAT/Mathematics Computer Science
mailLocalAddress: regular3@uwaterloo.internal
objectClass: inetLocalMailRecipient
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: regular3
mail: regular3@uwaterloo.internal
dn: uid=exec1,ou=UWLDAP,dc=csclub,dc=internal
displayName: Exec One
givenName: Exec
sn: One
cn: Exec One
ou: MAT/Mathematics Computer Science
mailLocalAddress: exec1@uwaterloo.internal
objectClass: inetLocalMailRecipient
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: exec1
mail: exec1@uwaterloo.internal
dn: uid=exec2,ou=UWLDAP,dc=csclub,dc=internal
displayName: Exec Two
givenName: Exec
sn: Two
cn: Exec Two
ou: MAT/Mathematics Computer Science
mailLocalAddress: exec2@uwaterloo.internal
objectClass: inetLocalMailRecipient
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: exec2
mail: exec2@uwaterloo.internal
dn: uid=exec3,ou=UWLDAP,dc=csclub,dc=internal
displayName: Exec Three
givenName: Exec
sn: Three
cn: Exec Three
ou: MAT/Mathematics Computer Science
mailLocalAddress: exec3@uwaterloo.internal
objectClass: inetLocalMailRecipient
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: exec3
mail: exec3@uwaterloo.internal
dn: uid=alumni1,ou=UWLDAP,dc=csclub,dc=internal
displayName: Alumni One
givenName: Alumni
sn: One
cn: Alumni One
ou: MAT/Mathematics Computer Science
mailLocalAddress: alumni1@uwaterloo.internal
objectClass: inetLocalMailRecipient
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
objectClass: top
uid: alumni1
mail: alumni1@uwaterloo.internal

View File

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

5
.githooks/pre-commit Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash
docker run --rm -v "$PWD:$PWD:z" -w "$PWD" python:3.9-bullseye scripts/lint-docker.sh
exit $?

30
.gitignore vendored
View File

@ -1,3 +1,27 @@
/build-stamp
/build
*.pyc
# If you update this file, please also update the extend-diff-ignore option
# in debian/source/options.
*.key
*.gpg
*.pgp
__pycache__/
/venv/
/dist/
/build/
/*.egg-info/
.vscode/
*.o
*.so
*.swp
.idea/
/docs/*.1
/docs/*.5
/debian/ceo/
/debian/ceod/
/debian/ceo-common/
/debian/tmp/
/debian/ceo.substvars
/debian/files
/debian/.debhelper/
/debian/debhelper-build-stamp

1
MANIFEST.in Normal file
View File

@ -0,0 +1 @@
include ceod/model/templates/*.j2

34
Makefile Normal file
View File

@ -0,0 +1,34 @@
#!/usr/bin/make -f
SCDFILES = $(wildcard docs/*.scd)
MANPAGES = $(patsubst docs/%.scd,docs/%,${SCDFILES})
CEO_HOME = /var/lib/ceo
build: docs venv
venv:
python3 -m venv venv && \
. venv/bin/activate && \
pip install --upgrade pip && \
pip install setuptools wheel && \
pip install -r requirements.txt && \
pip install .
install:
@# Prepare the virtualenv to be moved (dangerous!)
@# Make sure you don't have '|' in your paths
grep -IRl $(CURDIR)/venv venv/bin | \
xargs perl -pe 's|\Q$(CURDIR)/venv\E|$(CEO_HOME)/venv|g' -i
mkdir -p $(DESTDIR)$(CEO_HOME)
mv venv $(DESTDIR)$(CEO_HOME)
docs:
for file in ${SCDFILES} ; do \
scdoc < $$file > `echo $$file | grep -oP '.*(?=\.scd$$)'` ; \
done
clean:
rm -f ${MANPAGES}
rm -rf venv
rm -rf debian/{ceo,ceod,ceo-common,tmp}
.PHONY: build docs clean venv install

122
PACKAGING.md Normal file
View File

@ -0,0 +1,122 @@
# Packaging
This is a guide for creating Debian packages for ceo. The instructions below
probably do not follow best practices, but at least I am confident that they
work.
## Prerequisites
Make sure your GPG key is in /srv/debian/gpg on potassium-benzoate. See
[here](https://wiki.csclub.uwaterloo.ca/Debian_Repository#Step_1:_Add_to_Uploaders)
for instructions.
Make sure you are in the `csc-mirror` group too.
## Creating the package
Use Docker/Podman to avoid screwing up your main system.
For example, to create a package for bullseye (replace `podman` with `docker` in all instances below if you're using Docker):
```sh
podman run -it --name pyceo-packaging -v "$PWD":"$PWD":z -w "$PWD" --security-opt="label=disable" debian:bookworm bash
# if disconnected from shell, reconnect with:
podman start pyceo-packaging
podman exec -it pyceo-packaging bash
```
**Important**: Make sure to use a container image for the same distribution which you're packaging.
For example, if you're creating a package for bullseye, you should be using the debian:bullseye
Docker image (this is because the virtualenv symlinks python to the OS' version of python).
Here are some of the prerequisites you'll need to build the deb files
(run this inside the container):
```sh
apt update
apt install -y devscripts debhelper git-buildpackage vim
apt install -y python3-dev python3-venv libkrb5-dev libpq-dev libaugeas0 scdoc # dependencies for building ceo
```
Make sure to also install all of the packages in the 'Build-Depends' section in debian/control.
Update VERSION.txt to the next version, and do a git commit (or `dpkg-source --commit`).
Now run `dch -i` and edit the changelog (update version, add your uploader name/email, add changes).
Now you will build a signed package. Place your key ID after the `-k` argument, e.g.
```sh
# (pre-requisite) if container doesn't have your gpg key
## step 1: export from host/another computer with your keyring
gpg --armor --output private.key --export-secret-key <your pgp key's id email>
## step 2: import into build container
gpg --import private.key
## step 3: find your key's public key
gpg --list-secret-keys # get key id
## step 4: trust ids (before building)
gpg --edit <pub key id>
gpg> trust # run when gpg editing prompt appears
> 5 # "ultimate" trust
gpg> save # gpg will report no changes were made, but trust of ids should be changed
# alternatively, sign with `debsign` after creating unsigned package
# build (signed) package
gbp buildpackage --git-upstream-branch=master -k8E5568ABB0CF96BC367806ED127923BE10DA48DC --lintian-opts --no-lintian
```
This will create a bunch of files (deb, dsc, tar.gz, etc.) in the parent directory.
Now do another git commit (since you edited the changelog file).
To clean the packages (run this after uploading, ie. **do NOT run this if you just finished building**):
```sh
rm ../*.{xz,gz,dsc,build,buildinfo,changes,deb}
```
## Uploading the package
Inside the container, go up one directory, and create a tarball with all the package files:
```
cd .. # within the container, generated files are in the parent directory of your git repo
tar zcvf pyceo.tar.gz *.{xz,gz,dsc,build,buildinfo,changes,deb}
```
Outside of the container (i.e. on your personal machine), copy the tarball out of the
container into your current directory, e.g.
```
podman cp pyceo-packaging:/home/max/repos/pyceo.tar.gz .
# or generally, if you're in the pyceo repo:
podman cp pyceo-packaging:$(cd ../ && pwd)/pyceo.tar.gz .
```
(Replace `/home/max/repos` by the directory in the container with the tarball.)
Now upload the tarball to a CSC machine, e.g.
```
# on "HOST" machine
scp pyceo.tar.gz mannitol:~
```
SSH into that machine and extract the tarball into a separate directory:
```
ssh mannitol
mkdir pyceo-parent && mv pyceo.tar.gz pyceo-parent/ && cd pyceo-parent
rm -iv *.{xz,gz,dsc,build,buildinfo,changes,deb}
tar zxvf pyceo.tar.gz
```
At this point, you will need a dupload.conf file. Ask someone on syscom for a copy. Place the dupload config at `~/.dupload.conf` (as per manpage).
Now upload the package to potassium-benzoate:
```
kinit
dupload *.changes
```
Now SSH into potassium-benzoate and run the following:
```
# note: this is AUTOMATICALLY done (within 10-20 minutes by a cron job)
sudo /srv/debian/bin/rrr-incoming
```
To check if mirror has accepted the new package, visit: http://debian.csclub.uwaterloo.ca/dists/bookworm/
<<<<<<< Updated upstream
=======
To update CEO:
```
# repeat this for all systems, starting from ceod servers
sudo apt update
# NOTE: be careful of changing configs!!
sudo apt install --only-upgrade ceod
````
>>>>>>> Stashed changes
There, that wasn't so bad...right? :')

249
README.md Normal file
View File

@ -0,0 +1,249 @@
# pyceo
[![Build Status](https://ci.csclub.uwaterloo.ca/api/badges/public/pyceo/status.svg)](https://ci.csclub.uwaterloo.ca/public/pyceo)
![Main TUI view](https://wiki.csclub.uwaterloo.ca/images/b/bb/Pyceo.png)
CEO (**C**SC **E**lectronic **O**ffice) is the tool used by CSC to manage
club accounts and memberships. See [docs/architecture.md](docs/architecture.md) for an
overview of its architecture.
The API documentation is available as a plain HTML file in [docs/redoc-static.html](docs/redoc-static.html).
## Development
### Podman
If you are not modifying code related to email or Mailman, then you may use
Podman containers instead, which are much easier to work with than the VM.
If you are using Podman, make sure to set the `DOCKER_HOST` environment variable
if you have not done so already:
```bash
# Add the following to e.g. your ~/.bashrc
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock
```
The Podman socket also needs to be running:
```bash
# Enabled by default on Debian, but not on Fedora
systemctl --user enable --now podman.socket
```
First, create the container images:
```sh
scripts/build-all-images.sh
```
Then bring up the containers:
```sh
docker-compose up -d
```
This will create some containers with the bare minimum necessary for ceod to
run, and start ceod on each of phosphoric-acid, mail, and coffee container.
You can check the containers status using:
```sh
docker-compose logs -f
```
To use ceo, run the following:
```sh
docker-compose exec phosphoric-acid bash
su ctdalek
. venv/bin/activate
python -m ceo # the password is krb5
```
This should bring up the TUI.
Normally, ceod should autoamtically restart when the source files are changed.
To manually restart the service, run:
```sh
docker-compose kill -s SIGHUP phosphoric-acid
```
To stop the containers, run:
```sh
docker-compose down
```
Alternatively, if you started docker-compose in the foreground, just press Ctrl-C.
### VM
If you need the full environment running in VM, follow the guide on
[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.
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 peer
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_DEBUG=true
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 `scripts/clear_cache.sh`, then
restart the app.
## Interacting with the application
To use the TUI:
```
python -m ceo
```
To use the CLI:
```
python -m ceo --help
```
Alternatively, you may use curl to send HTTP requests.
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 add a user (in the Docker container):
```sh
# If you're root, switch to the ctdalek user first
su ctdalek
# Get a Kerberos TGT (password is krb5)
kinit
# Make the request
curl --negotiate -u : --service-name ceod --delegation always \
-d '{"uid":"test_1","cn":"Test One","given_name":"Test","sn":"One","program":"Math","terms":["s2021"]}' \
-X POST http://phosphoric-acid:9987/api/members
# To delete the user:
curl --negotiate -u : --service-name ceod --delegation always \
-X DELETE http://phosphoric-acid:9987/api/members/test_1
# In prod, use the following base URL instead:
# https://phosphoric-acid.csclub.uwaterloo.ca:9987
```
## Packaging
See [PACKAGING.md](./PACKAGING.md).

1
VERSION.txt Normal file
View File

@ -0,0 +1 @@
1.0.31

View File

@ -1,3 +0,0 @@
#!/usr/bin/python
import ceo.main
ceo.main.start()

View File

@ -0,0 +1,43 @@
from abc import ABC, abstractmethod
from typing import Union
import requests
class StreamResponseHandler(ABC):
"""
An abstract class to handle stream responses from the server.
The CLI and TUI should implement a child class.
"""
@abstractmethod
def handle_non_200(self, resp: requests.Response):
"""Handle a non-200 response."""
@abstractmethod
def begin(self):
"""Begin the transaction."""
@abstractmethod
def handle_aborted(self, err_msg: str):
"""Handle an aborted transaction."""
@abstractmethod
def handle_completed(self):
"""Handle a completed transaction."""
@abstractmethod
def handle_successful_operation(self):
"""Handle a successful operation."""
@abstractmethod
def handle_failed_operation(self, err_msg: Union[str, None]):
"""Handle a failed operation."""
@abstractmethod
def handle_skipped_operation(self):
"""Handle a skipped operation."""
@abstractmethod
def handle_unrecognized_operation(self, operation: str):
"""Handle an unrecognized operation."""

View File

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

44
ceo/__main__.py Normal file
View File

@ -0,0 +1,44 @@
import os
import sys
from zope import component
from .cli import cli
from .krb_check import krb_check
from .tui.start import main as tui_main
from ceo_common.interfaces import IConfig, IHTTPClient
from ceo_common.model import Config, HTTPClient
from ceo_common.utils import is_in_development
def register_services():
# Using base component directly so events get triggered
baseComponent = component.getGlobalSiteManager()
# Config
if 'CEO_CONFIG' in os.environ:
config_file = os.environ['CEO_CONFIG']
else:
if is_in_development():
config_file = './tests/ceo_dev.ini'
else:
config_file = '/etc/csc/ceo.ini'
cfg = Config(config_file)
baseComponent.registerUtility(cfg, IConfig)
# HTTPService
http_client = HTTPClient()
baseComponent.registerUtility(http_client, IHTTPClient)
def main():
krb_check()
register_services()
if len(sys.argv) > 1:
cli(obj={})
else:
tui_main()
if __name__ == '__main__':
main()

View File

@ -0,0 +1,70 @@
import sys
from typing import List, Union
import click
import requests
from ..StreamResponseHandler import StreamResponseHandler
from ..operation_strings import descriptions as op_desc
class Abort(click.ClickException):
"""Abort silently."""
def __init__(self, exit_code=1):
super().__init__('')
self.exit_code = exit_code
def show(self):
pass
class CLIStreamResponseHandler(StreamResponseHandler):
def __init__(self, operations: List[str]):
super().__init__()
self.operations = operations
self.idx = 0
def handle_non_200(self, resp: requests.Response):
click.echo('An error occurred:')
click.echo(resp.text.rstrip())
raise Abort()
def begin(self):
click.echo(op_desc[self.operations[0]] + '... ', nl=False)
def handle_aborted(self, err_msg: str):
click.echo(click.style('ABORTED', fg='red'))
click.echo('The transaction was rolled back.')
click.echo('The error was: ' + err_msg)
click.echo('Please check the ceod logs.')
sys.exit(1)
def handle_completed(self):
click.echo('Transaction successfully completed.')
def _go_to_next_op(self):
"""
Increment the operation index and print the next operation, if
there is one.
"""
self.idx += 1
if self.idx < len(self.operations):
click.echo(op_desc[self.operations[self.idx]] + '... ', nl=False)
def handle_successful_operation(self):
click.echo(click.style('Done', fg='green'))
self._go_to_next_op()
def handle_failed_operation(self, err_msg: Union[str, None]):
click.echo(click.style('Failed', fg='red'))
if err_msg is not None:
click.echo(' Error message: ' + err_msg)
self._go_to_next_op()
def handle_skipped_operation(self):
click.echo('Skipped')
self._go_to_next_op()
def handle_unrecognized_operation(self, operation: str):
click.echo('Unrecognized operation: ' + operation)

1
ceo/cli/__init__.py Normal file
View File

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

91
ceo/cli/cloud.py Normal file
View File

@ -0,0 +1,91 @@
import click
from zope import component
from ceo_common.interfaces import IConfig
from ..utils import http_post, http_put, http_get, http_delete
from .utils import Abort, handle_sync_response, print_colon_kv
@click.group(short_help='Perform operations on the CSC cloud')
def cloud():
pass
@cloud.group(short_help='Manage your cloud account')
def account():
pass
@account.command(short_help='Activate your cloud account')
def activate():
cfg = component.getUtility(IConfig)
base_domain = cfg.get('base_domain')
resp = http_post('/api/cloud/accounts/create')
handle_sync_response(resp)
lines = [
'Congratulations! Your cloud account has been activated.',
f'You may now login into https://cloud.{base_domain} with your CSC credentials.',
"Make sure to enter 'Members' for the domain (no quotes).",
'',
'Please note that your cloud account will be PERMANENTLY DELETED when',
'your CSC membership expires, so make sure to purchase enough membership',
'terms in advance. You will receive a warning email one week before your',
'cloud account is deleted, so please make sure to check your Junk folder.',
]
for line in lines:
click.echo(line)
@cloud.group(short_help='Manage cloud accounts')
def accounts():
pass
@accounts.command(short_help='Purge expired cloud accounts')
def purge():
resp = http_post('/api/cloud/accounts/purge')
result = handle_sync_response(resp)
click.echo('Accounts to be deleted: ' + ','.join(result['accounts_to_be_deleted']))
click.echo('Accounts which were deleted: ' + ','.join(result['accounts_deleted']))
@cloud.group(short_help='Manage your virtual hosts')
def vhosts():
pass
@vhosts.command(name='add', short_help='Add a virtual host')
@click.argument('domain')
@click.argument('ip_address')
def add_vhost(domain, ip_address):
body = {'ip_address': ip_address}
if '/' in domain:
raise Abort('invalid domain name')
click.echo('Please wait, this may take a while...')
resp = http_put('/api/cloud/vhosts/' + domain, json=body)
handle_sync_response(resp)
click.echo('Done.')
@vhosts.command(name='delete', short_help='Delete a virtual host')
@click.argument('domain')
def delete_vhost(domain):
if '/' in domain:
raise Abort('invalid domain name')
resp = http_delete('/api/cloud/vhosts/' + domain)
handle_sync_response(resp)
click.echo('Done.')
@vhosts.command(name='list', short_help='List virtual hosts')
def list_vhosts():
resp = http_get('/api/cloud/vhosts')
result = handle_sync_response(resp)
vhosts = result['vhosts']
if not vhosts:
click.echo('No vhosts found.')
return
pairs = [(d['domain'], d['ip_address']) for d in vhosts]
print_colon_kv(pairs)

70
ceo/cli/database.py Normal file
View File

@ -0,0 +1,70 @@
import os
from typing import Dict
import click
from zope import component
from ..utils import http_post, http_get, http_delete, write_db_creds
from .utils import handle_sync_response, check_if_in_development
from ceo_common.interfaces import IConfig
def db_cli_response(filename: str, user_dict: Dict, password: str, db_type: str, op: str):
cfg_srv = component.getUtility(IConfig)
db_host = cfg_srv.get(f'{db_type}_host')
if db_type == 'mysql':
db_type_name = 'MySQL'
else:
db_type_name = 'PostgreSQL'
wrote_to_file = write_db_creds(filename, user_dict, password, db_type, db_host)
if op == 'create':
click.echo(f'{db_type_name} database created.')
username = user_dict['uid']
click.echo(f'''Connection Information:
Database: {username}
Username: {username}
Password: {password}
Host: {db_host}''')
if wrote_to_file:
click.echo(f"\nThese settings have been written to {filename}.")
else:
click.echo(f"\nWe were unable to write these settings to {filename}.")
def create(username: str, db_type: str):
db_type_name = 'MySQL' if db_type == 'mysql' else 'PostgreSQL'
resp = http_get(f'/api/members/{username}')
user_dict = handle_sync_response(resp)
click.confirm(f'Are you sure you want to create a {db_type_name} database for {username}?', abort=True)
info_file_path = os.path.join(user_dict['home_directory'], f"ceo-{db_type}-info")
resp = http_post(f'/api/db/{db_type}/{username}')
result = handle_sync_response(resp)
password = result['password']
db_cli_response(info_file_path, user_dict, password, db_type, 'create')
def pwreset(username: str, db_type: str):
db_type_name = 'MySQL' if db_type == 'mysql' else 'PostgreSQL'
resp = http_get(f'/api/members/{username}')
user_dict = handle_sync_response(resp)
click.confirm(f'Are you sure you want reset the {db_type_name} password for {username}?', abort=True)
info_file_path = os.path.join(user_dict['home_directory'], f"ceo-{db_type}-info")
resp = http_post(f'/api/db/{db_type}/{username}/pwreset')
result = handle_sync_response(resp)
password = result['password']
db_cli_response(info_file_path, user_dict, password, db_type, 'pwreset')
def delete(username: str, db_type: str):
check_if_in_development()
db_type_name = 'MySQL' if db_type == 'mysql' else 'PostgreSQL'
click.confirm(f"Are you sure you want to delete the {db_type_name} database for {username}?", abort=True)
resp = http_delete(f'/api/db/{db_type}/{username}')
handle_sync_response(resp)

31
ceo/cli/entrypoint.py Normal file
View File

@ -0,0 +1,31 @@
import click
from .members import members
from .groups import groups
from .positions import positions
from .updateprograms import updateprograms
from .mysql import mysql
from .postgresql import postgresql
from .mailman import mailman
from .cloud import cloud
from .k8s import k8s
from .registry import registry
from .webhosting import webhosting
@click.group()
def cli():
pass
cli.add_command(members)
cli.add_command(groups)
cli.add_command(positions)
cli.add_command(updateprograms)
cli.add_command(mysql)
cli.add_command(postgresql)
cli.add_command(mailman)
cli.add_command(cloud)
cli.add_command(k8s)
cli.add_command(registry)
cli.add_command(webhosting)

163
ceo/cli/groups.py Normal file
View File

@ -0,0 +1,163 @@
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 = [
('cn', result['cn']),
('description', result.get('description', 'Unknown')),
('gid_number', str(result['gid_number'])),
]
for i, member in enumerate(result['members']):
if i == 0:
prefix = 'members'
else:
prefix = ''
lines.append((prefix, member['cn'] + ' (' + member['uid'] + ')'))
print_colon_kv(lines)
@groups.command(short_help='Get info about a group')
@click.argument('group_name')
def get(group_name):
resp = http_get('/api/groups/' + group_name)
result = handle_sync_response(resp)
print_group_lines(result)
@groups.command(short_help='Add one or more members to a group')
@click.argument('group_name')
@click.argument('username')
@click.argument('usernames', nargs=-1)
@click.option('--no-subscribe', is_flag=True, default=False,
help='Do not subscribe the member(s) to any auxiliary mailing lists.')
def addmember(group_name, username, usernames, no_subscribe):
usernames = [username, *usernames]
if len(usernames) == 1:
click.confirm(f'Are you sure you want to add {username} to {group_name}?',
abort=True)
else:
click.echo(f'The following users will be added to {group_name}:')
click.echo(', '.join(usernames))
click.confirm('Do you want to continue?', abort=True)
base_domain = component.getUtility(IConfig).get('base_domain')
operations = AddMemberToGroupTransaction.operations
if no_subscribe:
operations.remove('subscribe_user_to_auxiliary_mailing_lists')
for username in usernames:
url = f'/api/groups/{group_name}/members/{username}'
if no_subscribe:
url += '?subscribe_to_lists=false'
resp = http_post(url)
data = handle_stream_response(resp, operations)
result = data[-1]['result']
click.echo(f'Added {username} to ' + ', '.join(result['added_to_groups']))
if result.get('subscribed_to_lists'):
mailing_lists = [
mailing_list + '@' + base_domain
if '@' not in mailing_list
else mailing_list
for mailing_list in result['subscribed_to_lists']
]
click.echo(f'Subscribed {username} to ' + ', '.join(mailing_lists))
@groups.command(short_help='Remove one or more members from a group')
@click.argument('group_name')
@click.argument('username')
@click.argument('usernames', nargs=-1)
@click.option('--no-unsubscribe', is_flag=True, default=False,
help='Do not unsubscribe the member(s) from any auxiliary mailing lists.')
def removemember(group_name, username, usernames, no_unsubscribe):
usernames = [username, *usernames]
if len(usernames) == 1:
click.confirm(f'Are you sure you want to remove {username} from {group_name}?',
abort=True)
else:
click.echo(f'The following users will be removed from {group_name}:')
click.echo(', '.join(usernames))
click.confirm('Do you want to continue?', abort=True)
base_domain = component.getUtility(IConfig).get('base_domain')
operations = RemoveMemberFromGroupTransaction.operations
if no_unsubscribe:
operations.remove('unsubscribe_user_from_auxiliary_mailing_lists')
for username in usernames:
url = f'/api/groups/{group_name}/members/{username}'
if no_unsubscribe:
url += '?unsubscribe_from_lists=false'
resp = http_delete(url)
data = handle_stream_response(resp, operations)
result = data[-1]['result']
click.echo(f'Removed {username} from ' + ', '.join(result['removed_from_groups']))
if result.get('unsubscribed_from_lists'):
mailing_lists = [
mailing_list + '@' + base_domain
if '@' not in mailing_list
else mailing_list
for mailing_list in result['unsubscribed_from_lists']
]
click.echo(f'Unsubscribed {username} from ' + ', '.join(mailing_lists))
@groups.command(short_help='Delete a group')
@click.argument('group_name')
def delete(group_name):
check_if_in_development()
click.confirm(f"Are you sure you want to delete {group_name}?", abort=True)
resp = http_delete(f'/api/groups/{group_name}')
handle_stream_response(resp, DeleteGroupTransaction.operations)
@groups.command(short_help='Search for groups')
@click.argument('query')
@click.option('--count', default=10, help='number of results to show')
def search(query, count):
check_if_in_development()
resp = http_get(f'/api/groups/search/{query}/{count}')
result = handle_sync_response(resp)
for cn in result:
if cn != "":
click.echo(cn)

43
ceo/cli/k8s.py Normal file
View File

@ -0,0 +1,43 @@
import os
import traceback
import click
from ..utils import http_post
from .utils import handle_sync_response
@click.group(short_help='Manage your CSC Kubernetes resources')
def k8s():
pass
@k8s.group(short_help='Manage your CSC Kubernetes account')
def account():
pass
@account.command(short_help='Obtain a kubeconfig')
def activate():
kubedir = os.path.join(os.environ['HOME'], '.kube')
if not os.path.isdir(kubedir):
os.mkdir(kubedir)
kubeconfig = os.path.join(kubedir, 'config')
resp = http_post('/api/cloud/k8s/accounts/create')
result = handle_sync_response(resp)
try:
if os.path.isfile(kubeconfig):
kubeconfig_bak = os.path.join(kubedir, 'config.bak')
os.rename(kubeconfig, kubeconfig_bak)
with open(kubeconfig, 'w') as fo:
fo.write(result['kubeconfig'])
os.chmod(kubeconfig, 0o600)
except Exception:
click.echo(traceback.format_exc())
click.echo("We weren't able to write the kubeconfig file, so here it is.")
click.echo("Make sure to paste this into your ~/.kube/config.")
click.echo()
click.echo(result['kubeconfig'])
return
click.echo("Congratulations! You have a new kubeconfig in ~/.kube/config.")
click.echo("Run `kubectl cluster-info` to make sure everything is working.")

29
ceo/cli/mailman.py Normal file
View File

@ -0,0 +1,29 @@
import click
from ..utils import http_post, http_delete
from .utils import handle_sync_response
@click.group(short_help='Manage mailing list subscriptions')
def mailman():
pass
@mailman.command(short_help='Subscribe a member to a mailing list')
@click.argument('username')
@click.argument('mailing_list')
def subscribe(username, mailing_list):
click.confirm(f'Are you sure you want to subscribe {username} to {mailing_list}?', abort=True)
resp = http_post(f'/api/mailman/{mailing_list}/{username}')
handle_sync_response(resp)
click.echo('Done.')
@mailman.command(short_help='Unsubscribe a member from a mailing list')
@click.argument('username')
@click.argument('mailing_list')
def unsubscribe(username, mailing_list):
click.confirm(f'Are you sure you want to unsubscribe {username} from {mailing_list}?', abort=True)
resp = http_delete(f'/api/mailman/{mailing_list}/{username}')
handle_sync_response(resp)
click.echo('Done.')

232
ceo/cli/members.py Normal file
View File

@ -0,0 +1,232 @@
import sys
from typing import Dict
import click
from zope import component
from ceo_common.utils import validate_username
from ..term_utils import get_terms_for_renewal_for_user
from ..utils import http_post, http_get, http_patch, http_delete, \
get_failed_operations, user_dict_lines, get_adduser_operations
from .utils import handle_stream_response, handle_sync_response, print_lines, \
check_if_in_development
from ceo_common.interfaces import IConfig
from ceo_common.model.Term import get_terms_for_new_user
from ceod.transactions.members import DeleteMemberTransaction
@click.group(short_help='Perform operations on CSC members and club reps')
def members():
pass
@members.command(short_help='Add a new member or club rep')
@click.argument('username')
@click.option('--cn', help='Full name', required=False)
@click.option('--given-name', help='First name', required=False)
@click.option('--sn', help='Last name', required=False)
@click.option('--program', required=False, help='Academic program')
@click.option('--terms', 'num_terms', type=click.IntRange(1, 100),
help='Number of terms to add', default=1)
@click.option('--clubrep', is_flag=True, default=False,
help='Add non-member terms instead of member terms')
@click.option('--forwarding-address', required=False,
help=('Forwarding address to set in ~/.forward. '
'Default is UW address. '
'Set to the empty string to disable forwarding.'))
def add(username, cn, given_name, sn, program, num_terms, clubrep, forwarding_address):
cfg = component.getUtility(IConfig)
uw_domain = cfg.get('uw_domain')
# Verify that the username is valid before requesting data from UWLDAP
username_validator = validate_username(username)
if not username_validator.is_valid:
return click.echo("The provided username is invalid")
# Try to get info from UWLDAP
resp = http_get('/api/uwldap/' + username)
if resp.ok:
result = handle_sync_response(resp)
if cn is None and result.get('cn'):
cn = result['cn']
if given_name is None and result.get('given_name'):
given_name = result['given_name']
if sn is None and result.get('sn'):
sn = result['sn']
if program is None and result.get('program'):
program = result['program']
if forwarding_address is None and result.get('mail_local_addresses'):
forwarding_address = result['mail_local_addresses'][0]
if cn is None:
cn = click.prompt('Full name')
if given_name is None:
given_name = click.prompt('First name')
if sn is None:
sn = click.prompt('Last name')
if forwarding_address is None:
forwarding_address = username + '@' + uw_domain
terms = get_terms_for_new_user(num_terms)
body = {
'uid': username,
'cn': cn,
'given_name': given_name,
'sn': sn,
}
if program is not None:
body['program'] = program
if clubrep:
body['non_member_terms'] = terms
else:
body['terms'] = terms
if forwarding_address != '':
body['forwarding_addresses'] = [forwarding_address]
else:
body['forwarding_addresses'] = []
click.echo("The following user will be created:")
print_user_lines(body)
click.confirm('Do you want to continue?', abort=True)
operations = get_adduser_operations(body)
resp = http_post('/api/members', json=body)
data = handle_stream_response(resp, operations)
result = data[-1]['result']
print_user_lines(result)
failed_operations = get_failed_operations(data)
if 'send_welcome_message' in failed_operations:
click.echo(click.style(
'Warning: welcome message was not sent. You now need to manually '
'send the user their password.', fg='yellow'))
def print_user_lines(d: Dict):
"""Pretty-print a serialized User."""
print_lines(user_dict_lines(d))
@members.command(short_help='Get info about a user')
@click.argument('username')
def get(username):
resp = http_get('/api/members/' + username)
result = handle_sync_response(resp)
print_user_lines(result)
@members.command(short_help="Replace a user's login shell or forwarding addresses")
@click.argument('username')
@click.option('--login-shell', required=False, help='Login shell')
@click.option('--forwarding-addresses', required=False,
help=(
'Comma-separated list of forwarding addresses. '
'Set to the empty string to disable forwarding.'
))
def modify(username, login_shell, forwarding_addresses):
if login_shell is None and forwarding_addresses is None:
click.echo('Nothing to do.')
sys.exit()
operations = []
body = {}
if login_shell is not None:
body['login_shell'] = login_shell
operations.append('replace_login_shell')
click.echo('Login shell will be set to: ' + login_shell)
if forwarding_addresses is not None:
if forwarding_addresses == '':
forwarding_addresses = []
else:
forwarding_addresses = forwarding_addresses.split(',')
body['forwarding_addresses'] = forwarding_addresses
operations.append('replace_forwarding_addresses')
prefix = '~/.forward will be set to: '
if len(forwarding_addresses) > 0:
click.echo(prefix + forwarding_addresses[0])
for address in forwarding_addresses[1:]:
click.echo((' ' * len(prefix)) + address)
else:
click.echo(prefix)
click.confirm('Do you want to continue?', abort=True)
resp = http_patch('/api/members/' + username, json=body)
handle_stream_response(resp, operations)
@members.command(short_help="Renew a member or club rep's membership")
@click.argument('username')
@click.option('--terms', 'num_terms', type=click.IntRange(1, 100),
help='Number of terms to add', prompt='Number of terms')
@click.option('--clubrep', is_flag=True, default=False,
help='Add non-member terms instead of member terms')
def renew(username, num_terms, clubrep):
terms = get_terms_for_renewal_for_user(username, num_terms, clubrep)
if clubrep:
body = {'non_member_terms': terms}
click.echo('The following non-member terms will be added: ' + ','.join(terms))
else:
body = {'terms': terms}
click.echo('The following member terms will be added: ' + ','.join(terms))
click.confirm('Do you want to continue?', abort=True)
resp = http_post(f'/api/members/{username}/renew', json=body)
handle_sync_response(resp)
click.echo('Done.')
@members.command(short_help="Reset a user's password")
@click.argument('username')
def pwreset(username):
click.confirm(f"Are you sure you want to reset {username}'s password?", abort=True)
resp = http_post(f'/api/members/{username}/pwreset')
result = handle_sync_response(resp)
click.echo('New password: ' + result['password'])
@members.command(short_help="Delete a user")
@click.argument('username')
def delete(username):
check_if_in_development()
click.confirm(f"Are you sure you want to delete {username}?", abort=True)
resp = http_delete(f'/api/members/{username}')
handle_stream_response(resp, DeleteMemberTransaction.operations)
@members.command(short_help="Check for and mark expired members")
@click.option('--dry-run', is_flag=True, default=False)
def expire(dry_run):
resp = http_post(f'/api/members/expire?dry_run={dry_run and "yes" or "no"}')
result = handle_sync_response(resp)
if len(result) > 0:
if dry_run:
click.echo("The following members will be marked as expired:")
else:
click.echo("The following members has been marked as expired:")
for username in result:
click.echo(username)
@members.command(short_help="Send renewal reminder emails to expiring members")
@click.option('--dry-run', is_flag=True, default=False)
def remindexpire(dry_run):
url = '/api/members/remindexpire'
if dry_run:
url += '?dry_run=true'
resp = http_post(url)
result = handle_sync_response(resp)
if len(result) > 0:
if dry_run:
click.echo("The following members will be sent membership renewal reminders:")
else:
click.echo("The following members were sent membership renewal reminders:")
for username in result:
click.echo(username)
else:
click.echo("No members are pending expiration.")

26
ceo/cli/mysql.py Normal file
View File

@ -0,0 +1,26 @@
import click
from .database import create as db_create, pwreset as db_pwreset, delete as db_delete
@click.group(short_help='Perform operations on MySQL')
def mysql():
pass
@mysql.command(short_help='Create a MySQL database for a user')
@click.argument('username')
def create(username):
db_create(username, 'mysql')
@mysql.command(short_help='Reset the password of a MySQL user')
@click.argument('username')
def pwreset(username):
db_pwreset(username, 'mysql')
@mysql.command(short_help="Delete the database of a MySQL user")
@click.argument('username')
def delete(username):
db_delete(username, 'mysql')

53
ceo/cli/positions.py Normal file
View File

@ -0,0 +1,53 @@
import click
from zope import component
from ..utils import http_get, http_post
from .utils import handle_sync_response, handle_stream_response, print_colon_kv
from ceo_common.interfaces import IConfig
from ceod.transactions.members import UpdateMemberPositionsTransaction
@click.group(short_help='List or change exec positions')
def positions():
update_commands()
@positions.command(short_help='Get current positions')
def get():
resp = http_get('/api/positions')
result = handle_sync_response(resp)
print_colon_kv([
(position, ', '.join(usernames))
for position, usernames in result.items()
])
@positions.command(short_help='Update positions')
def set(**kwargs):
body = {
k.replace('_', '-'): v.replace(' ', '').split(',') if v else None
for k, v in kwargs.items()
}
print_body = {
k: ', '.join(v) if v else ''
for k, v in body.items()
}
click.echo('The positions will be updated:')
print_colon_kv(print_body.items())
click.confirm('Do you want to continue?', abort=True)
resp = http_post('/api/positions', json=body)
handle_stream_response(resp, UpdateMemberPositionsTransaction.operations)
# Provides dynamic parameters for `set' command using config file
def update_commands():
global set
cfg = component.getUtility(IConfig)
avail = cfg.get('positions_available')
required = cfg.get('positions_required')
for pos in avail:
r = pos in required
set = click.option(f'--{pos}', metavar='USERNAME', required=r, prompt=r)(set)

26
ceo/cli/postgresql.py Normal file
View File

@ -0,0 +1,26 @@
import click
from .database import create as db_create, pwreset as db_pwreset, delete as db_delete
@click.group(short_help='Perform operations on PostgreSQL')
def postgresql():
pass
@postgresql.command(short_help='Create a PostgreSQL database for a user')
@click.argument('username')
def create(username):
db_create(username, 'postgresql')
@postgresql.command(short_help='Reset the password of a PostgreSQL user')
@click.argument('username')
def pwreset(username):
db_pwreset(username, 'postgresql')
@postgresql.command(short_help="Delete the database of a PostgreSQL user")
@click.argument('username')
def delete(username):
db_delete(username, 'postgresql')

21
ceo/cli/registry.py Normal file
View File

@ -0,0 +1,21 @@
import click
from ..utils import http_post
from .utils import handle_sync_response
@click.group(short_help='Manage your container registry account')
def registry():
pass
@registry.group(short_help='Manage your container registry project')
def project():
pass
@project.command(short_help='Create a registry project')
def create():
resp = http_post('/api/cloud/registry/projects')
handle_sync_response(resp)
click.echo('Congratulations! Your registry project was successfully created.')

35
ceo/cli/updateprograms.py Normal file
View File

@ -0,0 +1,35 @@
import click
from ..utils import http_post
from .utils import handle_sync_response, print_colon_kv
@click.command(short_help="Sync the 'program' attribute with UWLDAP")
@click.option('--dry-run', is_flag=True, default=False)
@click.option('--members', required=False)
def updateprograms(dry_run, members):
body = {}
if dry_run:
body['dry_run'] = True
if members is not None:
body['members'] = ','.split(members)
if not dry_run:
click.confirm('Are you sure that you want to sync programs with UWLDAP?', abort=True)
resp = http_post('/api/uwldap/updateprograms', json=body)
result = handle_sync_response(resp)
if len(result) == 0:
click.echo('All programs are up-to-date.')
return
if dry_run:
click.echo('Members whose program would be changed:')
else:
click.echo('Members whose program was changed:')
lines = []
for uid, csc_program, uw_program in result:
csc_program = csc_program or 'Unknown'
csc_program = click.style(csc_program, fg='yellow')
uw_program = click.style(uw_program, fg='green')
lines.append((uid, csc_program + ' -> ' + uw_program))
print_colon_kv(lines)

57
ceo/cli/utils.py Normal file
View File

@ -0,0 +1,57 @@
from typing import List, Tuple, Dict
import click
import requests
from ceo_common.utils import is_in_development
from ..utils import space_colon_kv, generic_handle_stream_response
from .CLIStreamResponseHandler import CLIStreamResponseHandler
class Abort(click.ClickException):
"""Abort silently."""
def __init__(self, exit_code=1):
super().__init__('')
self.exit_code = exit_code
def show(self):
pass
def print_lines(lines: List[str]):
"""Print multiple lines to stdout."""
for line in lines:
click.echo(line)
def print_colon_kv(pairs: List[Tuple[str, str]]):
"""
Pretty-print a list of key-value pairs.
"""
for line in space_colon_kv(pairs):
click.echo(line)
def handle_stream_response(resp: requests.Response, operations: List[str]) -> List[Dict]:
handler = CLIStreamResponseHandler(operations)
return generic_handle_stream_response(resp, operations, handler)
def handle_sync_response(resp: requests.Response):
"""
Exit the program if the request was not successful.
Returns the parsed JSON response.
"""
if resp.status_code != 200:
click.echo('An error occurred:')
click.echo(resp.text.rstrip())
raise Abort()
return resp.json()
def check_if_in_development() -> bool:
"""Aborts if we are not currently in the dev environment."""
if not is_in_development():
click.echo('This command may only be called during development.')
raise Abort()

37
ceo/cli/webhosting.py Normal file
View File

@ -0,0 +1,37 @@
import click
from ..utils import http_post
from .utils import handle_sync_response
@click.group(short_help='Manage websites hosted by the main CSC web server')
def webhosting():
pass
@webhosting.command(short_help='Disable club sites with no active club reps')
@click.option('--dry-run', is_flag=True, default=False)
@click.option('--remove-inactive-club-reps', is_flag=True, default=False)
def disableclubsites(dry_run, remove_inactive_club_reps):
params = {}
if dry_run:
params['dry_run'] = 'true'
if remove_inactive_club_reps:
params['remove_inactive_club_reps'] = 'true'
if not dry_run:
click.confirm('Are you sure you want to disable the websites of clubs with no active club reps?', abort=True)
resp = http_post('/api/webhosting/disableclubsites', params=params)
disabled_club_names = handle_sync_response(resp)
if len(disabled_club_names) == 0:
if dry_run:
click.echo('No websites would have been disabled.')
else:
click.echo('No websites were disabled.')
else:
if dry_run:
click.echo('The following club websites would have been disabled:')
else:
click.echo('The following club websites were disabled:')
for club_name in disabled_club_names:
click.echo(club_name)

View File

@ -1,162 +0,0 @@
"""
Configuration Utility Module
This module contains functions to load and verify very simple configuration
files. Python supports ".ini" files, which suck, so this module is used
instead.
Example Configuration File:
include /path/to/other.cf
# these values are the same:
name_protected = "Michael Spang"
name_unprotected = Michael Spang
# these values are not the same:
yes_no = " yes"
no_yes = yes
# this value is an integer
arbitrary_number=2
# this value is not an integer
arbitrary_string="2"
# this is a key with no value
csclub
# this key contains whitespace
white space = sure, why not
# these two lines are treated as one
long line = first line \\
second line
Resultant Dictionary:
{
'name_protected': 'Michael Spang',
'name_unprotected:' 'Michael Spang',
'yes_no': ' yes',
'no_yes': 'yes',
'arbirary_number': 2,
'arbitrary_string': '2',
'csclub': None,
'white space': 'sure, why not'
'long line': 'first line \\n second line'
... (data from other.cf) ...
}
"""
from curses.ascii import isspace
class ConfigurationException(Exception):
"""Exception class for incomplete and incorrect configurations."""
def read(filename, included=None):
"""
Function to read a configuration file into a dictionary.
Parmaeters:
filename - the file to read
included - files previously read (internal)
Exceptions:
IOError - when the configuration file cannot be read
"""
if not included:
included = []
if filename in included:
return {}
included.append(filename)
conffile = open(filename)
options = {}
while True:
line = conffile.readline()
if line == '':
break
# remove comments
if '#' in line:
line = line[:line.find('#')]
# combine lines when the newline is escaped with \
while len(line) > 1 and line[-2] == '\\':
line = line[:-2] + line[-1]
next = conffile.readline()
line += next
if next == '':
break
line = line.strip()
# process include statements
if line.find("include") == 0 and isspace(line[7]):
filename = line[8:].strip()
options.update(read(filename, included))
continue
# split 'key = value' into key and value and strip results
pair = map(str.strip, line.split('=', 1))
# found key and value
if len(pair) == 2:
key, val = pair
# found quoted string?
if val and val[0] == val[-1] == '"':
val = val[1:-1]
# unquoted, found num?
elif val:
try:
if "." in val:
val = float(val)
elif val[0] == '0':
val = int(val, 8)
else:
val = int(val)
except ValueError:
pass
# save key and value
options[key] = val
# found only key, value = None
elif len(pair[0]) > 1:
key = pair[0]
options[key] = None
return options
def check_string_fields(filename, field_list, cfg):
"""Function to verify thatfields are strings."""
for field in field_list:
if field not in cfg or type(cfg[field]) is not str:
raise ConfigurationException('expected string value for option "%s" in "%s"' % (field, filename))
def check_integer_fields(filename, field_list, cfg):
"""Function to verify that fields are integers."""
for field in field_list:
if field not in cfg or type(cfg[field]) not in (int, long):
raise ConfigurationException('expected numeric value for option "%s" in "%s"' % (field, filename))
def check_float_fields(filename, field_list, cfg):
"""Function to verify that fields are integers or floats."""
for field in field_list:
if field not in cfg or type(cfg[field]) not in (float, long, int):
raise ConfigurationException('expected float value for option "%s" in "%s"' % (field, filename))

View File

@ -1 +0,0 @@
"""Console Interface"""

View File

@ -1,25 +0,0 @@
import ldap
from ceo import members, uwldap
class ExpiredAccounts:
def main(self, args):
send_email = False
if len(args) == 1 and args[0] == '--email':
sys.stderr.write("If you want to send an account expiration notice to " \
"these users then type 'Yes, do this' and hit enter\n")
if raw_input() == 'Yes, do this':
send_email = True
uwl = ldap.initialize(uwldap.uri())
mlist = members.expired_accounts()
for member in mlist.values():
uid = member['uid'][0]
name = member['cn'][0]
email = None
if send_email:
members.send_account_expired_email(name, uid)
user = uwl.search_s(uwldap.base(), ldap.SCOPE_SUBTREE,
'(uid=%s)' % ldapi.escape(uid))
if len(user) > 0 and 'mailLocalAddress' in user[0][1]:
email = user[0][1]['mailLocalAddress'][0]
members.send_account_expired_email(name, email)
print '%s %s' % (uid.ljust(12), name.ljust(30))

View File

@ -1,21 +0,0 @@
from ceo import members, terms
def max_term(term1, term2):
if terms.compare(term1, term2) > 0:
return term1
else:
return term2
class Inactive:
def main(self, args):
if len(args) != 1:
print "Usage: ceo inactive delta-terms"
return
delta = int(args[0])
mlist = members.list_all()
for member in mlist.values():
term = "f0000"
term = reduce(max_term, member.get("term", []), term)
term = reduce(max_term, member.get("nonMemberTerm", []), term)
if terms.delta(term, terms.current()) >= delta:
print "%s %s" % (member['uid'][0].ljust(12), term)

View File

@ -1,34 +0,0 @@
import sys, ldap, termios
from getopt import getopt
from ceo import members, terms, uwldap, ldapi
from ceo.console.memberlist import MemberList
from ceo.console.updateprograms import UpdatePrograms
from ceo.console.expiredaccounts import ExpiredAccounts
from ceo.console.inactive import Inactive
commands = {
'memberlist' : MemberList(),
'updateprograms' : UpdatePrograms(),
'expiredaccounts' : ExpiredAccounts(),
'inactive': Inactive(),
}
shortopts = [
]
longopts = [
]
def start():
(opts, args) = getopt(sys.argv[1:], shortopts, longopts)
if len(args) >= 1:
if args[0] in commands:
commands[args[0]].main(args[1:])
else:
print "Invalid command '%s'" % args[0]
def help():
print 'Available commands:'
for c in commands:
print ' %s' % c

View File

@ -1,14 +0,0 @@
from ceo import members, terms
class MemberList:
def main(self, args):
mlist = members.list_term(terms.current())
dns = mlist.keys()
dns.sort()
for dn in dns:
member = mlist[dn]
print '%s %s %s' % (
member['uid'][0].ljust(12),
member['cn'][0].ljust(30),
member.get('program', [''])[0]
)

View File

@ -1,42 +0,0 @@
import ldap, sys, termios
from ceo import members, uwldap, ldapi
class UpdatePrograms:
def main(self, args):
mlist = members.list_all().items()
uwl = ldap.initialize(uwldap.uri())
fd = sys.stdin.fileno()
for (dn, member) in mlist:
uid = member['uid'][0]
user = uwl.search_s(uwldap.base(), ldap.SCOPE_SUBTREE,
'(uid=%s)' % ldapi.escape(uid))
if len(user) == 0:
continue
user = user[0][1]
oldprog = member.get('program', [''])[0]
newprog = user.get('ou', [''])[0]
if oldprog == newprog or newprog == '':
continue
sys.stdout.write("%s: '%s' => '%s'? (y/n) " % (uid, oldprog, newprog))
new = old = termios.tcgetattr(fd)
new[3] = new[3] & ~termios.ICANON
try:
termios.tcsetattr(fd, termios.TCSANOW, new)
try:
if sys.stdin.read(1) != 'y':
continue
except KeyboardInterrupt:
return ''
finally:
print ''
termios.tcsetattr(fd, termios.TCSANOW, old)
old = new = {}
if oldprog != '':
old = {'program': [oldprog]}
if newprog != '':
new = {'program': [newprog]}
mlist = ldapi.make_modlist(old, new)
# TODO: don't use members.ld directly
#if newprog != '':
# members.set_program(uid, newprog)
members.ld.modify_s(dn, mlist)

View File

@ -1,13 +0,0 @@
"""
Exceptions Module
This module provides some simple but generally useful exception classes.
"""
class InvalidArgument(Exception):
"""Exception class for bad argument values."""
def __init__(self, argname, argval, explanation):
Exception.__init__(self)
self.argname, self.argval, self.explanation = argname, argval, explanation
def __str__(self):
return 'Bad argument value "%s" for %s: %s' % (self.argval, self.argname, self.explanation)

35
ceo/krb_check.py Normal file
View File

@ -0,0 +1,35 @@
import subprocess
import gssapi
_username = None
def get_username():
"""Get the user currently logged into CEO."""
return _username
def krb_check():
"""
Spawns a `kinit` process if no credentials are available or the
credentials have expired.
Stores the username for later use by get_username().
"""
global _username
for _ in range(2):
try:
creds = gssapi.Credentials(usage='initiate')
result = creds.inquire()
princ = str(result.name)
_username = princ[:princ.index('@')]
return
except (gssapi.raw.misc.GSSError, gssapi.raw.exceptions.ExpiredCredentialsError):
kinit()
raise Exception('could not acquire GSSAPI credentials')
def kinit():
subprocess.run(['kinit'], check=True)

View File

@ -1,141 +0,0 @@
"""
LDAP Utilities
This module makes use of python-ldap, a Python module with bindings
to libldap, OpenLDAP's native C client library.
"""
import ldap.modlist, os, pwd
from subprocess import Popen, PIPE
def connect_sasl(uri, mech, realm, password):
# open the connection
ld = ldap.initialize(uri)
# authenticate
sasl = Sasl(mech, realm, password)
ld.sasl_interactive_bind_s('', sasl)
return ld
def abslookup(ld, dn, objectclass=None):
# search for the specified dn
try:
if objectclass:
search_filter = '(objectclass=%s)' % escape(objectclass)
matches = ld.search_s(dn, ldap.SCOPE_BASE, search_filter)
else:
matches = ld.search_s(dn, ldap.SCOPE_BASE)
except ldap.NO_SUCH_OBJECT:
return None
# dn was found, but didn't match the objectclass filter
if len(matches) < 1:
return None
# return the attributes of the single successful match
match = matches[0]
match_dn, match_attributes = match
return match_attributes
def lookup(ld, rdntype, rdnval, base, objectclass=None):
dn = '%s=%s,%s' % (rdntype, escape(rdnval), base)
return abslookup(ld, dn, objectclass)
def search(ld, base, search_filter, params=[], scope=ldap.SCOPE_SUBTREE, attrlist=None, attrsonly=0):
real_filter = search_filter % tuple(escape(x) for x in params)
# search for entries that match the filter
matches = ld.search_s(base, scope, real_filter, attrlist, attrsonly)
return matches
def modify(ld, rdntype, rdnval, base, mlist):
dn = '%s=%s,%s' % (rdntype, escape(rdnval), base)
ld.modify_s(dn, mlist)
def modify_attrs(ld, rdntype, rdnval, base, old, attrs):
dn = '%s=%s,%s' % (rdntype, escape(rdnval), base)
# build list of modifications to make
changes = ldap.modlist.modifyModlist(old, attrs)
# apply changes
ld.modify_s(dn, changes)
def modify_diff(ld, rdntype, rdnval, base, old, new):
dn = '%s=%s,%s' % (rdntype, escape(rdnval), base)
# build list of modifications to make
changes = make_modlist(old, new)
# apply changes
ld.modify_s(dn, changes)
def escape(value):
"""
Escapes special characters in a value so that it may be safely inserted
into an LDAP search filter.
"""
value = str(value)
value = value.replace('\\', '\\5c').replace('*', '\\2a')
value = value.replace('(', '\\28').replace(')', '\\29')
value = value.replace('\x00', '\\00')
return value
def make_modlist(old, new):
keys = set(old.keys()).union(set(new))
mlist = []
for key in keys:
if key in old and not key in new:
mlist.append((ldap.MOD_DELETE, key, list(set(old[key]))))
elif key in new and not key in old:
mlist.append((ldap.MOD_ADD, key, list(set(new[key]))))
else:
to_add = list(set(new[key]) - set(old[key]))
if len(to_add) > 0:
mlist.append((ldap.MOD_ADD, key, to_add))
to_del = list(set(old[key]) - set(new[key]))
if len(to_del) > 0:
mlist.append((ldap.MOD_DELETE, key, to_del))
return mlist
def format_ldaperror(ex):
desc = ex[0].get('desc', '')
info = ex[0].get('info', '')
if desc and info:
return "%s: %s" % (desc, info)
elif desc:
return desc
else:
return str(ex)
class Sasl:
def __init__(self, mech, realm, password):
self.mech = mech
self.realm = realm
if mech == 'GSSAPI' and password is not None:
userid = pwd.getpwuid(os.getuid()).pw_name
kinit = '/usr/bin/kinit'
kinit_args = [ kinit, '%s@%s' % (userid, realm) ]
kinit = Popen(kinit_args, stdin=PIPE, stdout=PIPE, stderr=PIPE)
kinit.stdin.write('%s\n' % password)
kinit.wait()
def callback(self, id, challenge, prompt, defresult):
return ''

View File

@ -1,33 +0,0 @@
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) >= 2 and sys.argv[1] == '--help':
ceo.console.main.help()
sys.exit(0)
members.connect(AuthCallback())
if len(sys.argv) == 1:
ceo.urwid.main.start()
else:
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:
sys.stderr.write("Password: ")
return getpass("")
except KeyboardInterrupt:
print ""
sys.exit(1)

View File

@ -1,526 +0,0 @@
"""
CSC Member Management
This module contains functions for registering new members, registering
members for terms, searching for members, and other member-related
functions.
Transactions are used in each method that modifies the database.
Future changes to the members database that need to be atomic
must also be moved into this module.
"""
import os, re, subprocess, ldap
from ceo import conf, ldapi, terms
from ceo.excep import InvalidArgument
### Configuration ###
CONFIG_FILE = '/etc/csc/accounts.cf'
cfg = {}
def configure():
"""Load Members Configuration"""
string_fields = [ 'username_regex', 'shells_file', 'server_url',
'users_base', 'groups_base', 'sasl_mech', 'sasl_realm',
'admin_bind_keytab', 'admin_bind_userid', 'realm',
'admin_principal', 'admin_keytab', 'expired_account_email' ]
numeric_fields = [ 'min_password_length' ]
# read configuration file
cfg_tmp = conf.read(CONFIG_FILE)
# verify configuration
conf.check_string_fields(CONFIG_FILE, string_fields, cfg_tmp)
conf.check_integer_fields(CONFIG_FILE, numeric_fields, cfg_tmp)
# update the current configuration with the loaded values
cfg.update(cfg_tmp)
### Exceptions ###
class MemberException(Exception):
"""Base exception class for member-related errors."""
def __init__(self, ex=None):
Exception.__init__(self)
self.ex = ex
def __str__(self):
return str(self.ex)
class InvalidTerm(MemberException):
"""Exception class for malformed terms."""
def __init__(self, term):
MemberException.__init__(self)
self.term = term
def __str__(self):
return "Term is invalid: %s" % self.term
class NoSuchMember(MemberException):
"""Exception class for nonexistent members."""
def __init__(self, memberid):
MemberException.__init__(self)
self.memberid = memberid
def __str__(self):
return "Member not found: %d" % self.memberid
class ChildFailed(MemberException):
def __init__(self, program, status, output):
MemberException.__init__(self)
self.program, self.status, self.output = program, status, output
def __str__(self):
msg = '%s failed with status %d' % (self.program, self.status)
if self.output:
msg += ': %s' % self.output
return msg
### Connection Management ###
# global directory connection
ld = None
def connect(auth_callback):
"""Connect to LDAP."""
configure()
global ld
password = None
tries = 0
while ld is None:
try:
ld = ldapi.connect_sasl(cfg['server_url'], cfg['sasl_mech'],
cfg['sasl_realm'], password)
except ldap.LOCAL_ERROR, e:
tries += 1
if tries > 3:
raise e
password = auth_callback.callback(e)
if password == None:
raise e
def disconnect():
"""Disconnect from LDAP."""
global ld
ld.unbind_s()
ld = None
def connected():
"""Determine whether the connection has been established."""
return ld and ld.connected()
### Members ###
def create_member(username, password, name, program):
"""
Creates a UNIX user account with options tailored to CSC members.
Parameters:
username - the desired UNIX username
password - the desired UNIX password
name - the member's real name
program - the member's program of study
Exceptions:
InvalidArgument - on bad account attributes provided
Returns: the uid number of the new account
See: create()
"""
# check username format
if not username or not re.match(cfg['username_regex'], username):
raise InvalidArgument("username", username, "expected format %s" % repr(cfg['username_regex']))
# check password length
if not password or len(password) < cfg['min_password_length']:
raise InvalidArgument("password", "<hidden>", "too short (minimum %d characters)" % cfg['min_password_length'])
try:
args = [ "/usr/bin/addmember", "--stdin", username, name, program ]
addmember = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = addmember.communicate(password)
status = addmember.wait()
except OSError, e:
raise MemberException(e)
if status:
raise ChildFailed("addmember", status, out+err)
def get(userid):
"""
Look up attributes of a member by userid.
Returns: a dictionary of attributes
Example: get('mspang') -> {
'cn': [ 'Michael Spang' ],
'program': [ 'Computer Science' ],
...
}
"""
return ldapi.lookup(ld, 'uid', userid, cfg['users_base'])
def uid2dn(uid):
return 'uid=%s,%s' % (ldapi.escape(uid), cfg['users_base'])
def list_term(term):
"""
Build a list of members in a term.
Parameters:
term - the term to match members against
Returns: a list of members
Example: list_term('f2006'): -> {
'uid=mspang, ou=...': { 'cn': 'Michael Spang', ... },
'uid=ctdalek, ou=...': { 'cn': 'Calum T. Dalek', ... },
...
}
"""
members = ldapi.search(ld, cfg['users_base'],
'(&(objectClass=member)(term=%s))', [ term ])
return dict([(member[0], member[1]) for member in members])
def list_name(name):
"""
Build a list of members with matching names.
Parameters:
name - the name to match members against
Returns: a list of member dictionaries
Example: list_name('Spang'): -> {
'uid=mspang, ou=...': { 'cn': 'Michael Spang', ... },
...
]
"""
members = ldapi.search(ld, cfg['users_base'],
'(&(objectClass=member)(cn~=%s))', [ name ])
return dict([(member[0], member[1]) for member in members])
def list_group(group):
"""
Build a list of members in a group.
Parameters:
group - the group to match members against
Returns: a list of member dictionaries
Example: list_name('syscom'): -> {
'uid=mspang, ou=...': { 'cn': 'Michael Spang', ... },
...
]
"""
members = group_members(group)
ret = {}
if members:
for member in members:
info = get(member)
if info:
ret[uid2dn(member)] = info
return ret
def list_all():
"""
Build a list of all members
Returns: a list of member dictionaries
Example: list_name('Spang'): -> {
'uid=mspang, ou=...': { 'cn': 'Michael Spang', ... },
...
]
"""
members = ldapi.search(ld, cfg['users_base'], '(objectClass=member)')
return dict([(member[0], member[1]) for member in members])
def list_positions():
"""
Build a list of positions
Returns: a list of positions and who holds them
Example: list_positions(): -> {
'president': { 'mspang': { 'cn': 'Michael Spang', ... } } ],
...
]
"""
members = ld.search_s(cfg['users_base'], ldap.SCOPE_SUBTREE, '(position=*)')
positions = {}
for (_, member) in members:
for position in member['position']:
if not position in positions:
positions[position] = {}
positions[position][member['uid'][0]] = member
return positions
def set_position(position, members):
"""
Sets a position
Parameters:
position - the position to set
members - an array of members that hold the position
Example: set_position('president', ['dtbartle'])
"""
res = ld.search_s(cfg['users_base'], ldap.SCOPE_SUBTREE,
'(&(objectClass=member)(position=%s))' % ldapi.escape(position))
old = set([ member['uid'][0] for (_, member) in res ])
new = set(members)
mods = {
'del': set(old) - set(new),
'add': set(new) - set(old),
}
if len(mods['del']) == 0 and len(mods['add']) == 0:
return
for action in ['del', 'add']:
for userid in mods[action]:
dn = 'uid=%s,%s' % (ldapi.escape(userid), cfg['users_base'])
entry1 = {'position' : [position]}
entry2 = {} #{'position' : []}
entry = ()
if action == 'del':
entry = (entry1, entry2)
elif action == 'add':
entry = (entry2, entry1)
mlist = ldapi.make_modlist(entry[0], entry[1])
ld.modify_s(dn, mlist)
def change_group_member(action, group, userid):
user_dn = 'uid=%s,%s' % (ldapi.escape(userid), cfg['users_base'])
group_dn = 'cn=%s,%s' % (ldapi.escape(group), cfg['groups_base'])
entry1 = {'uniqueMember' : []}
entry2 = {'uniqueMember' : [user_dn]}
entry = []
if action == 'add' or action == 'insert':
entry = (entry1, entry2)
elif action == 'remove' or action == 'delete':
entry = (entry2, entry1)
else:
raise InvalidArgument("action", action, "invalid action")
mlist = ldapi.make_modlist(entry[0], entry[1])
ld.modify_s(group_dn, mlist)
### Shells ###
def get_shell(userid):
member = ldapi.lookup(ld, 'uid', userid, cfg['users_base'])
if not member:
raise NoSuchMember(userid)
if 'loginShell' not in member:
return
return member['loginShell'][0]
def get_shells():
return [ sh for sh in open(cfg['shells_file']).read().split("\n")
if sh
and sh[0] == '/'
and not '#' in sh
and os.access(sh, os.X_OK) ]
def set_shell(userid, shell):
if not shell in get_shells():
raise InvalidArgument("shell", shell, "is not in %s" % cfg['shells_file'])
ldapi.modify(ld, 'uid', userid, cfg['users_base'], [ (ldap.MOD_REPLACE, 'loginShell', [ shell ]) ])
### Clubs ###
def create_club(username, name):
"""
Creates a UNIX user account with options tailored to CSC-hosted clubs.
Parameters:
username - the desired UNIX username
name - the club name
Exceptions:
InvalidArgument - on bad account attributes provided
Returns: the uid number of the new account
See: create()
"""
# check username format
if not username or not re.match(cfg['username_regex'], username):
raise InvalidArgument("username", username, "expected format %s" % repr(cfg['username_regex']))
try:
args = [ "/usr/bin/addclub", username, name ]
addclub = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = addclub.communicate()
status = addclub.wait()
except OSError, e:
raise MemberException(e)
if status:
raise ChildFailed("addclub", status, out+err)
### Terms ###
def register(userid, term_list):
"""
Registers a member for one or more terms.
Parameters:
userid - the member's username
term_list - the term to register for, or a list of terms
Exceptions:
InvalidTerm - if a term is malformed
Example: register(3349, "w2007")
Example: register(3349, ["w2007", "s2007"])
"""
user_dn = 'uid=%s,%s' % (ldapi.escape(userid), cfg['users_base'])
if type(term_list) in (str, unicode):
term_list = [ term_list ]
ldap_member = get(userid)
if ldap_member and 'term' not in ldap_member:
ldap_member['term'] = []
if not ldap_member:
raise NoSuchMember(userid)
new_member = ldap_member.copy()
new_member['term'] = new_member['term'][:]
for term in term_list:
# check term syntax
if not re.match('^[wsf][0-9]{4}$', term):
raise InvalidTerm(term)
# add the term to the entry
if not term in ldap_member['term']:
new_member['term'].append(term)
mlist = ldapi.make_modlist(ldap_member, new_member)
ld.modify_s(user_dn, mlist)
def register_nonmember(userid, term_list):
"""Registers a non-member for one or more terms."""
user_dn = 'uid=%s,%s' % (ldapi.escape(userid), cfg['users_base'])
if type(term_list) in (str, unicode):
term_list = [ term_list ]
ldap_member = get(userid)
if not ldap_member:
raise NoSuchMember(userid)
if 'term' not in ldap_member:
ldap_member['term'] = []
if 'nonMemberTerm' not in ldap_member:
ldap_member['nonMemberTerm'] = []
new_member = ldap_member.copy()
new_member['nonMemberTerm'] = new_member['nonMemberTerm'][:]
for term in term_list:
# check term syntax
if not re.match('^[wsf][0-9]{4}$', term):
raise InvalidTerm(term)
# add the term to the entry
if not term in ldap_member['nonMemberTerm'] \
and not term in ldap_member['term']:
new_member['nonMemberTerm'].append(term)
mlist = ldapi.make_modlist(ldap_member, new_member)
ld.modify_s(user_dn, mlist)
def registered(userid, term):
"""
Determines whether a member is registered
for a term.
Parameters:
userid - the member's username
term - the term to check
Returns: whether the member is registered
Example: registered("mspang", "f2006") -> True
"""
member = get(userid)
return 'term' in member and term in member['term']
def group_members(group):
"""
Returns a list of group members
"""
group = ldapi.lookup(ld, 'cn', group, cfg['groups_base'])
if group:
if 'uniqueMember' in group:
r = re.compile('^uid=([^,]*)')
return map(lambda x: r.match(x).group(1), group['uniqueMember'])
elif 'memberUid' in group:
return group['memberUid']
else:
return []
else:
return []
def expired_accounts():
members = ldapi.search(ld, cfg['users_base'],
'(&(objectClass=member)(!(|(term=%s)(nonMemberTerm=%s))))' %
(terms.current(), terms.current()))
return dict([(member[0], member[1]) for member in members])
def send_account_expired_email(name, email):
args = [ cfg['expired_account_email'], name, email ]
os.spawnv(os.P_WAIT, cfg['expired_account_email'], args)

30
ceo/operation_strings.py Normal file
View File

@ -0,0 +1,30 @@
# These descriptions are printed to the console while a transaction
# is performed, in real time.
descriptions = {
'add_user_to_ldap': 'Add user to LDAP',
'add_group_to_ldap': 'Add group to LDAP',
'add_user_to_kerberos': 'Add user to Kerberos',
'create_home_dir': 'Create home directory',
'set_forwarding_addresses': 'Set forwarding addresses',
'send_welcome_message': 'Send welcome message',
'subscribe_to_mailing_list': 'Subscribe to mailing list',
'announce_new_user': 'Announce new user to mailing list',
'replace_login_shell': 'Replace login shell',
'replace_forwarding_addresses': 'Replace forwarding addresses',
'remove_user_from_ldap': 'Remove user from LDAP',
'remove_group_from_ldap': 'Remove group from LDAP',
'remove_user_from_kerberos': 'Remove user from Kerberos',
'delete_home_dir': 'Delete home directory',
'unsubscribe_from_mailing_list': 'Unsubscribe from mailing list',
'add_sudo_role': 'Add sudo role to LDAP',
'add_user_to_group': 'Add user to group',
'add_user_to_auxiliary_groups': 'Add user to auxiliary groups',
'subscribe_user_to_auxiliary_mailing_lists': 'Subscribe user to auxiliary mailing lists',
'remove_user_from_group': 'Remove user from group',
'remove_user_from_auxiliary_groups': 'Remove user from auxiliary groups',
'unsubscribe_user_from_auxiliary_mailing_lists': 'Unsubscribe user from auxiliary mailing lists',
'remove_sudo_role': 'Remove sudo role from LDAP',
'update_positions_ldap': 'Update positions in LDAP',
'update_exec_group_ldap': 'Update executive group in LDAP',
'subscribe_to_mailing_lists': 'Subscribe to mailing lists',
}

22
ceo/term_utils.py Normal file
View File

@ -0,0 +1,22 @@
from typing import List
from .utils import http_get
from ceo_common.model.Term import get_terms_for_renewal
import ceo.cli.utils as cli_utils
import ceo.tui.utils as tui_utils
def get_terms_for_renewal_for_user(
username: str, num_terms: int, clubrep: bool, tui_controller=None,
) -> List[str]:
resp = http_get('/api/members/' + username)
# FIXME: this is ugly, we shouldn't need a hacky if statement like this
if tui_controller is None:
result = cli_utils.handle_sync_response(resp)
else:
result = tui_utils.handle_sync_response(resp, tui_controller)
if clubrep:
return get_terms_for_renewal(result.get('non_member_terms'), num_terms)
else:
return get_terms_for_renewal(result.get('terms'), num_terms)

View File

@ -1,254 +0,0 @@
"""
Terms Routines
This module contains functions for manipulating terms, such as determining
the current term, finding the next or previous term, converting dates to
terms, and more.
"""
import time, datetime, re
# year to count terms from
EPOCH = 1970
# seasons list
SEASONS = [ 'w', 's', 'f' ]
def validate(term):
"""
Determines whether a term is well-formed.
Parameters:
term - the term string
Returns: whether the term is valid (boolean)
Example: validate("f2006") -> True
"""
regex = '^[wsf][0-9]{4}$'
return re.match(regex, term) is not None
def parse(term):
"""Helper function to convert a term string to the number of terms
since the epoch. Such numbers are intended for internal use only."""
if not validate(term):
raise Exception("malformed term: %s" % term)
year = int( term[1:] )
season = SEASONS.index( term[0] )
return (year - EPOCH) * len(SEASONS) + season
def generate(term):
"""Helper function to convert a year and season to a term string."""
year = int(term / len(SEASONS)) + EPOCH
season = term % len(SEASONS)
return "%s%04d" % ( SEASONS[season], year )
def next(term):
"""
Returns the next term. (convenience function)
Parameters:
term - the term string
Retuns: the term string of the following term
Example: next("f2006") -> "w2007"
"""
return add(term, 1)
def previous(term):
"""
Returns the previous term. (convenience function)
Parameters:
term - the term string
Returns: the term string of the preceding term
Example: previous("f2006") -> "s2006"
"""
return add(term, -1)
def add(term, offset):
"""
Calculates a term relative to some base term.
Parameters:
term - the base term
offset - the number of terms since term (may be negative)
Returns: the term that comes offset terms after term
"""
return generate(parse(term) + offset)
def delta(initial, final):
"""
Calculates the distance between two terms.
It should be true that add(a, delta(a, b)) == b.
Parameters:
initial - the base term
final - the term at some offset from the base term
Returns: the offset of final relative to initial
"""
return parse(final) - parse(initial)
def compare(first, second):
"""
Compares two terms. This function is suitable
for use with list.sort().
Parameters:
first - base term for comparison
second - term to compare to
Returns: > 0 (if first > second)
= 0 (if first == second)
< 0 (if first < second)
"""
return delta(second, first)
def interval(base, count):
"""
Returns a list of adjacent terms.
Parameters:
base - the first term in the interval
count - the number of terms to include
Returns: a list of count terms starting with initial
Example: interval('f2006', 3) -> [ 'f2006', 'w2007', 's2007' ]
"""
terms = []
for num in xrange(count):
terms.append( add(base, num) )
return terms
def tstamp(timestamp):
"""Helper to convert seconds since the epoch
to terms since the epoch."""
# let python determine the month and year
date = datetime.date.fromtimestamp(timestamp)
# determine season
if date.month <= 4:
season = SEASONS.index('w')
elif date.month <= 8:
season = SEASONS.index('s')
else:
season = SEASONS.index('f')
return (date.year - EPOCH) * len(SEASONS) + season
def from_timestamp(timestamp):
"""
Converts a number of seconds since
the epoch to a number of terms since
the epoch.
This function notes that:
WINTER = JANUARY to APRIL
SPRING = MAY to AUGUST
FALL = SEPTEMBER to DECEMBER
Parameters:
timestamp - number of seconds since the epoch
Returns: the number of terms since the epoch
Example: from_timestamp(1166135779) -> 'f2006'
"""
return generate( tstamp(timestamp) )
def curr():
"""Helper to determine the current term."""
return tstamp( time.time() )
def current():
"""
Determines the current term.
Returns: current term
Example: current() -> 'f2006'
"""
return generate( curr() )
def next_unregistered(registered):
"""
Find the first future or current unregistered term.
Intended as the 'default' for registrations.
Parameters:
registered - a list of terms a member is registered for
Returns: the next unregistered term
"""
# get current term number
now = curr()
# never registered -> current term is next
if len( registered) < 1:
return generate( now )
# return the first unregistered, or the current term (whichever is greater)
return generate(max([max(map(parse, registered))+1, now]))
### Tests ###
if __name__ == '__main__':
from ceo.test import test, assert_equal, success
test(parse); assert_equal(110, parse('f2006')); success()
test(generate); assert_equal('f2006', generate(110)); success()
test(next); assert_equal('w2007', next('f2006')); success()
test(previous); assert_equal('s2006', previous('f2006')); success()
test(delta); assert_equal(1, delta('f2006', 'w2007')); success()
test(compare); assert_equal(-1, compare('f2006', 'w2007')); success()
test(add); assert_equal('w2010', add('f2006', delta('f2006', 'w2010'))); success()
test(interval); assert_equal(['f2006', 'w2007', 's2007'], interval('f2006', 3)); success()
test(from_timestamp); assert_equal('f2006', from_timestamp(1166135779)); success()
test(current); assert_equal(True, parse( current() ) >= 110 ); success()
test(next_unregistered)
assert_equal( next(current()), next_unregistered([ current() ]))
assert_equal( current(), next_unregistered([]))
assert_equal( current(), next_unregistered([ previous(current()) ]))
assert_equal( current(), next_unregistered([ add(current(), -2) ]))
success()

View File

@ -1,42 +0,0 @@
"""
Common Test Routines
This module contains helpful functions called by each module's test suite.
"""
from types import FunctionType, MethodType, ClassType, TypeType
class TestException(Exception):
"""Exception class for test failures."""
def test(subject):
"""Print a test message."""
if type(subject) in (MethodType, FunctionType, ClassType, TypeType):
print "testing %s()..." % subject.__name__,
else:
print "testing %s..." % subject,
def success():
"""Print a success message."""
print "pass."
def assert_equal(expected, actual):
if expected != actual:
message = "Expected (%s)\nWas (%s)" % (repr(expected), repr(actual))
fail(message)
def fail(message):
print "failed!"
raise TestException("Test failed:\n%s" % message)
def negative(call, args, excep, message):
try:
call(*args)
fail(message)
except excep:
pass

0
ceo/tui/__init__.py Normal file
View File

30
ceo/tui/app.py Normal file
View File

@ -0,0 +1,30 @@
import os
from queue import SimpleQueue
class App:
REL_WIDTH_PCT = 60
REL_HEIGHT_PCT = 70
# On a full-screen (1366x768) gnome-terminal window,
# I had 168 cols and 36 rows
WIDTH = int(0.6 * 168)
HEIGHT = int(0.7 * 36)
def __init__(self, loop, main_widget):
self.loop = loop
self.main_widget = main_widget
self.history = []
self.queued_pipe_callbacks = SimpleQueue()
self.pipefd = loop.watch_pipe(self._pipe_callback)
def run_in_main_loop(self, func):
self.queued_pipe_callbacks.put(func)
os.write(self.pipefd, b'\x00')
def _pipe_callback(self, data):
# We need to clear the whole queue because select()
# will only send one "notification" if there are two
# consecutive writes
while not self.queued_pipe_callbacks.empty():
self.queued_pipe_callbacks.get()()
return True

View File

@ -0,0 +1,36 @@
from .Controller import Controller
from .TransactionController import TransactionController
from ceo.tui.models import TransactionModel
from ceo.tui.views import AddGroupConfirmationView, TransactionView
from ceod.transactions.groups import AddGroupTransaction
class AddGroupController(Controller):
def __init__(self, model, app):
super().__init__(model, app)
def on_next_button_pressed(self, button):
try:
self.model.name = self.get_group_name_from_view()
self.model.description = self.view.description_edit.edit_text
if not self.model.description:
self.view.popup('Description must not be empty')
raise Controller.InvalidInput()
except Controller.InvalidInput:
return
view = AddGroupConfirmationView(self.model, self, self.app)
self.switch_to_view(view)
def on_confirmation_button_pressed(self, button):
body = {
'cn': self.model.name,
'description': self.model.description,
}
model = TransactionModel(
AddGroupTransaction.operations,
'POST', '/api/groups', json=body
)
controller = TransactionController(model, self.app)
view = TransactionView(model, controller, self.app)
controller.view = view
self.switch_to_view(view)

View File

@ -0,0 +1,37 @@
from .Controller import Controller
from ceod.transactions.groups import AddMemberToGroupTransaction
from .TransactionController import TransactionController
from ceo.tui.models import TransactionModel
from ceo.tui.views import AddMemberToGroupConfirmationView, TransactionView
class AddMemberToGroupController(Controller):
def __init__(self, model, app):
super().__init__(model, app)
def on_list_subscribe_checkbox_change(self, checkbox, new_state):
self.model.subscribe_to_lists = new_state
def on_next_button_pressed(self, button):
try:
self.model.name = self.get_group_name_from_view()
self.model.username = self.get_username_from_view()
except Controller.InvalidInput:
return
view = AddMemberToGroupConfirmationView(self.model, self, self.app)
self.switch_to_view(view)
def on_confirmation_button_pressed(self, button):
cn = self.model.name
uid = self.model.username
url = f'/api/groups/{cn}/members/{uid}'
if not self.model.subscribe_to_lists:
url += '?subscribe_to_lists=false'
model = TransactionModel(
AddMemberToGroupTransaction.operations,
'POST', url
)
controller = TransactionController(model, self.app)
view = TransactionView(model, controller, self.app)
controller.view = view
self.switch_to_view(view)

View File

@ -0,0 +1,110 @@
from threading import Thread
from ...utils import http_get
from .Controller import Controller
from .AddUserTransactionController import AddUserTransactionController
from ceo.tui.models import TransactionModel
from ceo.tui.views import AddUserConfirmationView, TransactionView
from ceo_common.model.Term import get_terms_for_new_user
from ceod.transactions.members import AddMemberTransaction
class AddUserController(Controller):
def __init__(self, model, app):
super().__init__(model, app)
self.right_col_idx = 0
self.prev_searched_username = None
def on_confirmation_button_pressed(self, button):
body = {
'uid': self.model.username,
'cn': self.model.full_name,
'given_name': self.model.first_name,
'sn': self.model.last_name,
}
if self.model.program:
body['program'] = self.model.program
if self.model.forwarding_address:
body['forwarding_addresses'] = [self.model.forwarding_address]
new_terms = get_terms_for_new_user(self.model.num_terms)
if self.model.membership_type == 'club_rep':
body['non_member_terms'] = new_terms
else:
body['terms'] = new_terms
model = TransactionModel(
AddMemberTransaction.operations,
'POST', '/api/members',
json=body
)
controller = AddUserTransactionController(model, self.app)
view = TransactionView(model, controller, self.app)
controller.view = view
self.switch_to_view(view)
def on_next_button_pressed(self, button):
try:
username = self.get_username_from_view()
num_terms = self.get_num_terms_from_view()
except Controller.InvalidInput:
return
full_name = self.view.full_name_edit.edit_text
# TODO: share validation logic between CLI and TUI
if not full_name:
self.view.popup('Full name must not be empty')
return
self.model.username = username
self.model.full_name = full_name
self.model.first_name = self.view.first_name_edit.edit_text
self.model.last_name = self.view.last_name_edit.edit_text
self.model.program = self.view.program_edit.edit_text
self.model.forwarding_address = self.view.forwarding_address_edit.edit_text
self.model.num_terms = num_terms
view = AddUserConfirmationView(self.model, self, self.app)
self.switch_to_view(view)
def on_membership_type_changed(self, radio_button, new_state, selected_type):
if new_state:
self.model.membership_type = selected_type
def on_row_focus_changed(self):
_, idx = self.view.listwalker.get_focus()
old_idx = self.right_col_idx
self.right_col_idx = idx
# The username field is the third row, so when
# idx changes from 2 to 3, this means the user
# moved from the username field to the next field
if old_idx == 2 and idx == 3:
Thread(
target=self._lookup_user,
args=(self.view.username_edit.edit_text,)
).start()
def _set_flash_text(self, *args):
self.view.flash_text.set_text('Looking up user...')
def _clear_flash_text(self):
self.view.flash_text.set_text('')
def _on_lookup_user_success(self):
self._clear_flash_text()
self.view.update_fields()
def _lookup_user(self, username):
if not username:
return
if username == self.prev_searched_username:
return
self.prev_searched_username = username
self.app.run_in_main_loop(self._set_flash_text)
resp = http_get('/api/uwldap/' + username)
if not resp.ok:
self.app.run_in_main_loop(self._clear_flash_text)
return
data = resp.json()
self.model.full_name = data.get('cn', '')
self.model.first_name = data.get('given_name', '')
self.model.last_name = data.get('sn', '')
self.model.program = data.get('program', '')
self.model.forwarding_address = (data.get('mail_local_addresses') or [''])[0]
self.app.run_in_main_loop(self._on_lookup_user_success)

View File

@ -0,0 +1,38 @@
from typing import Dict, List
from ...utils import get_failed_operations
from .TransactionController import TransactionController
class AddUserTransactionController(TransactionController):
def __init__(self, model, app):
super().__init__(model, app)
def handle_completed(self):
# We don't want to write to the message_text yet, but
# we still need to enable the Next button.
self.app.run_in_main_loop(self.view.enable_next_button)
def write_extra_txn_info(self, data: List[Dict]):
if data[-1]['status'] != 'completed':
return
result = data[-1]['result']
failed_operations = get_failed_operations(data)
lines = []
if failed_operations:
lines.append('Transaction successfully completed with some errors.')
else:
lines.append('Transaction successfully completed.')
lines.append('')
lines.append('User password is: ' + result['password'])
if 'send_welcome_message' in failed_operations:
lines.extend([
'',
'Since the welcome message was not sent, '
'you need to email this password to the user.'
])
def target():
self._show_lines(lines)
self.app.run_in_main_loop(target)

View File

@ -0,0 +1,79 @@
from threading import Thread
from ...utils import http_get
from .Controller import Controller
from .TransactionController import TransactionController
from ceo.tui.models import TransactionModel
from ceo.tui.views import ChangeLoginShellConfirmationView, TransactionView
class ChangeLoginShellController(Controller):
def __init__(self, model, app):
super().__init__(model, app)
self.right_col_idx = 0
self.prev_searched_username = None
def on_next_button_pressed(self, button):
try:
self.model.username = self.get_username_from_view()
self.model.login_shell = self.view.login_shell_edit.edit_text
if not self.model.login_shell:
self.view.popup('Login shell must not be empty')
raise Controller.InvalidInput()
except Controller.InvalidInput:
return
view = ChangeLoginShellConfirmationView(self.model, self, self.app)
self.switch_to_view(view)
def on_confirmation_button_pressed(self, button):
body = {'login_shell': self.model.login_shell}
model = TransactionModel(
['replace_login_shell'],
'PATCH', f'/api/members/{self.model.username}',
json=body
)
controller = TransactionController(model, self.app)
view = TransactionView(model, controller, self.app)
controller.view = view
self.switch_to_view(view)
# TODO: reduce code duplication with AddUserController
def on_row_focus_changed(self):
_, idx = self.view.listwalker.get_focus()
old_idx = self.right_col_idx
self.right_col_idx = idx
# The username field is the first row, so when
# idx changes from 0 to 1, this means the user
# moved from the username field to the next field
if old_idx == 0 and idx == 1:
Thread(
target=self._lookup_user,
args=(self.view.username_edit.edit_text,)
).start()
def _set_flash_text(self, *args):
self.view.flash_text.set_text('Looking up user...')
def _clear_flash_text(self):
self.view.flash_text.set_text('')
def _on_lookup_user_success(self):
self._clear_flash_text()
self.view.update_fields()
def _lookup_user(self, username):
if not username:
return
if username == self.prev_searched_username:
return
self.prev_searched_username = username
self.app.run_in_main_loop(self._set_flash_text)
resp = http_get('/api/members/' + username)
if not resp.ok:
self.app.run_in_main_loop(self._clear_flash_text)
return
data = resp.json()
self.model.login_shell = data.get('login_shell', '')
self.app.run_in_main_loop(self._on_lookup_user_success)

View File

@ -0,0 +1,80 @@
from abc import ABC
import ceo.tui.utils as utils
from ceo_common.utils import validate_username
# NOTE: one controller can control multiple views,
# but each view must have exactly one controller
class Controller(ABC):
class InvalidInput(Exception):
pass
class RequestFailed(Exception):
pass
def __init__(self, model, app):
super().__init__()
self.model = model
self.app = app
# Since the view and the controller both have a reference to each
# other, this needs to be initialized in a separate step
self.view = None
def _push_history(self, old_view, new_view):
if new_view.model.name == 'Welcome':
self.app.history.clear()
else:
self.app.history.append(old_view)
def switch_to_view(self, new_view):
self._push_history(self.view, new_view)
self.view = new_view
new_view.activate()
def go_to_next_menu(self, next_menu_name):
_, new_view, _ = utils.get_mvc(self.app, next_menu_name)
self._push_history(self.view, new_view)
new_view.activate()
def prev_menu_callback(self, button):
prev_view = self.app.history.pop()
prev_view.controller.view = prev_view
prev_view.activate()
def next_menu_callback(self, button, next_menu_name):
self.go_to_next_menu(next_menu_name)
def get_next_menu_callback(self, next_menu_name):
def callback(button):
self.next_menu_callback(button, next_menu_name)
return callback
def get_username_from_view(self):
username = self.view.username_edit.edit_text
# TODO: share validation logic between CLI and TUI
verification_res = validate_username(username)
if not verification_res.is_valid:
self.view.popup(verification_res.error_message)
raise Controller.InvalidInput()
return username
def get_group_name_from_view(self):
name = self.view.name_edit.edit_text
# TODO: share validation logic between CLI and TUI
if not name:
self.view.popup('Name must not be empty')
raise Controller.InvalidInput()
return name
def get_num_terms_from_view(self):
num_terms_str = self.view.num_terms_edit.edit_text
if num_terms_str:
num_terms = int(num_terms_str)
else:
num_terms = 0
# TODO: share validation logic between CLI and TUI
if num_terms <= 0:
self.view.popup('Number of terms must be a positive integer')
raise Controller.InvalidInput()
return num_terms

View File

@ -0,0 +1,50 @@
import os
from zope import component
from ...utils import http_get, http_post, write_db_creds
from .SyncRequestController import SyncRequestController
import ceo.krb_check as krb
from ceo.tui.views import CreateDatabaseConfirmationView, CreateDatabaseResponseView
from ceo_common.interfaces import IConfig
class CreateDatabaseController(SyncRequestController):
def __init__(self, model, app):
super().__init__(model, app)
def on_db_type_changed(self, radio_button, new_state, selected_type):
if new_state:
self.model.db_type = selected_type
def on_next_button_pressed(self, button):
view = CreateDatabaseConfirmationView(self.model, self, self.app)
self.switch_to_view(view)
def get_resp(self):
db_type = self.model.db_type
username = krb.get_username()
resp = http_get(f'/api/members/{username}')
if not resp.ok:
return resp
self.model.user_dict = resp.json()
return http_post(f'/api/db/{db_type}/{username}')
def get_response_view(self):
return CreateDatabaseResponseView(self.model, self, self.app)
def write_db_creds_to_file(self):
password = self.model.resp_json['password']
db_type = self.model.db_type
cfg = component.getUtility(IConfig)
db_host = cfg.get(f'{db_type}_host')
homedir = self.model.user_dict['home_directory']
filename = os.path.join(homedir, f"ceo-{db_type}-info")
wrote_to_file = write_db_creds(
filename, self.model.user_dict, password, db_type, db_host
)
self.model.password = password
self.model.db_host = db_host
self.model.filename = filename
self.model.wrote_to_file = wrote_to_file

View File

@ -0,0 +1,22 @@
from ...utils import http_get
from .Controller import Controller
from .SyncRequestController import SyncRequestController
from ceo.tui.views import GetGroupResponseView
class GetGroupController(SyncRequestController):
def __init__(self, model, app):
super().__init__(model, app)
def get_resp(self):
return http_get(f'/api/groups/{self.model.name}')
def get_response_view(self):
return GetGroupResponseView(self.model, self, self.app)
def on_next_button_pressed(self, button):
try:
self.model.name = self.get_group_name_from_view()
except Controller.InvalidInput:
return
self.on_confirmation_button_pressed(button)

View File

@ -0,0 +1,29 @@
from threading import Thread
from ...utils import http_get
from .Controller import Controller
import ceo.tui.utils as tui_utils
class GetPositionsController(Controller):
def __init__(self, model, app):
super().__init__(model, app)
def lookup_positions_async(self):
self.view.flash_text.set_text('Looking up positions...')
Thread(target=self.lookup_positions_sync).start()
def lookup_positions_sync(self):
resp = http_get('/api/positions')
try:
positions = tui_utils.handle_sync_response(resp, self)
except Controller.RequestFailed:
return
for pos, usernames in positions.items():
self.model.positions[pos] = ','.join(usernames)
def target():
self.view.flash_text.set_text('')
self.view.update_fields()
self.app.run_in_main_loop(target)

View File

@ -0,0 +1,22 @@
from ...utils import http_get
from .Controller import Controller
from .SyncRequestController import SyncRequestController
from ceo.tui.views import GetUserResponseView
class GetUserController(SyncRequestController):
def __init__(self, model, app):
super().__init__(model, app)
def get_resp(self):
return http_get(f'/api/members/{self.model.username}')
def get_response_view(self):
return GetUserResponseView(self.model, self, self.app)
def on_next_button_pressed(self, button):
try:
self.model.username = self.get_username_from_view()
except Controller.InvalidInput:
return
self.on_confirmation_button_pressed(button)

View File

@ -0,0 +1,37 @@
from .Controller import Controller
from ceod.transactions.groups import RemoveMemberFromGroupTransaction
from .TransactionController import TransactionController
from ceo.tui.models import TransactionModel
from ceo.tui.views import RemoveMemberFromGroupConfirmationView, TransactionView
class RemoveMemberFromGroupController(Controller):
def __init__(self, model, app):
super().__init__(model, app)
def on_list_unsubscribe_checkbox_change(self, checkbox, new_state):
self.model.unsubscribe_from_lists = new_state
def on_next_button_pressed(self, button):
try:
self.model.name = self.get_group_name_from_view()
self.model.username = self.get_username_from_view()
except Controller.InvalidInput:
return
view = RemoveMemberFromGroupConfirmationView(self.model, self, self.app)
self.switch_to_view(view)
def on_confirmation_button_pressed(self, button):
cn = self.model.name
uid = self.model.username
url = f'/api/groups/{cn}/members/{uid}'
if not self.model.unsubscribe_from_lists:
url += '?unsubscribe_from_lists=false'
model = TransactionModel(
RemoveMemberFromGroupTransaction.operations,
'DELETE', url
)
controller = TransactionController(model, self.app)
view = TransactionView(model, controller, self.app)
controller.view = view
self.switch_to_view(view)

View File

@ -0,0 +1,54 @@
from threading import Thread
from ...utils import http_post
from .Controller import Controller
from .SyncRequestController import SyncRequestController
import ceo.term_utils as term_utils
from ceo.tui.views import RenewUserConfirmationView
class RenewUserController(SyncRequestController):
def __init__(self, model, app):
super().__init__(model, app)
def on_membership_type_changed(self, radio_button, new_state, selected_type):
if new_state:
self.model.membership_type = selected_type
def on_next_button_pressed(self, button):
try:
username = self.get_username_from_view()
num_terms = self.get_num_terms_from_view()
except Controller.InvalidInput:
return
self.model.username = username
self.model.num_terms = num_terms
self.view.flash_text.set_text('Looking up user...')
Thread(target=self._get_next_terms).start()
def _get_next_terms(self):
try:
self.model.new_terms = term_utils.get_terms_for_renewal_for_user(
self.model.username,
self.model.num_terms,
self.model.membership_type == 'club_rep',
self
)
except Controller.RequestFailed:
return
def target():
self.view.flash_text.set_text('')
view = RenewUserConfirmationView(self.model, self, self.app)
self.switch_to_view(view)
self.app.run_in_main_loop(target)
def get_resp(self):
uid = self.model.username
body = {'uid': uid}
if self.model.membership_type == 'club_rep':
body['non_member_terms'] = self.model.new_terms
else:
body['terms'] = self.model.new_terms
return http_post(f'/api/members/{uid}/renew', json=body)

View File

@ -0,0 +1,49 @@
import os
from zope import component
from ...utils import http_get, http_post, write_db_creds
from .SyncRequestController import SyncRequestController
import ceo.krb_check as krb
from ceo.tui.views import ResetDatabasePasswordConfirmationView, ResetDatabasePasswordResponseView
from ceo_common.interfaces import IConfig
class ResetDatabasePasswordController(SyncRequestController):
def __init__(self, model, app):
super().__init__(model, app)
def on_db_type_changed(self, radio_button, new_state, selected_type):
if new_state:
self.model.db_type = selected_type
def on_next_button_pressed(self, button):
view = ResetDatabasePasswordConfirmationView(self.model, self, self.app)
self.switch_to_view(view)
def get_resp(self):
db_type = self.model.db_type
username = krb.get_username()
resp = http_get(f'/api/members/{username}')
if not resp.ok:
return resp
self.model.user_dict = resp.json()
return http_post(f'/api/db/{db_type}/{username}/pwreset')
def get_response_view(self):
return ResetDatabasePasswordResponseView(self.model, self, self.app)
def write_db_creds_to_file(self):
password = self.model.resp_json['password']
db_type = self.model.db_type
cfg = component.getUtility(IConfig)
db_host = cfg.get(f'{db_type}_host')
homedir = self.model.user_dict['home_directory']
filename = os.path.join(homedir, f"ceo-{db_type}-info")
wrote_to_file = write_db_creds(
filename, self.model.user_dict, password, db_type, db_host
)
self.model.password = password
self.model.filename = filename
self.model.wrote_to_file = wrote_to_file

View File

@ -0,0 +1,27 @@
from ...utils import http_post
from .Controller import Controller
from .SyncRequestController import SyncRequestController
import ceo.krb_check as krb
from ceo.tui.views import ResetPasswordUsePasswdView, ResetPasswordConfirmationView, ResetPasswordResponseView
class ResetPasswordController(SyncRequestController):
def __init__(self, model, app):
super().__init__(model, app)
def get_resp(self):
return http_post(f'/api/members/{self.model.username}/pwreset')
def get_response_view(self):
return ResetPasswordResponseView(self.model, self, self.app)
def on_next_button_pressed(self, button):
try:
self.model.username = self.get_username_from_view()
except Controller.InvalidInput:
return
if self.model.username == krb.get_username():
view = ResetPasswordUsePasswdView(self.model, self, self.app)
else:
view = ResetPasswordConfirmationView(self.model, self, self.app)
self.switch_to_view(view)

View File

@ -0,0 +1,40 @@
from ceo.utils import http_get
from .Controller import Controller
from .SyncRequestController import SyncRequestController
from ceo.tui.views import SearchGroupResponseView, GetGroupResponseView
# this is a little bit bad because it relies on zero coupling between
# the GetGroupResponseView and the GetGroupController
# coupling is also introduced between this controller and the
# SearchGroupResponseView as it requires this class's callback
class SearchGroupController(SyncRequestController):
def __init__(self, model, app):
super().__init__(model, app)
def get_resp(self):
if self.model.want_info:
return http_get(f'/api/groups/{self.model.name}')
else:
return http_get(f'/api/groups/search/{self.model.name}/{self.model.count}')
def get_response_view(self):
if self.model.want_info:
return GetGroupResponseView(self.model, self, self.app)
else:
return SearchGroupResponseView(self.model, self, self.app)
def group_info_callback(self, button, cn):
self.model.name = cn
self.model.want_info = True
self.request_in_progress = False
self.on_next_button_pressed(button)
def on_next_button_pressed(self, button):
try:
if not self.model.want_info:
self.model.name = self.get_username_from_view()
self.model.count = 10
except Controller.InvalidInput:
return
self.on_confirmation_button_pressed(button)

View File

@ -0,0 +1,47 @@
from threading import Thread
from ...utils import http_get
from .Controller import Controller
from .TransactionController import TransactionController
from ceo.tui.models import TransactionModel
import ceo.tui.utils as tui_utils
from ceo.tui.views import TransactionView
from ceod.transactions.members import UpdateMemberPositionsTransaction
class SetPositionsController(Controller):
def __init__(self, model, app):
super().__init__(model, app)
def on_next_button_pressed(self, button):
body = {}
for pos, field in self.view.position_fields.items():
if field.edit_text != '':
body[pos] = field.edit_text.replace(' ', '').split(',')
model = TransactionModel(
UpdateMemberPositionsTransaction.operations,
'POST', '/api/positions', json=body
)
controller = TransactionController(model, self.app)
view = TransactionView(model, controller, self.app)
controller.view = view
self.switch_to_view(view)
def lookup_positions_async(self):
self.view.flash_text.set_text('Looking up positions...')
Thread(target=self.lookup_positions_sync).start()
def lookup_positions_sync(self):
resp = http_get('/api/positions')
try:
positions = tui_utils.handle_sync_response(resp, self)
except Controller.RequestFailed:
return
for pos, usernames in positions.items():
self.model.positions[pos] = ','.join(usernames)
def target():
self.view.flash_text.set_text('')
self.view.update_fields()
self.app.run_in_main_loop(target)

View File

@ -0,0 +1,39 @@
from threading import Thread
from .Controller import Controller
import ceo.tui.utils as tui_utils
from ceo.tui.views import SyncResponseView
class SyncRequestController(Controller):
def __init__(self, model, app):
super().__init__(model, app)
self.request_in_progress = False
def get_resp(self):
# To be implemented by child classes
raise NotImplementedError()
def get_response_view(self):
return SyncResponseView(self.model, self, self.app)
def on_confirmation_button_pressed(self, button):
if self.request_in_progress:
return
self.request_in_progress = True
self.view.flash_text.set_text('Sending request...')
def main_loop_target():
self.view.flash_text.set_text('')
view = self.get_response_view()
self.switch_to_view(view)
def thread_target():
resp = self.get_resp()
try:
self.model.resp_json = tui_utils.handle_sync_response(resp, self)
except Controller.RequestFailed:
return
self.app.run_in_main_loop(main_loop_target)
Thread(target=thread_target).start()

View File

@ -0,0 +1,110 @@
from threading import Thread
from typing import Dict, List
from ...StreamResponseHandler import StreamResponseHandler
from ...utils import http_request, generic_handle_stream_response
from .Controller import Controller
class TransactionController(Controller, StreamResponseHandler):
def __init__(self, model, app):
super().__init__(model, app)
self.op_idx = 0
self.error_messages = []
def start(self):
Thread(target=self._start_txn).start()
def _start_txn(self):
resp = http_request(
self.model.http_verb,
self.model.req_path,
**self.model.req_kwargs
)
data = generic_handle_stream_response(resp, self.model.operations, self)
self.write_extra_txn_info(data)
# to be overridden in child classes if desired
def write_extra_txn_info(self, data: List[Dict]):
pass
def _show_lines(self, lines):
num_lines = len(lines)
# Since the message_text is at the bottom of the window,
# we want to add sufficient padding to the bottom of the text
lines += [''] * max(4 - num_lines, 0)
for i, line in enumerate(lines):
if type(line) is str:
lines[i] = line + '\n'
else: # tuple (attr, text)
lines[i] = (line[0], line[1] + '\n')
self.view.message_text.set_text(lines)
def _abort(self):
for elem in self.view.right_col_elems[self.op_idx:]:
elem.set_text(('red', 'ABORTED'))
self.view.enable_next_button()
def begin(self):
pass
def handle_non_200(self, resp):
def target():
self._abort()
lines = ['An error occurred:']
if resp.headers.get('content-type') == 'application/json':
err_msg = resp.json()['error']
else:
err_msg = resp.text
lines.extend(err_msg.split('\n'))
self._show_lines(lines)
self.app.run_in_main_loop(target)
def handle_aborted(self, err_msg):
def target():
self._abort()
lines = [
'The transaction was rolled back.',
'The error was:',
'',
*err_msg.split('\n'),
]
self._show_lines(lines)
self.app.run_in_main_loop(target)
def handle_completed(self):
def target():
lines = ['Transaction successfully completed.']
if len(self.error_messages) > 0:
lines.append('There were some errors:')
for msg in self.error_messages:
lines.extend(msg.split('\n'))
self._show_lines(lines)
self.view.enable_next_button()
self.app.run_in_main_loop(target)
def handle_successful_operation(self):
def target():
self.view.right_col_elems[self.op_idx].set_text(('green', 'Done'))
self.op_idx += 1
self.app.run_in_main_loop(target)
def handle_failed_operation(self, err_msg):
def target():
self.view.right_col_elems[self.op_idx].set_text(('red', 'Failed'))
self.op_idx += 1
if err_msg is not None:
self.error_messages.append(err_msg)
self.app.run_in_main_loop(target)
def handle_skipped_operation(self):
def target():
self.view.right_col_elems[self.op_idx].set_text('Skipped')
self.op_idx += 1
self.app.run_in_main_loop(target)
def handle_unrecognized_operation(self, operation):
def target():
self.error_messages.append('Unrecognized operation: ' + operation)
self.op_idx += 1
self.app.run_in_main_loop(target)

View File

@ -0,0 +1,6 @@
from .Controller import Controller
class WelcomeController(Controller):
def __init__(self, model, app):
super().__init__(model, app)

View File

@ -0,0 +1,19 @@
from .Controller import Controller
from .WelcomeController import WelcomeController
from .AddUserController import AddUserController
from .AddUserTransactionController import AddUserTransactionController
from .RenewUserController import RenewUserController
from .GetUserController import GetUserController
from .ResetPasswordController import ResetPasswordController
from .ChangeLoginShellController import ChangeLoginShellController
from .AddGroupController import AddGroupController
from .GetGroupController import GetGroupController
from .SearchGroupController import SearchGroupController
from .AddMemberToGroupController import AddMemberToGroupController
from .RemoveMemberFromGroupController import RemoveMemberFromGroupController
from .CreateDatabaseController import CreateDatabaseController
from .ResetDatabasePasswordController import ResetDatabasePasswordController
from .GetPositionsController import GetPositionsController
from .SetPositionsController import SetPositionsController
from .TransactionController import TransactionController
from .SyncRequestController import SyncRequestController

View File

@ -0,0 +1,7 @@
class AddGroupModel:
name = 'AddGroup'
title = 'Add group'
def __init__(self):
self.name = ''
self.description = ''

View File

@ -0,0 +1,8 @@
class AddMemberToGroupModel:
name = 'AddMemberToGroup'
title = 'Add member to group'
def __init__(self):
self.name = ''
self.username = ''
self.subscribe_to_lists = True

View File

@ -0,0 +1,13 @@
class AddUserModel:
name = 'AddUser'
title = 'Add user'
def __init__(self):
self.membership_type = 'general_member'
self.username = ''
self.full_name = ''
self.first_name = ''
self.last_name = ''
self.program = ''
self.forwarding_address = ''
self.num_terms = 1

View File

@ -0,0 +1,8 @@
class ChangeLoginShellModel:
name = 'ChangeLoginShell'
title = 'Change login shell'
def __init__(self):
self.username = ''
self.login_shell = ''
self.resp_json = None

View File

@ -0,0 +1,12 @@
class CreateDatabaseModel:
name = 'CreateDatabase'
title = 'Create database'
def __init__(self):
self.db_type = 'mysql'
self.user_dict = None
self.resp_json = None
self.password = None
self.db_host = None
self.filename = None
self.wrote_to_file = False

View File

@ -0,0 +1,7 @@
class GetGroupModel:
name = 'GetGroup'
title = 'Get group members'
def __init__(self):
self.name = ''
self.resp_json = None

View File

@ -0,0 +1,4 @@
class GetPositionsModel:
name = 'GetPositions'
title = 'Get positions'
positions = {}

View File

@ -0,0 +1,7 @@
class GetUserModel:
name = 'GetUser'
title = 'Get user info'
def __init__(self):
self.username = ''
self.resp_json = None

View File

@ -0,0 +1,8 @@
class RemoveMemberFromGroupModel:
name = 'RemoveMemberFromGroup'
title = 'Remove member from group'
def __init__(self):
self.name = ''
self.username = ''
self.unsubscribe_from_lists = True

View File

@ -0,0 +1,10 @@
class RenewUserModel:
name = 'RenewUser'
title = 'Renew user'
def __init__(self):
self.membership_type = 'general_member'
self.username = ''
self.num_terms = 1
self.new_terms = None
self.resp_json = None

View File

@ -0,0 +1,11 @@
class ResetDatabasePasswordModel:
name = 'ResetDatabasePassword'
title = 'Reset database password'
def __init__(self):
self.db_type = 'mysql'
self.user_dict = None
self.resp_json = None
self.password = None
self.filename = None
self.wrote_to_file = False

View File

@ -0,0 +1,7 @@
class ResetPasswordModel:
name = 'ResetPassword'
title = 'Reset password'
def __init__(self):
self.username = ''
self.resp_json = None

View File

@ -0,0 +1,9 @@
class SearchGroupModel:
name = 'SearchGroup'
title = 'Search groups'
def __init__(self):
self.name = ''
self.resp_json = None
self.count = 10
self.want_info = False

View File

@ -0,0 +1,4 @@
class SetPositionsModel:
name = 'SetPositions'
title = 'Set positions'
positions = {}

Some files were not shown because too many files have changed in this diff Show More