├── libs ├── __init__.py ├── google │ ├── __init__.py │ ├── controller.py │ ├── oauth.py │ ├── drive.py │ ├── gmail.py │ └── calendar.py ├── gunicorn_wrapper.py ├── http_controller.py ├── helper_functions.py ├── ldap_client.py ├── BUILD ├── duo │ └── __init__.py └── pagerduty │ └── __init__.py ├── 3rdparty └── python │ ├── BUILD │ └── requirements.txt ├── pants.ini ├── tests ├── libs │ ├── google │ │ ├── mock_results │ │ │ ├── asp_delete │ │ │ ├── imap_resource_malformed │ │ │ ├── suspend │ │ │ ├── imap_resource │ │ │ ├── un_suspend │ │ │ ├── group_member_delete │ │ │ ├── pop_resource_malformed │ │ │ ├── forwarding_resource_malformed │ │ │ ├── pop_resource │ │ │ ├── drive_list_resource_malformed │ │ │ ├── get_user_name │ │ │ ├── event_rule_resource │ │ │ ├── forwarding_resource │ │ │ ├── drive_transfer_file_owner_pass │ │ │ ├── file_info_resource │ │ │ ├── file_info_resource_malformed │ │ │ ├── move_calendar_resource │ │ │ ├── calendar_id_resource_malformed │ │ │ ├── calendar_id_resource │ │ │ ├── group_member_list_fail │ │ │ ├── org_unit_change_fail │ │ │ ├── delete_user_resource_fail │ │ │ ├── group_member_delete_fail │ │ │ ├── ooo_resource_malformed │ │ │ ├── ooo_resource │ │ │ ├── drive_transfer_file_owner_fail │ │ │ ├── tokens_resource_malformed │ │ │ ├── tokens_resource │ │ │ ├── asps_resource_malformed │ │ │ ├── user_resource_malformed │ │ │ ├── org_unit_change │ │ │ ├── user_resource │ │ │ ├── decline_event_resource │ │ │ ├── group_member_list │ │ │ ├── cancel_recurrence_resource │ │ │ ├── asps_resource │ │ │ ├── move_event_resource │ │ │ ├── drive_list_resource │ │ │ ├── verification_resource_malformed │ │ │ ├── verification_resource │ │ │ └── admin.directory.user │ │ ├── controller_tests.py │ │ ├── BUILD │ │ ├── drive_tests.py │ │ ├── calendar_tests.py │ │ ├── gmail_tests.py │ │ └── admin_tests.py │ ├── duo │ │ ├── mock_results │ │ │ ├── get_user_resource_fail │ │ │ ├── list_users_resource_fail │ │ │ ├── delete_user_resource_fail │ │ │ ├── get_user_resource │ │ │ └── list_users_resource │ │ ├── BUILD │ │ └── duo_tests.py │ └── pagerduty │ │ ├── mock_results │ │ └── delete_user_error │ │ ├── BUILD │ │ └── pagerduty_tests.py └── BUILD ├── static ├── favicon.png ├── bower.json ├── js │ └── gatekeeper.js └── css │ └── typeaheadjs.css ├── .github └── ISSUE_TEMPLATE │ ├── custom.md │ ├── feature_request.md │ └── bug_report.md ├── .travis.yml ├── sse.py ├── BUILD ├── config ├── gatekeeper.example.aurora └── config.example.yml ├── Dockerfile ├── templates ├── navbar.html ├── lost_asset.html ├── gdrive.html └── index.html ├── .gitignore ├── forms.py ├── CODE_OF_CONDUCT.md ├── pants ├── runner.py ├── LICENSE └── README.md /libs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libs/google/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /3rdparty/python/BUILD: -------------------------------------------------------------------------------- 1 | python_requirements() 2 | -------------------------------------------------------------------------------- /pants.ini: -------------------------------------------------------------------------------- 1 | [GLOBAL] 2 | pants_version: 1.6.0 3 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/asp_delete: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /tests/libs/duo/mock_results/get_user_resource_fail: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /tests/libs/duo/mock_results/list_users_resource_fail: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/imap_resource_malformed: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/suspend: -------------------------------------------------------------------------------- 1 | { 2 | "suspended": true 3 | } 4 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/imap_resource: -------------------------------------------------------------------------------- 1 | { 2 | "enabled": false 3 | } 4 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/un_suspend: -------------------------------------------------------------------------------- 1 | { 2 | "suspended": false 3 | } 4 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/group_member_delete: -------------------------------------------------------------------------------- 1 | { 2 | "members": "" 3 | } 4 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twitter/gatekeeper-service/HEAD/static/favicon.png -------------------------------------------------------------------------------- /tests/libs/google/mock_results/pop_resource_malformed: -------------------------------------------------------------------------------- 1 | { 2 | "disposition": "leaveInInbox" 3 | } 4 | -------------------------------------------------------------------------------- /tests/libs/duo/mock_results/delete_user_resource_fail: -------------------------------------------------------------------------------- 1 | "RuntimeError: Received 404 Resource not found" 2 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/forwarding_resource_malformed: -------------------------------------------------------------------------------- 1 | { 2 | "forwardingEmail": "hkantas@testdomain.com", 3 | } 4 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/pop_resource: -------------------------------------------------------------------------------- 1 | { 2 | "accessWindow": "disabled", 3 | "disposition": "leaveInInbox" 4 | } 5 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/drive_list_resource_malformed: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "drive#fileList", 3 | "files": [ 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/get_user_name: -------------------------------------------------------------------------------- 1 | {"fullName": "Harry Kantas", "givenName": "Harry", "familyName": "Kantas"} 2 | 3 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/event_rule_resource: -------------------------------------------------------------------------------- 1 | { 2 | "recurrence": [ 3 | "RRULE:FREQ=WEEKLY;BYDAY=MO" 4 | ] 5 | } 6 | 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | 5 | --- 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/forwarding_resource: -------------------------------------------------------------------------------- 1 | { 2 | "forwardingEmail": "hkantas@testdomain.com", 3 | "verificationStatus": "accepted" 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | install: 5 | - pip install -r 3rdparty/python/requirements.txt 6 | script: 7 | - ./pants clean-all test tests: 8 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/drive_transfer_file_owner_pass: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "drive#permission", 3 | "id": "01193435495075157756", 4 | "type": "user", 5 | "role": "owner" 6 | } 7 | 8 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/file_info_resource: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "kind": "drive#file", 4 | "id": "0B-NxYpteIrCmZ2lIU3lheHA1dkE", 5 | "name": "ldap_cache.txt", 6 | "mimeType": "text/plain" 7 | } 8 | -------------------------------------------------------------------------------- /tests/libs/duo/BUILD: -------------------------------------------------------------------------------- 1 | python_tests( 2 | name = "duo_tests", 3 | sources = ["duo_tests.py"], 4 | dependencies = [ 5 | "3rdparty/python:mock", 6 | "libs:duo", 7 | ], 8 | ) 9 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/file_info_resource_malformed: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "kind": "drive#file", 4 | "id": "atphfbhejenb58gf4tervfg7qk", 5 | "name": "ldap_cache.txt", 6 | "mimeType": "text/plain" 7 | } 8 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/move_calendar_resource: -------------------------------------------------------------------------------- 1 | {"scope": {"type": "user", "value": "mat@testdomain.com"}, "kind": "calendar#aclRule", "etag": "00001491549959651000", "role": "owner", "id": "user:mat@testdomain.com"} 2 | -------------------------------------------------------------------------------- /tests/libs/pagerduty/mock_results/delete_user_error: -------------------------------------------------------------------------------- 1 | {"error": {"conflicts": [{"url": "/schedules/PCC3WPY", "type": "schedule", "id": "PCC3WPY", "name": "Gtest_Primary"}], "errors": ["The user cannot be deleted as they are currently in use"]}} -------------------------------------------------------------------------------- /tests/libs/google/mock_results/calendar_id_resource_malformed: -------------------------------------------------------------------------------- 1 | 2 | { 3 | 4 | "kind": "calendar#calendar", 5 | "etag": "\"ybKbeHGjj0OTLZKfoaWdkTlGIPo/nILKwlXhy-cZi2ZW4TLd7Thmoyw\"", 6 | "summary": "hkantas@testdomain.com", 7 | "timeZone": "America/Los_Angeles" 8 | } 9 | -------------------------------------------------------------------------------- /tests/libs/pagerduty/BUILD: -------------------------------------------------------------------------------- 1 | python_tests( 2 | name = "pagerduty_tests", 3 | sources = ["pagerduty_tests.py"], 4 | dependencies = [ 5 | "3rdparty/python:mock", 6 | "3rdparty/python:pycrypto", 7 | "libs:pagerduty", 8 | ], 9 | ) 10 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/calendar_id_resource: -------------------------------------------------------------------------------- 1 | 2 | { 3 | 4 | "kind": "calendar#calendar", 5 | "etag": "\"ybKbeHGjj0OTLZKfoaWdkTlGIPo/nILKwlXhy-cZi2ZW4TLd7Thmoyw\"", 6 | "id": "hkantas@testdomain.com", 7 | "summary": "hkantas@testdomain.com", 8 | "timeZone": "America/Los_Angeles" 9 | } 10 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/group_member_list_fail: -------------------------------------------------------------------------------- 1 | { 2 | "error": { 3 | "errors": [ 4 | { 5 | "domain": "global", 6 | "reason": "notFound", 7 | "message": "Resource Not Found: groupKey" 8 | } 9 | ], 10 | "code": 404, 11 | "message": "Resource Not Found: groupKey" 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/org_unit_change_fail: -------------------------------------------------------------------------------- 1 | { 2 | "error": { 3 | "errors": [ 4 | { 5 | "domain": "global", 6 | "reason": "invalid", 7 | "message": "Invalid Input: INVALID_OU_ID" 8 | } 9 | ], 10 | "code": 400, 11 | "message": "Invalid Input: INVALID_OU_ID" 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/delete_user_resource_fail: -------------------------------------------------------------------------------- 1 | { 2 | "error": { 3 | "errors": [ 4 | { 5 | "domain": "global", 6 | "reason": "notFound", 7 | "message": "Resource Not Found: userKey" 8 | } 9 | ], 10 | "code": 404, 11 | "message": "Resource Not Found: userKey" 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/group_member_delete_fail: -------------------------------------------------------------------------------- 1 | { 2 | "error": { 3 | "errors": [ 4 | { 5 | "domain": "global", 6 | "reason": "notFound", 7 | "message": "Resource Not Found: memberKey" 8 | } 9 | ], 10 | "code": 404, 11 | "message": "Resource Not Found: memberKey" 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/ooo_resource_malformed: -------------------------------------------------------------------------------- 1 | { 2 | "responseSubject": "OOO message", 3 | "responseBodyHtml": ""I am no longer here. "\n "Please resend your message to blah@testdomain.com. Thank You."", 4 | "restrictToDomain": false 5 | } 6 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/ooo_resource: -------------------------------------------------------------------------------- 1 | { 2 | "enableAutoReply": true, 3 | "responseSubject": "OOO message", 4 | "responseBodyHtml": ""I am no longer here. "\n "Please resend your message to blah@testdomain.com. Thank You."", 5 | "restrictToDomain": false 6 | } 7 | -------------------------------------------------------------------------------- /static/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "static", 3 | "authors": [ 4 | "Harry Kantas " 5 | ], 6 | "description": "", 7 | "main": "", 8 | "license": "MIT", 9 | "homepage": "", 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components", 14 | "test", 15 | "tests" 16 | ], 17 | "dependencies": { 18 | "bootstrap": "^3.3.7", 19 | "typeahead.js": "^0.11.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/drive_transfer_file_owner_fail: -------------------------------------------------------------------------------- 1 | { 2 | "error": { 3 | "errors": [ 4 | { 5 | "domain": "global", 6 | "reason": "invalidSharingRequest", 7 | "message": "Bad Request. User message: \"You do not have permission to share these item(s): Carrier Targeting PMB/FAQ\"" 8 | } 9 | ], 10 | "code": 400, 11 | "message": "Bad Request. User message: \"You do not have permission to share these item(s): Carrier Targeting PMB/FAQ\"" 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /sse.py: -------------------------------------------------------------------------------- 1 | class ServerSentEvent(object): 2 | 3 | def __init__(self, data): 4 | self.data = data 5 | self.event = None 6 | self.id = None 7 | self.desc_map = { 8 | self.data: "data", 9 | self.event: "event", 10 | self.id: "id" 11 | } 12 | 13 | def encode(self): 14 | if not self.data: 15 | return "" 16 | lines = ["%s: %s" % (v, k) 17 | for k, v in iter(self.desc_map.items()) if k] 18 | 19 | return "%s\n\n" % "\n".join(lines) 20 | -------------------------------------------------------------------------------- /tests/BUILD: -------------------------------------------------------------------------------- 1 | target( 2 | name = "tests", 3 | dependencies = [ 4 | ":google_api", 5 | ":pagerduty_api", 6 | ":duo_api", 7 | ], 8 | ) 9 | 10 | target( 11 | name = "google_api", 12 | dependencies = [ 13 | "tests/libs/google:google_api_tests", 14 | ], 15 | ) 16 | 17 | target( 18 | name = "pagerduty_api", 19 | dependencies = [ 20 | "tests/libs/pagerduty:pagerduty_tests", 21 | ], 22 | ) 23 | 24 | target( 25 | name = "duo_api", 26 | dependencies = [ 27 | "tests/libs/duo:duo_tests", 28 | ], 29 | ) 30 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/tokens_resource_malformed: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "admin#directory#tokenList", 3 | "etag": "\"lC5l2xZhzgy1zYIWApDice-fN_A/yGP-wrO0gkoc-jgMteM8YIuvawU\"", 4 | "items": [ 5 | { 6 | "kind": "admin#directory#token", 7 | "etag": "\"lC5l2xZhzgy1zYIWApDice-fN_A/djLeYUTWZzjSV1hJ3iVUlFGjK6w\"", 8 | "displayText": "OpenTable", 9 | "anonymous": false, 10 | "nativeApp": false, 11 | "userKey": "117737896912012196849", 12 | "scopes": [ 13 | "https://www.googleapis.com/auth/userinfo.profile", 14 | "https://www.googleapis.com/auth/userinfo.email" 15 | ] 16 | } 17 | ] 18 | } 19 | 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/tokens_resource: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "admin#directory#tokenList", 3 | "etag": "\"lC5l2xZhzgy1zYIWApDice-fN_A/yGP-wrO0gkoc-jgMteM8YIuvawU\"", 4 | "items": [ 5 | { 6 | "kind": "admin#directory#token", 7 | "etag": "\"lC5l2xZhzgy1zYIWApDice-fN_A/djLeYUTWZzjSV1hJ3iVUlFGjK6w\"", 8 | "clientId": "1011230515163-arsl01gv134b0sjidu62bkp3hub3nuj3.apps.googleusercontent.com", 9 | "displayText": "OpenTable", 10 | "anonymous": false, 11 | "nativeApp": false, 12 | "userKey": "117737896912012196849", 13 | "scopes": [ 14 | "https://www.googleapis.com/auth/userinfo.profile", 15 | "https://www.googleapis.com/auth/userinfo.email" 16 | ] 17 | } 18 | ] 19 | } 20 | 21 | -------------------------------------------------------------------------------- /tests/libs/duo/mock_results/get_user_resource: -------------------------------------------------------------------------------- 1 | [{"status": "active", "username": "mat", "desktoptokens": [], "user_id": "DUVCWCBAENYFZ0R3LO32", "realname": "Mat Clinton", "firstname": null, "created": 1527195029, "alias2": null, "alias3": null, "notes": "", "alias1": null, "alias4": null, "tokens": [], "last_login": null, "last_directory_sync": null, "groups": [], "lastname": null, "phones": [{"name": "", "extension": "", "sms_passcodes_sent": false, "phone_id": "DPA96OGS9LRWTVWUCONU", "activated": false, "number": "+14159903977", "capabilities": ["auto", "sms", "phone"], "platform": "Generic Smartphone", "predelay": "0", "postdelay": "0", "type": "Mobile", "last_seen": ""}], "u2ftokens": [], "email": "mat@somewhere.com"}] 2 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/asps_resource_malformed: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "admin#directory#aspList", 3 | "etag": "\"mVtRK5AeRZHHotPwF4vAn-nsx7I/dwh0vO4G7O5dNYOl9GFtPbPSerc\"", 4 | "items": [ 5 | { 6 | "kind": "admin#directory#asp", 7 | "etag": "\"mVtRK5AeRZHHotPwF4vAn-nsx7I/VHV-MUiTDEVQh9cpoNoYWqbubJw\"", 8 | "name": "Mail on my iPhone", 9 | "creationTime": "1468452010432", 10 | "lastTimeUsed": "1468452095000", 11 | "userKey": "105930792424411065789" 12 | }, 13 | { 14 | "kind": "admin#directory#asp", 15 | "etag": "\"mVtRK5AeRZHHotPwF4vAn-nsx7I/CwoSbD9TIJcjNChah7oo5uLuWtY\"", 16 | "name": "Calendar on my BlackBerry", 17 | "creationTime": "1477721946980", 18 | "lastTimeUsed": "0", 19 | "userKey": "105930792424411065789" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information, where applicable):** 24 | - OS: [e.g. Linux/Ubuntu 16.04] 25 | - Browser [e.g. chrome, safari] 26 | - Python Version [e.g. 2.7.15] 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/user_resource_malformed: -------------------------------------------------------------------------------- 1 | { 2 | "agreedToTerms": true, 3 | "kind": "admin#directory#user", 4 | "includeInGlobalAddressList": true, 5 | "name": { 6 | "fullName": "Harry Kantas", 7 | "givenName": "Harry", 8 | "familyName": "Kantas" 9 | }, 10 | "ipWhitelisted": false, 11 | "creationTime": "2016-01-14T17:23:51.000Z", 12 | "primaryEmail": "stillings@testdomain.com", 13 | "emails": [ 14 | { 15 | "primary": true, 16 | "address": "stillings@testdomain.com" 17 | }, 18 | { 19 | "address": "still@testdomain.com" 20 | } 21 | ], 22 | "changePasswordAtNextLogin": false, 23 | "isDelegatedAdmin": false, 24 | "isMailboxSetup": true, 25 | "isAdmin": false, 26 | "id": "117737896912012196849", 27 | "orgUnitPath": "/", 28 | "aliases": [ 29 | "harry@testdomain.com" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /3rdparty/python/requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2017.11.5 2 | chardet==3.0.4 3 | click==6.7 4 | duo_client==3.2.1 5 | Flask==1.0.0 6 | Flask-WTF==0.14.2 7 | gevent==1.3.4 8 | google-api-python-client==1.7.3 9 | gunicorn==19.8.1 10 | httplib2==0.11.3 11 | idna==2.6 12 | itsdangerous==0.24 13 | Jinja2==2.10 14 | MarkupSafe==1.0 15 | mock==2.0.0 16 | oauth2client==4.1.2 17 | pyasn1==0.4.2 18 | pyasn1-modules==0.2.1 19 | pycrypto==2.6.1 20 | python-ldap==3.1.0 21 | PyYAML==3.12 22 | requests==2.19.1 23 | rsa==3.4.2 24 | six==1.11.0 25 | SocksiPy-branch==1.1 26 | twitter.common.app==0.3.9 27 | twitter.common.collections==0.3.9 28 | twitter.common.contextutil==0.3.9 29 | twitter.common.dirutil==0.3.9 30 | twitter.common.lang==0.3.9 31 | twitter.common.log==0.3.9 32 | twitter.common.options==0.3.9 33 | twitter.common.process==0.3.9 34 | twitter.common.string==0.3.9 35 | twitter.common.util==0.3.9 36 | uritemplate==3.0.0 37 | urllib3==1.23 38 | Werkzeug==0.13 39 | WTForms==2.1 40 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/org_unit_change: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "admin#directory#user", 3 | "id": "117737896912012196849", 4 | "etag": "\"YDi4yMhknQ5-kwx7j9TvuW4RFwg/q3-Nk83Kj2fBXz-XFLVYh1MRfTs\"", 5 | "primaryEmail": "stillings@testdomain.com", 6 | "name": { 7 | "givenName": "Alex", 8 | "familyName": "Stillings", 9 | "fullName": "Alex Stillings" 10 | }, 11 | "isAdmin": false, 12 | "isDelegatedAdmin": false, 13 | "lastLoginTime": "2017-03-14T04:00:32.000Z", 14 | "creationTime": "2016-01-14T17:23:51.000Z", 15 | "agreedToTerms": true, 16 | "suspended": true, 17 | "suspensionReason": "ADMIN", 18 | "changePasswordAtNextLogin": false, 19 | "ipWhitelisted": false, 20 | "emails": [ 21 | { 22 | "address": "stillings@testdomain.com", 23 | "primary": true 24 | } 25 | ], 26 | "aliases": [ 27 | "still@testdomain.com" 28 | ], 29 | "customerId": "C00sl2ia7", 30 | "isMailboxSetup": true, 31 | "includeInGlobalAddressList": true 32 | } 33 | 34 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/user_resource: -------------------------------------------------------------------------------- 1 | { 2 | "agreedToTerms": true, 3 | "kind": "admin#directory#user", 4 | "includeInGlobalAddressList": true, 5 | "name": { 6 | "fullName": "Harry Kantas", 7 | "givenName": "Harry", 8 | "familyName": "Kantas" 9 | }, 10 | "ipWhitelisted": false, 11 | "creationTime": "2016-01-14T17:23:51.000Z", 12 | "primaryEmail": "stillings@testdomain.com", 13 | "emails": [ 14 | { 15 | "primary": true, 16 | "address": "stillings@testdomain.com" 17 | }, 18 | { 19 | "address": "still@testdomain.com" 20 | } 21 | ], 22 | "changePasswordAtNextLogin": false, 23 | "isDelegatedAdmin": false, 24 | "isMailboxSetup": true, 25 | "isAdmin": false, 26 | "etag": "\"mVtRK5AeRZHHotPwF4vAn-nsx7I/pb1G2-6hROJmPwil_Nvozfe9BSY\"", 27 | "suspended": false, 28 | "lastLoginTime": "2016-02-04T17:44:00.000Z", 29 | "customerId": "C00sl2ia7", 30 | "id": "117737896912012196849", 31 | "orgUnitPath": "/", 32 | "aliases": [ 33 | "harry@testdomain.com" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/decline_event_resource: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "calendar#event", 3 | "etag": "\"2979949537698000\"", 4 | "id": "fuccsmq6ipqmpeeb93e6bdvtc0", 5 | "status": "confirmed", 6 | "htmlLink": "https://www.google.com/calendar/event?eid=ZnVjY3NtcTZpcHFtcGVlYjkzZTZiZHZ0YzAgc3RpbGxpbmdzQGd0ZXN0LnR3aXR02XIuY29t", 7 | "created": "2017-03-20T01:46:17.000Z", 8 | "updated": "2017-03-20T01:52:48.849Z", 9 | "summary": "Harry-Mat", 10 | "creator": { 11 | "email": "mat@testdomain.com" 12 | }, 13 | "organizer": { 14 | "email": "hkantas@testdomain.com", 15 | "self": true 16 | }, 17 | "start": { 18 | "dateTime": "2017-03-19T19:00:00-07:00" 19 | }, 20 | "end": { 21 | "dateTime": "2017-03-19T20:00:00-07:00" 22 | }, 23 | "iCalUID": "fuccsmq6ip1mpeeb93e6bdvtc0@google.com", 24 | "sequence": 0, 25 | "attendees": [ 26 | { 27 | "email": "hkantas@testdomain.com", 28 | "responseStatus": "declined" 29 | } 30 | ], 31 | "hangoutLink": "https://meet.google.com/ccp-pjw1-ejb", 32 | "reminders": { 33 | "useDefault": true 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /tests/libs/pagerduty/pagerduty_tests.py: -------------------------------------------------------------------------------- 1 | import json 2 | from unittest import TestCase 3 | import os 4 | 5 | from pagerduty import PagerDutyApi 6 | 7 | from mock import patch, MagicMock 8 | 9 | 10 | CALL_PAGERDUTY_API = "pagerduty.HttpController.api_request" 11 | 12 | 13 | class PagerDutyApiTests(TestCase): 14 | 15 | def read_resource_file(self, res_file): 16 | mock_results_dir = "tests/libs/pagerduty/mock_results" 17 | try: 18 | mock_resource_file = os.path.join(mock_results_dir, res_file) 19 | with open(mock_resource_file, "r") as fp: 20 | self.mock_resource = json.loads(fp.read()) 21 | fp.close() 22 | except IOError: 23 | self.mock_resource = None 24 | 25 | return self.mock_resource 26 | 27 | @patch(CALL_PAGERDUTY_API) 28 | def test_delete_user_pass(self, mock_pagerduty_api): 29 | mock_pagerduty_api.return_value = "" 30 | pagerduty_admin = PagerDutyApi(use_proxy=False, config=MagicMock()) 31 | delete_user = pagerduty_admin.delete_user(user_id="PP7WT8E") 32 | assert delete_user == "" 33 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/group_member_list: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "admin#directory#members", 3 | "etag": "\"YDi4yMhknQ5-kwx7j9TvuW4RFwg/r22IFII7uSdOxgt1in6COQLMa78\"", 4 | "members": [ 5 | { 6 | "kind": "admin#directory#member", 7 | "etag": "\"YDi4yMhknQ5-kwx7j9TvuW4RFwg/eLkIMJdbA2BRlH6suxQBw9OtNL8\"", 8 | "id": "106864481816593370080", 9 | "email": "hkantas@testdomain.com", 10 | "role": "MEMBER", 11 | "type": "USER", 12 | "status": "ACTIVE" 13 | }, 14 | { 15 | "kind": "admin#directory#member", 16 | "etag": "\"YDi4yMhknQ5-kwx7j9TvuW4RFwg/cx58LbP6UImWFfl8oihP8Qauyic\"", 17 | "id": "114274569748888155325", 18 | "email": "phewson@testdomain.com", 19 | "role": "MEMBER", 20 | "type": "USER", 21 | "status": "ACTIVE" 22 | }, 23 | { 24 | "kind": "admin#directory#member", 25 | "etag": "\"YDi4yMhknQ5-kwx7j9TvuW4RFwg/lwyMzUgBvjdh7bMZvaxkUWaVyqo\"", 26 | "id": "109786339485749300084", 27 | "email": "larry@testdomain.com", 28 | "role": "MEMBER", 29 | "type": "USER", 30 | "status": "ACTIVE" 31 | } 32 | ] 33 | } 34 | 35 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/cancel_recurrence_resource: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "calendar#event", 3 | "etag": "\"2983717123522000\"", 4 | "id": "e2i9ln9qjl3eo9ejgb3484bh78", 5 | "status": "confirmed", 6 | "htmlLink": "https://www.google.com/calendar/event?eid=ZTJpOWxuOXFqbDNlbzllamdiMzQ4NGJoNzhfMjAxNzA0MDNUMjMwMDAwWiBzdGlsbGlu23NAZ3Rlc3QudHdpdHRlci5jb20", 7 | "created": "2017-04-10T20:40:11.000Z", 8 | "updated": "2017-04-10T21:09:21.761Z", 9 | "summary": "Harry - Some Event", 10 | "creator": { 11 | "email": "mclinton@testdomain.com" 12 | }, 13 | "organizer": { 14 | "email": "hkantas@testdomain.com", 15 | "self": true 16 | }, 17 | "start": { 18 | "dateTime": "2017-04-03T16:00:00-07:00", 19 | "timeZone": "America/Los_Angeles" 20 | }, 21 | "end": { 22 | "dateTime": "2017-04-03T17:00:00-07:00", 23 | "timeZone": "America/Los_Angeles" 24 | }, 25 | "recurrence": [ 26 | "RRULE:FREQ=WEEKLY;UNTIL=20170410T210921Z;BYDAY=MO" 27 | ], 28 | "iCalUID": "e2i9ln9qjl3eo9ejgb3484bh78@google.com", 29 | "sequence": 4, 30 | "reminders": { 31 | "useDefault": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /BUILD: -------------------------------------------------------------------------------- 1 | python_binary( 2 | name = "gatekeeper", 3 | source = "gatekeeper.py", 4 | zip_safe = False, 5 | dependencies = [ 6 | ":gatekeeper_dependencies", 7 | ], 8 | ) 9 | 10 | python_library( 11 | name = "gatekeeper_dependencies", 12 | sources = globs("*.py", "templates/*"), 13 | dependencies = [ 14 | "3rdparty/python:twitter.common.app", 15 | "3rdparty/python:Flask", 16 | "3rdparty/python:Flask-WTF", 17 | "3rdparty/python:MarkupSafe", 18 | "3rdparty/python:WTForms", 19 | "3rdparty/python:gevent", 20 | "libs:gunicorn_wrapper", 21 | ":runner_dependencies", 22 | ], 23 | ) 24 | 25 | python_binary( 26 | name = "runner", 27 | source = "runner.py", 28 | dependencies = [ 29 | ":runner_dependencies", 30 | ], 31 | ) 32 | 33 | python_library( 34 | name = "runner_dependencies", 35 | sources = globs("runner.py"), 36 | dependencies = [ 37 | "3rdparty/python:twitter.common.log", 38 | "libs:helper_functions", 39 | "libs:google_api", 40 | "libs:ldap_client", 41 | "libs:pagerduty", 42 | "libs:duo" 43 | ], 44 | ) 45 | 46 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/asps_resource: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "admin#directory#aspList", 3 | "etag": "\"mVtRK5AeRZHHotPwF4vAn-nsx7I/dwh0vO4G7O5dNYOl9GFtPbPSerc\"", 4 | "items": [ 5 | { 6 | "kind": "admin#directory#asp", 7 | "etag": "\"mVtRK5AeRZHHotPwF4vAn-nsx7I/VHV-MUiTDEVQh9cpoNoYWqbubJw\"", 8 | "codeId": 0, 9 | "name": "Mail on my iPhone", 10 | "creationTime": "1468452010432", 11 | "lastTimeUsed": "1468452095000", 12 | "userKey": "105930792424411065789" 13 | }, 14 | { 15 | "kind": "admin#directory#asp", 16 | "etag": "\"mVtRK5AeRZHHotPwF4vAn-nsx7I/CwoSbD9TIJcjNChah7oo5uLuWtY\"", 17 | "codeId": 2, 18 | "name": "Calendar on my BlackBerry", 19 | "creationTime": "1477721946980", 20 | "lastTimeUsed": "0", 21 | "userKey": "105930792424411065789" 22 | }, 23 | { 24 | "kind": "admin#directory#asp", 25 | "etag": "\"mVtRK5AeRZHHotPwF4vAn-nsx7I/gVrQd4WfAEFTg5SXemOOoEQMWqE\"", 26 | "codeId": 3, 27 | "name": "YouTube on my iPhone", 28 | "creationTime": "1477935410253", 29 | "lastTimeUsed": "0", 30 | "userKey": "105930792424411065789" 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /config/gatekeeper.example.aurora: -------------------------------------------------------------------------------- 1 | run_service = Process( 2 | name = 'run_service', 3 | cmdline = ''' 4 | chmod +x ./gatekeeper.pex && 5 | unzip ./gatekeeper.pex && 6 | unzip -o ./gatekeeper-res.zip && 7 | ./gatekeeper.pex --port {{thermos.ports[http]}} --env {{environment}} 8 | ''') 9 | 10 | stage_src = Packer.copy('gatekeeper') 11 | stage_res = Packer.copy('gatekeeper-res') 12 | 13 | task = SequentialTask( 14 | name = 'gatekeeper', 15 | processes = [stage_src, stage_res, run_service], 16 | resources = Resources(cpu = 2, ram = 8 * GB, disk = 1 * GB)) 17 | 18 | job = Service( 19 | name = 'gatekeeper', 20 | task = task, 21 | role = 'gatekeepers', 22 | contact = 'CHANGE_ME', 23 | announce = Announcer(), 24 | constraints = job_constraints) 25 | 26 | jobs = [ 27 | job(cluster = 'dc1', environment = 'devel', instances = 1), 28 | job(cluster = 'dc2a', environment = 'test', instances = 1), 29 | job(cluster = 'dc2b', environment = 'test', instances = 1), 30 | job(cluster = 'dc3a', environment = 'prod', instances = 1), 31 | job(cluster = 'dc3b', environment = 'prod', instances = 1), 32 | ] 33 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/move_event_resource: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "calendar#event", 3 | "etag": "\"2979918629470000\"", 4 | "id": "ltk5efr9set0qaogfnpapgvcec", 5 | "status": "confirmed", 6 | "htmlLink": "https://www.google.com/calendar/event?eid=bHRrNWVmcjlzZXQwcWFvZ2ZucGFwZ3ZjZWMgbWF0QGd0ZXN0LnR3aXR0ZXIuY29t", 7 | "created": "2016-11-17T17:56:43.000Z", 8 | "updated": "2017-03-19T21:35:14.735Z", 9 | "summary": "Harry Test", 10 | "creator": { 11 | "email": "mat@testdomain.com", 12 | "self": true 13 | }, 14 | "organizer": { 15 | "email": "stillings@testdomain.com" 16 | }, 17 | "start": { 18 | "dateTime": "2016-11-17T17:30:00-08:00" 19 | }, 20 | "end": { 21 | "dateTime": "2016-11-17T18:30:00-08:00" 22 | }, 23 | "iCalUID": "ltk5efr9set0qaogfnpapgvcec@google.com", 24 | "sequence": 0, 25 | "attendees": [ 26 | { 27 | "email": "mat@testdomain.com", 28 | "self": true, 29 | "responseStatus": "accepted" 30 | }, 31 | { 32 | "email": "hkantas@testdomain.com", 33 | "responseStatus": "needsAction" 34 | } 35 | ], 36 | "hangoutLink": "https://plus.google.com/hangouts/_/testdomain.com/mat?hceid=bWF0QGd0ZXN0LnR3aXR0ZXIuY29t.ltk5efr9set0qaogfnpapgvcec", 37 | "reminders": { 38 | "useDefault": true 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:9-stretch as base 2 | 3 | # Install packages needed on linux 4 | RUN apt-get update && apt-get install -y \ 5 | apt-utils \ 6 | python2.7 \ 7 | python-dev \ 8 | python-pip \ 9 | build-essential \ 10 | libldap2-dev \ 11 | libssl-dev \ 12 | libsasl2-dev \ 13 | openjdk-8-jdk 14 | 15 | # Set the working directory to /app 16 | WORKDIR /app 17 | 18 | # Copy the current directory contents into the container at /app 19 | ADD . /app 20 | 21 | # Install packages from requirements.txt 22 | RUN pip install --upgrade pip 23 | RUN pip install -r 3rdparty/python/requirements.txt 24 | 25 | # Install bower 26 | RUN npm i -g bower 27 | 28 | # Change directory to install bower packages 29 | RUN cd static && bower install --allow-root 30 | 31 | # Make port 5000 available outside the container 32 | EXPOSE 5000 33 | 34 | # Create the pex file 35 | RUN ./pants clean-all binary :gatekeeper 36 | 37 | # Import python and remove base container 38 | FROM python:2.7.14 39 | 40 | # Set the working directory to /app 41 | WORKDIR /app 42 | 43 | # Copy project files 44 | ADD . /app 45 | 46 | # Copy pex from base build 47 | COPY --from=base /app/dist /app/dist 48 | COPY --from=base /app/static /app/static 49 | 50 | # Run gatekeeper.pex when container starts 51 | CMD ["python", "dist/gatekeeper.pex"] 52 | -------------------------------------------------------------------------------- /templates/navbar.html: -------------------------------------------------------------------------------- 1 | {% block navbar %} 2 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /libs/gunicorn_wrapper.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | 3 | from gunicorn.six import iteritems 4 | import gunicorn.app.base 5 | 6 | 7 | class StandaloneApplication(gunicorn.app.base.BaseApplication): 8 | def __init__(self, wsgi_app, port, debug=False): 9 | self.port = port 10 | self.debug = debug 11 | self.wsgi_app = wsgi_app 12 | if self.debug: 13 | self.log_level = "debug" 14 | else: 15 | self.log_level = "info" 16 | self.num_workers = (multiprocessing.cpu_count() * 2) + 1 17 | 18 | self.options = { 19 | "bind": "%s:%s" % ("0.0.0.0", self.port), 20 | "worker_class": "gevent", 21 | "workers": self.num_workers, 22 | "worker_connections": 100, 23 | "timeout": 60, 24 | "keepalive": 10, 25 | "loglevel": self.log_level, 26 | "spew": self.debug, 27 | "accesslog": "-", 28 | "errorlog": "-", 29 | "access_log_format": '%(t)s %(h)s %(l)s %(u)s "%(r)s" %(s)s %(b)s %(L)s "%(f)s" "%(a)s"' 30 | } 31 | super(StandaloneApplication, self).__init__() 32 | 33 | def load_config(self): 34 | config = dict([(key, value) for (key, value) in iteritems(self.options) 35 | if key in self.cfg.settings and value is not None]) 36 | for key, value in iteritems(config): 37 | self.cfg.set(key.lower(), value) 38 | 39 | def load(self): 40 | return self.wsgi_app 41 | -------------------------------------------------------------------------------- /tests/libs/google/controller_tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | from unittest import TestCase 5 | 6 | from mock import MagicMock, patch 7 | from google.controller import GoogleApiController 8 | 9 | CALL_GOOGLE_API = "google.controller.GoogleApiController.call_google_api" 10 | 11 | 12 | class SomeGoogleApiController(GoogleApiController): 13 | def __init__(self, oauth): 14 | self.oauth = oauth 15 | self.service = MagicMock() 16 | 17 | 18 | class GoogleApiControllerTests(TestCase): 19 | 20 | def setUp(self): 21 | mock_results_dir = "tests/libs/google/mock_results" 22 | mock_resource_file = os.path.join(mock_results_dir, "user_resource") 23 | try: 24 | with open(mock_resource_file, "r") as mock_resource_fp: 25 | self.mock_resource = json.loads(mock_resource_fp.read()) 26 | mock_resource_fp.close() 27 | except IOError: 28 | self.mock_resource = None 29 | 30 | @patch(CALL_GOOGLE_API) 31 | def test_is_suspended_pass(self, mock_google_api): 32 | mock_google_api.return_value = json.dumps(self.mock_resource["suspended"]) 33 | google_controller = SomeGoogleApiController(oauth=MagicMock()) 34 | is_suspended = google_controller.call_google_api(service=None, 35 | api_resource="users", 36 | api_method="get", 37 | response_field="suspended") 38 | assert json.loads(is_suspended) is False 39 | -------------------------------------------------------------------------------- /tests/libs/google/BUILD: -------------------------------------------------------------------------------- 1 | target( 2 | name = "google_api_tests", 3 | dependencies = [ 4 | ":google_admin_tests", 5 | ":google_calendar_tests", 6 | ":google_controller_tests", 7 | ":google_drive_tests", 8 | ":google_gmail_tests", 9 | ":google_oauth_tests", 10 | ], 11 | ) 12 | 13 | python_tests( 14 | name = "google_oauth_tests", 15 | sources = ["oauth_tests.py"], 16 | dependencies = [ 17 | "3rdparty/python:mock", 18 | "libs:google_api_oauth", 19 | ], 20 | ) 21 | 22 | python_tests( 23 | name = "google_controller_tests", 24 | sources = ["controller_tests.py"], 25 | dependencies = [ 26 | "3rdparty/python:mock", 27 | "libs:google_api_controller", 28 | ], 29 | ) 30 | 31 | python_tests( 32 | name = "google_admin_tests", 33 | sources = ["admin_tests.py"], 34 | dependencies = [ 35 | "3rdparty/python:mock", 36 | "3rdparty/python:pycrypto", 37 | "libs:google_api_admin", 38 | ], 39 | ) 40 | 41 | python_tests( 42 | name = "google_gmail_tests", 43 | sources = ["gmail_tests.py"], 44 | dependencies = [ 45 | "3rdparty/python:mock", 46 | "libs:google_api_gmail", 47 | ], 48 | ) 49 | 50 | python_tests( 51 | name = "google_drive_tests", 52 | sources = ["drive_tests.py"], 53 | dependencies = [ 54 | "3rdparty/python:mock", 55 | "libs:google_api_drive", 56 | ], 57 | ) 58 | 59 | python_tests( 60 | name = "google_calendar_tests", 61 | sources = ["calendar_tests.py"], 62 | dependencies = [ 63 | "3rdparty/python:mock", 64 | "libs:google_api_calendar", 65 | ], 66 | ) 67 | -------------------------------------------------------------------------------- /libs/google/controller.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from apiclient import discovery 4 | from oauth2client.client import HttpAccessTokenRefreshError 5 | 6 | 7 | class GoogleApiController(object): 8 | def __init__(self, oauth): 9 | self.oauth = oauth 10 | 11 | def _get_service(self, google_service, google_api): 12 | """ 13 | Builds a connector to the admin service via the discovery api. 14 | :return: 15 | service, the service connector. 16 | """ 17 | try: 18 | service = discovery.build(google_service, google_api, http=self.oauth) 19 | except (discovery.HttpError, HttpAccessTokenRefreshError) as e: 20 | print("Error building a service connector. %s" % e) 21 | service = None 22 | return service 23 | 24 | def call_google_api(self, service, api_resource, api_method, response_field, **kwargs): 25 | """ 26 | Performs a call to Google's API Services. 27 | :args: 28 | api_resource, the Google Resource object to create. 29 | api_method, the method to perform. 30 | :return: 31 | resource, the resulting resource json object field. 32 | """ 33 | try: 34 | api_resources = api_resource.split(".") 35 | google_resource = getattr(service, api_resources[0])() 36 | for res in api_resources[1:]: 37 | google_resource = getattr(google_resource, res)() 38 | resource_method = getattr(google_resource, api_method)(**kwargs) 39 | results = resource_method.execute() 40 | if response_field is not None: 41 | results = results.get(response_field) 42 | except discovery.HttpError: 43 | print("Error talking to Google API") 44 | results = None 45 | return json.dumps(results) 46 | -------------------------------------------------------------------------------- /libs/http_controller.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | class HttpController(object): 5 | def __init__(self, base_url, use_proxy=False, proxy_config=None, 6 | max_retries=3, timeout_secs=10): 7 | self.base_url = base_url 8 | self.use_proxy = use_proxy 9 | self.max_retries = max_retries 10 | self.timeout_secs = timeout_secs 11 | if self.use_proxy is True: 12 | self.proxy_config = proxy_config 13 | self.proxy_info = self._get_proxy_info() 14 | else: 15 | self.proxy_info = None 16 | 17 | def _get_proxy_info(self): 18 | """ 19 | Retrieve Proxy Config settings 20 | :return: proxy_info dict 21 | """ 22 | proxy_info = { 23 | "http": "http://{proxy_user}:{proxy_pass}@{proxy_url}:{proxy_port}" 24 | .format(**self.proxy_config), 25 | "https": "https://{proxy_user}:{proxy_pass}@{proxy_url}:{proxy_port}" 26 | .format(**self.proxy_config) 27 | } 28 | return proxy_info 29 | 30 | def api_request(self, method, endpoint, response_type="json", **kwargs): 31 | """ 32 | Generic REST API Controller. 33 | """ 34 | r = None 35 | session = requests.Session() 36 | adapter = requests.adapters.HTTPAdapter(max_retries=self.max_retries) 37 | api_endpoint = self.base_url + endpoint 38 | session.mount(api_endpoint, adapter) 39 | try: 40 | r = getattr(session, method)(url=api_endpoint, 41 | timeout=self.timeout_secs, 42 | proxies=self.proxy_info, **kwargs) 43 | except (requests.exceptions.ConnectionError, requests.exceptions.RequestException, 44 | AttributeError, TypeError) as e: 45 | print(e) 46 | if response_type == "json": 47 | return r.json() 48 | elif response_type == "text": 49 | return r.text 50 | elif response_type == "xml": 51 | return r.content 52 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/drive_list_resource: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "drive#fileList", 3 | "files": [ 4 | { 5 | "kind": "drive#file", 6 | "id": "0B3uO8LigBWypeVhiQUd1NWNHeUU", 7 | "name": "My New Text Document", 8 | "mimeType": "text/plain" 9 | }, 10 | { 11 | "kind": "drive#file", 12 | "id": "0B3uO8LigBWypYkxxLUROdndaaTg", 13 | "name": "10942320_10204431173593155_1030026430750114785_o.jpg", 14 | "mimeType": "image/jpeg" 15 | }, 16 | { 17 | "kind": "drive#file", 18 | "id": "1Phu55dLmOrDx7UdyUjxQMc4NkkNd4fzaMhwdG-hvCpY", 19 | "name": "2 Factor Report #1", 20 | "mimeType": "application/vnd.google-apps.spreadsheet" 21 | }, 22 | { 23 | "kind": "drive#file", 24 | "id": "1YlL-qU3EHOY9DEvny5F7JVlbVvSqmeJFychDbhpEP10", 25 | "name": "TEST", 26 | "mimeType": "application/vnd.google-apps.document" 27 | }, 28 | { 29 | "kind": "drive#file", 30 | "id": "0B93oaIhNbiBcc3RhcnRlcl9maWxlX2Rhc2hlclYw", 31 | "name": "Getting started", 32 | "mimeType": "application/pdf" 33 | }, 34 | { 35 | "kind": "drive#file", 36 | "id": "1dsA2m1m-OjX59mmNq2j8ZZHYFQBEg93-6j_VenWULN8", 37 | "name": "testdomain.com - Admin Activity Report", 38 | "mimeType": "application/vnd.google-apps.spreadsheet" 39 | }, 40 | { 41 | "kind": "drive#file", 42 | "id": "1l5RdM-taBp7_Czp7zC0prSdAFaUPRW-DVvVB7JmobzQ", 43 | "name": "testdomain.com - Admin Activity Report", 44 | "mimeType": "application/vnd.google-apps.spreadsheet" 45 | }, 46 | { 47 | "kind": "drive#file", 48 | "id": "0B-NxYpteIrCmc3RhcnRlcl9maWxlX2Rhc2hlclYw", 49 | "name": "Getting started", 50 | "mimeType": "application/pdf" 51 | }, 52 | { 53 | "kind": "drive#file", 54 | "id": "0B-NxYpteIrCmZ2lIU3lheHA1dkE", 55 | "name": "ldap_cache.txt", 56 | "mimeType": "text/plain" 57 | }, 58 | { 59 | "kind": "drive#file", 60 | "id": "0B3uO8LigBWypc3RhcnRlcl9maWxlX2Rhc2hlclYw", 61 | "name": "Getting started", 62 | "mimeType": "application/pdf" 63 | } 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /libs/helper_functions.py: -------------------------------------------------------------------------------- 1 | import string 2 | import hashlib 3 | from datetime import datetime, timedelta 4 | 5 | import yaml 6 | from Crypto.Random import random 7 | 8 | APP_CONFIG = 'config/config.yml' 9 | 10 | 11 | class HelperFunctions(object): 12 | @classmethod 13 | def read_config_from_yaml(cls, filename=APP_CONFIG): 14 | config = None 15 | with open(filename, "r") as stream: 16 | try: 17 | config = yaml.load(stream) 18 | except yaml.YAMLError as e: 19 | print(e) 20 | return config 21 | 22 | @classmethod 23 | def password_gen(cls): 24 | chars = string.letters + string.digits 25 | pwd_length = 50 26 | return ''.join((random.choice(chars)) for x in range(pwd_length)) 27 | 28 | @classmethod 29 | def hash_passwd(cls): 30 | password = hashlib.sha1(cls.password_gen()).hexdigest() 31 | return password 32 | 33 | @classmethod 34 | def date_now(cls): 35 | now = datetime.utcnow().isoformat() + 'Z' 36 | return now 37 | 38 | @classmethod 39 | def date_less_one_week(cls): 40 | last_week = (datetime.utcnow() - timedelta(weeks=1)).isoformat() + 'Z' 41 | return last_week 42 | 43 | @classmethod 44 | def rfc_datetime_now(cls): 45 | date_time = cls.date_now().split('.')[0] 46 | rrule_time = date_time.translate(None, '-:') + 'Z' 47 | until_time = "UNTIL=" + rrule_time 48 | return until_time 49 | 50 | @classmethod 51 | def updated_event_rule(cls, rrule): 52 | rule_list = [] 53 | rule_ends = False 54 | for rule in rrule: 55 | if rule.startswith("RRULE"): 56 | updated_rule = [] 57 | for item in rule.split(';'): 58 | if item.startswith('UNTIL') or item.startswith('COUNT'): 59 | rule_ends = True 60 | updated_rule.append(cls.rfc_datetime_now()) 61 | else: 62 | updated_rule.append(item) 63 | if not rule_ends: 64 | updated_rule.append(cls.rfc_datetime_now()) 65 | rule_list.append(";".join(updated_rule)) 66 | else: 67 | rule_list.append(rule) 68 | return rule_list 69 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/verification_resource_malformed: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "admin#directory#verificationCodesList", 3 | "etag": "\"mVtRK5AeRZHHotPwF4vAn-nsx7I/sXIryHxh0VYqf83Uv2msDj-2ICs\"", 4 | "items": [ 5 | { 6 | "kind": "admin#directory#verificationCode", 7 | "etag": "\"mVtRK5AeRZHHotPwF4vAn-nsx7I/tqC4jDhcV9K5IcugymYr5nItYQM\"", 8 | "userId": "105930792424411065789", 9 | }, 10 | { 11 | "kind": "admin#directory#verificationCode", 12 | "etag": "\"mVtRK5AeRZHHotPwF4vAn-nsx7I/v9jcmKHZHFKn_bcEDTnJzVNR3Ks\"", 13 | "userId": "105930792424411065789", 14 | }, 15 | { 16 | "kind": "admin#directory#verificationCode", 17 | "etag": "\"mVtRK5AeRZHHotPwF4vAn-nsx7I/mOf9uo2mwlggWAwUGjpIwtKF43g\"", 18 | "userId": "105930792424411065789", 19 | "verificationCode": "###############" 20 | }, 21 | { 22 | "kind": "admin#directory#verificationCode", 23 | "etag": "\"mVtRK5AeRZHHotPwF4vAn-nsx7I/ch_6oFq1jxXRLYhFf5lOPgayY9A\"", 24 | "userId": "105930792424411065789", 25 | "verificationCode": "#######" 26 | }, 27 | { 28 | "kind": "admin#directory#verificationCode", 29 | "etag": "\"mVtRK5AeRZHHotPwF4vAn-nsx7I/DrkGeW9jRhPTI_oqX7YKtbDcg70\"", 30 | "userId": "105930792424411065789", 31 | }, 32 | { 33 | "kind": "admin#directory#verificationCode", 34 | "etag": "\"mVtRK5AeRZHHotPwF4vAn-nsx7I/4wnMEnpl-xY-f81kgzI5ddIT8QQ\"", 35 | "userId": "105930792424411065789", 36 | }, 37 | { 38 | "kind": "admin#directory#verificationCode", 39 | "etag": "\"mVtRK5AeRZHHotPwF4vAn-nsx7I/1AdpLOnP2njXdSfAmZvA6qW-_0k\"", 40 | "userId": "105930792424411065789", 41 | }, 42 | { 43 | "kind": "admin#directory#verificationCode", 44 | "etag": "\"mVtRK5AeRZHHotPwF4vAn-nsx7I/h1jdFuiT5P8IBscLmxLjzjrfV4s\"", 45 | "userId": "105930792424411065789", 46 | }, 47 | { 48 | "kind": "admin#directory#verificationCode", 49 | "etag": "\"mVtRK5AeRZHHotPwF4vAn-nsx7I/zNQYadR5YrQXkRbWPZBz8NBngUQ\"", 50 | "userId": "105930792424411065789", 51 | }, 52 | { 53 | "kind": "admin#directory#verificationCode", 54 | "etag": "\"mVtRK5AeRZHHotPwF4vAn-nsx7I/istdELt_EgosNvXFeTe1KlQHrWc\"", 55 | "userId": "105930792424411065789", 56 | } 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /libs/google/oauth.py: -------------------------------------------------------------------------------- 1 | import socks 2 | from httplib2 import Http, HttpLib2ErrorWithResponse, ProxyInfo 3 | from oauth2client.service_account import ServiceAccountCredentials 4 | 5 | 6 | class GoogleOAuthApi(object): 7 | 8 | def __init__(self, config): 9 | self.config = config 10 | self.SCOPES = self.config["google_apps"]["api_scopes"] 11 | self.proxy_info = None 12 | if self.config["defaults"]["http_proxy"]["use_proxy"]: 13 | self._PROXY_HOST = self.config["defaults"]["http_proxy"]["proxy_url"] 14 | self._PROXY_PORT = self.config["defaults"]["http_proxy"]["proxy_port"] 15 | self._PROXY_USER = self.config["defaults"]["http_proxy"]["proxy_user"] 16 | self._PROXY_PASS = self.config["defaults"]["http_proxy"]["proxy_pass"] 17 | self.proxy_info = ProxyInfo(proxy_type=socks.PROXY_TYPE_HTTP, 18 | proxy_host=self._PROXY_HOST, 19 | proxy_port=self._PROXY_PORT, 20 | proxy_user=self._PROXY_USER, 21 | proxy_pass=self._PROXY_PASS) 22 | self.credentials = self._get_credentials() 23 | 24 | def _get_credentials(self): 25 | """ 26 | Gets valid user credentials from storage. 27 | :return: 28 | credentials, the obtained credentials. 29 | """ 30 | credentials_file = self.config["google_apps"]["credentials_keyfile"] 31 | credentials = ServiceAccountCredentials.from_json_keyfile_name(credentials_file, 32 | scopes=self.SCOPES) 33 | if not credentials or credentials.invalid: 34 | print('Credentials are invalid!') 35 | return credentials 36 | 37 | def get_oauth_token(self, delegated_user): 38 | """ 39 | Authorizes the credentials read from storage with the google oauth api servers. 40 | :param delegated_user, user email to delegate access to. 41 | :return: 42 | oauth, the authorized oauth token. 43 | """ 44 | oauth = None 45 | try: 46 | delegated_credentials = self.credentials.create_delegated(delegated_user) 47 | oauth = delegated_credentials.authorize(Http(proxy_info=self.proxy_info)) 48 | except HttpLib2ErrorWithResponse as e: 49 | print("Error building a connector to the service: %s" % e) 50 | return oauth 51 | -------------------------------------------------------------------------------- /static/js/gatekeeper.js: -------------------------------------------------------------------------------- 1 | function typeaheadInit(strs) { 2 | return function findMatches(q, cb) { 3 | var matches, substringRegex; 4 | matches = []; 5 | substringRegex = new RegExp(q, 'i'); 6 | $.each(strs, function(i, str) { 7 | if (substringRegex.test(str)) { 8 | matches.push(str); 9 | } 10 | }); 11 | cb(matches); 12 | }; 13 | } 14 | 15 | function typeaheadRun(typeaheadId, sourceName, source) { 16 | $(typeaheadId + ' .typeahead').typeahead( 17 | { 18 | hint: true, 19 | highlight: true, 20 | minLength: 0 21 | }, 22 | { 23 | limit: 20, 24 | name: sourceName, 25 | source: typeaheadInit(source) 26 | } 27 | ); 28 | } 29 | 30 | function typeaheadSubmitFormOnSelect(typeaheadId, formId) { 31 | $(typeaheadId).on('typeahead:selected', function () { 32 | $(formId).submit(); 33 | }); 34 | } 35 | 36 | function selectAllCheckboxes(selectAllId, formOptionsId) { 37 | $(selectAllId).on('click', function () { 38 | if (this.checked == true) { 39 | $(formOptionsId).find('input[type="checkbox"]').prop('checked', true); 40 | $('.collapse').collapse('show'); 41 | } else { 42 | $(formOptionsId).find('input[type="checkbox"]').prop('checked', false); 43 | $('.collapse').collapse('hide'); 44 | } 45 | }); 46 | } 47 | 48 | function selectDefaultCheckboxes(defaultCheckboxes) { 49 | for (var i=0; i 0: 29 | return True 30 | else: 31 | return False 32 | 33 | def is_active_user(self, user): 34 | is_active_user_query = self.config["queries"]["user_is_active"] 35 | query = is_active_user_query.replace("USER", user) 36 | ldap_search = self.ldap_search(filterstr=query) 37 | if len(ldap_search) > 0: 38 | return True 39 | else: 40 | return False 41 | 42 | def get_user_info(self, user): 43 | user_info_query = self.config["queries"]["user_info"] 44 | query = user_info_query.replace("USER", user) 45 | user_info = self.ldap_search(filterstr=query) 46 | return user_info[0] 47 | 48 | def sync_users(self): 49 | users = [] 50 | all_users_query = self.config["queries"]["all_users"] 51 | ldap_search = self.ldap_search(filterstr=all_users_query) 52 | for result in ldap_search: 53 | if "uid" in result: 54 | users.append(result["uid"][0]) 55 | return users 56 | 57 | 58 | class LdapSyncThread(object): 59 | def __init__(self, t, hfunction): 60 | self.t=t 61 | self.hfunction = hfunction 62 | self.thread = Timer(self.t, self.handle_function) 63 | 64 | def handle_function(self): 65 | self.hFunction() 66 | self.thread = Timer(self.t, self.handle_function) 67 | self.thread.start() 68 | 69 | def start(self): 70 | self.thread.start() 71 | 72 | def cancel(self): 73 | self.thread.cancel() 74 | -------------------------------------------------------------------------------- /config/config.example.yml: -------------------------------------------------------------------------------- 1 | defaults: 2 | debug: false 3 | base_dir: "." 4 | http_proxy: 5 | use_proxy: false 6 | proxy_url: "" 7 | proxy_port: 8080 8 | proxy_user: "" 9 | proxy_pass: "" 10 | 11 | ldap: 12 | base_dn: "" 13 | uri: "" 14 | user: "" 15 | pass: "" 16 | queries: 17 | all_users: "" 18 | user_is_valid: "" 19 | user_is_active: "" 20 | user_info: "" 21 | fields: 22 | full_name: "" 23 | first_name: "" 24 | role: "" 25 | team: "" 26 | org: "" 27 | location: "" 28 | start_date: "" 29 | uid_number: "" 30 | groups: "" 31 | photo_url: "" 32 | 33 | pagerduty: 34 | base_url: "https://api.pagerduty.com/" 35 | api_key: "" 36 | 37 | duo: 38 | host: "" 39 | ikey: "" 40 | skey: "" 41 | ca_certs: "" 42 | 43 | google_apps: 44 | admin_user: "gatekeeper-admin" 45 | offboarded_ou: "/Offboarded Users" 46 | domain: "" 47 | credentials_keyfile: "config/google_api_service_account_keyfile.json" 48 | api_scopes: 49 | - "https://www.googleapis.com/auth/admin.directory.user" 50 | - "https://www.googleapis.com/auth/admin.directory.user.security" 51 | - "https://www.googleapis.com/auth/admin.directory.group.member" 52 | - "https://www.googleapis.com/auth/gmail.settings.basic" 53 | - "https://www.googleapis.com/auth/gmail.settings.sharing" 54 | - "https://www.googleapis.com/auth/calendar" 55 | - "https://www.googleapis.com/auth/drive" 56 | 57 | # You normally donot need to modify the config beyond this point. 58 | # Note: the items in the following lists MUST match the names of the fields for each form. 59 | # See forms.py for more info. 60 | actions: 61 | google_admin: &google_admin 62 | RESET_PASSWORD: true 63 | DELETE_ASPS: true 64 | DELETE_TOKENS: true 65 | INVALIDATE_BACKUP_CODES: true 66 | ORG_UNIT_CHANGE: true 67 | ORG_UNIT_RESET: true 68 | google_gmail: &google_gmail 69 | SET_OOO_MSG: true 70 | DISABLE_IMAP: true 71 | DISABLE_POP: true 72 | google_calendar: &google_calendar 73 | CHANGE_EVENTS_OWNERSHIP: true 74 | REMOVE_FUTURE_EVENTS: true 75 | google_drive: &google_drive 76 | NEW_FILES_OWNER: true 77 | FILE_SEARCH: true 78 | pagerduty: &pagerduty 79 | REMOVE_FROM_ONCALLS: true 80 | duo: &duo 81 | REMOVE_FROM_DUO: true 82 | 83 | # This is where you can select the default options available for each action group. 84 | action_groups: 85 | offboard: 86 | <<: *google_admin 87 | <<: *google_gmail 88 | <<: *google_calendar 89 | <<: *pagerduty 90 | lost_asset: 91 | - RESET_PASSWORD 92 | - DELETE_ASPS 93 | - DELETE_TOKENS 94 | - INVALIDATE_BACKUP_CODES 95 | - DISABLE_IMAP 96 | - DISABLE_POP 97 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## PROJECT SPECIFIC SECTION 2 | 3 | # Ignore config files 4 | config/config.yml 5 | 6 | # Ignore GApps keyfiles 7 | config/google_api_service_account_keyfile.json 8 | 9 | # Ignore static JS libs 10 | static/bower_components/* 11 | 12 | # Docker project generated files to ignore 13 | .vagrant* 14 | bin 15 | docker/docker 16 | .*.swp 17 | a.out 18 | *.orig 19 | build_src 20 | .flymake* 21 | .idea 22 | .DS_Store 23 | docs/_build 24 | docs/_static 25 | docs/_templates 26 | .gopath/ 27 | .dotcloud 28 | *.test 29 | bundles/ 30 | .hg/ 31 | .git/ 32 | vendor/pkg/ 33 | pyenv 34 | Vagrantfile 35 | 36 | 37 | ## MISC FILES SECTION 38 | 39 | # Temp files 40 | *# 41 | .#* 42 | *~ 43 | 44 | # Python output files 45 | .pex 46 | *.whl 47 | 48 | # IntelliJ files 49 | .idea/ 50 | out/ 51 | *.iml 52 | *.ipr 53 | *.iws 54 | 55 | # Pants files 56 | .pants.d 57 | dist 58 | .pids 59 | .pants.workdir.file_lock 60 | 61 | 62 | ## GITHUB TEMPLATE SECTION 63 | 64 | # Byte-compiled / optimized / DLL files 65 | __pycache__/ 66 | *.py[cod] 67 | *$py.class 68 | 69 | # C extensions 70 | *.so 71 | 72 | # Distribution / packaging 73 | .Python 74 | build/ 75 | develop-eggs/ 76 | dist/ 77 | downloads/ 78 | eggs/ 79 | .eggs/ 80 | lib/ 81 | lib64/ 82 | parts/ 83 | sdist/ 84 | var/ 85 | wheels/ 86 | *.egg-info/ 87 | .installed.cfg 88 | *.egg 89 | MANIFEST 90 | 91 | # PyInstaller 92 | # Usually these files are written by a python script from a template 93 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 94 | *.manifest 95 | *.spec 96 | 97 | # Installer logs 98 | pip-log.txt 99 | pip-delete-this-directory.txt 100 | 101 | # Unit test / coverage reports 102 | htmlcov/ 103 | .tox/ 104 | .coverage 105 | .coverage.* 106 | .cache 107 | nosetests.xml 108 | coverage.xml 109 | *.cover 110 | .hypothesis/ 111 | .pytest_cache/ 112 | 113 | # Translations 114 | *.mo 115 | *.pot 116 | 117 | # Django stuff: 118 | *.log 119 | local_settings.py 120 | db.sqlite3 121 | 122 | # Flask stuff: 123 | instance/ 124 | .webassets-cache 125 | 126 | # Scrapy stuff: 127 | .scrapy 128 | 129 | # Sphinx documentation 130 | docs/_build/ 131 | 132 | # PyBuilder 133 | target/ 134 | 135 | # Jupyter Notebook 136 | .ipynb_checkpoints 137 | 138 | # pyenv 139 | .python-version 140 | 141 | # celery beat schedule file 142 | celerybeat-schedule 143 | 144 | # SageMath parsed files 145 | *.sage.py 146 | 147 | # Environments 148 | .env 149 | .venv 150 | env/ 151 | venv/ 152 | ENV/ 153 | env.bak/ 154 | venv.bak/ 155 | 156 | # Spyder project settings 157 | .spyderproject 158 | .spyproject 159 | 160 | # Rope project settings 161 | .ropeproject 162 | 163 | # mkdocs documentation 164 | /site 165 | 166 | # mypy 167 | .mypy_cache/ 168 | 169 | -------------------------------------------------------------------------------- /tests/libs/duo/duo_tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from unittest import TestCase 4 | 5 | from duo import DuoAdminApi 6 | 7 | from mock import patch 8 | 9 | 10 | CALL_DUO_API = "duo.duo_client.Admin" 11 | 12 | 13 | class SomeDuoAdminApi(DuoAdminApi): 14 | def __init__(self, admin_api): 15 | self.admin_api = admin_api 16 | 17 | 18 | class DuoAdminApiTests(TestCase): 19 | 20 | def read_resource_file(self, res_file): 21 | mock_results_dir = "tests/libs/duo/mock_results" 22 | try: 23 | mock_resource_file = os.path.join(mock_results_dir, res_file) 24 | with open(mock_resource_file, "r") as fp: 25 | self.mock_resource = fp.read() 26 | fp.close() 27 | except IOError: 28 | self.mock_resource = None 29 | 30 | return self.mock_resource 31 | 32 | @patch(CALL_DUO_API) 33 | def test_list_users_pass(self, mock_duo_api): 34 | mock_duo_api.get_users.return_value = json.loads(self.read_resource_file('list_users_resource')) 35 | duo_api = SomeDuoAdminApi(admin_api=mock_duo_api) 36 | list_users = duo_api.list_users() 37 | assert list_users[0]["username"] == "mat" 38 | 39 | @patch(CALL_DUO_API) 40 | def test_list_users_fail(self, mock_duo_api): 41 | mock_duo_api.get_users.return_value = json.loads( 42 | self.read_resource_file('list_users_resource_fail')) 43 | duo_api = SomeDuoAdminApi(admin_api=mock_duo_api) 44 | list_users = duo_api.list_users() 45 | assert list_users == [] 46 | 47 | @patch(CALL_DUO_API) 48 | def test_get_user_pass(self, mock_duo_api): 49 | mock_duo_api.get_users_by_name.return_value = json.loads( 50 | self.read_resource_file('get_user_resource')) 51 | duo_api = SomeDuoAdminApi(admin_api=mock_duo_api) 52 | get_user = duo_api.get_user("mat") 53 | assert get_user[0]["realname"] == "Mat Clinton" 54 | 55 | @patch(CALL_DUO_API) 56 | def test_get_user_fail(self, mock_duo_api): 57 | mock_duo_api.get_users_by_name.return_value = json.loads( 58 | self.read_resource_file('get_user_resource_fail')) 59 | duo_api = SomeDuoAdminApi(admin_api=mock_duo_api) 60 | get_user = duo_api.get_user("mat") 61 | assert get_user == [] 62 | 63 | @patch(CALL_DUO_API) 64 | def test_delete_user_pass(self, mock_duo_api): 65 | mock_duo_api.delete_user.return_value = "" 66 | duo_api = SomeDuoAdminApi(admin_api=mock_duo_api) 67 | delete_user = duo_api.delete_user("DUVVVVVVVVVVV") 68 | assert delete_user is True 69 | 70 | @patch(CALL_DUO_API) 71 | def test_delete_user_fail(self, mock_duo_api): 72 | mock_duo_api.delete_user.return_value = json.loads( 73 | self.read_resource_file('delete_user_resource_fail')) 74 | duo_api = SomeDuoAdminApi(admin_api=mock_duo_api) 75 | delete_user = duo_api.delete_user("DUVVVVVVVVVVV") 76 | assert delete_user is False 77 | -------------------------------------------------------------------------------- /forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import BooleanField, StringField, TextAreaField, validators 3 | 4 | 5 | """ 6 | Note: field names must match the method names used by the various API client libraries. 7 | Libraries are located under twexit/lib. 8 | """ 9 | 10 | 11 | class BaseForm(FlaskForm): 12 | def __iter__(self): 13 | token = self.csrf_token 14 | yield token 15 | 16 | field_names = {token.name} 17 | for cls in self.__class__.__bases__: 18 | for field in cls(): 19 | field_name = field.name 20 | if field_name not in field_names: 21 | field_names.add(field_name) 22 | yield self[field_name] 23 | 24 | for field_name in self._fields: 25 | if field_name not in field_names: 26 | yield self[field_name] 27 | 28 | 29 | class UserIdForm(BaseForm): 30 | USER_ID = StringField('LDAP user name', [validators.DataRequired(), validators.Length(min=2)]) 31 | 32 | 33 | class GoogleAdminApiForm(BaseForm): 34 | RESET_PASSWORD = BooleanField('Reset Google apps password') 35 | DELETE_ASPS = BooleanField('Purge application specific passwords') 36 | DELETE_TOKENS = BooleanField('Purge 3rd party access tokens') 37 | INVALIDATE_BACKUP_CODES = BooleanField('Invalidate backup codes') 38 | ORG_UNIT_CHANGE = BooleanField('Move to Offboarded OU') 39 | ORG_UNIT_RESET = BooleanField('Restore move to Offboarded OU') 40 | 41 | 42 | class GoogleGmailApiForm(BaseForm): 43 | SET_OOO_MSG = BooleanField('Set Out Of Office message') 44 | OOO_MSG_TEXT = TextAreaField('Message text', [validators.Length(min=2)]) 45 | DISABLE_IMAP = BooleanField('Disable IMAP email') 46 | DISABLE_POP = BooleanField('Disable POP email') 47 | 48 | 49 | class GoogleCalendarApiForm(BaseForm): 50 | CHANGE_EVENTS_OWNERSHIP = BooleanField('Change events ownership') 51 | GCAL_NEW_OWNER = StringField('LDAP of new owner', [validators.Length(min=2)]) 52 | REMOVE_FUTURE_EVENTS = BooleanField('Delete future dated events') 53 | 54 | 55 | class PagerDutyApiForm(BaseForm): 56 | REMOVE_FROM_ONCALLS = BooleanField('Remove from OnCall rotas') 57 | 58 | 59 | class DuoApiForm(BaseForm): 60 | REMOVE_FROM_DUO = BooleanField('Remove from DUO') 61 | 62 | 63 | class GoogleApiForms(GoogleAdminApiForm, GoogleGmailApiForm, GoogleCalendarApiForm): 64 | pass 65 | 66 | 67 | class OffboardForm(UserIdForm, GoogleApiForms, PagerDutyApiForm, DuoApiForm): 68 | pass 69 | 70 | 71 | class LostAssetForm(UserIdForm, GoogleAdminApiForm, GoogleGmailApiForm): 72 | pass 73 | 74 | 75 | class GoogleMailForwardingForm(BaseForm): 76 | SET_MAIL_FORWARDING = BooleanField('Set a mail forwarding address') 77 | 78 | 79 | class GoogleDriveForm(BaseForm): 80 | FILE_SEARCH = StringField('File Search Query', [validators.Length(min=2)]) 81 | NEW_OWNER = StringField('New files owner', [validators.DataRequired(), validators.Length(min=2)]) 82 | 83 | 84 | class FilesOwnershipTransferForm(UserIdForm, GoogleDriveForm): 85 | pass 86 | -------------------------------------------------------------------------------- /libs/BUILD: -------------------------------------------------------------------------------- 1 | python_library( 2 | name = "google_api", 3 | sources = globs("google/*.py"), 4 | dependencies = [ 5 | ":google_api_admin", 6 | ":google_api_calendar", 7 | ":google_api_drive", 8 | ":google_api_gmail", 9 | ":google_api_oauth", 10 | ], 11 | ) 12 | 13 | python_library( 14 | name = "google_api_oauth", 15 | sources = globs("google/oauth.py"), 16 | dependencies = [ 17 | "3rdparty/python:SocksiPy-branch", 18 | "3rdparty/python:httplib2", 19 | "3rdparty/python:oauth2client", 20 | ], 21 | ) 22 | 23 | python_library( 24 | name = "google_api_controller", 25 | sources = globs("google/controller.py"), 26 | dependencies = [ 27 | "3rdparty/python:google-api-python-client", 28 | ], 29 | ) 30 | 31 | python_library( 32 | name = "google_api_admin", 33 | sources = globs("google/admin.py"), 34 | dependencies = [ 35 | ":google_api_controller", 36 | ":helper_functions", 37 | ], 38 | ) 39 | 40 | python_library( 41 | name = "google_api_gmail", 42 | sources = globs("google/gmail.py"), 43 | dependencies = [ 44 | ":google_api_controller", 45 | ":helper_functions", 46 | ], 47 | ) 48 | 49 | python_library( 50 | name = "google_api_drive", 51 | sources = globs("google/drive.py"), 52 | dependencies = [ 53 | ":google_api_controller", 54 | ":helper_functions", 55 | ], 56 | ) 57 | 58 | python_library( 59 | name = "google_api_calendar", 60 | sources = globs("google/calendar.py"), 61 | dependencies = [ 62 | ":google_api_controller", 63 | ":helper_functions", 64 | ], 65 | ) 66 | 67 | python_library( 68 | name = "pagerduty", 69 | sources = globs("pagerduty/*.py"), 70 | dependencies = [ 71 | ":http_controller", 72 | ], 73 | ) 74 | 75 | python_library( 76 | name = "duo", 77 | sources = globs("duo/*.py"), 78 | dependencies = [ 79 | "3rdparty/python:duo-client" 80 | ], 81 | ) 82 | 83 | python_library( 84 | name = "helper_functions", 85 | sources = globs("helper_functions.py"), 86 | dependencies = [ 87 | "3rdparty/python:PyYAML", 88 | "3rdparty/python:pycrypto", 89 | ], 90 | ) 91 | 92 | python_library( 93 | name = "http_controller", 94 | sources = globs("http_controller.py"), 95 | dependencies = [ 96 | "3rdparty/python:PyYAML", 97 | "3rdparty/python:requests", 98 | ], 99 | ) 100 | 101 | python_library( 102 | name = "gunicorn_wrapper", 103 | sources = globs("gunicorn_wrapper.py"), 104 | dependencies = [ 105 | "3rdparty/python:gunicorn", 106 | ], 107 | ) 108 | 109 | python_library( 110 | name = "ldap_client", 111 | sources = globs("ldap_client.py"), 112 | dependencies = [ 113 | "3rdparty/python:python-ldap", 114 | ], 115 | ) 116 | -------------------------------------------------------------------------------- /static/css/typeaheadjs.css: -------------------------------------------------------------------------------- 1 | span.twitter-typeahead .tt-menu, 2 | span.twitter-typeahead .tt-dropdown-menu { 3 | position: absolute; 4 | top: 100%; 5 | left: 0; 6 | z-index: 1000; 7 | display: none; 8 | float: left; 9 | min-width: 160px; 10 | padding: 5px 0; 11 | margin: 2px 0 0; 12 | list-style: none; 13 | font-size: 14px; 14 | text-align: left; 15 | background-color: #ffffff; 16 | border: 1px solid #cccccc; 17 | border: 1px solid rgba(0, 0, 0, 0.15); 18 | border-radius: 4px; 19 | -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); 20 | box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); 21 | background-clip: padding-box; 22 | } 23 | span.twitter-typeahead .tt-suggestion { 24 | display: block; 25 | padding: 3px 20px; 26 | clear: both; 27 | font-weight: normal; 28 | line-height: 1.42857143; 29 | color: #333333; 30 | white-space: nowrap; 31 | } 32 | span.twitter-typeahead .tt-suggestion.tt-cursor, 33 | span.twitter-typeahead .tt-suggestion:hover, 34 | span.twitter-typeahead .tt-suggestion:focus { 35 | color: #ffffff; 36 | text-decoration: none; 37 | outline: 0; 38 | background-color: #337ab7; 39 | } 40 | .input-group.input-group-lg span.twitter-typeahead .form-control { 41 | height: 46px; 42 | padding: 10px 16px; 43 | font-size: 18px; 44 | line-height: 1.3333333; 45 | border-radius: 6px; 46 | } 47 | .input-group.input-group-sm span.twitter-typeahead .form-control { 48 | height: 30px; 49 | padding: 5px 10px; 50 | font-size: 12px; 51 | line-height: 1.5; 52 | border-radius: 3px; 53 | } 54 | span.twitter-typeahead { 55 | width: 100%; 56 | } 57 | .input-group span.twitter-typeahead { 58 | display: block !important; 59 | height: 34px; 60 | } 61 | .input-group span.twitter-typeahead .tt-menu, 62 | .input-group span.twitter-typeahead .tt-dropdown-menu { 63 | top: 32px !important; 64 | } 65 | .input-group span.twitter-typeahead:not(:first-child):not(:last-child) .form-control { 66 | border-radius: 0; 67 | } 68 | .input-group span.twitter-typeahead:first-child .form-control { 69 | border-top-left-radius: 4px; 70 | border-bottom-left-radius: 4px; 71 | border-top-right-radius: 0; 72 | border-bottom-right-radius: 0; 73 | } 74 | .input-group span.twitter-typeahead:last-child .form-control { 75 | border-top-left-radius: 0; 76 | border-bottom-left-radius: 0; 77 | border-top-right-radius: 4px; 78 | border-bottom-right-radius: 4px; 79 | } 80 | .input-group.input-group-sm span.twitter-typeahead { 81 | height: 30px; 82 | } 83 | .input-group.input-group-sm span.twitter-typeahead .tt-menu, 84 | .input-group.input-group-sm span.twitter-typeahead .tt-dropdown-menu { 85 | top: 30px !important; 86 | } 87 | .input-group.input-group-lg span.twitter-typeahead { 88 | height: 46px; 89 | } 90 | .input-group.input-group-lg span.twitter-typeahead .tt-menu, 91 | .input-group.input-group-lg span.twitter-typeahead .tt-dropdown-menu { 92 | top: 46px !important; 93 | } -------------------------------------------------------------------------------- /libs/duo/__init__.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | import duo_client 4 | 5 | 6 | class DuoAdminApi(object): 7 | def __init__(self, config=None, use_proxy=False, proxy_config=None): 8 | self.config = config 9 | self.use_proxy = use_proxy 10 | self.proxy_config = proxy_config 11 | self.admin_api = self._create_duo_client() 12 | 13 | def _create_duo_client(self): 14 | """ 15 | Creates a DUO Admin API Client object. 16 | :return: DUO client object 17 | """ 18 | if self.config["ca_certs"] == "": 19 | client = duo_client.Admin(ikey=self.config["ikey"], 20 | skey=self.config["skey"], 21 | host=self.config["host"]) 22 | else: 23 | client = duo_client.Admin(ikey=self.config["ikey"], 24 | skey=self.config["skey"], 25 | host=self.config["host"], 26 | ca_certs=self.config["ca_certs"]) 27 | 28 | if self.use_proxy is True: 29 | self.proxy_headers = {"Proxy-Authorization": "Basic " + base64.b64encode(b"%s:%s" % ( 30 | self.proxy_config["proxy_user"], 31 | self.proxy_config["proxy_pass"])).decode("utf-8")} 32 | 33 | client.set_proxy(host=self.proxy_config["proxy_url"], 34 | port=self.proxy_config["proxy_port"], 35 | headers=self.proxy_headers, 36 | proxy_type="CONNECT") 37 | return client 38 | 39 | def list_users(self): 40 | """ 41 | List of all users. 42 | :return: list of user objects. 43 | """ 44 | r = self.admin_api.get_users() 45 | return r 46 | 47 | def get_user(self, username): 48 | """ 49 | Return a single user object by username. 50 | :param: username: username 51 | :return: user object. 52 | """ 53 | try: 54 | r = self.admin_api.get_users_by_name(username) 55 | return r 56 | except AttributeError as e: 57 | return "Error connecting to Duo: %s" % e 58 | 59 | def delete_user(self, user_id): 60 | """ 61 | Delete user by id. 62 | :param: user_id: user_id 63 | :return: empty string when successful 64 | """ 65 | r = self.admin_api.delete_user(user_id) 66 | if r == "": 67 | return True 68 | else: 69 | return False 70 | 71 | def remove_from_duo(self, username): 72 | """ 73 | Delete user by username. 74 | :param: username: username 75 | :return: Bool 76 | Note: This returns a bool to show user was deleted. 77 | """ 78 | try: 79 | result = None 80 | user = username.split('@')[0] 81 | user_data = self.get_user(user) 82 | if len(user_data) == 0: 83 | result = True 84 | elif user_data is not None: 85 | for item in user_data: 86 | if item["username"] == user: 87 | user_id = item["user_id"] 88 | if self.delete_user(user_id) is True: 89 | result = True 90 | else: 91 | result = False 92 | return result 93 | 94 | except(AttributeError, KeyError): 95 | return "Error connecting to Duo." 96 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at opensource@twitter.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /tests/libs/google/drive_tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | from unittest import TestCase 5 | 6 | from mock import MagicMock, patch 7 | from google.drive import GoogleDriveApi 8 | 9 | 10 | CALL_GOOGLE_API = "google.drive.GoogleApiController.call_google_api" 11 | 12 | 13 | class SomeGoogleDriveApi(GoogleDriveApi): 14 | def __init__(self, oauth): 15 | self.oauth = oauth 16 | self.service = MagicMock() 17 | 18 | 19 | class GoogleDriveApiTests(TestCase): 20 | 21 | def read_resource_file(self, res_file): 22 | mock_results_dir = "tests/libs/google/mock_results" 23 | try: 24 | mock_resource_file = os.path.join(mock_results_dir, res_file) 25 | with open(mock_resource_file, "r") as mock_resource_fp: 26 | self.mock_resource = json.loads(mock_resource_fp.read()) 27 | mock_resource_fp.close() 28 | except IOError: 29 | self.mock_resource = None 30 | 31 | return self.mock_resource 32 | 33 | @patch(CALL_GOOGLE_API) 34 | def test_get_files_list_pass(self, mock_google_api): 35 | file_ids = [] 36 | mock_google_api.return_value = json.dumps(self.read_resource_file 37 | ('drive_list_resource')) 38 | google_drive = SomeGoogleDriveApi(oauth=MagicMock()) 39 | get_files_list = google_drive.get_files_list("someuser@somehwere.org") 40 | for file_id in get_files_list: 41 | file_ids.append(file_id['id']) 42 | assert "0B3uO8LigBWypc3RhcnRlcl9maWxlX2Rhc2hlclYw" in file_ids 43 | 44 | @patch(CALL_GOOGLE_API) 45 | def test_get_files_list_fail(self, mock_google_api): 46 | mock_google_api.return_value = self.read_resource_file('drive_list_resource_malformed') 47 | google_drive = SomeGoogleDriveApi(oauth=MagicMock()) 48 | get_files_list = google_drive.get_files_list("someuser@somehwere.org") 49 | assert get_files_list is None 50 | 51 | @patch(CALL_GOOGLE_API) 52 | def test_get_file_info_pass(self, mock_google_api): 53 | mock_google_api.return_value = json.dumps(self.read_resource_file('file_info_resource')["id"]) 54 | google_drive = SomeGoogleDriveApi(oauth=MagicMock()) 55 | get_file_info = google_drive.get_file_info("file_id") 56 | assert get_file_info == "0B-NxYpteIrCmZ2lIU3lheHA1dkE" 57 | 58 | @patch(CALL_GOOGLE_API) 59 | def test_get_file_info_fail(self, mock_google_api): 60 | mock_google_api.return_value = self.read_resource_file('file_info_resource_malformed')["id"] 61 | google_drive = SomeGoogleDriveApi(oauth=MagicMock()) 62 | get_file_info = google_drive.get_file_info("file_id") 63 | assert get_file_info is None 64 | 65 | @patch(CALL_GOOGLE_API) 66 | def test_drive_transfer_file_owner_pass(self, mock_google_api): 67 | mock_google_api.return_value = json.dumps(self.read_resource_file 68 | ('drive_transfer_file_owner_pass')["role"]) 69 | google_drive = SomeGoogleDriveApi(oauth=MagicMock()) 70 | transfer_file_owner = google_drive.transfer_file_owner("file_id", "user_email") 71 | assert transfer_file_owner is True 72 | 73 | @patch(CALL_GOOGLE_API) 74 | def test_drive_transfer_file_owner_fail(self, mock_google_api): 75 | mock_google_api.return_value = json.dumps(self.read_resource_file 76 | ('drive_transfer_file_owner_fail')) 77 | google_drive = SomeGoogleDriveApi(oauth=MagicMock()) 78 | transfer_file_owner = google_drive.transfer_file_owner("file_id", "user_email") 79 | assert transfer_file_owner is False 80 | -------------------------------------------------------------------------------- /pants: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2015 Pants project contributors (see CONTRIBUTORS.md). 3 | # Licensed under the Apache License, Version 2.0 (see LICENSE). 4 | 5 | # =============================== NOTE =============================== 6 | # This pants bootstrap script comes from the pantsbuild/setup 7 | # project and is intended to be checked into your code repository so 8 | # that any developer can check out your code and be building it with 9 | # pants with no prior setup needed. 10 | # 11 | # You can learn more here: https://pantsbuild.github.io/setup 12 | # ==================================================================== 13 | 14 | set -e 15 | 16 | PYTHON=${PYTHON:-$(which python2.7)} 17 | 18 | PANTS_HOME="${PANTS_HOME:-${XDG_CACHE_HOME:-$HOME/.cache}/pants/setup}" 19 | PANTS_BOOTSTRAP="${PANTS_HOME}/bootstrap-$(uname -s)-$(uname -m)" 20 | 21 | VENV_VERSION=${VENV_VERSION:-15.2.0} 22 | 23 | # This requirement is installed before pantsbuild.pants to hack around 24 | # interpreters that have newer setuptools already installed, effectively 25 | # re-installing an older setuptools and pinning low to a version known to be 26 | # ingestable by both pants and pex for all reasonable versions of pants. 27 | # See: 28 | # https://github.com/pantsbuild/pants/issues/3948 29 | # https://github.com/pantsbuild/setup/issues/14 30 | # https://github.com/pantsbuild/setup/issues/19 31 | SETUPTOOLS_REQUIREMENT="setuptools==5.4.1" 32 | 33 | VENV_PACKAGE=virtualenv-${VENV_VERSION} 34 | VENV_TARBALL=${VENV_PACKAGE}.tar.gz 35 | 36 | # The high-level flow: 37 | # 1.) Grab pants version from pants.ini or default to latest. 38 | # 2.) Check for a venv via a naming/path convention and execute if found. 39 | # 3.) Otherwise create venv and re-exec self. 40 | # 41 | # After that pants itself will handle making sure any requested plugins 42 | # are installed and up to date. 43 | 44 | function tempdir { 45 | mktemp -d "$1"/pants.XXXXXX 46 | } 47 | 48 | # TODO(John Sirois): GC race loser tmp dirs leftover from bootstrap_XXX 49 | # functions. Any tmp dir w/o a symlink pointing to it can go. 50 | 51 | function bootstrap_venv { 52 | if [[ ! -d "${PANTS_BOOTSTRAP}/${VENV_PACKAGE}" ]] 53 | then 54 | ( 55 | mkdir -p "${PANTS_BOOTSTRAP}" && \ 56 | staging_dir=$(tempdir "${PANTS_BOOTSTRAP}") && \ 57 | cd "${staging_dir}" && \ 58 | curl -LO https://pypi.io/packages/source/v/virtualenv/${VENV_TARBALL} && \ 59 | tar -xzf ${VENV_TARBALL} && \ 60 | ln -s "${staging_dir}/${VENV_PACKAGE}" "${staging_dir}/latest" && \ 61 | mv "${staging_dir}/latest" "${PANTS_BOOTSTRAP}/${VENV_PACKAGE}" 62 | ) 1>&2 63 | fi 64 | echo "${PANTS_BOOTSTRAP}/${VENV_PACKAGE}" 65 | } 66 | 67 | function bootstrap_pants { 68 | pants_requirement="pantsbuild.pants" 69 | pants_version=$( 70 | grep -E "^[[:space:]]*pants_version" pants.ini 2>/dev/null | \ 71 | cut -f2 -d: | tr -d " " 72 | ) 73 | if [[ -n "${pants_version}" ]] 74 | then 75 | pants_requirement="${pants_requirement}==${pants_version}" 76 | else 77 | pants_version="unspecified" 78 | fi 79 | 80 | if [[ ! -d "${PANTS_BOOTSTRAP}/${pants_version}" ]] 81 | then 82 | ( 83 | # NB: We setup the virtualenv with no setuptools to ensure our 84 | # ${SETUPTOOLS_REQUIREMENT} wins. 85 | venv_path="$(bootstrap_venv)" && \ 86 | staging_dir=$(tempdir "${PANTS_BOOTSTRAP}") && \ 87 | "${PYTHON}" "${venv_path}/virtualenv.py" --no-setuptools --no-download \ 88 | "${staging_dir}/install" && \ 89 | "${staging_dir}/install/bin/python" \ 90 | "${staging_dir}/install/bin/pip" install \ 91 | "${SETUPTOOLS_REQUIREMENT}" && \ 92 | "${staging_dir}/install/bin/python" \ 93 | "${staging_dir}/install/bin/pip" install \ 94 | "${pants_requirement}" && \ 95 | ln -s "${staging_dir}/install" "${staging_dir}/${pants_version}" && \ 96 | mv "${staging_dir}/${pants_version}" "${PANTS_BOOTSTRAP}/${pants_version}" 97 | ) 1>&2 98 | fi 99 | echo "${PANTS_BOOTSTRAP}/${pants_version}" 100 | } 101 | pants_dir=$(bootstrap_pants) && \ 102 | exec "${pants_dir}/bin/python" "${pants_dir}/bin/pants" "$@" 103 | -------------------------------------------------------------------------------- /tests/libs/google/calendar_tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | from unittest import TestCase 5 | 6 | from mock import MagicMock, patch 7 | from google.calendar import GoogleCalendarApi 8 | 9 | 10 | CALL_GOOGLE_API = "google.calendar.GoogleApiController.call_google_api" 11 | 12 | 13 | class SomeGoogleCalendarApi(GoogleCalendarApi): 14 | def __init__(self, oauth): 15 | self.oauth = oauth 16 | self.service = MagicMock() 17 | 18 | 19 | class GoogleCalendarApiTests(TestCase): 20 | 21 | def read_resource_file(self, res_file): 22 | mock_results_dir = "tests/libs/google/mock_results" 23 | try: 24 | mock_resource_file = os.path.join(mock_results_dir, res_file) 25 | with open(mock_resource_file, "r") as mock_resource_fp: 26 | self.mock_resource = json.loads(mock_resource_fp.read()) 27 | mock_resource_fp.close() 28 | except IOError: 29 | self.mock_resource = None 30 | 31 | return self.mock_resource 32 | 33 | @patch(CALL_GOOGLE_API) 34 | def test_get_calendar_id_pass(self, mock_google_api): 35 | mock_google_api.return_value = json.dumps(self.read_resource_file('calendar_id_resource')["id"]) 36 | google_calendar = SomeGoogleCalendarApi(oauth=MagicMock()) 37 | get_calendar_id = google_calendar.get_calendar_id() 38 | assert get_calendar_id == "hkantas@testdomain.com" 39 | 40 | @patch(CALL_GOOGLE_API) 41 | def test_get_calendar_id_fail(self, mock_google_api): 42 | mock_google_api.return_value = self.read_resource_file('calendar_id_resource_malformed') 43 | google_calendar = SomeGoogleCalendarApi(oauth=MagicMock()) 44 | get_calendar_id = google_calendar.get_calendar_id() 45 | assert get_calendar_id is None 46 | 47 | @patch(CALL_GOOGLE_API) 48 | def test_list_events(self, mock_google_api): 49 | mock_google_api.return_value = json.dumps(self.read_resource_file 50 | ('list_events_resource')["items"]) 51 | google_calendar = SomeGoogleCalendarApi(oauth=MagicMock()) 52 | list_events = google_calendar.list_events("someuser@somewhere.org") 53 | assert list_events[0] == "ksk6hnh8lbe0qdn02tc1vsp7qk" 54 | 55 | @patch(CALL_GOOGLE_API) 56 | def test_move_event(self, mock_google_api): 57 | mock_google_api.return_value = json.dumps(self.read_resource_file 58 | ('move_event_resource')["id"]) 59 | google_calendar = SomeGoogleCalendarApi(oauth=MagicMock()) 60 | move_event = google_calendar.move_event("ltk5efr9set0qaogfnpapgvcec", 61 | "stillings@testdomain.com") 62 | assert move_event is True 63 | 64 | @patch(CALL_GOOGLE_API) 65 | def test_move_calendar_ownership(self, mock_google_api): 66 | mock_google_api.return_value = json.dumps(self.read_resource_file 67 | ('move_calendar_resource')["role"]) 68 | google_calendar = SomeGoogleCalendarApi(oauth=MagicMock()) 69 | move_calendar = google_calendar.move_calendar_ownership("mat@testdomain.com") 70 | assert move_calendar == "owner" 71 | 72 | @patch(CALL_GOOGLE_API) 73 | def test_decline_event(self, mock_google_api): 74 | mock_google_api.return_value = json.dumps(self.read_resource_file 75 | ('decline_event_resource')["attendees"]) 76 | google_calendar = SomeGoogleCalendarApi(oauth=MagicMock()) 77 | decline_event = google_calendar.decline_event("fuccsmq6ipqmpeeb93e6bdvtc0", 78 | "stillings@testdomain.com") 79 | assert decline_event[0]["responseStatus"] == "declined" 80 | 81 | @patch(CALL_GOOGLE_API) 82 | def test_get_event_rule(self, mock_google_api): 83 | mock_google_api.return_value = json.dumps(self.read_resource_file 84 | ('event_rule_resource')["recurrence"]) 85 | google_calendar = SomeGoogleCalendarApi(oauth=MagicMock()) 86 | get_event_rule = google_calendar.get_event_rule("event_id") 87 | assert get_event_rule == ["RRULE:FREQ=WEEKLY;BYDAY=MO"] 88 | 89 | @patch(CALL_GOOGLE_API) 90 | def test_cancel_recurrence(self, mock_google_api): 91 | mock_google_api.return_value = json.dumps(self.read_resource_file 92 | ('cancel_recurrence_resource')["id"]) 93 | google_calendar = SomeGoogleCalendarApi(oauth=MagicMock()) 94 | cancel_recurrence = google_calendar.cancel_recurrence("event_id", "new_event_rule") 95 | assert cancel_recurrence == "e2i9ln9qjl3eo9ejgb3484bh78" 96 | -------------------------------------------------------------------------------- /tests/libs/google/mock_results/admin.directory.user: -------------------------------------------------------------------------------- 1 | [{ 2 | u 'agreedToTerms': True, 3 | u 'kind': u 'admin#directory#user', 4 | u 'name': { 5 | u 'fullName': u 'Harry Kantas', 6 | u 'givenName': u 'Harry', 7 | u 'familyName': u 'Kantas' 8 | }, 9 | u 'ipWhitelisted': False, 10 | u 'creationTime': u '2016-01-21T18:03:29.000Z', 11 | u 'primaryEmail': u 'hkantas@testdomain.com', 12 | u 'changePasswordAtNextLogin': False, 13 | u 'isDelegatedAdmin': False, 14 | u 'isMailboxSetup': True, 15 | u 'includeInGlobalAddressList': True, 16 | u 'etag': u '"mVtRK5AeRZHHotPwF4vAn-nsx7I/HypPBIoWGiKxHfSNNZ5DQ24BuKU"', 17 | u 'suspended': False, 18 | u 'id': u '108700750363721028501', 19 | u 'lastLoginTime': u '2016-08-12T18:23:53.000Z', 20 | u 'customerId': u 'C00sl2ia7', 21 | u 'emails': [{ 22 | u 'primary': True, 23 | u 'address': u 'hkantas@testdomain.com' 24 | }], 25 | u 'orgUnitPath': u '/', 26 | u 'isAdmin': True 27 | }, { 28 | u 'agreedToTerms': True, 29 | u 'kind': u 'admin#directory#user', 30 | u 'name': { 31 | u 'fullName': u 'Paul Hewson', 32 | u 'givenName': u 'Paul', 33 | u 'familyName': u 'Hewson' 34 | }, 35 | u 'ipWhitelisted': False, 36 | u 'creationTime': u '2016-01-21T18:03:29.000Z', 37 | u 'primaryEmail': u 'phewson@testdomain.com', 38 | u 'changePasswordAtNextLogin': False, 39 | u 'isDelegatedAdmin': False, 40 | u 'isMailboxSetup': True, 41 | u 'includeInGlobalAddressList': True, 42 | u 'etag': u '"mVtRK5AeRZHHotPwF4vAn-nsx7I/HypPBIoWGiKxHfSNNZ5DQ24BuKU"', 43 | u 'suspended': False, 44 | u 'id': u '108700750363721028502', 45 | u 'lastLoginTime': u '2016-08-12T18:23:53.000Z', 46 | u 'customerId': u 'C00sl2ia7', 47 | u 'emails': [{ 48 | u 'primary': True, 49 | u 'address': u 'phewson@testdomain.com' 50 | }], 51 | u 'orgUnitPath': u '/', 52 | u 'isAdmin': True 53 | }, { 54 | u 'agreedToTerms': True, 55 | u 'kind': u 'admin#directory#user', 56 | u 'name': { 57 | u 'fullName': u 'Dave Evans', 58 | u 'givenName': u 'Dave', 59 | u 'familyName': u 'Evans' 60 | }, 61 | u 'ipWhitelisted': False, 62 | u 'creationTime': u '2016-01-21T18:03:29.000Z', 63 | u 'primaryEmail': u 'devans@testdomain.com', 64 | u 'changePasswordAtNextLogin': False, 65 | u 'isDelegatedAdmin': False, 66 | u 'isMailboxSetup': True, 67 | u 'includeInGlobalAddressList': True, 68 | u 'etag': u '"mVtRK5AeRZHHotPwF4vAn-nsx7I/HypPBIoWGiKxHfSNNZ5DQ24BuKU"', 69 | u 'suspended': False, 70 | u 'id': u '108700750363721028503', 71 | u 'lastLoginTime': u '2016-08-12T18:23:53.000Z', 72 | u 'customerId': u 'C00sl2ia7', 73 | u 'emails': [{ 74 | u 'primary': True, 75 | u 'address': u 'devans@testdomain.com' 76 | }], 77 | u 'orgUnitPath': u '/', 78 | u 'isAdmin': True 79 | }, { 80 | u 'agreedToTerms': True, 81 | u 'kind': u 'admin#directory#user', 82 | u 'name': { 83 | u 'fullName': u 'Adam Clayton', 84 | u 'givenName': u 'Adam', 85 | u 'familyName': u 'Clayton' 86 | }, 87 | u 'ipWhitelisted': False, 88 | u 'creationTime': u '2016-01-21T18:03:29.000Z', 89 | u 'primaryEmail': u 'aclayton@testdomain.com', 90 | u 'changePasswordAtNextLogin': False, 91 | u 'isDelegatedAdmin': False, 92 | u 'isMailboxSetup': True, 93 | u 'includeInGlobalAddressList': True, 94 | u 'etag': u '"mVtRK5AeRZHHotPwF4vAn-nsx7I/HypPBIoWGiKxHfSNNZ5DQ24BuKU"', 95 | u 'suspended': False, 96 | u 'id': u '108700750363721028504', 97 | u 'lastLoginTime': u '2016-08-12T18:23:53.000Z', 98 | u 'customerId': u 'C00sl2ia7', 99 | u 'emails': [{ 100 | u 'primary': True, 101 | u 'address': u 'aclayton@testdomain.com' 102 | }], 103 | u 'orgUnitPath': u '/', 104 | u 'isAdmin': True 105 | }, { 106 | u 'agreedToTerms': True, 107 | u 'kind': u 'admin#directory#user', 108 | u 'name': { 109 | u 'fullName': u 'Larry Mullen', 110 | u 'givenName': u 'Larry', 111 | u 'familyName': u 'Mullen' 112 | }, 113 | u 'ipWhitelisted': False, 114 | u 'creationTime': u '2016-01-21T18:03:29.000Z', 115 | u 'primaryEmail': u 'lmullen@testdomain.com', 116 | u 'changePasswordAtNextLogin': False, 117 | u 'isDelegatedAdmin': False, 118 | u 'isMailboxSetup': True, 119 | u 'includeInGlobalAddressList': True, 120 | u 'etag': u '"mVtRK5AeRZHHotPwF4vAn-nsx7I/HypPBIoWGiKxHfSNNZ5DQ24BuKU"', 121 | u 'suspended': False, 122 | u 'id': u '108700750363721028505', 123 | u 'lastLoginTime': u '2016-08-12T18:23:53.000Z', 124 | u 'customerId': u 'C00sl2ia7', 125 | u 'emails': [{ 126 | u 'primary': True, 127 | u 'address': u 'lmullen@testdomain.com' 128 | }], 129 | u 'orgUnitPath': u '/', 130 | u 'isAdmin': True 131 | }] 132 | -------------------------------------------------------------------------------- /tests/libs/google/gmail_tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | from unittest import TestCase 5 | 6 | from mock import MagicMock, patch 7 | from google.gmail import GoogleGmailApi 8 | 9 | CALL_GOOGLE_API = "google.gmail.GoogleApiController.call_google_api" 10 | 11 | 12 | class SomeGoogleGmailApi(GoogleGmailApi): 13 | def __init__(self, oauth): 14 | self.oauth = oauth 15 | self.service = MagicMock() 16 | 17 | 18 | class GoogleGmailApiTests(TestCase): 19 | 20 | def read_resource_file(self, res_file): 21 | mock_results_dir = "tests/libs/google/mock_results" 22 | try: 23 | mock_resource_file = os.path.join(mock_results_dir, res_file) 24 | with open(mock_resource_file, "r") as mock_resource_fp: 25 | self.mock_resource = json.loads(mock_resource_fp.read()) 26 | mock_resource_fp.close() 27 | except IOError: 28 | self.mock_resource = None 29 | 30 | return self.mock_resource 31 | 32 | @patch(CALL_GOOGLE_API) 33 | def test_set_ooo_msg_pass(self, mock_google_api): 34 | mock_google_api.return_value = json.dumps(self.read_resource_file 35 | ('ooo_resource')["enableAutoReply"]) 36 | google_gmail = SomeGoogleGmailApi(oauth=MagicMock()) 37 | set_ooo_msg = google_gmail.set_ooo_msg("someuser@somewhere.org", "text") 38 | assert set_ooo_msg is True 39 | 40 | @patch(CALL_GOOGLE_API) 41 | def test_set_ooo_msg_fail(self, mock_google_api): 42 | mock_google_api.return_value = self.read_resource_file('ooo_resource_malformed') 43 | google_gmail = SomeGoogleGmailApi(oauth=MagicMock()) 44 | set_ooo_msg = google_gmail.set_ooo_msg("someuser@somewhere.org", "text") 45 | assert set_ooo_msg is False 46 | 47 | @patch(CALL_GOOGLE_API) 48 | def test_forwarding_address_enabled_pass(self, mock_google_api): 49 | mock_google_api.return_value = json.dumps(self.read_resource_file 50 | ('forwarding_resource')["verificationStatus"]) 51 | google_gmail = SomeGoogleGmailApi(oauth=MagicMock()) 52 | is_forwarding_address = google_gmail.is_forwarding_address("someuser@somewhere.org", 53 | "someuser@gtest.twiter.com") 54 | assert is_forwarding_address is True 55 | 56 | @patch(CALL_GOOGLE_API) 57 | def test_forwarding_address_enabled_fail(self, mock_google_api): 58 | mock_google_api.return_value = self.read_resource_file('forwarding_resource') 59 | google_gmail = SomeGoogleGmailApi(oauth=MagicMock()) 60 | is_forwarding_address = google_gmail.is_forwarding_address("someuser@somewhere.org", 61 | "someuser@somewhere.org") 62 | assert is_forwarding_address is False 63 | 64 | @patch(CALL_GOOGLE_API) 65 | def test_create_forwarding_address(self, mock_google_api): 66 | mock_google_api.return_value = json.dumps(self.read_resource_file 67 | ('forwarding_resource')["verificationStatus"]) 68 | google_gmail = SomeGoogleGmailApi(oauth=MagicMock()) 69 | create_forwarding_address = google_gmail.create_forwarding_address("someuser@somewhere.org", 70 | "someuser@somewhere.org") 71 | assert create_forwarding_address is True 72 | 73 | @patch(CALL_GOOGLE_API) 74 | def test_disable_pop_pass(self, mock_google_api): 75 | mock_google_api.return_value = json.dumps(self.read_resource_file 76 | ('pop_resource')["accessWindow"]) 77 | google_gmail = SomeGoogleGmailApi(oauth=MagicMock()) 78 | disable_pop = google_gmail.disable_pop("someuser@somewhere.org") 79 | assert disable_pop is True 80 | 81 | @patch(CALL_GOOGLE_API) 82 | def test_disable_pop_fail(self, mock_google_api): 83 | mock_google_api.return_value = self.read_resource_file('pop_resource_malformed') 84 | google_gmail = SomeGoogleGmailApi(oauth=MagicMock()) 85 | disable_pop = google_gmail.disable_pop("someuser@somewhere.org") 86 | assert disable_pop is False 87 | 88 | @patch(CALL_GOOGLE_API) 89 | def test_disable_imap_pass(self, mock_google_api): 90 | mock_google_api.return_value = json.dumps(self.read_resource_file 91 | ('imap_resource')["enabled"]) 92 | google_gmail = SomeGoogleGmailApi(oauth=MagicMock()) 93 | disable_imap = google_gmail.disable_imap("someuser@somewhere.org") 94 | assert disable_imap is True 95 | 96 | @patch(CALL_GOOGLE_API) 97 | def test_disable_imap_fail(self, mock_google_api): 98 | mock_google_api.return_value = self.read_resource_file('imap_resource_malformed') 99 | google_gmail = SomeGoogleGmailApi(oauth=MagicMock()) 100 | disable_imap = google_gmail.disable_imap("someuser@somewhere.org") 101 | assert disable_imap is False 102 | -------------------------------------------------------------------------------- /libs/google/drive.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from google.controller import GoogleApiController 4 | 5 | 6 | class GoogleDriveApi(GoogleApiController): 7 | def __init__(self, oauth): 8 | self.oauth = oauth 9 | self.service = self._get_service("drive", "v3") 10 | 11 | def get_files_list(self, owner): 12 | """ 13 | Retrieves a user's drive files 14 | :return: list of files owned by the user. 15 | """ 16 | files_list = [] 17 | try: 18 | r = json.loads(self.call_google_api(service=self.service, 19 | q=owner, 20 | api_resource="files", 21 | api_method="list", 22 | response_field=None, 23 | corpus="user", 24 | spaces="drive")) 25 | files_list.extend(r["files"]) 26 | 27 | if "nextPageToken" in r: 28 | next_page_token = r["nextPageToken"] 29 | while next_page_token is not None: 30 | r = json.loads(self.call_google_api(service=self.service, 31 | q=owner, 32 | api_resource="files", 33 | api_method="list", 34 | response_field=None, 35 | corpus="user", 36 | spaces="drive", 37 | pageToken=next_page_token)) 38 | files_list.extend(r["files"]) 39 | if "nextPageToken" in r: 40 | next_page_token = r["nextPageToken"] 41 | else: 42 | next_page_token = None 43 | 44 | return files_list 45 | except(ValueError, KeyError, TypeError): 46 | return None 47 | 48 | def search_files_list(self, owner="'me' in owners", drive_query=""): 49 | """ 50 | Searches users drives files 51 | :param drive_query: q 52 | :param owner: q 53 | :return: list of files with name containing the searched term. 54 | """ 55 | files_list = [] 56 | try: 57 | file_search = "%s and name contains '%s'" % (owner, drive_query) 58 | r = json.loads(self.call_google_api(service=self.service, 59 | q=file_search, 60 | api_resource="files", 61 | api_method="list", 62 | response_field=None, 63 | corpus="user", 64 | spaces="drive")) 65 | files_list.extend(r["files"]) 66 | 67 | if "nextPageToken" in r: 68 | next_page_token = r["nextPageToken"] 69 | while next_page_token is not None: 70 | r = json.loads(self.call_google_api(service=self.service, 71 | q=file_search, 72 | api_resource="files", 73 | api_method="list", 74 | response_field=None, 75 | corpus="user", 76 | spaces="drive", 77 | pageToken=next_page_token)) 78 | files_list.extend(r["files"]) 79 | if "nextPageToken" in r: 80 | next_page_token = r["nextPageToken"] 81 | else: 82 | next_page_token = None 83 | 84 | return files_list 85 | except(ValueError, KeyError, TypeError): 86 | return None 87 | 88 | def get_file_info(self, file_id): 89 | """ 90 | Lists a file's metadata. 91 | :param file_id: fileId 92 | :return: files resource 93 | """ 94 | try: 95 | r = json.loads(self.call_google_api(service=self.service, 96 | api_resource="files", 97 | api_method="get", 98 | fields="owners", 99 | fileId=file_id, 100 | response_field=None)) 101 | return r 102 | except(ValueError, KeyError, TypeError): 103 | return None 104 | 105 | def transfer_file_owner(self, file_id, user_email): 106 | """ 107 | Assigned new owner for the file 108 | :param file_id: fileId 109 | :param user_email: permissionId 110 | :return: bool 111 | """ 112 | permissions_settings = { 113 | "kind": "drive#permission", 114 | "role": "owner", 115 | "type": "user", 116 | "emailAddress": user_email 117 | } 118 | try: 119 | r = json.loads(self.call_google_api(service=self.service, 120 | api_resource="permissions", 121 | api_method="create", 122 | response_field="role", 123 | fileId=file_id, 124 | transferOwnership=True, 125 | body=permissions_settings)) 126 | 127 | if r == "owner": 128 | return True 129 | else: 130 | return False 131 | except(ValueError, KeyError, TypeError): 132 | return False 133 | -------------------------------------------------------------------------------- /libs/google/gmail.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from google.controller import GoogleApiController 4 | 5 | 6 | class GoogleGmailApi(GoogleApiController): 7 | def __init__(self, oauth): 8 | self.oauth = oauth 9 | self.service = self._get_service("gmail", "v1") 10 | 11 | def set_ooo_msg(self, user_id, text): 12 | """ 13 | Sets a vacation responder for the user. 14 | :param user_id: userId 15 | :return: bool 16 | """ 17 | 18 | vacation_settings = { 19 | 'enableAutoReply': True, 20 | 'responseBodyHtml': text, 21 | 'restrictToDomain': False, 22 | } 23 | try: 24 | r = json.loads(self.call_google_api(service=self.service, 25 | api_resource="users.settings", 26 | api_method="updateVacation", 27 | response_field="enableAutoReply", 28 | userId=user_id, 29 | body=vacation_settings)) 30 | return r 31 | except(ValueError, KeyError, TypeError): 32 | return False 33 | 34 | def is_forwarding_address(self, user_id, fwd_email): 35 | """ 36 | Checks whether an address is registered and verified for mail forwarding. 37 | :param user_id: userId 38 | :param email_address: emailAddress 39 | :return: bool 40 | """ 41 | try: 42 | r = json.loads(self.call_google_api(service=self.service, 43 | api_resource="users.settings.forwardingAddresses", 44 | api_method="get", 45 | response_field="verificationStatus", 46 | userId=user_id, 47 | forwardingEmail=fwd_email)) 48 | if r == "accepted": 49 | return True 50 | else: 51 | return False 52 | except(ValueError, KeyError, TypeError): 53 | return False 54 | 55 | def create_forwarding_address(self, user_id, fwd_email): 56 | """ 57 | Creates mail forwarding for a user. 58 | :param user_id: userId 59 | :param fwd_email: emailAddress 60 | :return: bool 61 | """ 62 | forwarding_settings = { 63 | 'forwardingEmail': fwd_email, 64 | 'verificationStatus': 'accepted' 65 | } 66 | try: 67 | r = json.loads(self.call_google_api(service=self.service, 68 | api_resource="users.settings.forwardingAddresses", 69 | api_method="create", 70 | response_field="verificationStatus", 71 | userId=user_id, 72 | body=forwarding_settings)) 73 | if r == "accepted": 74 | return True 75 | else: 76 | return False 77 | except(ValueError, TypeError, KeyError): 78 | return False 79 | 80 | def set_mail_forwarding(self, user_id, fwd_email): 81 | """ 82 | Enables mail forwarding for the user. 83 | :param user_id: userId 84 | :param fwd_email: emailAddress 85 | :return: bool 86 | """ 87 | 88 | if not self.is_forwarding_address(user_id=user_id, fwd_email=fwd_email): 89 | self.create_forwarding_address(user_id=user_id, fwd_email=fwd_email) 90 | 91 | forwarding_settings = { 92 | 'emailAddress': fwd_email, 93 | 'enabled': True, 94 | 'disposition': 'leaveInInbox', 95 | } 96 | 97 | try: 98 | r = json.loads(self.call_google_api(service=self.service, 99 | api_resource="users.settings", 100 | api_method="updateAutoForwarding", 101 | response_field="enabled", 102 | userId=user_id, 103 | body=forwarding_settings)) 104 | return r 105 | except(ValueError, TypeError, KeyError): 106 | return False 107 | 108 | def disable_pop(self, user_id): 109 | """ 110 | Disables POP for the user. 111 | :param user_id: 112 | :return: bool 113 | Note: If accessWindow=disabled, then POP is disabled. 114 | """ 115 | pop_setting = { 116 | 'accessWindow': 'disabled' 117 | } 118 | 119 | try: 120 | r = json.loads(self.call_google_api(service=self.service, 121 | api_resource="users.settings", 122 | api_method="updatePop", 123 | response_field="accessWindow", 124 | userId=user_id, 125 | body=pop_setting)) 126 | if r == "disabled": 127 | return True 128 | else: 129 | return False 130 | except(ValueError, KeyError, TypeError): 131 | return False 132 | 133 | def disable_imap(self, user_id): 134 | """ 135 | Disables IMAP for the user. 136 | :param user_id: 137 | :return: bool 138 | Note: If enabled=False, then IMAP is disabled. 139 | """ 140 | imap_setting = { 141 | 'enabled': 'False' 142 | } 143 | 144 | try: 145 | r = json.loads(self.call_google_api(service=self.service, 146 | api_resource="users.settings", 147 | api_method="updateImap", 148 | response_field="enabled", 149 | userId=user_id, 150 | body=imap_setting)) 151 | if r is False: 152 | return True 153 | else: 154 | return False 155 | except(ValueError, KeyError, TypeError): 156 | return False 157 | -------------------------------------------------------------------------------- /runner.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from Crypto import Random 4 | from twitter.common import log 5 | 6 | from google.admin import GoogleAdminApi 7 | from google.calendar import GoogleCalendarApi 8 | from google.drive import GoogleDriveApi 9 | from google.gmail import GoogleGmailApi 10 | from google.oauth import GoogleOAuthApi 11 | from helper_functions import HelperFunctions 12 | from ldap_client import LDAPClient 13 | from pagerduty import PagerDutyApi 14 | from duo import DuoAdminApi 15 | 16 | config = HelperFunctions().read_config_from_yaml() 17 | 18 | NO_SUSPEND_ACTIONS = ["remove_from_oncalls", "org_unit_reset"] 19 | 20 | 21 | class Runner(object): 22 | def __init__(self, user=None): 23 | Random.atfork() 24 | self.ADMIN_USER = config["google_apps"]["admin_user"] 25 | self.DOMAIN = config["google_apps"]["domain"] 26 | self.use_proxy = config["defaults"]["http_proxy"]["use_proxy"] 27 | 28 | self.user = user 29 | self.admin_email = "%s@%s" % (self.ADMIN_USER, self.DOMAIN) 30 | self.user_email = "%s@%s" % (user, self.DOMAIN) 31 | 32 | self.google_oauth = GoogleOAuthApi(config=config) 33 | self.oauth_admin = self.google_oauth.get_oauth_token(self.admin_email) 34 | self.oauth_user = self.google_oauth.get_oauth_token(self.user_email) 35 | 36 | self.admin_api = GoogleAdminApi(self.oauth_admin, config=config["google_apps"]) 37 | self.gmail_api = GoogleGmailApi(self.oauth_user) 38 | self.drive_api = GoogleDriveApi(self.oauth_user) 39 | self.calendar_api = GoogleCalendarApi(self.oauth_user) 40 | 41 | self.ldap_client = LDAPClient(config=config["ldap"]) 42 | 43 | self.is_valid_user = self._is_valid_user() 44 | self.is_suspended_user = self._is_suspended_user() 45 | 46 | self.pagerduty_api = PagerDutyApi(config=config["pagerduty"], 47 | use_proxy=self.use_proxy, 48 | proxy_config=config["defaults"]["http_proxy"]) 49 | 50 | self.duo_api = DuoAdminApi(config=config["duo"], 51 | use_proxy=self.use_proxy, 52 | proxy_config=config["defaults"]["http_proxy"]) 53 | 54 | def _is_valid_user(self): 55 | """ 56 | Checks whether the user is a valid LDAP user. 57 | :return: bool 58 | Note: Additional check now makes sure the user's name on LDAP matches than on Google Apps. 59 | """ 60 | name_on_gapps = "" 61 | is_valid = self.ldap_client.is_valid_user(user=self.user) 62 | user_info = self.ldap_client.get_user_info(user=self.user) 63 | name_on_ldap = user_info["sn"][0] 64 | if self.oauth_admin is not None: 65 | try: 66 | name_on_gapps = ("{familyName}" 67 | .format(**self.admin_api.get_user_name(self.user_email))) 68 | except (TypeError, UnicodeEncodeError) as e: 69 | log.info("is_valid: %s" % e) 70 | log.info("user: %s - is_valid: %r - name_on_ldap: %s - name_on_gapps: %s" % 71 | (self.user, is_valid, name_on_ldap, name_on_gapps)) 72 | if is_valid and name_on_ldap == name_on_gapps: 73 | return True 74 | else: 75 | return False 76 | 77 | def _is_suspended_user(self): 78 | """ 79 | Checks whether the user is suspended. 80 | :return: bool 81 | """ 82 | is_suspended = False 83 | if self.is_valid_user: 84 | is_suspended = self.admin_api.is_suspended(self.user_email) 85 | log.info("user: %s - is_suspended: %r" % (self.user, is_suspended)) 86 | return is_suspended 87 | 88 | def suspend_user(self, suspend): 89 | """ 90 | Suspends or un-suspends a user. 91 | :param: suspend: bool 92 | """ 93 | msg = "" 94 | if self.is_valid_user: 95 | if suspend and self._is_suspended_user(): 96 | msg = "%s - User already suspended" % self.user 97 | elif suspend and not self._is_suspended_user(): 98 | self.admin_api.suspend(self.user_email) 99 | msg = "%s - User was suspended" % self.user 100 | elif not suspend and self._is_suspended_user(): 101 | self.admin_api.un_suspend(self.user_email) 102 | while self._is_suspended_user(): # workaround for google api delays in propagation 103 | time.sleep(8) 104 | msg = "%s - User was un-suspended" % self.user 105 | elif not suspend and not self._is_suspended_user(): 106 | msg = "%s - User already un-suspended" % self.user 107 | else: 108 | msg = "%s - Not a valid LDAP user" % self.user 109 | log.info(msg) 110 | return msg 111 | 112 | def perform_action(self, api_connector, action, kwargs): 113 | """ 114 | Performs offboarding actions selected from the Web UI form. 115 | :param api_connector: string of the API connector to use 116 | :param action: name of action to be performed 117 | :return: msg log for action performed, along with status 118 | Note: action item must match function names of the equivalent API class. 119 | """ 120 | msg = "" 121 | if self.is_valid_user: 122 | if self.is_suspended_user and action not in NO_SUSPEND_ACTIONS: 123 | self.suspend_user(False) 124 | connector = getattr(self, api_connector) 125 | result = getattr(connector, action)(self.user_email, **kwargs) 126 | 127 | if type(result) is dict: 128 | results = [] 129 | for k, v in result.items(): 130 | if v is True: 131 | results.append("%s" % k) 132 | elif v is False: 133 | results.append("%s" % k) 134 | else: 135 | results.append(k) 136 | if not results: 137 | results = "SUCCESS" 138 | msg = "

%s: %s

" % (action.replace("_", " ").upper(), results) 139 | else: 140 | if result is False: 141 | msg_color = "danger" 142 | msg_text = "FAILED" 143 | elif result is True: 144 | msg_color = "success" 145 | msg_text = "SUCCESS" 146 | elif result is None: 147 | msg_color = "danger" 148 | msg_text = "FAILED (EMPTY RESULT)" 149 | else: 150 | msg_color = "success" 151 | msg_text = "SUCCESS" 152 | msg = ("

%s: %s

" % 153 | (action.replace("_", " ").upper(), msg_color, msg_text)) 154 | else: 155 | msg = "

FAILED - INVALID USER

" 156 | log.info(msg) 157 | return msg 158 | -------------------------------------------------------------------------------- /tests/libs/google/admin_tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | from unittest import TestCase 5 | 6 | from mock import MagicMock, patch 7 | from google.admin import GoogleAdminApi 8 | 9 | CALL_GOOGLE_API = "google.admin.GoogleApiController.call_google_api" 10 | 11 | 12 | class SomeGoogleAdminApi(GoogleAdminApi): 13 | def __init__(self, oauth): 14 | self.config = MagicMock() 15 | self.oauth = oauth 16 | self.service = MagicMock() 17 | 18 | 19 | class GoogleAdminApiTests(TestCase): 20 | def read_resource_file(self, res_file): 21 | mock_results_dir = "tests/libs/google/mock_results" 22 | try: 23 | mock_resource_file = os.path.join(mock_results_dir, res_file) 24 | with open(mock_resource_file, "r") as mock_resource_fp: 25 | self.mock_resource = json.loads(mock_resource_fp.read()) 26 | mock_resource_fp.close() 27 | except IOError: 28 | self.mock_resource = None 29 | 30 | return self.mock_resource 31 | 32 | @patch(CALL_GOOGLE_API) 33 | def test_get_user_name(self, mock_google_api): 34 | mock_google_api.return_value = json.dumps(self.read_resource_file('get_user_name')["fullName"]) 35 | google_admin = SomeGoogleAdminApi(oauth=MagicMock()) 36 | get_user_name = google_admin.get_user_name("hkantas@testdomain.com") 37 | assert get_user_name == "Harry Kantas" 38 | 39 | @patch(CALL_GOOGLE_API) 40 | def test_is_suspended_pass(self, mock_google_api): 41 | mock_google_api.return_value = json.dumps(self.read_resource_file('user_resource')["suspended"]) 42 | google_admin = SomeGoogleAdminApi(oauth=MagicMock()) 43 | is_suspended = google_admin.is_suspended("someuser@somewhere.org") 44 | assert is_suspended is False 45 | 46 | @patch(CALL_GOOGLE_API) 47 | def test_is_suspended_fail(self, mock_google_api): 48 | mock_google_api.return_value = self.read_resource_file('user_resource_malformed') 49 | google_admin = SomeGoogleAdminApi(oauth=MagicMock()) 50 | is_suspended = google_admin.is_suspended("someuser@somewhere.org") 51 | assert is_suspended is None 52 | 53 | @patch(CALL_GOOGLE_API) 54 | def test_suspend(self, mock_google_api): 55 | mock_google_api.return_value = json.dumps(self.read_resource_file('suspend')["suspended"]) 56 | google_admin = SomeGoogleAdminApi(oauth=MagicMock()) 57 | suspend = google_admin.suspend("someuser@somewhere.org") 58 | assert suspend is True 59 | 60 | @patch(CALL_GOOGLE_API) 61 | def test_un_suspend(self, mock_google_api): 62 | mock_google_api.return_value = json.dumps(self.read_resource_file('un_suspend')["suspended"]) 63 | google_admin = SomeGoogleAdminApi(oauth=MagicMock()) 64 | suspend = google_admin.suspend("someuser@somewhere.org") 65 | assert suspend is True 66 | 67 | @patch(CALL_GOOGLE_API) 68 | def test_delete_user_pass(self, mock_google_api): 69 | mock_google_api.return_value = "" 70 | google_admin = SomeGoogleAdminApi(oauth=MagicMock()) 71 | delete_user = google_admin.delete_user("someuser@somewhere.org") 72 | assert delete_user is True 73 | 74 | @patch(CALL_GOOGLE_API) 75 | def test_delete_user_fail(self, mock_google_api): 76 | mock_google_api.return_value = json.dumps(self.read_resource_file('delete_user_resource_fail')) 77 | google_admin = SomeGoogleAdminApi(oauth=MagicMock()) 78 | delete_user = google_admin.delete_user("someuser@somewhere.org") 79 | assert delete_user is False 80 | 81 | @patch(CALL_GOOGLE_API) 82 | def test_list_asps_pass(self, mock_google_api): 83 | mock_google_api.return_value = json.dumps(self.read_resource_file('asps_resource')["items"]) 84 | google_admin = SomeGoogleAdminApi(oauth=MagicMock()) 85 | list_asps = google_admin.list_asps("items") 86 | assert list_asps == [0, 2, 3] 87 | 88 | @patch(CALL_GOOGLE_API) 89 | def test_list_asps_fail(self, mock_google_api): 90 | mock_google_api.return_value = self.read_resource_file('asps_resource_malformed')["items"] 91 | google_admin = SomeGoogleAdminApi(oauth=MagicMock()) 92 | list_asps = google_admin.list_asps("items") 93 | assert list_asps is False 94 | 95 | @patch(CALL_GOOGLE_API) 96 | def test_tokens_list_pass(self, mock_google_api): 97 | mock_google_api.return_value = json.dumps(self.read_resource_file('tokens_resource')["items"]) 98 | google_admin = SomeGoogleAdminApi(oauth=MagicMock()) 99 | list_tokens = google_admin.list_tokens("clientId") 100 | assert list_tokens == [("1011230515163-arsl01gv134b0sjidu62bkp3hub3nuj3." 101 | "apps.googleusercontent.com")] 102 | 103 | @patch(CALL_GOOGLE_API) 104 | def test_tokens_list_fail(self, mock_google_api): 105 | mock_google_api.return_value = self.read_resource_file('tokens_resource_malformed')["items"] 106 | google_admin = SomeGoogleAdminApi(oauth=MagicMock()) 107 | list_tokens = google_admin.list_tokens("clientId") 108 | assert list_tokens is None 109 | 110 | @patch(CALL_GOOGLE_API) 111 | def test_list_backup_codes_pass(self, mock_google_api): 112 | mock_google_api.return_value = json.dumps(self.read_resource_file 113 | ('verification_resource')["items"]) 114 | google_admin = SomeGoogleAdminApi(oauth=MagicMock()) 115 | list_backup_codes = google_admin.list_backup_codes("verificationCode") 116 | assert "26975385" in list_backup_codes 117 | 118 | @patch(CALL_GOOGLE_API) 119 | def test_list_backup_codes_fail(self, mock_google_api): 120 | mock_google_api.return_value = self.read_resource_file('verification_resource_fail') 121 | google_admin = SomeGoogleAdminApi(oauth=MagicMock()) 122 | list_backup_codes = google_admin.list_backup_codes("verificationCode") 123 | assert list_backup_codes is False 124 | 125 | @patch(CALL_GOOGLE_API) 126 | def test_org_unit_change_pass(self, mock_google_api): 127 | mock_google_api.return_value = json.dumps(self.read_resource_file 128 | ('org_unit_change')["primaryEmail"]) 129 | google_admin = SomeGoogleAdminApi(oauth=MagicMock()) 130 | org_unit_change = google_admin.org_unit_change("stillings@testdomain.com") 131 | assert org_unit_change is True 132 | 133 | @patch(CALL_GOOGLE_API) 134 | def test_org_unit_change_fail(self, mock_google_api): 135 | mock_google_api.return_value = self.read_resource_file('org_unit_change')["primaryEmail"] 136 | google_admin = SomeGoogleAdminApi(oauth=MagicMock()) 137 | org_unit_change = google_admin.org_unit_change("stillings@testdomain.com") 138 | assert org_unit_change is False 139 | 140 | @patch(CALL_GOOGLE_API) 141 | def test_group_members_list_pass(self, mock_google_api): 142 | mock_google_api.return_value = json.dumps(self.read_resource_file 143 | ('group_member_list')["members"]) 144 | google_admin = SomeGoogleAdminApi(oauth=MagicMock()) 145 | group_member_list = google_admin.group_member_list("group_key") 146 | assert group_member_list[1] == "phewson@testdomain.com" 147 | 148 | @patch(CALL_GOOGLE_API) 149 | def test_group_members_list_fail(self, mock_google_api): 150 | mock_google_api.return_value = json.dumps(self.read_resource_file('group_member_list_fail')) 151 | google_admin_api = SomeGoogleAdminApi(oauth=MagicMock()) 152 | group_member_list = google_admin_api.group_member_list("group_key") 153 | assert group_member_list is None 154 | 155 | @patch(CALL_GOOGLE_API) 156 | def test_group_member_delete_pass(self, mock_google_api): 157 | mock_google_api.return_value = json.dumps(self.read_resource_file 158 | ('group_member_delete')["members"]) 159 | google_admin_api = SomeGoogleAdminApi(oauth=MagicMock()) 160 | group_member_delete = google_admin_api.group_member_delete("group_key", "user_key") 161 | assert group_member_delete is True 162 | 163 | @patch(CALL_GOOGLE_API) 164 | def test_group_member_delete_fail(self, mock_google_api): 165 | mock_google_api.return_value = json.dumps(self.read_resource_file('group_member_delete_fail')) 166 | google_admin_api = SomeGoogleAdminApi(oauth=MagicMock()) 167 | group_member_delete = google_admin_api.group_member_delete("group_key", "user_key") 168 | assert group_member_delete is False 169 | -------------------------------------------------------------------------------- /tests/libs/duo/mock_results/list_users_resource: -------------------------------------------------------------------------------- 1 | [{"status": "disabled", "username": "mat", "desktoptokens": [], "user_id": "DUUCQHSE99RRJBVOLV0W", "realname": "Mat Clinton", "firstname": "", "created": 1528219451, "alias2": null, "alias3": null, "notes": "", "alias1": null, "alias4": null, "tokens": [], "last_login": null, "last_directory_sync": null, "groups": [], "lastname": "", "phones": [], "u2ftokens": [], "email": "mat@somewhere.com"}, {"status": "active", "username": "adatta", "desktoptokens": [], "user_id": "DUWB1SHWG1Z7X2F92G3G", "realname": "Ari D", "firstname": "", "created": 1522362515, "alias2": null, "alias3": null, "notes": "", "alias1": null, "alias4": null, "tokens": [], "last_login": null, "last_directory_sync": null, "groups": [], "lastname": "", "phones": [], "u2ftokens": [], "email": "smamidigumpula@somewhere.com"}, {"status": "active", "username": "akaliyeva", "desktoptokens": [], "user_id": "DUMMTPF5X85O0PZM4IJW", "realname": "akaliyeva", "firstname": "", "created": 1522176173, "alias2": null, "alias3": null, "notes": "", "alias1": null, "alias4": null, "tokens": [], "last_login": null, "last_directory_sync": null, "groups": [], "lastname": "", "phones": [], "u2ftokens": [], "email": "akaliyeva@twittertest.com"}, {"status": "active", "username": "amysaper", "desktoptokens": [], "user_id": "DUR5N15DYKU4PT9NBZA0", "realname": "Amy Saper", "firstname": "", "created": 1528488822, "alias2": null, "alias3": null, "notes": "", "alias1": null, "alias4": null, "tokens": [], "last_login": null, "last_directory_sync": null, "groups": [], "lastname": "", "phones": [], "u2ftokens": [], "email": "amysaper@somewhere.com"}, {"status": "active", "username": "andrewpabian", "desktoptokens": [], "user_id": "DU01VAE8UETGPOHWI845", "realname": "Andrew Pabian", "firstname": "", "created": 1528218796, "alias2": null, "alias3": null, "notes": "", "alias1": null, "alias4": null, "tokens": [], "last_login": null, "last_directory_sync": null, "groups": [], "lastname": "", "phones": [], "u2ftokens": [], "email": "andrewpabian@somewhere.com"}, {"status": "active", "username": "apadin", "desktoptokens": [], "user_id": "DU9G7NLVPB4L9E4EKXLF", "realname": "Anthony Padin", "firstname": "", "created": 1528157441, "alias2": null, "alias3": null, "notes": "", "alias1": null, "alias4": null, "tokens": [], "last_login": null, "last_directory_sync": null, "groups": [], "lastname": "", "phones": [], "u2ftokens": [], "email": "apadin@somewhere.com"}, {"status": "active", "username": "asamsudin", "desktoptokens": [], "user_id": "DU3ZWSBUZYJ5GCXHXRNZ", "realname": "Adi Hardiana Samsudin", "firstname": "", "created": 1524030926, "alias2": null, "alias3": null, "notes": "", "alias1": null, "alias4": null, "tokens": [], "last_login": null, "last_directory_sync": null, "groups": [], "lastname": "", "phones": [], "u2ftokens": [], "email": "smamidigumpula@somewhere.com"}, {"status": "disabled", "username": "athavorides", "desktoptokens": [], "user_id": "DUUJLKM1GR8OZRIQKED9", "realname": "Alyssa Thavorides", "firstname": "", "created": 1522175177, "alias2": null, "alias3": null, "notes": "", "alias1": null, "alias4": null, "tokens": [], "last_login": null, "last_directory_sync": null, "groups": [], "lastname": "", "phones": [{"name": "", "extension": "", "sms_passcodes_sent": false, "phone_id": "DPFAJOZLHJ1UZYUC610F", "activated": false, "number": "+15106249949", "capabilities": ["auto", "sms", "phone"], "platform": "Unknown", "predelay": "", "postdelay": "", "type": "Unknown", "last_seen": ""}], "u2ftokens": [], "email": "athavorides@twitter.test.com"}, {"status": "disabled", "username": "ayan", "desktoptokens": [], "user_id": "DUGX3JKQRQ2D4GG1ZRCG", "realname": "Amanda Yan", "firstname": "", "created": 1522184079, "alias2": null, "alias3": null, "notes": "", "alias1": null, "alias4": null, "tokens": [], "last_login": null, "last_directory_sync": null, "groups": [], "lastname": "", "phones": [], "u2ftokens": [], "email": "ayan@twittertest.com"}, {"status": "active", "username": "cadamson", "desktoptokens": [], "user_id": "DUV8M292MES0N02WZ9LU", "realname": "Celeste Ridlen", "firstname": "", "created": 1528358668, "alias2": null, "alias3": null, "notes": "", "alias1": null, "alias4": null, "tokens": [], "last_login": null, "last_directory_sync": null, "groups": [], "lastname": "", "phones": [], "u2ftokens": [], "email": "cadamson@somewhere.com"}, {"status": "active", "username": "fhrenic", "desktoptokens": [], "user_id": "DUB962UBJAEYXNJ1U05G", "realname": "Filip Hrenic", "firstname": "", "created": 1528835283, "alias2": null, "alias3": null, "notes": "", "alias1": null, "alias4": null, "tokens": [], "last_login": null, "last_directory_sync": null, "groups": [], "lastname": "", "phones": [{"name": "", "extension": "", "sms_passcodes_sent": false, "phone_id": "DPK8UM7IT1GT6C96ASWY", "activated": true, "number": "+14153086702", "capabilities": ["auto", "push", "sms", "phone", "mobile_otp"], "platform": "Apple iOS", "predelay": "", "postdelay": "", "type": "Mobile", "last_seen": "2018-06-16T01:56:04"}], "u2ftokens": [], "email": "fhrenic@somewhere.com"}, {"status": "disabled", "username": "funtest", "desktoptokens": [], "user_id": "DUH7D0UZ9A912INRDBXI", "realname": "fun test", "firstname": "", "created": 1528966402, "alias2": null, "alias3": null, "notes": "", "alias1": null, "alias4": null, "tokens": [], "last_login": null, "last_directory_sync": null, "groups": [], "lastname": "", "phones": [], "u2ftokens": [], "email": "funtest@somewhere.com"}, {"status": "active", "username": "jstrauss", "desktoptokens": [], "user_id": "DUVCWCBAENYFZ0R3LO32", "realname": "Jeff Strauss", "firstname": null, "created": 1527195029, "alias2": null, "alias3": null, "notes": "", "alias1": null, "alias4": null, "tokens": [], "last_login": null, "last_directory_sync": null, "groups": [], "lastname": null, "phones": [{"name": "", "extension": "", "sms_passcodes_sent": false, "phone_id": "DPA96OGS9LRWTVWUCONU", "activated": false, "number": "+14159903977", "capabilities": ["auto", "sms", "phone"], "platform": "Generic Smartphone", "predelay": "0", "postdelay": "0", "type": "Mobile", "last_seen": ""}], "u2ftokens": [], "email": "jstrauss@somewhere.com"}, {"status": "disabled", "username": "mpe", "desktoptokens": [], "user_id": "DUKO01W1GGMLA9N8U65P", "realname": "Mary Lynne Joyce Pe", "firstname": "", "created": 1528391362, "alias2": null, "alias3": null, "notes": "", "alias1": null, "alias4": null, "tokens": [], "last_login": null, "last_directory_sync": null, "groups": [], "lastname": "", "phones": [], "u2ftokens": [], "email": "smamidigumpula@somewhere.com"}, {"status": "disabled", "username": "qazsdf", "desktoptokens": [], "user_id": "DULF73HH9HEQR11E4PCS", "realname": "qaz sdf", "firstname": "", "created": 1528452620, "alias2": null, "alias3": null, "notes": "", "alias1": null, "alias4": null, "tokens": [], "last_login": null, "last_directory_sync": null, "groups": [], "lastname": "", "phones": [], "u2ftokens": [], "email": "qazsdf@somewhere.com"}, {"status": "disabled", "username": "rfactor", "desktoptokens": [], "user_id": "DUS1BX1CDC3ILQ36LDB4", "realname": "rfactor", "firstname": "", "created": 1523038950, "alias2": null, "alias3": null, "notes": "", "alias1": null, "alias4": null, "tokens": [], "last_login": null, "last_directory_sync": null, "groups": [], "lastname": "", "phones": [], "u2ftokens": [], "email": "smamidigumpula@somewhere.com"}, {"status": "active", "username": "testduo1", "desktoptokens": [], "user_id": "DUI3Z64KIU0HP3946RMO", "realname": "", "firstname": null, "created": 1519153771, "alias2": null, "alias3": null, "notes": "", "alias1": null, "alias4": null, "tokens": [], "last_login": null, "last_directory_sync": null, "groups": [], "lastname": null, "phones": [], "u2ftokens": [], "email": ""}, {"status": "disabled", "username": "testqwe", "desktoptokens": [], "user_id": "DU2UAIRCGL8JAVL2CNL9", "realname": "Test qwe", "firstname": "", "created": 1528450975, "alias2": null, "alias3": null, "notes": "", "alias1": null, "alias4": null, "tokens": [], "last_login": null, "last_directory_sync": null, "groups": [], "lastname": "", "phones": [], "u2ftokens": [], "email": "testqwe@somewhere.com"}, {"status": "active", "username": "ttw1last7", "desktoptokens": [], "user_id": "DU1TFQ4722APHP11WNER", "realname": "tw1first7 tw1last7", "firstname": "", "created": 1528835263, "alias2": null, "alias3": null, "notes": "", "alias1": null, "alias4": null, "tokens": [], "last_login": null, "last_directory_sync": null, "groups": [], "lastname": "", "phones": [], "u2ftokens": [], "email": "ttw1last7@somewhere.com"}, {"status": "active", "username": "tweet5", "desktoptokens": [], "user_id": "DUMC70PBXWB3RFXDRLZ8", "realname": "tweetfn5 tweetln5", "firstname": "", "created": 1528868829, "alias2": null, "alias3": null, "notes": "", "alias1": null, "alias4": null, "tokens": [], "last_login": null, "last_directory_sync": null, "groups": [], "lastname": "", "phones": [], "u2ftokens": [], "email": "tweet5@somewhere.com"}, {"status": "active", "username": "twun10", "desktoptokens": [], "user_id": "DUCUXVAIHSDIHDTEUYYE", "realname": "tw1first10 tw1last10", "firstname": "", "created": 1528868812, "alias2": null, "alias3": null, "notes": "", "alias1": null, "alias4": null, "tokens": [], "last_login": null, "last_directory_sync": null, "groups": [], "lastname": "", "phones": [], "u2ftokens": [], "email": "twun10@somewhere.com"}, {"status": "active", "username": "ustest", "desktoptokens": [], "user_id": "DULAEI8X729VO8BDG6AT", "realname": "use test", "firstname": "", "created": 1528911840, "alias2": null, "alias3": null, "notes": "", "alias1": null, "alias4": null, "tokens": [], "last_login": null, "last_directory_sync": null, "groups": [], "lastname": "", "phones": [], "u2ftokens": [], "email": "ustest@somewhere.com"}] 2 | -------------------------------------------------------------------------------- /libs/pagerduty/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from http_controller import HttpController 4 | 5 | 6 | class PagerDutyApi(HttpController): 7 | def __init__(self, config=None, **kwargs): 8 | self.config = config 9 | self.base_url = self.config["base_url"] 10 | self.api_key = self.config["api_key"] 11 | self.HEADERS = {"Authorization": "Token token={0}".format(self.api_key), 12 | "Accept": "application/vnd.pagerduty+json;version=2", 13 | "Content-type": "application/json"} 14 | super(PagerDutyApi, self).__init__(base_url=self.base_url, **kwargs) 15 | 16 | def find_user_id(self, user_email): 17 | """ 18 | Returns a user, matching the given email. 19 | :param user_email: user email 20 | :return: user object 21 | """ 22 | user_id = None 23 | params = {"query": user_email} 24 | try: 25 | list_users = self.api_request(method="get", 26 | endpoint="users", 27 | params=params, 28 | headers=self.HEADERS) 29 | if "users" in list_users and len(list_users["users"]) > 0: 30 | for user in list_users["users"]: 31 | if user["email"] == user_email: 32 | user_id = user["id"] 33 | except ValueError as e: 34 | print(e) 35 | 36 | return user_id 37 | 38 | def get_schedule_by_id(self, schedule_id): 39 | """ 40 | retrieves a schedule by its ID. 41 | :param schedule_id 42 | :return: schedule object 43 | """ 44 | r = self.api_request(method="get", 45 | endpoint="schedules/{id}".format(id=schedule_id), 46 | headers=self.HEADERS) 47 | return r 48 | 49 | def get_escalation_policy_by_id(self, policy_id): 50 | """ 51 | retrieves an escalation policy by its ID. 52 | :param policy_id 53 | :return: escalation policy object 54 | """ 55 | r = self.api_request(method="get", 56 | endpoint="escalation_policies/{id}".format(id=policy_id), 57 | headers=self.HEADERS) 58 | return r 59 | 60 | def get_service_by_id(self, service_id): 61 | """ 62 | retrieves a service by its ID. 63 | :param service_id 64 | :return: service object 65 | """ 66 | params = {"include[]": "escalation_policies"} 67 | r = self.api_request(method="get", 68 | endpoint="services/{id}".format(id=service_id), 69 | params=params, 70 | headers=self.HEADERS) 71 | return r 72 | 73 | def update_schedule_by_id(self, schedule_id, schedule): 74 | """ 75 | Updates a schedule by ID. 76 | :param schedule_id: schedule ID 77 | :param schedule: schedule object 78 | :return: bool 79 | """ 80 | updated = True 81 | r = self.api_request(method="put", 82 | endpoint="schedules/{id}".format(id=schedule_id), 83 | data=json.dumps(schedule), 84 | headers=self.HEADERS) 85 | if "error" in r: 86 | updated = False 87 | return updated 88 | 89 | def update_escalation_policy_by_id(self, policy_id, policy): 90 | """ 91 | Updates an escalation policy by ID. 92 | :param policy_id: policy ID 93 | :param policy: escalation_policy object 94 | :return: bool 95 | """ 96 | updated = True 97 | r = self.api_request(method="put", 98 | endpoint="escalation_policies/{id}".format(id=policy_id), 99 | data=json.dumps(policy), 100 | headers=self.HEADERS) 101 | if "error" in r: 102 | updated = False 103 | 104 | return updated 105 | 106 | def update_service_by_id(self, service_id, service): 107 | """ 108 | Updates a service by ID. 109 | :param service_id: service ID 110 | :param service: service object 111 | :return: bool 112 | """ 113 | updated = True 114 | r = self.api_request(method="put", 115 | endpoint="services/{id}".format(id=service_id), 116 | data=json.dumps(service), 117 | headers=self.HEADERS) 118 | if "error" in r: 119 | updated = False 120 | 121 | return updated 122 | 123 | def delete_escalation_policy_by_id(self, policy_id): 124 | """ 125 | Deletes an escalation policy by ID. 126 | :param policy_id: policy ID 127 | :return: bool 128 | """ 129 | deleted = False 130 | r = self.api_request(method="delete", 131 | endpoint="escalation_policies/{id}".format(id=policy_id), 132 | response_type="text", 133 | headers=self.HEADERS) 134 | try: 135 | response = json.loads(r) 136 | if "error" in response: 137 | deleted = False 138 | except json.JSONDecoderError: 139 | if r == "": 140 | deleted = True 141 | return deleted 142 | 143 | def remove_user_from_schedule(self, schedule_id, user_id): 144 | """ 145 | remove user from a schedule. 146 | :param schedule_id: schedule_id 147 | :param user_id: user_id 148 | :return: bool 149 | """ 150 | schedule = self.get_schedule_by_id(schedule_id=schedule_id) 151 | 152 | for layer in schedule["schedule"]["schedule_layers"]: 153 | for user in layer["users"]: 154 | if user["user"]["id"] == user_id: 155 | user_idx = layer["users"].index(user) 156 | layer["users"].pop(user_idx) 157 | if len(layer["users"]) == 0: 158 | layer["users"].append(schedule["schedule"]["users"][0]) 159 | removed = self.update_schedule_by_id(schedule_id=schedule_id, schedule=schedule) 160 | 161 | return removed 162 | 163 | def remove_user_from_escalation_policy(self, policy_id, user_id, delete_empty=False): 164 | """ 165 | remove user from an escalation policy. 166 | :param policy_id: policy_id 167 | :param user_id: user_id 168 | :param delete_empty: whether to delete an escalation policy with no targets 169 | :return: bool 170 | """ 171 | removed = False 172 | policy = self.get_escalation_policy_by_id(policy_id=policy_id) 173 | 174 | if len(policy["escalation_policy"]["escalation_rules"]) == 1: 175 | rule = policy["escalation_policy"]["escalation_rules"][0] 176 | if len(rule["targets"]) > 1: 177 | for target in rule["targets"]: 178 | if target["id"] == user_id: 179 | target_idx = rule["targets"].index(target) 180 | rule["targets"].pop(target_idx) 181 | removed = self.update_escalation_policy_by_id(policy_id=policy_id, policy=policy) 182 | elif delete_empty is True: 183 | for service in policy["escalation_policy"]["services"]: 184 | self.delete_service(service_id=service["id"]) 185 | self.get_escalation_policy_by_id(policy_id=policy_id) 186 | removed = self.delete_escalation_policy_by_id(policy_id=policy_id) 187 | 188 | return removed 189 | 190 | def delete_service(self, service_id): 191 | """ 192 | Deletes a service. 193 | :param service_id: id 194 | :return: user object 195 | """ 196 | deleted = False 197 | r = self.api_request(method="delete", 198 | endpoint="services/{id}".format(id=service_id), 199 | response_type="text", 200 | headers=self.HEADERS) 201 | if r == "": 202 | deleted = True 203 | return deleted 204 | 205 | def delete_user(self, user_id): 206 | """ 207 | Deletes a user. 208 | :param user_id: id 209 | :return: user object 210 | """ 211 | r = self.api_request(method="delete", 212 | endpoint="users/{id}".format(id=user_id), 213 | response_type="text", 214 | headers=self.HEADERS) 215 | 216 | return r 217 | 218 | def remove_from_oncalls(self, user_email): 219 | """ 220 | Removes a user from oncalls, and then deletes the user. 221 | :param email: user email 222 | :return: bool 223 | """ 224 | deleted = False 225 | user_id = self.find_user_id(user_email=user_email) 226 | if user_id is not None: 227 | try: 228 | userdel = json.loads(self.delete_user(user_id=user_id)) 229 | if "error" in userdel: 230 | for conflict in userdel["error"]["conflicts"]: 231 | if conflict["type"] == "schedule": 232 | self.remove_user_from_schedule(schedule_id=conflict["id"], user_id=user_id) 233 | elif conflict["type"] == "escalation_policy": 234 | self.remove_user_from_escalation_policy(policy_id=conflict["id"], 235 | user_id=user_id, 236 | delete_empty=True) 237 | userdel = self.delete_user(user_id=user_id) 238 | if userdel == "": 239 | deleted = True 240 | except ValueError: 241 | deleted = True 242 | else: 243 | deleted = True 244 | 245 | return deleted 246 | 247 | def list_oncalls(self, escalation_policy_ids): 248 | """ 249 | Returns all oncalls. 250 | :param escalation_policy_ids: list of escalation policy ids 251 | :return: list of oncalls resources 252 | """ 253 | params = {"escalation_policy_ids[]": escalation_policy_ids} 254 | r = self.api_request(method="get", 255 | endpoint="oncalls", 256 | params=params, 257 | headers=self.HEADERS) 258 | if "oncalls" in r: 259 | return r["oncalls"] 260 | else: 261 | return None 262 | -------------------------------------------------------------------------------- /templates/lost_asset.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block head %} 5 | 6 | 7 | 8 | 9 | 10 | 16 | GateKeeper 17 | 18 | {% endblock %} 19 | 20 | 21 | 22 | {% include "navbar.html" %} 23 | 24 | {% block options %} 25 |
26 |
27 |
28 |
29 | {{ form.csrf_token }} 30 | {{ form.USER_ID(class_="form-control typeahead", placeholder="Enter LDAP user name", size=40) }} 31 |
32 |
33 |
34 |
35 |
36 | {% with messages = get_flashed_messages() %} 37 | {% if messages %} 38 |
    39 | {% for message in messages %} 40 |

    {{ message }}

    41 | {% endfor %} 42 |
43 | {% endif %} 44 | {% endwith %} 45 |
46 |
47 | 48 | 49 | 68 | 135 | 136 |
50 |

As per the Lost Asset protocol,
the following actions will be performed:

51 |
52 |
  • Invalidate backup codes
  • 53 |
  • Purge application specific passwords
  • 54 |
  • Reset Google apps password
  • 55 |
  • Purge 3rd party access tokens
  • 56 |
  • Disable IMAP email
  • 57 |
  • Disable POP email
  • 58 |
  • Reset Sign-In cookies
  • 59 |
    60 |
    61 | {{ form.csrf_token }} 62 | {% for field in form if field.name in actions %} 63 | {{ field(class_="checkbox") }} {{ field.label }} 64 |
    65 | {% endfor %} 66 |
    67 |
    69 |
    70 | {% if user_info %} 71 |
    72 |
    73 |
    74 |
    Name:
    75 |
    {{ user_info[ldap_fields['full_name']] }}
    76 |
    77 |
    78 |
    Role:
    79 |
    {{ user_info[ldap_fields['title']] }}
    80 |
    81 |
    82 |
    Team:
    83 |
    {{ user_info[ldap_fields['department']] }}
    84 |
    85 |
    86 |
    Org:
    87 |
    {{ user_info[ldap_fields['org_role']] }}
    88 |
    89 |
    90 |
    Location:
    91 |
    {{ user_info[ldap_fields['office']] }}
    92 |
    93 |
    94 |
    Start date:
    95 |
    {{ user_info[ldap_fields['start_date']] }}
    96 |
    97 |
    98 |
    LDAP UID:
    99 |
    {{ user_info[ldap_fields['uid_number']] }}
    100 |
    101 |
    102 |
    LDAP groups:
    103 |
    104 | {% for group in user_info[ldap_fields['member_of']] %}{{ group }} {% endfor %} 105 |
    106 |
    107 |
    108 |
    Status:
    109 |
    110 | {% if user_info['suspended'] %} 111 | INACTIVE 112 | {% else %} 113 | ACTIVE 114 | {% endif %} 115 |
    116 |
    117 |
    118 | {% if user_info[ldap_fields['photo_url']] %} 119 |
    120 | 121 |
    122 | {% endif %} 123 |
    124 |
    125 |
    126 |
    127 |
    128 | Click to submit changes for {{ user_info[ldap_fields['first_name']][0] }}. 129 | 130 |
    131 |
    132 | {% endif %} 133 |
    134 |
    137 |
    138 |
    139 | 145 |
    146 | {% endblock %} 147 | 148 | 149 | 150 | 151 | 152 | 203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /libs/google/calendar.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from google.controller import GoogleApiController 4 | from helper_functions import HelperFunctions 5 | 6 | 7 | class GoogleCalendarApi(GoogleApiController): 8 | def __init__(self, oauth): 9 | self.oauth = oauth 10 | self.service = self._get_service("calendar", "v3") 11 | 12 | def get_calendar_id(self): 13 | """ 14 | Gets the Calendar ID for a user. 15 | :return: str(calendar_id) 16 | """ 17 | try: 18 | r = json.loads(self.call_google_api(service=self.service, 19 | api_resource="calendars", 20 | api_method="get", 21 | response_field="id", 22 | calendarId="primary")) 23 | return r 24 | except(ValueError, KeyError, TypeError): 25 | return None 26 | 27 | def list_events(self, user_email, role="organizer"): 28 | """ 29 | Lists the future dated events of a user. 30 | :param user_email: user_email 31 | :param role: organizer: attendee: owned_single_event 32 | :return: List of event ids. 33 | """ 34 | now = HelperFunctions().date_now() 35 | 36 | events = [] 37 | r = json.loads(self.call_google_api(service=self.service, 38 | api_resource="events", 39 | api_method="list", 40 | response_field="items", 41 | singleEvents=True, 42 | timeMin=now, 43 | orderBy="startTime", 44 | calendarId="primary")) 45 | 46 | if r is not None: 47 | 48 | if role == "organizer": 49 | for event in r: 50 | if (("organizer" in event) and 51 | (("self" in event["organizer"] and event["organizer"]["self"]) or 52 | (event["organizer"]["email"] == user_email))): 53 | if ("recurringEventId" in event): 54 | events.append(event["recurringEventId"]) 55 | else: 56 | events.append(event["id"]) 57 | 58 | elif role == "attendee": 59 | for event in r: 60 | if (("organizer" in event) and 61 | (("self" not in event["organizer"]) or 62 | (event["organizer"]["email"] != user_email))): 63 | if ("recurringEventId" in event): 64 | events.append(event["recurringEventId"]) 65 | else: 66 | events.append(event["id"]) 67 | 68 | elif role == "owned_single_event": 69 | for event in r: 70 | if (("organizer" in event) and 71 | (("self" in event["organizer"] and event["organizer"]["self"]) or 72 | (event["organizer"]["email"] == user_email))): 73 | if ("recurringEventId" not in event): 74 | events.append(event["id"]) 75 | 76 | return list(set(events)) 77 | 78 | def move_calendar_ownership(self, owner_calendar_id): 79 | acl = { 80 | "kind": "calendar#aclRule", 81 | "scope": { 82 | "type": "user", 83 | "value": owner_calendar_id 84 | }, 85 | "role": "owner" 86 | } 87 | try: 88 | r = json.loads(self.call_google_api(service=self.service, 89 | api_resource="acl", 90 | api_method="insert", 91 | response_field=None, 92 | calendarId="primary", 93 | body=acl)) 94 | return r 95 | except(ValueError, KeyError, TypeError): 96 | return None 97 | 98 | def move_event(self, event_id, destination_calendar_id): 99 | """ 100 | Change calendar event owner. 101 | :param event_id: eventId 102 | :param destination_calendar_id: destination 103 | :return: bool 104 | """ 105 | try: 106 | r = json.loads(self.call_google_api(service=self.service, 107 | api_resource="events", 108 | api_method="move", 109 | response_field="id", 110 | calendarId="primary", 111 | eventId=event_id, 112 | destination=destination_calendar_id)) 113 | 114 | if r == event_id: 115 | return True 116 | else: 117 | return False 118 | except(ValueError, KeyError, TypeError): 119 | return None 120 | 121 | def delete_event(self, event_id): 122 | """ 123 | Remove user from a calendar event. 124 | :param event_id: eventId 125 | :return: empty response when successful 126 | """ 127 | try: 128 | r = json.loads(self.call_google_api(service=self.service, 129 | api_resource="events", 130 | api_method="delete", 131 | response_field=None, 132 | calendarId="primary", 133 | eventId=event_id, 134 | sendNotifications=False)) 135 | if r is None: 136 | return True 137 | else: 138 | return False 139 | except(ValueError, KeyError, TypeError): 140 | return False 141 | 142 | def remove_events_attendance(self, user_email): 143 | """ 144 | Deletes events a user owns from their calendar. 145 | :param user_email: user_email 146 | :return: list of event_ids removed. 147 | """ 148 | events = self.list_events(user_email=user_email, role="owned_single_event") 149 | events_removed = [] 150 | if events: 151 | for event in events: 152 | res = self.delete_event(event_id=event) 153 | if res: 154 | events_removed.append(event) 155 | return events_removed 156 | 157 | def decline_event(self, event_id, user_email): 158 | """ 159 | Declines event. 160 | :param event_id: eventId 161 | :param user_email: user_email 162 | :return: event details 163 | """ 164 | 165 | patch_body = {"attendees": [{"responseStatus": "declined", "email": user_email}]} 166 | try: 167 | r = json.loads(self.call_google_api(service=self.service, 168 | api_resource="events", 169 | api_method="patch", 170 | response_field="attendees", 171 | calendarId="primary", 172 | eventId=event_id, 173 | sendNotifications=False, 174 | body=patch_body)) 175 | return r 176 | except(ValueError, KeyError, TypeError): 177 | return None 178 | 179 | def decline_events_attendance(self, user_email): 180 | """ 181 | Declines all events from a user's calendar. 182 | :param user_email: userEmail 183 | :return: list of event_ids removed 184 | """ 185 | events = self.list_events(user_email=user_email, role="attendee") 186 | events_declined = [] 187 | if events: 188 | for event in events: 189 | res = self.decline_event(event_id=event, user_email=user_email) 190 | if res: 191 | events_declined.append(event) 192 | return events_declined 193 | 194 | def get_event_rule(self, event_id): 195 | """ 196 | Gets the recurrence rrule value for an event. 197 | :param event_id: eventId 198 | :return: Recurrence rule as a list 199 | """ 200 | try: 201 | r = json.loads(self.call_google_api(service=self.service, 202 | api_resource="events", 203 | api_method="get", 204 | response_field="recurrence", 205 | calendarId="primary", 206 | eventId=event_id)) 207 | 208 | return r 209 | except(ValueError, KeyError, TypeError): 210 | return None 211 | 212 | def cancel_recurrence(self, event_id, new_event_rule): 213 | """ 214 | Updates event recurrence rule to cancel future instances. 215 | :param event_id: eventId 216 | :param new_event_rule: body 217 | :return: list of canceled event ids. 218 | """ 219 | 220 | patch_body = { 221 | "recurrence": new_event_rule 222 | } 223 | try: 224 | r = json.loads(self.call_google_api(service=self.service, 225 | api_resource="events", 226 | api_method="patch", 227 | response_field="id", 228 | calendarId="primary", 229 | eventId=event_id, 230 | sendNotifications=False, 231 | body=patch_body)) 232 | return r 233 | except(ValueError, KeyError, TypeError): 234 | return None 235 | 236 | def remove_recurring_instances(self, user_email): 237 | """ 238 | Cancels future event instances from a users calendar. 239 | :param user_email: user_email 240 | :return: list of canceled event ids. 241 | """ 242 | updated_events = [] 243 | recurring_events_list = self.list_events(user_email) 244 | if recurring_events_list: 245 | for event in recurring_events_list: 246 | event_rule = self.get_event_rule(event) 247 | if event_rule: 248 | new_event_rule = HelperFunctions.updated_event_rule(event_rule) 249 | updated_event = self.cancel_recurrence(event, new_event_rule) 250 | updated_events.append(updated_event) 251 | return updated_events 252 | else: 253 | return None 254 | 255 | def remove_future_events(self, user_email): 256 | """ 257 | Removes attendance from future events, and cancels recurring events. 258 | :param self: 259 | :param user_email: 260 | :return: set of cancelled event ids. 261 | """ 262 | cancelled_events = [] 263 | cancelled_events.append(self.remove_events_attendance(user_email)) 264 | cancelled_events.append(self.remove_recurring_instances(user_email)) 265 | cancelled_events.append(self.decline_events_attendance(user_email)) 266 | 267 | return cancelled_events 268 | -------------------------------------------------------------------------------- /templates/gdrive.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | GateKeeper 16 | 17 | 18 | 19 | 20 | {% include "navbar.html" %} 21 | 22 | {% block options %} 23 |
    24 |
    25 |
    26 |
    27 | {{ form.csrf_token }} 28 |
    29 |
    {{ form.USER_ID(class_="form-control typeahead", placeholder="Enter LDAP user name", size=40) }}
    30 |
    31 | {% if form["USER_ID"].data %} 32 |
    {{ form.FILE_SEARCH(class_="form-control", placeholder="File Search", size=28) }}
    33 |
    34 | {% endif %} 35 |
    36 |
    37 |
    38 |
    39 |
    40 |
    41 | {% with messages = get_flashed_messages() %} 42 | {% if messages %} 43 |
      44 | {% for message in messages %} 45 |

      {{ message }}

      46 | {% endfor %} 47 |
    48 | {% endif %} 49 | {% endwith %} 50 |
    51 |
    52 | {{ form.csrf_token }} 53 | 54 | 55 | 171 | 172 |
    56 |
    57 | {% if user_info %} 58 |
    59 |
    60 |
    61 |
    Name:
    62 |
    {{ user_info[ldap_fields['full_name']] }}
    63 |
    64 |
    65 |
    Role:
    66 |
    {{ user_info[ldap_fields['title']] }}
    67 |
    68 |
    69 |
    Team:
    70 |
    {{ user_info[ldap_fields['department']] }}
    71 |
    72 |
    73 |
    Org:
    74 |
    {{ user_info[ldap_fields['org_role']] }}
    75 |
    76 |
    77 |
    Location:
    78 |
    {{ user_info[ldap_fields['office']] }}
    79 |
    80 |
    81 |
    Start date:
    82 |
    {{ user_info[ldap_fields['start_date']] }}
    83 |
    84 |
    85 |
    LDAP UID:
    86 |
    {{ user_info[ldap_fields['uid_number']] }}
    87 |
    88 |
    89 |
    LDAP groups:
    90 |
    91 | {% for group in user_info[ldap_fields['member_of']] %}{{ group }} {% endfor %} 92 |
    93 |
    94 |
    95 |
    Status:
    96 |
    97 | {% if user_info['suspended'] %} 98 | INACTIVE 99 | {% else %} 100 | ACTIVE 101 | {% endif %} 102 |
    103 |
    104 |
    105 | {% if user_info[ldap_fields['photo_url']] %} 106 |
    107 | 108 |
    109 | {% endif %} 110 |
    111 |
    112 | {% if files %} 113 |
    114 |

    Select the file(s) you want to transfer to a new owner

    115 |
    116 |
    117 |
    118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | {% for file in files %} 129 | 130 | 131 | 132 | 133 | 142 | 143 | {% endfor %} 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 |
    SelectNameType
    {{ file["name"] }}{{ file["mimeType"] }} 134 | {% if file["chown"] %} 135 | {% if file["chown"] == "False" %} 136 | FAILED 137 | {% else %} 138 | SUCCESS 139 | {% endif %} 140 | {% endif %} 141 |
    Select ALL files.
    152 |
    153 |
    154 |
    155 |
    {{ form.NEW_OWNER(class_="form-control typeahead", placeholder="New owner", size=20) }}
    156 |
    157 | 158 | Select new owner, then hit Submit to transfer file ownerships. 159 | 160 |
    161 |
    162 | {% endif %} 163 | {% if files|length == 0 and form["FILE_SEARCH"].data %} 164 |
    165 |
    No files were found matching your query.
    166 |
    167 | {% endif %} 168 | {% endif %} 169 |
    170 |
    173 |
    174 |
    175 | 177 |
    178 | {% endblock %} 179 | 180 | 181 | 182 | 183 | 184 | 203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GateKeeper 2 | 3 | [![status: unmaintained](https://opensource.twitter.dev/status/unmaintained.svg)](https://opensource.twitter.dev/status/#unmaintained) 4 | 5 | GateKeeper is a service built to automate the manual steps involved in onboarding, offboarding, or lost asset scenarios. The service will handle the flow of letting internal and external services know that a user needs to be activated, suspended, or deleted. 6 | 7 | This project is built with Flask, Gevent, Gunicorn, and Jinja2 templating. 8 | 9 | 10 | ## Sections 11 | 12 | 1. [Features](#features) 13 | 2. [Prerequisites](#prerequisites) 14 | 3. [Installation](#installation) 15 | 4. [Configuration](#configuration) 16 | 5. [Logs](#logging) 17 | 6. [Support](#support) 18 | 7. [Authors](#authors) 19 | 8. [License](#license) 20 | 9. [Security](#security) 21 | 10. [To-Do](#to-do) 22 | 23 | 24 | ## Features 25 | 26 | * **Services currently supported:** 27 | * LDAP 28 | * Google Apps (Admin, Gmail, Calendar, Drive) 29 | * PagerDuty 30 | * DUO 31 | 32 | * **Actions currently implemented:** 33 | * LDAP 34 | - Used to extract user information, and perform data validation against the GApps directory. 35 | * Google Admin (Directory) 36 | - Reset Google apps password 37 | - Purge application specific passwords 38 | - Purge 3rd party access tokens 39 | - Invalidate backup (verification) codes 40 | - Move a user to a custom Organizational Unit 41 | - Restore a user back to the default "/" OU 42 | * Google GMail 43 | - Set Out Of Office message (with a configurable message) 44 | - Disable IMAP email 45 | - Disable POP email 46 | * Google Calendar 47 | - Change events ownership (with a configurable assignee) 48 | - Delete future dated events (to free up resources like booked meeting rooms, equipment, etc) 49 | * Google Drive 50 | - Transfer ownership of files to another user (with regex filtered search) 51 | * PagerDuty 52 | - Remove from OnCall rotas 53 | * DUO Admin 54 | - Remove user from DUO 55 | 56 | * **Deployment methods available:** 57 | * Locally on MacOS/Linux (or a Virtual Machine) 58 | * Docker container 59 | * Mesos, via Aurora 60 | 61 | ## Prerequisites 62 | 63 | 1. Linux or MacOS 64 | Linux is highly recommended for a production installation. 65 | MacOS is also supported, but should only be used on local deployments, or for testing, due to performance and security concerns. 66 | Note: If you are installing GateKeeper on MacOS, you will also need to have the XCode Command Line Tools installed: ``` xcode-select --install ``` 67 | 68 | 2. Python 2.7.x 69 | You can get it via your package manager, or from [here](https://www.python.org/downloads/). 70 | 71 | 3. OpenJDK or Oracle JDK 7 or greater 72 | You can get it via your package manager, or from [OpenJDK](http://openjdk.java.net/install/) or [Oracle](https://www.oracle.com/downloads/index.html) respectively. 73 | 74 | 4. Bower 75 | You can get it via your package manager, or from [here](https://bower.io/). 76 | 77 | ## Installation 78 | 79 | #### Initial Configuration 80 | These steps apply to all the deployment methods listed below, and will need to be executed first. 81 | 82 | 1. Clone this repository. 83 | ``` 84 | git clone https://github.com/twitter/gatekeeper-service 85 | ``` 86 | 87 | 2. You will need an Admin User for GApps, to be able to run GateKeeper operations. 88 | This can be achieved either by using a Super Admin User, or by creating a Custom Administration Role for the service. 89 | The latter is highly recommended, as it is a much more secure way of restricting access to your GApps environment. 90 | You can create a Custom Administration Role, by following the instructions [here](https://support.google.com/a/answer/2406043?hl=en). 91 | 92 | 3. For GateKeeper to be able to act as a user under your domain, you will need a Service Account with Domain-Wide Delegation of Authority. 93 | Click [here](https://developers.google.com/admin-sdk/directory/v1/guides/delegation) for a guide on how to obtain these credentials. 94 | (Follow the guides under sections "Create the service account and its credentials", and "Delegate domain-wide authority to your service account") 95 | A list of scopes needed for GateKeeper's operations can be found on the config.example.yml file: 96 | ``` 97 | - "https://www.googleapis.com/auth/admin.directory.user" 98 | - "https://www.googleapis.com/auth/admin.directory.user.security" 99 | - "https://www.googleapis.com/auth/admin.directory.group.member" 100 | - "https://www.googleapis.com/auth/gmail.settings.basic" 101 | - "https://www.googleapis.com/auth/gmail.settings.sharing" 102 | - "https://www.googleapis.com/auth/calendar" 103 | - "https://www.googleapis.com/auth/drive" 104 | ``` 105 | Once complete, place your google_api_service_account_keyfile.json file in the config/ folder. 106 | 107 | 4. Create an OrgUnit in your GApps space, where you will be sending your offboarded users to. 108 | This is good practice, and allows for easy move of an offboarded user back to the org, if necessary. 109 | You can find instructions on how to add an OU [here](https://support.google.com/a/answer/182537?hl=en). 110 | 111 | 5. Create a copy of the file config.example.yml to config.yml and modify the file to reflect your settings and API keys. 112 | Consult the [Configuration](#configuration) section below for a short description of their usage. 113 | Note: It is advisable to create separate configs for your test, and production environments. 114 | ``` 115 | cd config 116 | cp config.example.yml config.yml 117 | ``` 118 | 119 | #### Docker 120 | The following instructions will help you create and launch a Docker container of GateKeeper. 121 | 122 | 1. Build the Docker image. 123 | ``` 124 | docker build -t twitter/gatekeeper . 125 | ``` 126 | 127 | 2. Create and execute a Docker container. 128 | ``` 129 | docker run -d -p 5000:5000 --name="gatekeeper" twitter/gatekeeper 130 | ``` 131 | Wait until the service is up. (You can monitor the logs with ```docker logs -f gatekeeper```) 132 | You can then access the GateKeeper UI at ```:5000``` (or the port you specified above, if different). 133 | 134 | 3. You can start/stop/restart the service, with: 135 | ``` 136 | docker start|stop|restart gatekeeper 137 | ``` 138 | 139 | 4. _(Optional)_ Remove any untagged or intermediary images created during the build process. 140 | ``` 141 | docker image prune 142 | ``` 143 | 144 | #### Local/VM Install 145 | The following instructions will help you launch an instance of GateKeeper locally, or a Virtual Machine. 146 | 147 | 1. Run the following command to install the javascript package dependencies. 148 | ``` 149 | cd static 150 | bower install 151 | ``` 152 | 153 | 2. Run the tests 154 | ``` 155 | ./pants test tests:: 156 | ``` 157 | 158 | 3. Run the service 159 | ``` 160 | ./pants run :gatekeeper 161 | ``` 162 | You can then access the GateKeeper UI at ```localhost:5000``` 163 | 164 | ## Configuration 165 | 166 | ```yaml 167 | defaults: 168 | debug: bool (use for troubleshooting. default: false) 169 | base_dir: string (base dir path. default: ".") 170 | http_proxy: 171 | use_proxy: bool (for routing traffic via a proxy. default: false) 172 | proxy_url: string (http proxy url, without the 'http(s)://' prefix) 173 | proxy_port: int (default: 8080) 174 | proxy_user: string (http proxy account username) 175 | proxy_pass: string (http proxy account password) 176 | 177 | ldap: 178 | base_dn: string (base dn for your LDAP) 179 | uri: string (prefixed with "ldap(s)://") 180 | user: string (username for LDAP login) 181 | pass: string (password for LDAP login) 182 | queries: 183 | all_users: string (LDAP query to return all active users. example: "(|(gidNumber=1000) (gidNumber=1001))". Leave empty when testing.) 184 | user_is_valid: string (LDAP query to return whether a user is valid, use "USER" as a var. example: "(& (uid=USER) (|(gidNumber=1000) (gidNumber=1001)))") 185 | user_is_active: string (LDAP query to return whether a user is active, use "USER" as a var. example: "(& (uid=USER) (gidNumber=1001))") 186 | user_info: string (LDAP query to return user attributes, use "USER" as a var. example: "(uid=USER)") 187 | fields: 188 | full_name: string (LDAP field for full name. example: "cn") 189 | first_name: string (LDAP field for first nane. example: "givenName") 190 | role: string (LDAP field for role.) 191 | team: string (LDAP field for team.) 192 | org: string (LDAP field for org.) 193 | location: string (LDAP field for location.) 194 | start_date: string (LDAP field for start date.) 195 | uid_number: string (LDAP field for uid. example: "uidNumber") 196 | groups: string (LDAP field for LDAP groups a user is a member of. example: "memberOf") 197 | photo_url: string (Optional - LDAP field for a user's profile photo/avatar location.) 198 | 199 | pagerduty: 200 | base_url: string (default: "https://api.pagerduty.com/") 201 | api_key: string (API Key for PagerDuty. Must be v2, and have R/W permissions.) 202 | 203 | duo: 204 | host: string (Hostname to the DUO Secure server.) 205 | ikey: string (Integration Key for DUO Secure.) 206 | skey: string (Secure Key for DUO Secure.) 207 | ca_certs: string (Custom SSL Certs location for use with DUO. Leave empty to use the default certs. default: "") 208 | 209 | google_apps: 210 | admin_user: string (GApps Account that will own and run the service. See the Installation section for more info. example: "gatekeeper-admin") 211 | offboarded_ou: string (GApps OrgUnit where the offboarded users will fall under. default: "/Offboarded Users") 212 | domain: string (Your GApps domain. example: "somedomain.com") 213 | credentials_keyfile: string (default: "config/google_api_service_account_keyfile.json") 214 | ``` 215 | 216 | ## Logging 217 | 218 | Logs are stored under /var/tmp, and will persist system reboots. 219 | If you are running GateKeeper on Docker, you can also get to the access logs with ```docker logs -f gatekeeper``` 220 | Be sure to include the relevant log line(s) with any issues submitted. 221 | 222 | ## Support 223 | 224 | Please create an issue on GitHub 225 | 226 | ## Authors 227 | 228 | * Harry Kantas 229 | * Mat Clinton 230 | 231 | Follow [@twitteross](https://twitter.com/twitteross) on Twitter for updates. 232 | 233 | ## License 234 | 235 | Copyright 2013-2018 Twitter, Inc. 236 | 237 | Licensed under the Apache License, Version 2.0: https://www.apache.org/licenses/LICENSE-2.0 238 | 239 | ## Security 240 | 241 | Please report sensitive security issues via Twitter's bug-bounty program (https://hackerone.com/twitter). 242 | 243 | Be mindful of the file ownership and permissions for your "google_api_service_account_keyfile.json" and "config.yml" files. 244 | These files will contain sensitive data that can grant API access to your platform and services. 245 | Please practise caution when choosing a deployment method to better suit your environment's security conditions. 246 | 247 | The WebUI is currently served in HTTP, since this service is meant to be deployed within your internal network. 248 | If your use case requires accessing GateKeeper via HTTPS, that can be achieved by redirecting all traffic to HTTPS with your own public facing proxy. 249 | 250 | ## To-Do 251 | 252 | * Implement more services. 253 | * Integration with JIRA and other ticketing systems. 254 | * Add the option to parse a batch of users at once, via a CSV file. 255 | * Expose a REST API for services to talk to GateKeeper directly. 256 | * Make the service independent to the presence of LDAP, for orgs that do not make use of it. 257 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block head %} 5 | 6 | 7 | 8 | 9 | 10 | 16 | GateKeeper 17 | 18 | {% endblock %} 19 | 20 | 21 | 22 | {% include "navbar.html" %} 23 | 24 | {% block options %} 25 |
    26 |
    27 |
    28 |
    29 | {{ form.csrf_token }} 30 | {{ form.USER_ID(class_="form-control typeahead", placeholder="Enter LDAP user name", size=40) }} 31 |
    32 |
    33 |
    34 |
    35 |
    36 | {% with messages = get_flashed_messages() %} 37 | {% if messages %} 38 |
      39 | {% for message in messages %} 40 |

      {{ message }}

      41 | {% endfor %} 42 |
    43 | {% endif %} 44 | {% endwith %} 45 |
    46 |
    47 | 48 | 49 | 108 | 175 | 176 |
    50 |
    51 | {{ form.csrf_token }} 52 |
    53 | 54 |
    55 |
    56 |
    57 |

    Google Directory

    58 | {% for field in form if field.name in google_admin_actions %} 59 | {{ field(class_="checkbox") }} {{ field.label }} 60 |
    61 | {% endfor %} 62 |
    63 |

    Google Gmail

    64 | {% for field in form if field.name in google_gmail_actions %} 65 | {% if field.name == "SET_OOO_MSG" %} 66 | 67 |
    68 |
    69 | {{ form.OOO_MSG_TEXT.label }} 70 |
    71 | {{ form.OOO_MSG_TEXT(class_="form-control", rows="4", cols="40", placeholder="Out Of Office Response") }} 72 |
    73 |
    74 | {% else %} 75 | {{ field(class_="checkbox") }} {{ field.label }} 76 | {% endif %} 77 |
    78 | {% endfor %} 79 |
    80 |

    Google Calendar

    81 | {% for field in form if field.name in google_calendar_actions %} 82 | {% if field.name == "CHANGE_EVENTS_OWNERSHIP" %} 83 | 84 |
    85 |
    86 | {{ form.GCAL_NEW_OWNER.label }} {{ form.GCAL_NEW_OWNER(class_="form-control typeahead", size=20) }} 87 |
    88 |
    89 | {% else %} 90 | {{ field(class_="checkbox") }} {{ field.label }} 91 | {% endif %} 92 |
    93 | {% endfor %} 94 |
    95 |

    PagerDuty

    96 | {% for field in form if field.name in pagerduty_actions %} 97 | {{ field(class_="checkbox") }} {{ field.label }} 98 |
    99 | {% endfor %} 100 |
    101 |

    DUO Secure

    102 | {% for field in form if field.name in duo_actions %} 103 | {{ field(class_="checkbox") }} {{ field.label }} 104 |
    105 | {% endfor %} 106 |
    107 |
    109 |
    110 | {% if user_info %} 111 |
    112 |
    113 |
    114 |
    Name:
    115 |
    {{ user_info[ldap_fields['full_name']] }}
    116 |
    117 |
    118 |
    Role:
    119 |
    {{ user_info[ldap_fields['role']] }}
    120 |
    121 |
    122 |
    Team:
    123 |
    {{ user_info[ldap_fields['team']] }}
    124 |
    125 |
    126 |
    Org:
    127 |
    {{ user_info[ldap_fields['org']] }}
    128 |
    129 |
    130 |
    Location:
    131 |
    {{ user_info[ldap_fields['location']] }}
    132 |
    133 |
    134 |
    Start date:
    135 |
    {{ user_info[ldap_fields['start_date']] }}
    136 |
    137 |
    138 |
    LDAP UID:
    139 |
    {{ user_info[ldap_fields['uid_number']] }}
    140 |
    141 |
    142 |
    LDAP groups:
    143 |
    144 | {% for group in user_info[ldap_fields['groups']] %}{{ group }} {% endfor %} 145 |
    146 |
    147 |
    148 |
    Status:
    149 |
    150 | {% if user_info['active'] == true %} 151 | ACTIVE 152 | {% else %} 153 | INACTIVE 154 | {% endif %} 155 |
    156 |
    157 |
    158 | {% if user_info[ldap_fields['photo_url']] %} 159 |
    160 | 161 |
    162 | {% endif %} 163 |
    164 |
    165 |
    166 |
    167 |
    168 | Click to submit changes for {{ user_info[ldap_fields['first_name']][0] }}. 169 | 170 |
    171 |
    172 | {% endif %} 173 |
    174 |
    177 |
    178 |
    179 | 185 |
    186 | {% endblock %} 187 | 188 | 189 | 190 | 191 | 192 | 247 | 248 | 249 | 250 | --------------------------------------------------------------------------------