├── .gitignore
├── public_html
├── key.png
├── logo-header-opera.png
├── screenshot-home.png
├── putty-key-generator.png
├── screenshot-account.png
├── screenshot-activity.png
├── screenshot-servers.png
├── screenshot-getting-started.png
├── bootstrap
│ ├── fonts
│ │ ├── glyphicons-halflings-regular.eot
│ │ ├── glyphicons-halflings-regular.ttf
│ │ ├── glyphicons-halflings-regular.woff
│ │ └── glyphicons-halflings-regular.woff2
│ └── js
│ │ └── npm.js
├── init.php
├── header.js
└── style.css
├── extensions
└── README
├── migrations
├── 003.php
└── 001.php
├── services
├── systemd
│ └── keys-sync.service
├── README
└── init.d
│ └── keys-sync
├── templates
├── pubkey_txt.php
├── error403.php
├── entity_pubkeys_txt.php
├── pubkey_json.php
├── tools.php
├── csrf.php
├── not_admin.php
├── entity_pubkeys_json.php
├── error503.php
├── key_upload_fail.php
├── pubkeys_json.php
├── signature_upload_fail.php
├── group_not_found.php
├── serveraccount_sync_status_json.php
├── invalid_hostname.php
├── user_not_found.php
├── bulk_mail_choose.php
├── server_not_found.php
├── server_account_not_found.php
├── invalid_group_name.php
├── activity.php
├── error404.php
├── unique_key_violation.php
├── group_json.php
├── user_pubkeys.php
├── server_json.php
├── users.php
├── server_sync_status_json.php
├── servers_json.php
├── bulk_mail.php
├── error500.php
├── base.php
├── pubkeys.php
├── groups.php
├── pubkey.php
└── access_options.php
├── model
├── accessoption.php
├── accessrequest.php
├── serverldapaccessoption.php
├── syncrequest.php
├── dbdirectory.php
├── serveraccountevent.php
├── publickeydestrule.php
├── userevent.php
├── groupevent.php
├── useralert.php
├── entityevent.php
├── serverevent.php
├── servernote.php
├── serveraccountdirectory.php
├── publickeysignature.php
├── migrationdirectory.php
├── syncrequestdirectory.php
├── access.php
├── eventdirectory.php
├── publickeydirectory.php
├── userdirectory.php
├── record.php
└── serverdirectory.php
├── views
├── error503.php
├── tools.php
├── users.php
├── csrf.php
├── error403.php
├── error404.php
├── server_sync_status.php
├── activity.php
├── serveraccount_sync_status.php
├── error500.php
├── help.php
├── pubkeys.php
├── serveraccount_pubkeys.php
├── user_pubkeys.php
├── home.php
├── bulk_mail.php
├── user.php
├── groups.php
├── access_options.php
├── servers.php
├── pubkey.php
└── group.php
├── scripts
├── pubkey_update.php
├── sync-common.php
├── ldap_update.php
└── syncd.php
├── router.php
├── routes.php
├── pagesection.php
├── requesthandler.php
├── ldap.php
├── NOTICE
├── config
└── config-sample.ini
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | config/config.ini
2 | config/keys-sync
3 | config/keys-sync.pub
4 | extensions/*.php
5 |
--------------------------------------------------------------------------------
/public_html/key.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/operasoftware/ssh-key-authority/HEAD/public_html/key.png
--------------------------------------------------------------------------------
/extensions/README:
--------------------------------------------------------------------------------
1 | For extending or replacing functionality.
2 | All .php files in this directory are automatically include()'d.
--------------------------------------------------------------------------------
/public_html/logo-header-opera.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/operasoftware/ssh-key-authority/HEAD/public_html/logo-header-opera.png
--------------------------------------------------------------------------------
/public_html/screenshot-home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/operasoftware/ssh-key-authority/HEAD/public_html/screenshot-home.png
--------------------------------------------------------------------------------
/public_html/putty-key-generator.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/operasoftware/ssh-key-authority/HEAD/public_html/putty-key-generator.png
--------------------------------------------------------------------------------
/public_html/screenshot-account.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/operasoftware/ssh-key-authority/HEAD/public_html/screenshot-account.png
--------------------------------------------------------------------------------
/public_html/screenshot-activity.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/operasoftware/ssh-key-authority/HEAD/public_html/screenshot-activity.png
--------------------------------------------------------------------------------
/public_html/screenshot-servers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/operasoftware/ssh-key-authority/HEAD/public_html/screenshot-servers.png
--------------------------------------------------------------------------------
/public_html/screenshot-getting-started.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/operasoftware/ssh-key-authority/HEAD/public_html/screenshot-getting-started.png
--------------------------------------------------------------------------------
/public_html/bootstrap/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/operasoftware/ssh-key-authority/HEAD/public_html/bootstrap/fonts/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/public_html/bootstrap/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/operasoftware/ssh-key-authority/HEAD/public_html/bootstrap/fonts/glyphicons-halflings-regular.ttf
--------------------------------------------------------------------------------
/public_html/bootstrap/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/operasoftware/ssh-key-authority/HEAD/public_html/bootstrap/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/public_html/bootstrap/fonts/glyphicons-halflings-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/operasoftware/ssh-key-authority/HEAD/public_html/bootstrap/fonts/glyphicons-halflings-regular.woff2
--------------------------------------------------------------------------------
/migrations/003.php:
--------------------------------------------------------------------------------
1 | database->query("
5 | ALTER TABLE `server` ADD COLUMN `port` int(10) unsigned NOT NULL DEFAULT 22
6 | ");
7 |
--------------------------------------------------------------------------------
/migrations/001.php:
--------------------------------------------------------------------------------
1 | database->query("
5 | CREATE TABLE `migration` (
6 | `id` int(10) unsigned NOT NULL,
7 | `name` text NOT NULL,
8 | `applied` datetime NOT NULL
9 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
10 | ");
11 |
--------------------------------------------------------------------------------
/services/systemd/keys-sync.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=SSH Key synchronization daemon
3 | Documentation=https://github.com/operasoftware/ssh-key-authority
4 | Requires=mysql.service
5 |
6 | [Service]
7 | Type=simple
8 | ExecStart=/srv/keys/scripts/syncd.php --systemd
9 | User=keys-sync
10 | StandardOutput=journal
11 | StandardError=journal
12 | PrivateDevices=on
13 | PrivateTmp=on
14 | ProtectHome=on
15 | ProtectSystem=on
16 |
17 | [Install]
18 | WantedBy=multi-user.target
19 |
--------------------------------------------------------------------------------
/public_html/bootstrap/js/npm.js:
--------------------------------------------------------------------------------
1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment.
2 | require('../../js/transition.js')
3 | require('../../js/alert.js')
4 | require('../../js/button.js')
5 | require('../../js/carousel.js')
6 | require('../../js/collapse.js')
7 | require('../../js/dropdown.js')
8 | require('../../js/modal.js')
9 | require('../../js/tooltip.js')
10 | require('../../js/popover.js')
11 | require('../../js/scrollspy.js')
12 | require('../../js/tab.js')
13 | require('../../js/affix.js')
--------------------------------------------------------------------------------
/services/README:
--------------------------------------------------------------------------------
1 | To install the sync service
2 | ===========================
3 |
4 | On a systemd system:
5 | 1) Copy the systemd/keys-sync.service file to /etc/systemd/system/
6 | 2) Modify ExecStart path and User as necessary. If SSH Key Authority is installed under /home, disable ProtectHome.
7 | 3) Run: systemctl daemon-reload
8 | 4) Run: systemctl enable keys-sync.service
9 |
10 | On a sysvinit system:
11 | 1) Copy the init.d/keys-sync file to /etc/init.d/
12 | 2) Modify SCRIPT path and USER as necessary.
13 | 3) Run: update-rc.d keys-sync defaults
14 |
--------------------------------------------------------------------------------
/public_html/init.php:
--------------------------------------------------------------------------------
1 | get('pubkey');
18 | out($pubkey->export()."\n", ESC_NONE);
19 |
--------------------------------------------------------------------------------
/templates/error403.php:
--------------------------------------------------------------------------------
1 |
18 |
Access denied
19 | Sorry, but you don't have permission to view this page.
20 |
--------------------------------------------------------------------------------
/templates/entity_pubkeys_txt.php:
--------------------------------------------------------------------------------
1 | get('pubkeys') as $pubkey) {
18 | out($pubkey->export()."\n", ESC_NONE);
19 | }
20 |
--------------------------------------------------------------------------------
/templates/pubkey_json.php:
--------------------------------------------------------------------------------
1 | get('pubkey');
18 | $json = pubkey_json($pubkey);
19 | out(json_encode($json), ESC_NONE);
20 |
--------------------------------------------------------------------------------
/templates/tools.php:
--------------------------------------------------------------------------------
1 |
18 | Tools
19 |
22 |
--------------------------------------------------------------------------------
/templates/csrf.php:
--------------------------------------------------------------------------------
1 |
18 | Form submission failed
19 | Your request was missing the required security token. Please try submitting your request again.
20 |
--------------------------------------------------------------------------------
/templates/not_admin.php:
--------------------------------------------------------------------------------
1 |
18 | Unable to fulfill request
19 | Your request cannot be fulfilled because you are not an administrator of the target entity.
20 |
--------------------------------------------------------------------------------
/templates/entity_pubkeys_json.php:
--------------------------------------------------------------------------------
1 | get('pubkeys') as $pubkey) {
19 | $json[] = pubkey_json($pubkey, true, false);
20 | }
21 | out(json_encode($json), ESC_NONE);
22 |
--------------------------------------------------------------------------------
/templates/error503.php:
--------------------------------------------------------------------------------
1 |
18 | System is down for maintenance
19 | Sorry for the inconvenience. We should be back soon though, so press the reload button in your browser in a few minutes to try again.
20 |
--------------------------------------------------------------------------------
/templates/key_upload_fail.php:
--------------------------------------------------------------------------------
1 |
18 | Public key upload failed
19 |
20 |
get('message')) ?> Please go back and try again.
21 |
22 |
--------------------------------------------------------------------------------
/templates/pubkeys_json.php:
--------------------------------------------------------------------------------
1 | public_keys = array();
19 | foreach($this->get('pubkeys') as $pubkey) {
20 | $json->public_keys[] = pubkey_json($pubkey, false, true);
21 | }
22 | out(json_encode($json), ESC_NONE);
23 |
--------------------------------------------------------------------------------
/templates/signature_upload_fail.php:
--------------------------------------------------------------------------------
1 |
18 | Signature upload failed
19 |
20 |
get('message')) ?> Please go back and try again.
21 |
22 |
--------------------------------------------------------------------------------
/templates/group_not_found.php:
--------------------------------------------------------------------------------
1 |
18 | Group not found
19 |
20 |
The group name you entered isn't yet known by the keys management server. Please go back and try again.
21 |
22 |
--------------------------------------------------------------------------------
/templates/serveraccount_sync_status_json.php:
--------------------------------------------------------------------------------
1 | get('sync_status');
18 | $pending = $this->get('pending');
19 | $json = new StdClass;
20 | $json->sync_status = $sync_status;
21 | $json->pending = $pending;
22 | out(json_encode($json), ESC_NONE);
23 |
--------------------------------------------------------------------------------
/templates/invalid_hostname.php:
--------------------------------------------------------------------------------
1 |
18 | Invalid hostname
19 |
20 |
"get('hostname'))?>" doesn't look like a valid hostname. Please go back and try again.
21 |
22 |
--------------------------------------------------------------------------------
/templates/user_not_found.php:
--------------------------------------------------------------------------------
1 |
18 | User not found
19 |
20 |
The user ID you entered doesn't appear to be a valid active LDAP user account. Please go back and try again.
21 |
22 |
--------------------------------------------------------------------------------
/model/accessoption.php:
--------------------------------------------------------------------------------
1 |
18 | Bulk mail
19 | Choose recipients:
20 |
24 |
--------------------------------------------------------------------------------
/model/accessrequest.php:
--------------------------------------------------------------------------------
1 |
18 | Server not found
19 |
20 |
The hostname you entered isn't yet known by the keys management server. You'll need to add it first. Please go back and try again.
21 |
22 |
--------------------------------------------------------------------------------
/views/error503.php:
--------------------------------------------------------------------------------
1 | set('title', 'Down for maintenance');
22 | $page->set('content', $content);
23 | $page->set('alerts', array());
24 | header('HTTP/1.1 503 Service Unavailable');
25 | echo $page->generate();
26 |
--------------------------------------------------------------------------------
/templates/server_account_not_found.php:
--------------------------------------------------------------------------------
1 |
18 | Server account not found
19 |
20 |
The server account you entered isn't yet known by the keys management server. You'll need to add it first. Please go back and try again.
21 |
22 |
--------------------------------------------------------------------------------
/templates/invalid_group_name.php:
--------------------------------------------------------------------------------
1 |
18 | Invalid project name
19 |
20 |
"get('project_name'))?>" doesn't look like a valid project name. Forward slashes (/) are not allowed in the project name. Please go back and try again.
21 |
22 |
--------------------------------------------------------------------------------
/model/serverldapaccessoption.php:
--------------------------------------------------------------------------------
1 | admin) {
19 | require('views/error403.php');
20 | die;
21 | }
22 |
23 | $content = new PageSection('tools');
24 |
25 | $page = new PageSection('base');
26 | $page->set('title', 'Tools');
27 | $page->set('content', $content);
28 | $page->set('alerts', $active_user->pop_alerts());
29 | echo $page->generate();
30 |
--------------------------------------------------------------------------------
/views/users.php:
--------------------------------------------------------------------------------
1 | set('users', $user_dir->list_users());
20 | $content->set('admin', $active_user->admin);
21 |
22 | $page = new PageSection('base');
23 | $page->set('title', 'Users');
24 | $page->set('content', $content);
25 | $page->set('alerts', $active_user->pop_alerts());
26 | echo $page->generate();
27 |
--------------------------------------------------------------------------------
/views/csrf.php:
--------------------------------------------------------------------------------
1 | set('address', $relative_request_url);
20 | $content->set('fulladdress', $absolute_request_url);
21 |
22 | $page = new PageSection('base');
23 | $page->set('title', 'Form submission failed');
24 | $page->set('content', $content);
25 | $page->set('alerts', array());
26 | echo $page->generate();
27 |
--------------------------------------------------------------------------------
/services/init.d/keys-sync:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | ### BEGIN INIT INFO
4 | # Provides: keys-sync
5 | # Required-Start: mysql
6 | # Required-Stop: mysql
7 | # Default-Start: 2 3 4 5
8 | # Default-Stop: 0 1 6
9 | # Short-Description: SSH key synchronization daemon
10 | ### END INIT INFO
11 |
12 | . /lib/lsb/init-functions
13 |
14 | SCRIPT=/srv/keys/scripts/syncd.php
15 | USER=keys-sync
16 | PIDFILE=/var/run/keys-sync.pid
17 |
18 | test -f $SCRIPT || exit 0
19 |
20 | case "$1" in
21 | start)
22 | log_daemon_msg "Starting keys-sync daemon"
23 | start-stop-daemon --start --quiet --pidfile $PIDFILE --startas $SCRIPT --user $USER --
24 | log_end_msg $?
25 | ;;
26 | stop)
27 | log_daemon_msg "Stopping keys-sync daemon"
28 | start-stop-daemon --stop --quiet --pidfile $PIDFILE --name syncd.php --user $USER
29 | log_end_msg $?
30 | rm -f $PIDFILE
31 | ;;
32 | restart)
33 | $0 stop && $0 start
34 | ;;
35 | *)
36 | log_action_msg "Usage: /etc/init.d/keys-sync {start|stop|restart}"
37 | exit 2
38 | ;;
39 | esac
40 | exit 0
41 |
--------------------------------------------------------------------------------
/scripts/pubkey_update.php:
--------------------------------------------------------------------------------
1 | #!/usr/bin/php
2 | list_public_keys();
23 | foreach($pubkeys as $pubkey) {
24 | try {
25 | $pubkey->import($pubkey->export(), null, true);
26 | $pubkey->update();
27 | } catch(InvalidArgumentException $e) {
28 | echo "Invalid public key {$pubkey->id}\n";
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/views/error403.php:
--------------------------------------------------------------------------------
1 | set('address', $relative_request_url);
20 | $content->set('fulladdress', $absolute_request_url);
21 |
22 | $page = new PageSection('base');
23 | $page->set('title', 'Access denied');
24 | $page->set('content', $content);
25 | $page->set('alerts', array());
26 | header('HTTP/1.1 403 Forbidden');
27 | echo $page->generate();
28 |
--------------------------------------------------------------------------------
/model/syncrequest.php:
--------------------------------------------------------------------------------
1 | processing = true;
32 | $this->update();
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/model/dbdirectory.php:
--------------------------------------------------------------------------------
1 | database = $database;
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/views/error404.php:
--------------------------------------------------------------------------------
1 | set('address', $relative_request_url);
20 | $content->set('fulladdress', $absolute_request_url);
21 | $content->set('referrer', isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '');
22 |
23 | $page = new PageSection('base');
24 | $page->set('title', 'Page not found');
25 | $page->set('content', $content);
26 | $page->set('alerts', array());
27 | header('HTTP/1.1 404 Not Found');
28 | echo $page->generate();
29 |
--------------------------------------------------------------------------------
/templates/activity.php:
--------------------------------------------------------------------------------
1 |
18 | Activity
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Entity
27 | User
28 | Activity
29 | Date (UTC )
30 |
31 |
32 |
33 | get('events') as $event) {
35 | show_event($event);
36 | }
37 | ?>
38 |
39 |
40 |
--------------------------------------------------------------------------------
/model/serveraccountevent.php:
--------------------------------------------------------------------------------
1 | data['entity_id']);
28 | return $group;
29 | default:
30 | return parent::__get($field);
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/public_html/header.js:
--------------------------------------------------------------------------------
1 | /*
2 | ##
3 | ## Copyright 2013-2017 Opera Software AS
4 | ##
5 | ## Licensed under the Apache License, Version 2.0 (the "License");
6 | ## you may not use this file except in compliance with the License.
7 | ## You may obtain a copy of the License at
8 | ##
9 | ## http://www.apache.org/licenses/LICENSE-2.0
10 | ##
11 | ## Unless required by applicable law or agreed to in writing, software
12 | ## distributed under the License is distributed on an "AS IS" BASIS,
13 | ## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | ## See the License for the specific language governing permissions and
15 | ## limitations under the License.
16 | ##
17 | */
18 | 'use strict';
19 |
20 | // Lightweight things to do before the page is displayed
21 | // This should not rely on any JQuery or other libraries
22 |
23 |
24 | // Hide the key fingerprints that we are not interested in
25 | var sheet = document.styleSheets[0];
26 | var fingerprint_hash;
27 | if(localStorage && localStorage.getItem('preferred_fingerprint_hash') == 'SHA256') {
28 | sheet.insertRule('span.fingerprint_md5 {display:none}', 0)
29 | } else {
30 | sheet.insertRule('span.fingerprint_sha256 {display:none}', 0)
31 | }
--------------------------------------------------------------------------------
/templates/error404.php:
--------------------------------------------------------------------------------
1 |
18 | Page not found
19 | Sorry, but the address you've given doesn't seem to point to a valid page.
20 | If you got here by following a link, please report it to us . Otherwise, please make sure that you have typed the address correctly, or just start browsing from the keys home page .
21 |
--------------------------------------------------------------------------------
/templates/unique_key_violation.php:
--------------------------------------------------------------------------------
1 | get('exception');
19 | ?>
20 | Naming conflict
21 |
22 | fields) == 1) { ?>
23 |
The fields)))?> "values))?>" already exists. Please go back and try again.
24 |
25 |
The values you provided are in conflict with existing records. Please go back and try again.
26 |
27 |
28 |
--------------------------------------------------------------------------------
/model/publickeydestrule.php:
--------------------------------------------------------------------------------
1 | data['entity_id']);
31 | return $user;
32 | default:
33 | return parent::__get($field);
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/model/groupevent.php:
--------------------------------------------------------------------------------
1 | data['entity_id']);
31 | return $group;
32 | default:
33 | return parent::__get($field);
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/views/server_sync_status.php:
--------------------------------------------------------------------------------
1 | get_server_by_hostname($router->vars['hostname']);
20 | } catch(ServerNotFoundException $e) {
21 | require('views/error404.php');
22 | die;
23 | }
24 | $page = new PageSection('server_sync_status_json');
25 | $page->set('sync_status', $server->sync_status);
26 | $page->set('last_sync', $server->get_last_sync_event());
27 | $page->set('pending', count($server->list_sync_requests()) > 0);
28 | $page->set('accounts', $server->list_accounts());
29 | header('Content-type: application/json; charset=utf-8');
30 | echo $page->generate();
31 |
--------------------------------------------------------------------------------
/views/activity.php:
--------------------------------------------------------------------------------
1 | admin && count($active_user->list_admined_servers()) == 0 && count($active_user->list_admined_groups()) == 0) {
19 | require('views/error403.php');
20 | die;
21 | }
22 |
23 | $content = new PageSection('activity');
24 | if($active_user->admin) {
25 | $content->set('events', $event_dir->list_events());
26 | } else {
27 | $content->set('events', $active_user->list_events());
28 | }
29 |
30 | $page = new PageSection('base');
31 | $page->set('title', 'Activity');
32 | $page->set('content', $content);
33 | $page->set('alerts', $active_user->pop_alerts());
34 | echo $page->generate();
35 |
--------------------------------------------------------------------------------
/templates/group_json.php:
--------------------------------------------------------------------------------
1 | users = array();
19 | $json->server_accounts = array();
20 | foreach($this->get('group_members') as $member) {
21 | $group_member = new StdClass;
22 | if(get_class($member) == 'User') {
23 | $group_member->uid = $member->uid;
24 | $group_member->email = $member->email;
25 | $json->users[] = $group_member;
26 | } elseif(get_class($member) == 'ServerAccount') {
27 | $group_member->name = $member->name;
28 | $group_member->hostname = $member->server->hostname;
29 | $json->server_accounts[] = $group_member;
30 | }
31 | }
32 | out(json_encode($json), ESC_NONE);
33 |
--------------------------------------------------------------------------------
/model/useralert.php:
--------------------------------------------------------------------------------
1 | data['class'])) $this->data['class'] = 'success';
33 | if(!isset($this->data['escaping'])) $this->data['escaping'] = ESC_HTML;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/views/serveraccount_sync_status.php:
--------------------------------------------------------------------------------
1 | get_server_by_hostname($router->vars['hostname']);
20 | $account = $server->get_account_by_name($router->vars['account']);
21 | } catch(ServerAccountNotFoundException $e) {
22 | require('views/error404.php');
23 | die;
24 | } catch(ServerNotFoundException $e) {
25 | require('views/error404.php');
26 | die;
27 | }
28 | $page = new PageSection('serveraccount_sync_status_json');
29 | $page->set('sync_status', $account->sync_status);
30 | $page->set('pending', $account->sync_is_pending());
31 | header('Content-type: application/json; charset=utf-8');
32 | echo $page->generate();
33 |
--------------------------------------------------------------------------------
/templates/user_pubkeys.php:
--------------------------------------------------------------------------------
1 |
18 |
19 | get('pubkeys') as $pubkey) { ?>
20 |
21 |
22 | Key data
23 | export())?>
24 | Key size
25 | keysize)?>
26 | Fingerprint (MD5)
27 | fingerprint_md5)?>
28 | Fingerprint (SHA256)
29 | fingerprint_sha256)?>
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/templates/server_json.php:
--------------------------------------------------------------------------------
1 | get('server');
18 | $last_sync_event = $this->get('last_sync_event');
19 | $json = new StdClass;
20 | $json->uuid = $server->uuid;
21 | $json->hostname = $server->hostname;
22 | $json->key_management = $server->key_management;
23 | $json->sync_status = $server->sync_status;
24 | $json->rsa_key_fingerprint = $server->rsa_key_fingerprint;
25 | if($last_sync_event) {
26 | $json->last_sync_event = new StdClass;
27 | $json->last_sync_event->details = $last_sync_event->details;
28 | $json->last_sync_event->date = $last_sync_event->date;
29 | } else {
30 | $json->last_sync_event = null;
31 | }
32 | out(json_encode($json), ESC_NONE);
33 |
--------------------------------------------------------------------------------
/views/error500.php:
--------------------------------------------------------------------------------
1 | set('error_number', $error_number);
20 | $content->set('admin_address', isset($config) ? $config['email']['admin_address'] : null);
21 | if(isset($active_user) && is_object($active_user) && isset($e)) {
22 | if($active_user->developer) {
23 | $content->set('exception_class', get_class($e));
24 | $content->set('error_details', $e);
25 | }
26 | }
27 |
28 | $page = new PageSection('base');
29 | $page->set('title', 'An error occurred');
30 | $page->set('content', $content);
31 | $page->set('alerts', array());
32 | header('HTTP/1.1 500 Internal Server Error');
33 | echo $page->generate();
34 |
--------------------------------------------------------------------------------
/templates/users.php:
--------------------------------------------------------------------------------
1 |
18 | Users
19 |
20 |
21 |
22 | Username
23 | Full name
24 | Public keys
25 |
26 |
27 |
28 | get('users') as $user) { ?>
29 | active) out(' class="text-muted"', ESC_NONE) ?>>
30 | uid)?>
31 | name)?>
32 | list_public_keys())))?>
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/views/help.php:
--------------------------------------------------------------------------------
1 | set('keys-sync-pubkey', file_get_contents('config/keys-sync.pub'));
21 | } else {
22 | $content->set('keys-sync-pubkey', 'Error: keyfile missing');
23 | }
24 | $content->set('admin_mail', $config['email']['admin_address']);
25 | $content->set('baseurl', $config['web']['baseurl']);
26 | $content->set('security_config', isset($config['security']) ? $config['security'] : array());
27 |
28 | $page = new PageSection('base');
29 | $page->set('title', 'Help');
30 | $page->set('content', $content);
31 | $page->set('alerts', $active_user->pop_alerts());
32 | echo $page->generate();
33 |
--------------------------------------------------------------------------------
/model/entityevent.php:
--------------------------------------------------------------------------------
1 | data['actor_id']);
38 | return $actor;
39 | default:
40 | return parent::__get($field);
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/templates/server_sync_status_json.php:
--------------------------------------------------------------------------------
1 | get('sync_status');
18 | $last_sync = $this->get('last_sync');
19 | $pending = $this->get('pending');
20 | $accounts = $this->get('accounts');
21 | $json = new StdClass;
22 | $json->sync_status = $sync_status;
23 | if(is_null($last_sync)) {
24 | $json->last_sync = null;
25 | } else {
26 | $json->last_sync = new StdClass;
27 | $json->last_sync->date = $last_sync->date;
28 | $json->last_sync->details = json_decode($last_sync->details)->value;
29 | }
30 | $json->accounts = array();
31 | foreach($accounts as $account) {
32 | $jsa = new StdClass;
33 | $jsa->name = $account->name;
34 | $jsa->sync_status = $account->sync_status;
35 | $jsa->pending = $account->sync_is_pending();
36 | $json->accounts[] = $jsa;
37 | }
38 | $json->pending = $pending;
39 | out(json_encode($json), ESC_NONE);
40 |
--------------------------------------------------------------------------------
/model/serverevent.php:
--------------------------------------------------------------------------------
1 | data['actor_id']);
38 | return $actor;
39 | case 'server':
40 | $server = new Server($this->data['server_id']);
41 | return $server;
42 | default:
43 | return parent::__get($field);
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/views/pubkeys.php:
--------------------------------------------------------------------------------
1 | list_public_keys(array(), $filter);
25 |
26 | if(isset($router->vars['format']) && $router->vars['format'] == 'json') {
27 | $page = new PageSection('pubkeys_json');
28 | $page->set('pubkeys', $pubkeys);
29 | header('Content-type: text/plain; charset=utf-8');
30 | echo $page->generate();
31 | } else {
32 | $content = new PageSection('pubkeys');
33 | $content->set('filter', $filter);
34 | $content->set('pubkeys', $pubkeys);
35 | $content->set('admin', $active_user->admin);
36 |
37 | $page = new PageSection('base');
38 | $page->set('title', 'Public keys');
39 | $page->set('content', $content);
40 | $page->set('alerts', $active_user->pop_alerts());
41 | echo $page->generate();
42 | }
43 |
--------------------------------------------------------------------------------
/views/serveraccount_pubkeys.php:
--------------------------------------------------------------------------------
1 | get_server_by_hostname($router->vars['hostname']);
20 | $account = $server->get_account_by_name($router->vars['account']);
21 | } catch(ServerAccountNotFoundException $e) {
22 | require('views/error404.php');
23 | die;
24 | } catch(ServerNotFoundException $e) {
25 | require('views/error404.php');
26 | die;
27 | }
28 | $pubkeys = $account->list_public_keys();
29 | if(isset($router->vars['format']) && $router->vars['format'] == 'txt') {
30 | $page = new PageSection('entity_pubkeys_txt');
31 | $page->set('pubkeys', $pubkeys);
32 | header('Content-type: text/plain; charset=utf-8');
33 | echo $page->generate();
34 | } elseif(isset($router->vars['format']) && $router->vars['format'] == 'json') {
35 | $page = new PageSection('entity_pubkeys_json');
36 | $page->set('pubkeys', $pubkeys);
37 | header('Content-type: application/json; charset=utf-8');
38 | echo $page->generate();
39 | }
40 |
--------------------------------------------------------------------------------
/templates/servers_json.php:
--------------------------------------------------------------------------------
1 | servers = array();
19 | foreach($this->get('servers') as $server) {
20 | $last_sync_event = $server->get_last_sync_event();
21 | $jsonserver = new StdClass;
22 | $jsonserver->uuid = $server->uuid;
23 | $jsonserver->hostname = $server->hostname;
24 | $jsonserver->key_management = $server->key_management;
25 | $jsonserver->sync_status = $server->sync_status;
26 | if($this->get('active_user')->admin) {
27 | $jsonserver->admins = array();
28 | foreach($server->list_effective_admins() as $admin) {
29 | if($admin->active) {
30 | $jsonserver->admins[] = $admin->uid;
31 | }
32 | }
33 | }
34 | if($last_sync_event) {
35 | $jsonserver->last_sync_event = new StdClass;
36 | $jsonserver->last_sync_event->details = $last_sync_event->details;
37 | $jsonserver->last_sync_event->date = $last_sync_event->date;
38 | } else {
39 | $jsonserver->last_sync_event = null;
40 | }
41 | $json->servers[] = $jsonserver;
42 | }
43 | out(json_encode($json), ESC_NONE);
44 |
--------------------------------------------------------------------------------
/model/servernote.php:
--------------------------------------------------------------------------------
1 | entity_id = $active_user->entity_id;
31 | }
32 |
33 | /**
34 | * Magic getter method - if server field requested, return Server object that note applies to;
35 | * if user field requested, return User object of the person who wrote the note.
36 | * @param string $field to retrieve
37 | * @return mixed data stored in field
38 | */
39 | public function &__get($field) {
40 | global $user_dir;
41 | switch($field) {
42 | case 'user':
43 | $user = new User($this->entity_id);
44 | return $user;
45 | case 'server':
46 | $server = new Server($this->server_id);
47 | return $server;
48 | default:
49 | return parent::__get($field);
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/templates/bulk_mail.php:
--------------------------------------------------------------------------------
1 |
18 | Bulk mail get('recipients')))?>
19 | This form will send a mail to all get('rcpt_desc'))?> the SSH Key Authority system!
20 |
34 |
--------------------------------------------------------------------------------
/model/serveraccountdirectory.php:
--------------------------------------------------------------------------------
1 | database->prepare("SELECT * FROM server_account WHERE entity_id = ?");
31 | $stmt->bind_param('d', $entity_id);
32 | $stmt->execute();
33 | $result = $stmt->get_result();
34 | if($row = $result->fetch_assoc()) {
35 | $account = new ServerAccount($row['entity_id'], $row);
36 | } else {
37 | throw new ServerAccountNotFoundException('Server account does not exist.');
38 | }
39 | $stmt->close();
40 | return $account;
41 | }
42 | }
43 |
44 | class ServerAccountNotFoundException extends Exception {}
45 | class ServerAccountNotDeletableException extends Exception {}
46 |
--------------------------------------------------------------------------------
/router.php:
--------------------------------------------------------------------------------
1 | route_vars = array();
27 | $path = preg_replace_callback('|\\\{([a-z]+)\\\}|', array($this, 'parse_route_variable'), preg_quote($path, '|'));
28 | $route = new StdClass;
29 | $route->view = $view;
30 | $route->vars = $this->route_vars;
31 | $route->public = $public;
32 | $this->routes[$path] = $route;
33 | }
34 |
35 | private function parse_route_variable($matches) {
36 | $this->route_vars[] = $matches[1];
37 | return '([^/]*)';
38 | }
39 |
40 | public function handle_request($request_path) {
41 | $request_path = preg_replace('|\?.*$|', '', $request_path);
42 | foreach($this->routes as $path => $route) {
43 | if(preg_match('|^'.$path.'$|', $request_path, $matches)) {
44 | $this->view = $route->view;
45 | $this->public = $route->public;
46 | $i = 0;
47 | foreach($route->vars as $var) {
48 | $i++;
49 | if(isset($matches[$i])) {
50 | $this->vars[$var] = urldecode($matches[$i]);
51 | }
52 | }
53 | }
54 | }
55 | if(is_null($this->view)) {
56 | $this->view = 'error404';
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/views/user_pubkeys.php:
--------------------------------------------------------------------------------
1 | get_user_by_uid($router->vars['username']);
20 | } catch(UserNotFoundException $e) {
21 | require('views/error404.php');
22 | die;
23 | }
24 | $pubkeys = $user->list_public_keys();
25 | if(isset($router->vars['format']) && $router->vars['format'] == 'txt') {
26 | $page = new PageSection('entity_pubkeys_txt');
27 | $page->set('pubkeys', $pubkeys);
28 | header('Content-type: text/plain; charset=utf-8');
29 | echo $page->generate();
30 | } elseif(isset($router->vars['format']) && $router->vars['format'] == 'json') {
31 | $page = new PageSection('entity_pubkeys_json');
32 | $page->set('pubkeys', $pubkeys);
33 | header('Content-type: application/json; charset=utf-8');
34 | echo $page->generate();
35 | } else {
36 | $content = new PageSection('user_pubkeys');
37 | $content->set('user', $user);
38 | $content->set('pubkeys', $pubkeys);
39 | $content->set('admin', $active_user->admin);
40 |
41 | $head = ' '."\n";
42 | $head .= ' '."\n";
43 |
44 | $page = new PageSection('base');
45 | $page->set('title', 'Public keys for '.$user->name);
46 | $page->set('head', $head);
47 | $page->set('content', $content);
48 | $page->set('alerts', $active_user->pop_alerts());
49 | echo $page->generate();
50 | }
51 |
--------------------------------------------------------------------------------
/model/publickeysignature.php:
--------------------------------------------------------------------------------
1 | verify($this->public_key->export().$line_ending, $this->signature);
36 | if(is_array($info)) {
37 | $sig = reset($info);
38 | if($sig['validity'] > 0) break;
39 | } else {
40 | throw new InvalidArgumentException("Signature doesn't seem valid");
41 | }
42 | }
43 | if($sig['validity'] == 0) {
44 | #throw new InvalidArgumentException("Signature doesn't validate against pubkey");
45 | }
46 | $this->fingerprint = $sig['fingerprint'];
47 | $this->sign_date = gmdate('Y-m-d H:i:s', $sig['timestamp']);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/model/migrationdirectory.php:
--------------------------------------------------------------------------------
1 | database->prepare('SELECT MAX(id) FROM migration');
31 | $stmt->execute();
32 | $result = $stmt->get_result();
33 | list($current_migration) = $result->fetch_row();
34 | } catch(mysqli_sql_exception $e) {
35 | if($e->getCode() === 1146) {
36 | $current_migration = 0;
37 | } else {
38 | throw $e;
39 | }
40 | }
41 | if($current_migration < self::LAST_MIGRATION) {
42 | $this->apply_pending_migrations($current_migration);
43 | }
44 | }
45 |
46 | private function apply_pending_migrations($current_migration) {
47 | openlog('dnsui', LOG_ODELAY, LOG_USER);
48 | for($migration_id = $current_migration + 1; $migration_id <= self::LAST_MIGRATION; $migration_id++) {
49 | $filename = str_pad($migration_id, 3, '0', STR_PAD_LEFT).'.php';
50 | syslog(LOG_INFO, "migration={$filename};object=database;action=apply");
51 | $migration_name = $filename;
52 | include('migrations/'.$filename);
53 | $stmt = $this->database->prepare('INSERT INTO migration VALUES (?, ?, NOW())');
54 | $stmt->bind_param('ds', $migration_id, $migration_name);
55 | $stmt->execute();
56 | }
57 | closelog();
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/views/home.php:
--------------------------------------------------------------------------------
1 | list_public_keys();
19 | $admined_servers = $active_user->list_admined_servers(array('pending_requests', 'admins'));
20 |
21 | if(isset($_POST['add_public_key'])) {
22 | try {
23 | $public_key = new PublicKey;
24 | $public_key->import($_POST['add_public_key'], $active_user->uid);
25 | $active_user->add_public_key($public_key);
26 | redirect();
27 | } catch(InvalidArgumentException $e) {
28 | $content = new PageSection('key_upload_fail');
29 | switch($e->getMessage()) {
30 | case 'Insufficient bits in public key':
31 | $content->set('message', "The public key you submitted is of insufficient strength; it must be at least 4096 bits.");
32 | break;
33 | default:
34 | $content->set('message', "The public key you submitted doesn't look valid.");
35 | }
36 | }
37 | } elseif(isset($_POST['delete_public_key'])) {
38 | foreach($public_keys as $public_key) {
39 | if($public_key->id == $_POST['delete_public_key']) {
40 | $key_to_delete = $public_key;
41 | }
42 | }
43 | if(isset($key_to_delete)) {
44 | $active_user->delete_public_key($key_to_delete);
45 | }
46 | redirect();
47 | } else {
48 | $content = new PageSection('home');
49 | $content->set('user_keys', $public_keys);
50 | $content->set('admined_servers', $admined_servers);
51 | $content->set('uid', $active_user->uid);
52 | }
53 |
54 | $page = new PageSection('base');
55 | $page->set('title', 'Keys management');
56 | $page->set('content', $content);
57 | $page->set('alerts', $active_user->pop_alerts());
58 | echo $page->generate();
59 |
--------------------------------------------------------------------------------
/templates/error500.php:
--------------------------------------------------------------------------------
1 |
18 | get('error_details')) { ?>
19 | Error
20 | get('exception_class')) ?> "get('error_details')->getMessage()) ?> "
21 | Occurred in get('error_details')->getFile().' line '.$this->get('error_details')->getLine()) ?>
22 | Stack trace
23 |
24 |
25 |
26 | Function
27 | Arguments
28 | Location
29 |
30 |
31 |
32 | get('error_details')->getTrace() as $stack_line) { ?>
33 |
34 |
35 |
36 |
37 |
38 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | Oops! Something went wrong!
53 | Sorry, but it looks like something needs fixing on the system. The problem has been automatically reported to the administrators, but if you wish, you can also provide additional information about what you were doing that may have triggered the error.
54 |
55 |
--------------------------------------------------------------------------------
/views/bulk_mail.php:
--------------------------------------------------------------------------------
1 | admin) {
19 | require('views/error403.php');
20 | die;
21 | }
22 |
23 | if(!empty($_POST['subject']) && !empty($_POST['body']) && !empty($router->vars['recipients'])) {
24 | $email = new Email;
25 | $email->subject = $_POST['subject'];
26 | $email->body = $_POST['body'];
27 | $email->add_reply_to($config['email']['admin_address'], $config['email']['admin_name']);
28 | $email->add_recipient('noreply', 'Undisclosed recipients');
29 | $filters = array();
30 | if($router->vars['recipients'] == 'server_admins') {
31 | $filters['admins_servers'] = 1;
32 | }
33 | foreach($user_dir->list_users(array(), $filters) as $user) {
34 | if($user->active) {
35 | $email->add_bcc($user->email, $user->name);
36 | }
37 | }
38 | $email->send();
39 | $alert = new UserAlert;
40 | $alert->content = "Mail sent!";
41 | $active_user->add_alert($alert);
42 | redirect();
43 | } elseif(empty($router->vars['recipients'])) {
44 | $content = new PageSection('bulk_mail_choose');
45 | } else {
46 | switch($router->vars['recipients']) {
47 | case 'all_users':
48 | $rcpt_desc = 'users of';
49 | $rcpt_role = 'user of';
50 | break;
51 | case 'server_admins':
52 | $rcpt_desc = 'server admins on';
53 | $rcpt_role = 'server admin on';
54 | break;
55 | default:
56 | require('views/error404.php');
57 | die;
58 | }
59 | $content = new PageSection('bulk_mail');
60 | $content->set('recipients', $router->vars['recipients']);
61 | $content->set('rcpt_desc', $rcpt_desc);
62 | $content->set('rcpt_role', $rcpt_role);
63 | }
64 |
65 | $page = new PageSection('base');
66 | $page->set('title', 'Bulk mail');
67 | $page->set('content', $content);
68 | $page->set('alerts', $active_user->pop_alerts());
69 | echo $page->generate();
70 |
--------------------------------------------------------------------------------
/model/syncrequestdirectory.php:
--------------------------------------------------------------------------------
1 | database->prepare("INSERT IGNORE INTO sync_request SET server_id = ?, account_name = ?");
33 | $stmt->bind_param('ds', $req->server_id, $req->account_name);
34 | $stmt->execute();
35 | $req->id = $stmt->insert_id;
36 | $stmt->close();
37 | }
38 |
39 | /**
40 | * Delete the sync request from the database.
41 | * @param SyncRequest $req object to delete
42 | */
43 | public function delete_sync_request(SyncRequest $req) {
44 | $stmt = $this->database->prepare("DELETE FROM sync_request WHERE id = ?");
45 | $stmt->bind_param('s', $req->id);
46 | $stmt->execute();
47 | $stmt->close();
48 | }
49 |
50 | /**
51 | * List the sync requests stored in the database that are not being processed yet.
52 | * @return array of SyncRequest objects
53 | */
54 | public function list_pending_sync_requests() {
55 | if(!isset($this->sync_list_stmt)) {
56 | $this->sync_list_stmt = $this->database->prepare("SELECT * FROM sync_request WHERE processing = 0 ORDER BY id");
57 | }
58 | $this->sync_list_stmt->execute();
59 | $result = $this->sync_list_stmt->get_result();
60 | $reqs = array();
61 | while($row = $result->fetch_assoc()) {
62 | $reqs[] = new SyncRequest($row['id'], $row);
63 | }
64 | return $reqs;
65 | }
66 | }
67 |
68 | class SyncRequestNotFoundException extends Exception {}
69 |
--------------------------------------------------------------------------------
/routes.php:
--------------------------------------------------------------------------------
1 | 'home',
20 | '/activity' => 'activity',
21 | '/bulk_mail' => 'bulk_mail',
22 | '/bulk_mail/{recipients}' => 'bulk_mail',
23 | '/groups' => 'groups',
24 | '/groups/{group}' => 'group',
25 | '/groups/{group}/members.{format}' => 'group',
26 | '/groups/{group}/access_rules/{access}' => 'access_options',
27 | '/help' => 'help',
28 | '/pubkeys' => 'pubkeys',
29 | '/pubkeys.{format}' => 'pubkeys',
30 | '/pubkeys/{key}' => 'pubkey',
31 | '/pubkeys/{key}.{format}' => 'pubkey',
32 | '/servers' => 'servers',
33 | '/servers.{format}' => 'servers',
34 | '/servers/{hostname}' => 'server',
35 | '/servers/{hostname}/accounts/{account}' => 'serveraccount',
36 | '/servers/{hostname}/accounts/{account}/access_rules/{access}' => 'access_options',
37 | '/servers/{hostname}/accounts/{account}/pubkeys.{format}' => 'serveraccount_pubkeys',
38 | '/servers/{hostname}/accounts/{account}/sync_status' => 'serveraccount_sync_status',
39 | '/servers/{hostname}/status.{format}' => 'server',
40 | '/servers/{hostname}/sync_status' => 'server_sync_status',
41 | '/tools' => 'tools',
42 | '/users' => 'users',
43 | '/users/{username}' => 'user',
44 | '/users/{username}/pubkeys' => 'user_pubkeys',
45 | '/users/{username}/pubkeys.{format}' => 'user_pubkeys',
46 | '/users/{username}/pubkeys/{key}' => 'pubkey',
47 | '/users/{username}/pubkeys/{key}.{format}' => 'pubkey',
48 | );
49 |
50 | $public_routes = array(
51 | '/groups/{group}/members.{format}' => true,
52 | '/pubkeys.{format}' => true,
53 | '/pubkeys/{key}.{format}' => true,
54 | '/servers/{hostname}/accounts/{account}/pubkeys.{format}' => true,
55 | '/users/{username}' => true,
56 | '/users/{username}/pubkeys.{format}' => true,
57 | '/users/{username}/pubkeys/{key}.{format}' => true,
58 | );
59 |
--------------------------------------------------------------------------------
/views/user.php:
--------------------------------------------------------------------------------
1 | get_user_by_uid($router->vars['username']);
20 | } catch(UserNotFoundException $e) {
21 | require('views/error404.php');
22 | die;
23 | }
24 | $access = $user->list_remote_access();
25 | $admined_servers = $user->list_admined_servers(array('pending_requests'));
26 | $admined_groups = $user->list_admined_groups(array('members', 'admins'));
27 | $groups = $user->list_group_memberships(array('members', 'admins'));
28 | usort($admined_servers, function($a, $b) {return strnatcasecmp($a->hostname, $b->hostname);});
29 |
30 | if(isset($_POST['reassign_servers']) && is_array($_POST['servers']) && $active_user->admin) {
31 | try {
32 | $new_admin = $user_dir->get_user_by_uid($_POST['reassign_to']);
33 | } catch(UserNotFoundException $e) {
34 | try {
35 | $new_admin = $group_dir->get_group_by_name($_POST['reassign_to']);
36 | } catch(GroupNotFoundException $e) {
37 | $content = new PageSection('user_not_found');
38 | }
39 | }
40 | if(isset($new_admin)) {
41 | foreach($admined_servers as $server) {
42 | if(in_array($server->hostname, $_POST['servers'])) {
43 | $server->add_admin($new_admin);
44 | $server->delete_admin($user);
45 | }
46 | }
47 | redirect('#details');
48 | }
49 | } elseif(isset($_POST['edit_user']) && $active_user->admin) {
50 | $user->force_disable = $_POST['force_disable'];
51 | $user->get_details_from_ldap();
52 | redirect('#settings');
53 | } else {
54 | $content = new PageSection('user');
55 | $content->set('user', $user);
56 | $content->set('user_access', $access);
57 | $content->set('user_admined_servers', $admined_servers);
58 | $content->set('user_admined_groups', $admined_groups);
59 | $content->set('user_groups', $groups);
60 | $content->set('user_keys', $user->list_public_keys());
61 | $content->set('admin', $active_user->admin);
62 | }
63 |
64 | $page = new PageSection('base');
65 | $page->set('title', $user->name);
66 | $page->set('content', $content);
67 | $page->set('alerts', $active_user->pop_alerts());
68 | echo $page->generate();
69 |
--------------------------------------------------------------------------------
/pagesection.php:
--------------------------------------------------------------------------------
1 | template = $template;
28 | $this->data = new StdClass;
29 | $this->data->menu_items = array();
30 | $this->data->menu_items['/'] = 'Home';
31 | $this->data->menu_items['/servers'] = 'Servers';
32 | $this->data->menu_items['/users'] = 'Users';
33 | $this->data->menu_items['/groups'] = 'Groups';
34 | $this->data->menu_items['/pubkeys'] = 'Public keys';
35 | if($active_user && ($active_user->admin || count($active_user->list_admined_servers()) > 0)) {
36 | $this->data->menu_items['/activity'] = 'Activity';
37 | }
38 | if($active_user && $active_user->admin) {
39 | $this->data->menu_items['/tools'] = 'Tools';
40 | }
41 | $this->data->menu_items['/help'] = 'Help';
42 | $this->data->relative_request_url = $relative_request_url;
43 | $this->data->active_user = $active_user;
44 | $this->data->web_config = $config['web'];
45 | $this->data->email_config = $config['email'];
46 | if($active_user && $active_user->developer) {
47 | $this->data->database = $database;
48 | }
49 | }
50 | public function set_by_array($array, $prefix = '') {
51 | foreach($array as $item => $data) {
52 | $this->setData($prefix.$item, $data);
53 | }
54 | }
55 | public function set($item, $data) {
56 | $this->data->$item = $data;
57 | }
58 | public function get($item) {
59 | if(isset($this->data->$item)) {
60 | if(is_object($this->data->$item) && get_class($this->data->$item) == 'PageSection') {
61 | return $this->data->$item->generate();
62 | } else {
63 | return $this->data->$item;
64 | }
65 | } else {
66 | return null;
67 | }
68 | }
69 | public function generate() {
70 | ob_start();
71 | $data = $this->data;
72 | include_once(path_join('templates', 'functions.php'));
73 | include(path_join('templates', $this->template.'.php'));
74 | $output = ob_get_contents();
75 | ob_end_clean();
76 | return $output;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/requesthandler.php:
--------------------------------------------------------------------------------
1 | get_user_by_uid($_SERVER['PHP_AUTH_USER'], true);
25 | } else {
26 | throw new Exception("Not logged in.");
27 | }
28 |
29 | // Work out where we are on the server
30 | $base_url = dirname($_SERVER['SCRIPT_NAME']);
31 | $request_url = $_SERVER['REQUEST_URI'];
32 | $relative_request_url = preg_replace('/^'.preg_quote($base_url, '/').'/', '/', $request_url);
33 | $absolute_request_url = 'http'.(isset($_SERVER['HTTPS']) ? 's' : '').'://'.$_SERVER['HTTP_HOST'].$request_url;
34 |
35 | if(empty($config['web']['enabled'])) {
36 | require('views/error503.php');
37 | die;
38 | }
39 |
40 | if(!$active_user->active) {
41 | require('views/error403.php');
42 | }
43 |
44 | if(!empty($_POST)) {
45 | // Check CSRF token
46 | if(isset($_SERVER['HTTP_X_BYPASS_CSRF_PROTECTION']) && $_SERVER['HTTP_X_BYPASS_CSRF_PROTECTION'] == 1) {
47 | // This is being called from script, not a web browser
48 | } elseif(!$active_user->check_csrf_token($_POST['csrf_token'])) {
49 | require('views/csrf.php');
50 | die;
51 | }
52 | }
53 |
54 | // Route request to the correct view
55 | $router = new Router;
56 | foreach($routes as $path => $service) {
57 | $public = array_key_exists($path, $public_routes);
58 | $router->add_route($path, $service, $public);
59 | }
60 | $router->handle_request($relative_request_url);
61 | if(isset($router->view)) {
62 | $view = path_join($base_path, 'views', $router->view.'.php');
63 | if(file_exists($view)) {
64 | if($active_user->auth_realm == 'LDAP' || $router->public) {
65 | require($view);
66 | } else {
67 | require('views/error403.php');
68 | }
69 | } else {
70 | throw new Exception("View file $view missing.");
71 | }
72 | }
73 |
74 | // Handler for uncaught exceptions
75 | function exception_handler($e) {
76 | global $active_user, $config;
77 | $error_number = time();
78 | error_log("$error_number: ".str_replace("\n", "\n$error_number: ", $e));
79 | while(ob_get_length()) {
80 | ob_end_clean();
81 | }
82 | require('views/error500.php');
83 | die;
84 | }
85 |
--------------------------------------------------------------------------------
/views/groups.php:
--------------------------------------------------------------------------------
1 | admin)) {
19 | $name = trim($_POST['name']);
20 | if(preg_match('|/|', $name)) {
21 | $content = new PageSection('invalid_group_name');
22 | $content->set('group_name', $name);
23 | } else {
24 | try {
25 | $new_admin = $user_dir->get_user_by_uid(trim($_POST['admin_uid']));
26 | } catch(UserNotFoundException $e) {
27 | $content = new PageSection('user_not_found');
28 | }
29 | if(isset($new_admin)) {
30 | $group = new Group;
31 | $group->name = $name;
32 | try {
33 | $group_dir->add_group($group);
34 | $group->add_admin($new_admin);
35 | $alert = new UserAlert;
36 | $alert->content = 'Group \''.hesc($name).' \' successfully created.';
37 | $alert->escaping = ESC_NONE;
38 | $active_user->add_alert($alert);
39 | } catch(GroupAlreadyExistsException $e) {
40 | $alert = new UserAlert;
41 | $alert->content = 'Group \''.hesc($name).' \' already exists.';
42 | $alert->escaping = ESC_NONE;
43 | $alert->class = 'danger';
44 | $active_user->add_alert($alert);
45 | }
46 | redirect('#add');
47 | }
48 | }
49 | } else {
50 | $defaults = array();
51 | $defaults['active'] = array('1');
52 | $defaults['name'] = '';
53 | $filter = simplify_search($defaults, $_GET);
54 | try {
55 | $groups = $group_dir->list_groups(array('admins', 'members'), $filter);
56 | } catch(GroupSearchInvalidRegexpException $e) {
57 | $groups = array();
58 | $alert = new UserAlert;
59 | $alert->content = "The group name search pattern '".$filter['hostname']."' is invalid.";
60 | $alert->class = 'danger';
61 | $active_user->add_alert($alert);
62 | }
63 | $content = new PageSection('groups');
64 | $content->set('filter', $filter);
65 | $content->set('admin', $active_user->admin);
66 | $content->set('groups', $groups);
67 | $content->set('all_users', $user_dir->list_users());
68 | }
69 |
70 | $page = new PageSection('base');
71 | $page->set('title', 'Groups');
72 | $page->set('content', $content);
73 | $page->set('alerts', $active_user->pop_alerts());
74 | echo $page->generate();
75 |
--------------------------------------------------------------------------------
/scripts/sync-common.php:
--------------------------------------------------------------------------------
1 | request = $request;
39 | $this->output = '';
40 | $descriptorspec = array(
41 | 0 => array("pipe", "r"), // stdin
42 | 1 => array("pipe", "w"), // stdout
43 | 2 => array("pipe", "w"), // stderr
44 | 3 => array("pipe", "w") //
45 | );
46 | switch ($timeout_util) {
47 | case "BusyBox":
48 | $commandline = '/usr/bin/timeout -t 60 '.$command.' '.implode(' ', array_map('escapeshellarg', $args));
49 | break;
50 | default:
51 | $commandline = '/usr/bin/timeout 60s '.$command.' '.implode(' ', array_map('escapeshellarg', $args));
52 | }
53 |
54 | $this->handle = proc_open($commandline, $descriptorspec, $this->pipes);
55 | stream_set_blocking($this->pipes[1], 0);
56 | stream_set_blocking($this->pipes[2], 0);
57 | }
58 |
59 | /**
60 | * Get data from the child process
61 | * @return string output from the child process
62 | */
63 | public function get_data() {
64 | if(isset($this->handle) && is_resource($this->handle)) {
65 | $out = fread($this->pipes[1], 4096);
66 | $this->output .= $out;
67 | $this->errors .= fread($this->pipes[2], 4096);
68 | if(feof($this->pipes[1]) && feof($this->pipes[2])) {
69 | foreach($this->pipes as $ref => $pipe) {
70 | fclose($this->pipes[$ref]);
71 | }
72 | unset($this->handle);
73 | if($this->errors) {
74 | echo $this->errors;
75 | $this->output = '';
76 | }
77 | return array('done' => true, 'output' => $this->output);
78 | }
79 | }
80 | }
81 |
82 | /**
83 | * Delete the request that triggered this sync
84 | */
85 | public function __destruct() {
86 | global $sync_request_dir;
87 | if(!is_null($this->request)) {
88 | $sync_request_dir->delete_sync_request($this->request);
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/ldap.php:
--------------------------------------------------------------------------------
1 | conn = null;
28 | $this->host = $host;
29 | $this->starttls = $starttls;
30 | $this->bind_dn = $bind_dn;
31 | $this->bind_password = $bind_password;
32 | $this->options = $options;
33 | }
34 |
35 | private function connect() {
36 | $this->conn = ldap_connect($this->host);
37 | if($this->conn === false) throw new LDAPConnectionFailureException('Invalid LDAP connection settings');
38 | if($this->starttls) {
39 | if(!ldap_start_tls($this->conn)) throw new LDAPConnectionFailureException('Could not initiate TLS connection to LDAP server');
40 | }
41 | foreach($this->options as $option => $value) {
42 | ldap_set_option($this->conn, $option, $value);
43 | }
44 | if(!empty($this->bind_dn)) {
45 | if(!ldap_bind($this->conn, $this->bind_dn, $this->bind_password)) throw new LDAPConnectionFailureException('Could not bind to LDAP server');
46 | }
47 | }
48 |
49 | public function search($basedn, $filter, $fields = array(), $sort = array()) {
50 | if(is_null($this->conn)) $this->connect();
51 | if(empty($fields)) $r = @ldap_search($this->conn, $basedn, $filter);
52 | else $r = @ldap_search($this->conn, $basedn, $filter, $fields);
53 | $sort = array_reverse($sort);
54 | foreach($sort as $field) {
55 | @ldap_sort($this->conn, $r, $field);
56 | }
57 | if($r) {
58 | // Fetch entries
59 | $result = @ldap_get_entries($this->conn, $r);
60 | unset($result['count']);
61 | $items = array();
62 | foreach($result as $item) {
63 | unset($item['count']);
64 | $itemResult = array();
65 | foreach($item as $key => $values) {
66 | if(!is_int($key)) {
67 | if(is_array($values)) {
68 | unset($values['count']);
69 | if(count($values) == 1) $values = $values[0];
70 | }
71 | $itemResult[$key] = $values;
72 | }
73 | }
74 | $items[] = $itemResult;
75 | }
76 | return $items;
77 | }
78 | return false;
79 | }
80 |
81 | public static function escape($str = '') {
82 | $metaChars = array("\\00", "\\", "(", ")", "*");
83 | $quotedMetaChars = array();
84 | foreach($metaChars as $key => $value) {
85 | $quotedMetaChars[$key] = '\\'. dechex(ord($value));
86 | }
87 | $str = str_replace($metaChars, $quotedMetaChars, $str);
88 | return $str;
89 | }
90 | }
91 |
92 | class LDAPConnectionFailureException extends RuntimeException {}
93 |
--------------------------------------------------------------------------------
/templates/base.php:
--------------------------------------------------------------------------------
1 | get('web_config');
18 | header('X-Frame-Options: DENY');
19 | header("Content-Security-Policy: default-src 'self'");
20 | ?>
21 |
22 |
23 |
24 | get('title'))?>
25 |
26 |
27 |
28 |
29 | get('head'), ESC_NONE) ?>
30 |
31 |
Skip to main content
32 |
33 |
34 |
47 |
48 |
49 | get('menu_items') as $url => $name) { ?>
50 | get('relative_request_url')) out(' class="active"', ESC_NONE); ?>>
51 |
52 |
53 |
54 |
55 |
56 |
57 | get('alerts') as $alert) { ?>
58 |
59 | ×
60 | content, $alert->escaping)?>
61 |
62 |
63 | get('content'), ESC_NONE) ?>
64 |
65 |
66 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/scripts/ldap_update.php:
--------------------------------------------------------------------------------
1 | #!/usr/bin/php
2 | list_users();
23 |
24 | // Use 'keys-sync' user as the active user (create if it does not yet exist)
25 | try {
26 | $active_user = $user_dir->get_user_by_uid('keys-sync');
27 | } catch(UserNotFoundException $e) {
28 | $active_user = new User;
29 | $active_user->uid = 'keys-sync';
30 | $active_user->name = 'Synchronization script';
31 | $active_user->email = '';
32 | $active_user->active = 1;
33 | $active_user->admin = 1;
34 | $active_user->developer = 0;
35 | $user_dir->add_user($active_user);
36 | }
37 |
38 | foreach($users as $user) {
39 | if($user->auth_realm == 'LDAP') {
40 | $active = $user->active;
41 | try {
42 | $user->get_details_from_ldap();
43 | if(isset($config['ldap']['user_superior'])) {
44 | $user->get_superior_from_ldap();
45 | }
46 | } catch(UserNotFoundException $e) {
47 | $user->active = 0;
48 | }
49 | if($active && !$user->active) {
50 | // Check for servers that will now be admin-less
51 | $servers = $user->list_admined_servers();
52 | foreach($servers as $server) {
53 | $server_admins = $server->list_effective_admins();
54 | $total_server_admins = 0;
55 | foreach($server_admins as $server_admin) {
56 | if($server_admin->active) $total_server_admins++;
57 | }
58 | if($total_server_admins == 0) {
59 | if(isset($config['ldap']['user_superior'])) {
60 | $rcpt = $user->superior;
61 | while(!is_null($rcpt) && !$rcpt->active) {
62 | $rcpt = $rcpt->superior;
63 | }
64 | }
65 | $email = new Email;
66 | $email->subject = "Server {$server->hostname} has been orphaned";
67 | $email->body = "{$user->name} ({$user->uid}) was an administrator for {$server->hostname}, but they have now been marked as a former employee and there are no active administrators remaining for this server.\n\n";
68 | $email->body .= "Please find a replacement owner for this server and inform {$config['email']['admin_address']} ASAP, otherwise the server will be registered for decommissioning.";
69 | $email->add_reply_to($config['email']['admin_address'], $config['email']['admin_name']);
70 | if(is_null($rcpt)) {
71 | $email->subject .= " - NO SUPERIOR EMPLOYEE FOUND";
72 | $email->body .= "\n\nWARNING: No suitable superior employee could be found!";
73 | $email->add_recipient($config['email']['report_address'], $config['email']['report_name']);
74 | } else {
75 | $email->add_recipient($rcpt->email, $rcpt->name);
76 | $email->add_cc($config['email']['report_address'], $config['email']['report_name']);
77 | }
78 | $email->send();
79 | }
80 | }
81 | }
82 | $user->update();
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/model/access.php:
--------------------------------------------------------------------------------
1 | id)) throw new BadMethodCallException('Access rule must be in directory before options can be added');
34 | $stmt = $this->database->prepare("INSERT INTO access_option SET access_id = ?, `option` = ?, value = ?");
35 | $stmt->bind_param('dss', $this->id, $option->option, $option->value);
36 | $stmt->execute();
37 | $stmt->close();
38 | }
39 |
40 | /**
41 | * Remove an SSH option from the access rule
42 | * @param AccessOption $option to be removed
43 | */
44 | public function delete_option(AccessOption $option) {
45 | if(is_null($this->id)) throw new BadMethodCallException('Access rule must be in directory before options can be deleted');
46 | $stmt = $this->database->prepare("DELETE FROM access_option WHERE access_id = ? AND `option` = ?");
47 | $stmt->bind_param('ds', $this->id, $option->option);
48 | $stmt->execute();
49 | $stmt->close();
50 | }
51 |
52 | /**
53 | * Replace the current list of SSH access options with the provided array of options.
54 | * This is a crude implementation - just deletes all existing options and adds new ones, with
55 | * table locking for a small measure of safety.
56 | * @param array $options array of AccessOption objects
57 | */
58 | public function update_options(array $options) {
59 | $stmt = $this->database->query("LOCK TABLES access_option WRITE");
60 | $oldoptions = $this->list_options();
61 | foreach($oldoptions as $oldoption) {
62 | $this->delete_option($oldoption);
63 | }
64 | foreach($options as $option) {
65 | $this->add_option($option);
66 | }
67 | $stmt = $this->database->query("UNLOCK TABLES");
68 | $this->dest_entity->sync_access();
69 | }
70 |
71 | /**
72 | * List all current SSH access options applied to the access rule.
73 | * @return array of AccessOption objects
74 | */
75 | public function list_options() {
76 | if(is_null($this->id)) throw new BadMethodCallException('Access rule must be in directory before options can be listed');
77 | $stmt = $this->database->prepare("
78 | SELECT *
79 | FROM access_option
80 | WHERE access_id = ?
81 | ORDER BY `option`
82 | ");
83 | $stmt->bind_param('d', $this->id);
84 | $stmt->execute();
85 | $result = $stmt->get_result();
86 | $options = array();
87 | while($row = $result->fetch_assoc()) {
88 | $options[$row['option']] = new AccessOption($row['option'], $row);
89 | }
90 | $stmt->close();
91 | return $options;
92 | }
93 | }
94 |
95 | class AccessNotFoundException extends Exception {}
96 |
--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | NOTICE
2 |
3 | Copyright 2013-2017 Opera Software AS
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 |
17 |
18 | THIRD PARTY ACKNOWLEDGEMENTS
19 |
20 | Component: Bootstrap Framework
21 |
22 | The MIT License (MIT)
23 |
24 | Copyright (c) 2011-2017 Twitter, Inc.
25 | Copyright (c) 2011-2017 The Bootstrap Authors
26 |
27 | Permission is hereby granted, free of charge, to any person obtaining a copy
28 | of this software and associated documentation files (the "Software"), to deal
29 | in the Software without restriction, including without limitation the rights
30 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
31 | copies of the Software, and to permit persons to whom the Software is
32 | furnished to do so, subject to the following conditions:
33 |
34 | The above copyright notice and this permission notice shall be included in
35 | all copies or substantial portions of the Software.
36 |
37 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
38 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
39 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
40 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
41 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
42 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
43 | THE SOFTWARE.
44 |
45 | Component: jQuery Javascript Library
46 |
47 | Copyright JS Foundation and other contributors, https://js.foundation/
48 |
49 | This software consists of voluntary contributions made by many
50 | individuals. For exact contribution history, see the revision history
51 | available at https://github.com/jquery/jquery
52 |
53 | The following license applies to all parts of this software except as
54 | documented below:
55 |
56 | ====
57 |
58 | Permission is hereby granted, free of charge, to any person obtaining
59 | a copy of this software and associated documentation files (the
60 | "Software"), to deal in the Software without restriction, including
61 | without limitation the rights to use, copy, modify, merge, publish,
62 | distribute, sublicense, and/or sell copies of the Software, and to
63 | permit persons to whom the Software is furnished to do so, subject to
64 | the following conditions:
65 |
66 | The above copyright notice and this permission notice shall be
67 | included in all copies or substantial portions of the Software.
68 |
69 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
70 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
71 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
72 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
73 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
74 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
75 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
76 |
77 | ====
78 |
79 | All files located in the node_modules and external directories are
80 | externally maintained libraries used by this software which have their
81 | own licenses; we recommend you read them, as their terms may differ from
82 | the terms above.
--------------------------------------------------------------------------------
/views/access_options.php:
--------------------------------------------------------------------------------
1 | vars['hostname'])) {
19 | try {
20 | $server = $server_dir->get_server_by_hostname($router->vars['hostname']);
21 | $server_admin = $active_user->admin_of($server);
22 | $account_admin = false;
23 | if(!$server_admin && !$active_user->admin) {
24 | try {
25 | $account = $server->get_account_by_name($router->vars['account']);
26 | $account_admin = $active_user->admin_of($account);
27 | } catch(ServerAccountNotFoundException $e) {
28 | }
29 | if(!$account_admin) {
30 | require('views/error403.php');
31 | die;
32 | }
33 | } else {
34 | $account = $server->get_account_by_name($router->vars['account']);
35 | }
36 | $access = $account->get_access_by_id($router->vars['access']);
37 | $entity = $account;
38 | } catch(ServerNotFoundException $e) {
39 | require('views/error404.php');
40 | die;
41 | } catch(ServerAccountNotFoundException $e) {
42 | require('views/error404.php');
43 | die;
44 | } catch(AccessNotFoundException $e) {
45 | require('views/error404.php');
46 | die;
47 | }
48 | } elseif(isset($router->vars['group'])) {
49 | try {
50 | $group = $group_dir->get_group_by_name($router->vars['group']);
51 | $group_admin = $active_user->admin_of($group);
52 | if(!$active_user->admin && !$group_admin) {
53 | require('views/error403.php');
54 | die;
55 | }
56 | $access = $group->get_access_by_id($router->vars['access']);
57 | $entity = $group;
58 | } catch(GroupNotFoundException $e) {
59 | require('views/error404.php');
60 | die;
61 | } catch(AccessNotFoundException $e) {
62 | require('views/error404.php');
63 | die;
64 | }
65 | } else {
66 | require('views/error404.php');
67 | die;
68 | }
69 | if(isset($_POST['update_access'])) {
70 | $options = array();
71 | if (isset($group) && !$active_user->admin && !$group_admin) { // not really needed, just future-proofing
72 | require('views/error403.php');
73 | die;
74 | }
75 | if(isset($_POST['access_option'])) {
76 | foreach($_POST['access_option'] as $k => $v) {
77 | if($v['enabled']) {
78 | $option = new AccessOption();
79 | $option->option = $k;
80 | if(isset($v['value'])) {
81 | $option->value = $v['value'];
82 | } else {
83 | $option->value = null;
84 | }
85 | $options[] = $option;
86 | }
87 | }
88 | }
89 | $access->update_options($options);
90 | if(isset($server)) {
91 | redirect('/servers/'.urlencode($router->vars['hostname']).'/accounts/'.urlencode($router->vars['account']).'#access');
92 | } elseif(isset($group)) {
93 | redirect('/groups/'.urlencode($router->vars['group']).'#access');
94 | }
95 | } else {
96 | $content = new PageSection('access_options');
97 | $content->set('entity', $entity);
98 | $content->set('options', $access->list_options());
99 | $content->set('admin', $active_user->admin);
100 | $content->set('remote_entity', $access->source_entity);
101 | $content->set('mode', 'edit');
102 | }
103 |
104 | $page = new PageSection('base');
105 | if(isset($server)) {
106 | $page->set('title', $account->name.'@'.$server->hostname);
107 | } elseif(isset($group)) {
108 | $page->set('title', $group->name);
109 | }
110 | $page->set('content', $content);
111 | $page->set('alerts', $active_user->pop_alerts());
112 | echo $page->generate();
113 |
--------------------------------------------------------------------------------
/model/eventdirectory.php:
--------------------------------------------------------------------------------
1 | array("se.id", "se.server_id", "NULL as `entity_id`", "se.actor_id", "se.date", "se.details"),
33 | 'group' => array("ee.id", "NULL AS server_id", "ee.entity_id", "ee.actor_id", "ee.date", "ee.details")
34 | );
35 | $joins = array('server' => array(), 'group' => array());
36 | $where = array('server' => array(), 'group' => array());
37 | foreach($filter as $field => $value) {
38 | if($value) {
39 | switch($field) {
40 | case 'admin':
41 | // Filter for events from servers that the user is an admin of
42 | $joins['server']['adminsearch'] = "INNER JOIN server_admin AS admin_search ON admin_search.server_id = se.server_id";
43 | $where['server'][] = "admin_search.entity_id = ".intval($value);
44 | // Filter for events from server accounts or groups that the user is an admin of
45 | // (possibly indirectly for the former as a result of being server admin)
46 | $joins['group']['adminsearch'] = "LEFT JOIN entity_admin AS admin_search ON admin_search.entity_id = ee.entity_id";
47 | $joins['group']['account'] = "LEFT JOIN server_account AS sa ON sa.entity_id = ee.entity_id";
48 | $joins['group']['server'] = "LEFT JOIN server AS s ON s.id = sa.server_id";
49 | $joins['group']['parentadminsearch'] = "LEFT JOIN server_admin AS parent_admin_search ON parent_admin_search.server_id = s.id";
50 | $where['group'][] = "admin_search.admin = ".intval($value)." OR parent_admin_search.entity_id = ".intval($value);
51 | break;
52 | }
53 | }
54 | }
55 | $stmt = $this->database->prepare("
56 | (SELECT ".implode(", ", $fields['server']).", 'server' AS event_type
57 | FROM server_event se ".implode(" ", $joins['server'])."
58 | ".(count($where['server']) == 0 ? "" : "WHERE (".implode(") AND (", $where['server']).")")."
59 | GROUP BY se.id
60 | ORDER BY se.id DESC)
61 | UNION
62 | (SELECT ".implode(", ", $fields['group']).", e.type AS event_type
63 | FROM entity_event ee ".implode(" ", $joins['group'])."
64 | INNER JOIN entity e ON e.id = ee.entity_id
65 | ".(count($where['group']) == 0 ? "" : "WHERE (".implode(") AND (", $where['group']).")")."
66 | GROUP BY ee.id
67 | ORDER BY ee.id DESC)
68 | ORDER BY `date` DESC, id DESC
69 | ".(is_null($limit) ? '' : 'LIMIT '.intval($limit))."
70 | ");
71 | $stmt->execute();
72 | $result = $stmt->get_result();
73 | $events = array();
74 | while($row = $result->fetch_assoc()) {
75 | if($row['event_type'] == 'server') {
76 | $events[] = new ServerEvent($row['id'], $row);
77 | } elseif($row['event_type'] == 'user') {
78 | $events[] = new UserEvent($row['id'], $row);
79 | } elseif($row['event_type'] == 'server account') {
80 | $events[] = new ServerAccountEvent($row['id'], $row);
81 | } elseif($row['event_type'] == 'group') {
82 | $events[] = new GroupEvent($row['id'], $row);
83 | }
84 | }
85 | $stmt->close();
86 | return $events;
87 | }
88 | }
89 |
90 | class EventNotFoundException extends Exception {}
91 |
--------------------------------------------------------------------------------
/templates/pubkeys.php:
--------------------------------------------------------------------------------
1 |
18 | Public keys
19 |
20 |
21 |
22 |
Filter options
23 |
24 |
25 |
26 |
27 |
28 |
29 | Fingerprint
30 |
31 |
32 |
33 | Key type
34 |
35 |
36 |
43 |
50 |
51 | Display results
52 |
53 |
54 |
55 |
56 |
57 | get('pubkeys')); out(number_format($total).' public key'.($total == 1 ? '' : 's').' found')?>
58 |
59 |
60 |
61 | Fingerprint
62 | Type
63 | Size
64 | Comment
65 | Owner
66 |
67 |
68 |
69 | get('pubkeys') as $pubkey) {
71 | ?>
72 |
73 |
74 |
75 | fingerprint_md5)?>
76 | fingerprint_sha256)?>
77 |
78 |
79 | type)?>
80 | keysize < 4095) out(' class="danger"', ESC_NONE)?>>keysize)?>
81 | comment)?>
82 |
83 | owner)) {
85 | case 'User':
86 | ?>
87 | owner->uid)?>
88 | owner->active) out(' Inactive ', ESC_NONE) ?>
89 |
93 | owner->name.'@'.$pubkey->owner->server->hostname)?>
94 | owner->server->key_management == 'decommissioned') out(' Inactive ', ESC_NONE) ?>
95 |
99 |
100 |
101 |
104 |
105 |
106 |
--------------------------------------------------------------------------------
/views/servers.php:
--------------------------------------------------------------------------------
1 | admin)) {
19 | $hostname = trim($_POST['hostname']);
20 | if(!preg_match('|.*\..*\..*|', $hostname)) {
21 | $content = new PageSection('invalid_hostname');
22 | $content->set('hostname', $hostname);
23 | } else {
24 | $admin_names = preg_split('/[\s,]+/', $_POST['admins'], -1, PREG_SPLIT_NO_EMPTY);
25 | $admins = array();
26 | foreach($admin_names as $admin_name) {
27 | $admin_name = trim($admin_name);
28 | try {
29 | $new_admin = null;
30 | $new_admin = $user_dir->get_user_by_uid($admin_name);
31 | if(isset($new_admin)) {
32 | $admins[] = $new_admin;
33 | }
34 | } catch(UserNotFoundException $e) {
35 | try {
36 | $new_admin = $group_dir->get_group_by_name($admin_name);
37 | if(isset($new_admin)) {
38 | $admins[] = $new_admin;
39 | }
40 | } catch(GroupNotFoundException $e) {
41 | $content = new PageSection('user_not_found');
42 | }
43 | }
44 | }
45 | if(count($admins) == count($admin_names)) {
46 | $server = new Server;
47 | $server->hostname = $hostname;
48 | $server->port = $_POST['port'];
49 | try {
50 | $server_dir->add_server($server);
51 | foreach($admins as $admin) {
52 | $server->add_admin($admin);
53 | }
54 | $alert = new UserAlert;
55 | $alert->content = 'Server \''.hesc($hostname).' \' successfully created.';
56 | $alert->escaping = ESC_NONE;
57 | $active_user->add_alert($alert);
58 | } catch(ServerAlreadyExistsException $e) {
59 | $alert = new UserAlert;
60 | $alert->content = 'Server \''.hesc($hostname).' \' is already known by SSH Key Authority.';
61 | $alert->escaping = ESC_NONE;
62 | $alert->class = 'danger';
63 | $active_user->add_alert($alert);
64 | }
65 | redirect('#add');
66 | }
67 | }
68 | } else {
69 | $defaults = array();
70 | $defaults['key_management'] = array('keys');
71 | $defaults['sync_status'] = array('sync success', 'sync warning', 'sync failure', 'not synced yet');
72 | $defaults['hostname'] = '';
73 | $defaults['ip_address'] = '';
74 | $filter = simplify_search($defaults, $_GET);
75 | try {
76 | $servers = $server_dir->list_servers(array('pending_requests', 'admins'), $filter);
77 | } catch(ServerSearchInvalidRegexpException $e) {
78 | $servers = array();
79 | $alert = new UserAlert;
80 | $alert->content = "The hostname search pattern '".$filter['hostname']."' is invalid.";
81 | $alert->class = 'danger';
82 | $active_user->add_alert($alert);
83 | }
84 | if(isset($router->vars['format']) && $router->vars['format'] == 'json') {
85 | $page = new PageSection('servers_json');
86 | $page->set('servers', $servers);
87 | header('Content-type: application/json; charset=utf-8');
88 | echo $page->generate();
89 | exit;
90 | } else {
91 | $content = new PageSection('servers');
92 | $content->set('filter', $filter);
93 | $content->set('admin', $active_user->admin);
94 | $content->set('servers', $servers);
95 | $content->set('all_users', $user_dir->list_users());
96 | $content->set('all_groups', $group_dir->list_groups());
97 | if(file_exists('config/keys-sync.pub')) {
98 | $content->set('keys-sync-pubkey', file_get_contents('config/keys-sync.pub'));
99 | } else {
100 | $content->set('keys-sync-pubkey', 'Error: keyfile missing');
101 | }
102 | }
103 | }
104 |
105 | $page = new PageSection('base');
106 | $page->set('title', 'Servers');
107 | $page->set('content', $content);
108 | $page->set('alerts', $active_user->pop_alerts());
109 | echo $page->generate();
110 |
--------------------------------------------------------------------------------
/model/publickeydirectory.php:
--------------------------------------------------------------------------------
1 | database->prepare("
30 | SELECT public_key.*, entity.type AS entity_type
31 | FROM public_key
32 | INNER JOIN entity ON entity.id = public_key.entity_id
33 | WHERE public_key.id = ?
34 | ");
35 | $stmt->bind_param('d', $id);
36 | $stmt->execute();
37 | $result = $stmt->get_result();
38 | if($row = $result->fetch_assoc()) {
39 | switch($row['entity_type']) {
40 | case 'user': $row['owner'] = new User($row['entity_id']); break;
41 | case 'server account': $row['owner'] = new ServerAccount($row['entity_id']); break;
42 | }
43 | $key = new PublicKey($row['id'], $row);
44 | } else {
45 | throw new PublicKeyNotFoundException('Public key does not exist.');
46 | }
47 | $stmt->close();
48 | return $key;
49 | }
50 |
51 | /**
52 | * List stored public keys, optionally filtered by various parameters.
53 | * See also Entity::list_public_keys function for retrieving keys belonging to a specific entity.
54 | * @param array $include list of extra data to include in response - currently unused
55 | * @param array $filter list of field/value pairs to filter results on
56 | * @return array of PublicKey objects
57 | */
58 | public function list_public_keys($include = array(), $filter = array()) {
59 | // WARNING: The search query is not parameterized - be sure to properly escape all input
60 | $fields = array("public_key.*, entity.type AS entity_type");
61 | $joins = array();
62 | $where = array();
63 | foreach($filter as $field => $value) {
64 | if($value) {
65 | switch($field) {
66 | case 'type':
67 | $where[] = "public_key.type = '".$this->database->escape_string($value)."'";
68 | break;
69 | case 'keysize-min':
70 | $where[] = "public_key.keysize >= ".intval($this->database->escape_string($value));
71 | break;
72 | case 'keysize-max':
73 | $where[] = "public_key.keysize <= ".intval($this->database->escape_string($value));
74 | break;
75 | case 'fingerprint':
76 | $where[] = "public_key.fingerprint_md5 = '".$this->database->escape_string($value)."' OR public_key.fingerprint_sha256 = '".$this->database->escape_string($value)."'";
77 | break;
78 | }
79 | }
80 | }
81 | $stmt = $this->database->prepare("
82 | SELECT ".implode(", ", $fields)."
83 | FROM public_key ".implode(" ", $joins)."
84 | INNER JOIN entity ON entity.id = public_key.entity_id
85 | LEFT JOIN user ON user.entity_id = entity.id
86 | LEFT JOIN server_account ON server_account.entity_id = entity.id
87 | LEFT JOIN server ON server.id = server_account.server_id
88 | ".(count($where) == 0 ? "" : "WHERE (".implode(") AND (", $where).")")."
89 | ORDER BY entity.type, user.uid, server.hostname, server_account.name
90 | ");
91 | $stmt->execute();
92 | $result = $stmt->get_result();
93 | $pubkeys = array();
94 | while($row = $result->fetch_assoc()) {
95 | switch($row['entity_type']) {
96 | case 'user': $row['owner'] = new User($row['entity_id']); break;
97 | case 'server account': $row['owner'] = new ServerAccount($row['entity_id']); break;
98 | }
99 | $pubkeys[] = new PublicKey($row['id'], $row);
100 | }
101 | return $pubkeys;
102 | }
103 | }
104 |
105 | class PublicKeyNotFoundException extends Exception {}
106 |
--------------------------------------------------------------------------------
/public_html/style.css:
--------------------------------------------------------------------------------
1 | /*
2 | ##
3 | ## Copyright 2013-2017 Opera Software AS
4 | ##
5 | ## Licensed under the Apache License, Version 2.0 (the "License");
6 | ## you may not use this file except in compliance with the License.
7 | ## You may obtain a copy of the License at
8 | ##
9 | ## http://www.apache.org/licenses/LICENSE-2.0
10 | ##
11 | ## Unless required by applicable law or agreed to in writing, software
12 | ## distributed under the License is distributed on an "AS IS" BASIS,
13 | ## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | ## See the License for the specific language governing permissions and
15 | ## limitations under the License.
16 | ##
17 | */
18 | html,
19 | body {
20 | height: 100%;
21 | /* The html and body elements cannot have any padding or margin. */
22 | }
23 |
24 | /* Wrapper for page content to push down footer */
25 | #wrap {
26 | min-height: 100%;
27 | height: auto;
28 | /* Negative indent footer by its height */
29 | margin: 0 auto -60px;
30 | /* Pad bottom by footer height */
31 | padding: 0 0 60px;
32 | }
33 | #wrap > .container {
34 | padding: 60px 15px 0;
35 | }
36 | #content *:first-child {
37 | margin-top: 0;
38 | }
39 | .nav-tabs {
40 | margin-bottom: 20px;
41 | }
42 | #footer {
43 | height: 60px;
44 | background-color: #f5f5f5;
45 | }
46 | #footer > .container {
47 | padding: 20px 15px 0 15px;
48 | }
49 | .navbar-brand img {
50 | height: 100%;
51 | float: left;
52 | margin-right: 1em;
53 | }
54 | .panel-group + p {
55 | margin-top: 1em;
56 | }
57 | a.group, a.server, a.serveraccount, a.user {
58 | white-space: nowrap;
59 | }
60 | a.group::before {
61 | content: "\e032";
62 | /*content: "\e056";*/
63 | display: inline-block;
64 | font-family: "Glyphicons Halflings";
65 | font-style: normal;
66 | font-weight: 400;
67 | line-height: 1;
68 | position: relative;
69 | top: 1px;
70 | padding-right: 0.4em;
71 | }
72 | a.server::before {
73 | content: "\e121";
74 | display: inline-block;
75 | font-family: "Glyphicons Halflings";
76 | font-style: normal;
77 | font-weight: 400;
78 | line-height: 1;
79 | position: relative;
80 | top: 1px;
81 | padding-right: 0.4em;
82 | }
83 | a.serveraccount::before {
84 | content: "\e161";
85 | display: inline-block;
86 | font-family: "Glyphicons Halflings";
87 | font-style: normal;
88 | font-weight: 400;
89 | line-height: 1;
90 | position: relative;
91 | top: 1px;
92 | padding-right: 0.4em;
93 | }
94 | a.user::before {
95 | content: "\e008";
96 | display: inline-block;
97 | font-family: "Glyphicons Halflings";
98 | font-style: normal;
99 | font-weight: 400;
100 | line-height: 1;
101 | position: relative;
102 | top: 1px;
103 | padding-right: 0.4em;
104 | }
105 | .input-group-addon label {
106 | margin: 0;
107 | }
108 | .indented {
109 | padding-left: 2em !important;
110 | }
111 | dl.oneline dt::before {
112 | content: '\A';
113 | white-space: pre;
114 | }
115 | dl.oneline dt:first-child::before {
116 | white-space: normal;
117 | }
118 | dl.oneline dt {
119 | display: inline;
120 | }
121 | dl.oneline dd {
122 | display: inline;
123 | padding-left: 0.5em;
124 | }
125 | dl.spaced dd {
126 | margin-bottom: 1em;
127 | }
128 | ul.compact {
129 | margin: 0;
130 | padding: 0;
131 | list-style-type: none;
132 | }
133 | pre {
134 | white-space: pre-wrap;
135 | }
136 | pre.ascii-art {
137 | line-height: 1;
138 | }
139 | .pre-formatted {
140 | word-break: break-all;
141 | word-wrap: break-word;
142 | white-space: pre-wrap;
143 | }
144 | span.date {
145 | white-space: nowrap;
146 | }
147 | .nowrap {
148 | white-space: nowrap;
149 | }
150 | .spinner {
151 | display: inline-block;
152 | width: 12px;
153 | height: 12px;
154 | -webkit-animation: spinner 1s infinite linear;
155 | animation: spinner 1s infinite linear;
156 | border-radius:7px;
157 | border-left:2px solid gray;
158 | border-bottom:2px solid gray;
159 | }
160 | @-webkit-keyframes spinner {
161 | to {
162 | -webkit-transform: rotate(360deg);
163 | }
164 | }
165 | @keyframes spinner {
166 | to {
167 | transform: rotate(360deg);
168 | }
169 | }
170 | .monospace {
171 | font-family: monospace;
172 | }
173 | td.date {
174 | width: 11em;
175 | }
176 |
177 | /* Now with 100% more pink! */
178 | div.navbar-default {
179 | background-color: #fff1f9;
180 | border-color: #f7e7f6;
181 | }
182 | .navbar-default .navbar-nav>.active>a, .navbar-default .navbar-nav>.active>a:focus, .navbar-default .navbar-nav>.active>a:hover {
183 | background-color: #ffdfef;
184 | }
185 | a {
186 | color: #af3578;
187 | }
188 | a:focus, a:hover {
189 | color: #611d42;
190 | }
191 |
--------------------------------------------------------------------------------
/scripts/syncd.php:
--------------------------------------------------------------------------------
1 | #!/usr/bin/php
2 | list_pending_sync_requests();
127 | foreach($reqs as $req) {
128 | $args = array();
129 | $args[] = '--id';
130 | $args[] = $req->server_id;
131 | if(!is_null($req->account_name)) {
132 | $args[] = '--user';
133 | $args[] = $req->account_name;
134 | }
135 | if(count($sync_procs) > MAX_PROCS) break;
136 | $req->set_in_progress();
137 | dlog("Sync process spawning for: {$req->server_id}/{$req->account_name}");
138 | $sync_procs[] = new SyncProcess(__DIR__.'/sync.php', $args, $req);
139 | }
140 | } catch(mysqli_sql_exception $e) {
141 | if($e->getMessage() == 'MySQL server has gone away') {
142 | dlog("MySQL server has gone away");
143 | $connected = false;
144 | while(!$connected) {
145 | try {
146 | setup_database();
147 | $connected = true;
148 | dlog("MySQL connection re-established");
149 | } catch(mysqli_sql_exception $e2) {
150 | dlog("Attempt to reconnect failed: ".$e2->getMessage());
151 | sleep(5);
152 | }
153 | }
154 | }
155 | }
156 | foreach($sync_procs as $ref => &$sync_proc) {
157 | $data = $sync_proc->get_data();
158 | if(!empty($data)) {
159 | dlog($data['output']);
160 | unset($sync_proc);
161 | unset($sync_procs[$ref]);
162 | }
163 | }
164 | sleep(1);
165 | }
166 | dlog("Received exit signal");
167 |
168 | if(!isset($options['systemd'])) {
169 | // Release lock
170 | flock($lock, LOCK_UN);
171 | fclose($lock);
172 | }
173 |
--------------------------------------------------------------------------------
/templates/groups.php:
--------------------------------------------------------------------------------
1 |
18 | Groups
19 | get('admin')) { ?>
20 |
24 |
25 |
26 |
27 |
28 |
29 |
Group list
30 |
31 |
32 |
33 |
34 | Filter options
35 |
36 |
37 |
38 |
39 |
40 |
46 |
47 |
Status
48 | $label) {
53 | $checked = in_array($value, $this->get('filter')['active']) ? ' checked' : '';
54 | ?>
55 |
>
56 |
57 |
58 |
59 | Display results
60 |
61 |
62 |
63 |
64 | get('groups')) == 0) { ?>
65 |
No groups found.
66 |
67 |
68 | get('active_user')->get_csrf_field(), ESC_NONE) ?>
69 |
70 |
71 |
72 | Group
73 | Members
74 | Admins
75 | get('admin')) { ?>
76 | Actions
77 |
78 |
79 |
80 |
81 | get('groups') as $group) { ?>
82 | active) out(' class="text-muted"', ESC_NONE) ?>>
83 | name) ?>
84 | member_count))?>
85 | admins)?>
86 | get('admin')) { ?>
87 |
88 | Manage group
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | get('admin')) { ?>
99 |
119 |
120 |
121 |
--------------------------------------------------------------------------------
/views/pubkey.php:
--------------------------------------------------------------------------------
1 | vars['key'])) {
19 | try {
20 | $pubkey = $pubkey_dir->get_public_key_by_id($router->vars['key']);
21 | } catch(PublicKeyNotFoundException $e) {
22 | require('views/error404.php');
23 | die;
24 | }
25 | } else {
26 | $pubkeys = $pubkey_dir->list_public_keys(array(), array('fingerprint' => $router->vars['key']));
27 | if(count($pubkeys) == 1) {
28 | redirect('/pubkeys/'.urlencode($pubkeys[0]->id));
29 | } elseif(count($pubkeys) > 1) {
30 | redirect('/pubkeys?fingerprint='.urlencode($router->vars['key']));
31 | } else {
32 | require('views/error404.php');
33 | }
34 | exit;
35 | }
36 | $dest_rules = $pubkey->list_destination_rules();
37 | $signatures = $pubkey->list_signatures();
38 | $user_is_owner = false;
39 | switch(get_class($pubkey->owner)) {
40 | case 'User':
41 | $title = 'Public key '.$pubkey->comment.' for '.$pubkey->owner->name;
42 | if($pubkey->owner->uid == $active_user->uid) {
43 | $user_is_owner = true;
44 | }
45 | break;
46 | case 'ServerAccount':
47 | $title = 'Public key '.$pubkey->comment.' for '.$pubkey->owner->name.'@'.$pubkey->owner->server->hostname;
48 | if($active_user->admin_of($pubkey->owner) || $active_user->admin_of($pubkey->owner->server)) {
49 | $user_is_owner = true;
50 | }
51 | break;
52 | default:
53 | require('views/error404.php');
54 | die;
55 | }
56 | if(isset($router->vars['format']) && $router->vars['format'] == 'txt') {
57 | $page = new PageSection('pubkey_txt');
58 | $page->set('pubkey', $pubkey);
59 | header('Content-type: text/plain; charset=utf-8');
60 | echo $page->generate();
61 | } elseif(isset($router->vars['format']) && $router->vars['format'] == 'json') {
62 | $page = new PageSection('pubkey_json');
63 | $page->set('pubkey', $pubkey);
64 | header('Content-type: application/json; charset=utf-8');
65 | echo $page->generate();
66 | } else {
67 | if(isset($_POST['add_signature']) && ($user_is_owner || $active_user->admin)) {
68 | $sig = new PublicKeySignature;
69 | $sig->signature = file_get_contents($_FILES['signature']['tmp_name']);
70 | $sig->public_key = $pubkey;
71 | try {
72 | $pubkey->add_signature($sig);
73 | redirect('#sig');
74 | } catch(InvalidArgumentException $e) {
75 | $content = new PageSection('signature_upload_fail');
76 | switch($e->getMessage()) {
77 | case "Signature doesn't validate against pubkey":
78 | $content->set('message', "The signature you submitted doesn't seem to validate against this public key.");
79 | break;
80 | default:
81 | $content->set('message', "The signature you submitted doesn't look valid.");
82 | }
83 | }
84 | } elseif(isset($_POST['delete_signature']) && ($user_is_owner || $active_user->admin)) {
85 | foreach($signatures as $sig) {
86 | if($sig->id == $_POST['delete_signature']) {
87 | $sig_to_delete = $sig;
88 | }
89 | }
90 | if(isset($sig_to_delete)) {
91 | $pubkey->delete_signature($sig_to_delete);
92 | }
93 | redirect('#sig');
94 | } elseif(isset($_POST['add_dest_rule']) && ($user_is_owner || $active_user->admin)) {
95 | $rule = new PublicKeyDestRule;
96 | $rule->account_name_filter = $_POST['account_name_filter'];
97 | $rule->hostname_filter = $_POST['hostname_filter'];
98 | $pubkey->add_destination_rule($rule);
99 | redirect('#dest');
100 | } elseif(isset($_POST['delete_dest_rule']) && ($user_is_owner || $active_user->admin)) {
101 | foreach($dest_rules as $rule) {
102 | if($rule->id == $_POST['delete_dest_rule']) {
103 | $rule_to_delete = $rule;
104 | }
105 | }
106 | if(isset($rule_to_delete)) {
107 | $pubkey->delete_destination_rule($rule_to_delete);
108 | }
109 | redirect('#dest');
110 | } else {
111 | $content = new PageSection('pubkey');
112 | $content->set('pubkey', $pubkey);
113 | $content->set('admin', $active_user->admin);
114 | $content->set('user_is_owner', $user_is_owner);
115 | $content->set('signatures', $signatures);
116 | $content->set('dest_rules', $dest_rules);
117 | }
118 | $head = ' '."\n";
119 | $head .= ' '."\n";
120 | $page = new PageSection('base');
121 | $page->set('title', $title);
122 | $page->set('head', $head);
123 | $page->set('content', $content);
124 | $page->set('alerts', $active_user->pop_alerts());
125 | echo $page->generate();
126 | }
127 |
--------------------------------------------------------------------------------
/model/userdirectory.php:
--------------------------------------------------------------------------------
1 | ldap = $ldap;
35 | $this->cache_uid = array();
36 | }
37 |
38 | /**
39 | * Create the new user in the database.
40 | * @param User $user object to add
41 | */
42 | public function add_user(User $user) {
43 | $user_id = $user->uid;
44 | $user_name = $user->name;
45 | $user_active = $user->active;
46 | $user_admin = $user->admin;
47 | $user_email = $user->email;
48 | $stmt = $this->database->prepare("INSERT INTO entity SET type = 'user'");
49 | $stmt->execute();
50 | $user->entity_id = $stmt->insert_id;
51 | $stmt = $this->database->prepare("INSERT INTO user SET entity_id = ?, uid = ?, name = ?, email = ?, active = ?, admin = ?");
52 | $stmt->bind_param('dsssdd', $user->entity_id, $user_id, $user_name, $user_email, $user_active, $user_admin);
53 | $stmt->execute();
54 | $stmt->close();
55 | }
56 |
57 | /**
58 | * Get a user from the database by its entity ID.
59 | * @param int $entity_id of user
60 | * @return User with specified entity ID
61 | * @throws UserNotFoundException if no user with that entity ID exists
62 | */
63 | public function get_user_by_id($id) {
64 | $stmt = $this->database->prepare("SELECT * FROM user WHERE entity_id = ?");
65 | $stmt->bind_param('d', $id);
66 | $stmt->execute();
67 | $result = $stmt->get_result();
68 | if($row = $result->fetch_assoc()) {
69 | $user = new User($row['entity_id'], $row);
70 | } else {
71 | throw new UserNotFoundException('User does not exist.');
72 | }
73 | $stmt->close();
74 | return $user;
75 | }
76 |
77 | /**
78 | * Get a user from the database by its uid. If it does not exist in the database, retrieve it
79 | * from LDAP and store in the database.
80 | * @param string $uid of user
81 | * @param bool $login true if getting user as part of login process
82 | * @return User with specified entity uid
83 | * @throws UserNotFoundException if no user with that uid exists
84 | */
85 | public function get_user_by_uid($uid, $login = false) {
86 | if(isset($this->cache_uid[$uid])) {
87 | return $this->cache_uid[$uid];
88 | }
89 | $stmt = $this->database->prepare("SELECT * FROM user WHERE uid = ?");
90 | $stmt->bind_param('s', $uid);
91 | $stmt->execute();
92 | $result = $stmt->get_result();
93 | if($row = $result->fetch_assoc()) {
94 | $user = new User($row['entity_id'], $row);
95 | $this->cache_uid[$uid] = $user;
96 | } else {
97 | $user = new User;
98 | $user->uid = $uid;
99 | $this->cache_uid[$uid] = $user;
100 | $user->get_details_from_ldap($login);
101 | }
102 | $stmt->close();
103 | return $user;
104 | }
105 |
106 | /**
107 | * List all users in the database.
108 | * @param array $include list of extra data to include in response - currently unused
109 | * @param array $filter list of field/value pairs to filter results on
110 | * @return array of User objects
111 | */
112 | public function list_users($include = array(), $filter = array()) {
113 | // WARNING: The search query is not parameterized - be sure to properly escape all input
114 | $fields = array("user.*");
115 | $joins = array();
116 | $where = array();
117 | foreach($filter as $field => $value) {
118 | if($value) {
119 | switch($field) {
120 | case 'uid':
121 | $where[] = "uid = '".$this->database->escape_string($value)."'";
122 | break;
123 | case 'name':
124 | $where[] = "name = '".$this->database->escape_string($value)."'";
125 | break;
126 | case 'admins_servers':
127 | $joins[] = "INNER JOIN server_admin ON server_admin.entity_id = user.entity_id";
128 | $joins[] = "INNER JOIN server ON server.id = server_admin.server_id AND server.key_management <> 'decommissioned'";
129 | break;
130 | }
131 | }
132 | }
133 | $stmt = $this->database->prepare("
134 | SELECT ".implode(", ", $fields)."
135 | FROM user ".implode(" ", $joins)."
136 | ".(count($where) == 0 ? "" : "WHERE (".implode(") AND (", $where).")")."
137 | GROUP BY user.entity_id
138 | ORDER BY user.uid
139 | ");
140 | $stmt->execute();
141 | $result = $stmt->get_result();
142 | $users = array();
143 | while($row = $result->fetch_assoc()) {
144 | $users[] = new User($row['entity_id'], $row);
145 | }
146 | return $users;
147 | }
148 | }
149 |
150 | class UserNotFoundException extends Exception {}
151 |
--------------------------------------------------------------------------------
/config/config-sample.ini:
--------------------------------------------------------------------------------
1 | ; SSH Key Authority config file
2 | [web]
3 | enabled = 1
4 | baseurl = https://ska.example.com
5 | logo = /logo-header-opera.png
6 | ; footer may contain HTML. Literal & " < and > should be escaped as &
7 | ; " < $gt;
8 | footer = 'Developed by Opera Software .'
9 |
10 | [general]
11 | ; Use timeout --version to find out the current version
12 | ; used on e.g. debian
13 | timeout_util = GNU coreutils
14 | ; used on e.g. alpine
15 | ; timeout_util = BusyBox
16 |
17 | [security]
18 | ; It is important that SKA is able to verify that it has connected to the
19 | ; server that it expected to connect to (otherwise it could be tricked into
20 | ; syncing the wrong keys to a server). The simplest way to accomplish this is
21 | ; through SSH host key verification. Setting either of the 2 options below to
22 | ; '0' can weaken the protection that SSH host key verification provides.
23 |
24 | ; Determine who can reset a server's SSH host key in SKA:
25 | ; 0: Allow server admins to reset the SSH host key for servers that they
26 | ; administer
27 | ; 1: Full SKA admin access is required to reset a server's host key
28 | host_key_reset_restriction = 1
29 |
30 | ; Determine what happens if multiple servers have the same SSH host key:
31 | ; 0: Allow sync to proceed
32 | ; 1: Abort sync of affected servers and report an error
33 | ; It is not recommended to leave this set to '0' indefinitely
34 | host_key_collision_protection = 1
35 |
36 |
37 | ; Hostname verification is a supplement to SSH host key verification for
38 | ; making sure that the sync process has connected to the server that it
39 | ; expected to.
40 |
41 | ; Determine how hostname verification is performed:
42 | ; 0: Do not perform hostname verification
43 | ; 1: Compare with the result of `hostname -f`
44 | ; 2: Compare with /var/local/keys-sync/.hostnames, fall back to `hostname -f`
45 | ; if the file does not exist
46 | ; 3: Compare with /var/local/keys-sync/.hostnames, abort sync if the file
47 | ; does not exist
48 | ; The last option provides the most solid verification, as a server will only
49 | ; be synced to if it has been explicitly allowed on the server itself.
50 | hostname_verification = 0
51 |
52 | [defaults]
53 | ; This setting will cause new servers to always have a managed account called
54 | ; "root" and for that account to be automatically added into the
55 | ; "root-accounts" group:
56 | ;
57 | ; account_groups[root] = "root-accounts"
58 | ;
59 | ; Any number of these can be specified
60 | account_groups[root] = "root-accounts"
61 |
62 | [email]
63 | enabled = 1
64 | ; The mail address that outgoing mails will be sent from
65 | from_address = ska@example.com
66 | from_name = "SSH Key Authority system"
67 | ; Where to mail security notifications to
68 | report_address = reports@example.com
69 | report_name = "SSH Key Authority reports"
70 | ; Where users should contact for help
71 | admin_address = admin@example.com
72 | admin_name = "SSH Key Authority administrators"
73 | ; You can use the reroute directive to redirect all outgoing mail to a single
74 | ; mail address - typically for temporary testing purposes
75 | ;reroute = test@example.com
76 |
77 | [database]
78 | ; Connection details to the MySQL database
79 | hostname = localhost
80 | port = 3306
81 | username = ska-user
82 | password = password
83 | database = ska-db
84 |
85 | [ldap]
86 | ; Address to connect to LDAP server
87 | host = ldaps://ldap.example.com:636
88 | ; Use StartTLS for connection security (recommended if using ldap:// instead
89 | ; of ldaps:// above)
90 | starttls = 0
91 | ; LDAP subtree containing USER entries
92 | dn_user = "ou=users,dc=example,dc=com"
93 | ; LDAP subtree containing GROUP entries
94 | dn_group = "ou=groups,dc=example,dc=com"
95 | ; (Optional) filter for matching user objects
96 | ;user_filter = "(objectClass=inetOrgPerson)"
97 | ; (Optional) filter for matching group objects
98 | ;group_filter = "(objectClass=posixGroup)"
99 |
100 | ; Set to 1 if the LDAP library should process referrals. In most cases this
101 | ; is not needed, and for AD servers it can cause errors when querying the
102 | ; whole tree.
103 | follow_referrals = 0
104 |
105 | ; Leave bind_dn empty if binding is not required
106 | bind_dn =
107 | bind_password =
108 |
109 | ; User attributes
110 | user_id = uid
111 | user_name = cn
112 | user_email = mail
113 | ;user_superior = superioremployee
114 |
115 | ; If inactive users exist in your LDAP directory, filter with the following
116 | ; settings:
117 | ; Field to filter on:
118 | ;user_active = organizationalstatus
119 | ; Use *one* of user_active_true or user_active_false
120 | ; user_active_true means user is active if the user_active field equals its
121 | ; value
122 | ;user_active_true = 'current'
123 | ; user_active_false means user is active if the user_active field does not
124 | ; equal its value
125 | ;user_active_false = 'former'
126 |
127 | ; Group membership attributes. Examples below are for typical setups:
128 | ;
129 | ; POSIX groups
130 | ; group_member = memberUid
131 | ; group_member_value = uid
132 | ;
133 | ; Group-of-names groups
134 | ; group_member = member
135 | ; group_member_value = dn
136 | ;
137 | ; Attribute of group where members are stored
138 | group_member = memberUid
139 | ; User attribute to compare with
140 | group_member_value = uid
141 |
142 | ; Members of admin_group are given full admin access to SSH Key Authority web
143 | ; interface
144 | admin_group_cn = ska-administrators
145 |
146 | ; Other LDAP groups that should have their memberships synced
147 | ;sync_groups[] = ldap_group_name
148 |
149 | [inventory]
150 | ; SSH Key Authority will read the contents of the file /etc/uuid (if it
151 | ; exists) when syncing with a server. If a value is found, it can be used as a
152 | ; link to an inventory system.
153 | ; %s in the url directive will be replaced with the value found in /etc/uuid
154 | ;url = "https://inventory.example.com/device/%s"
155 |
156 | [gpg]
157 | ; SSH Key Authority can GPG sign outgoing emails sent from the
158 | ; email.from_address. To do this it needs to know an appropriate key ID to use
159 | ;key_id = 0123456789ABCDEF0123456789ABCDEF01234567
160 |
--------------------------------------------------------------------------------
/model/record.php:
--------------------------------------------------------------------------------
1 | database = &$database;
56 | $this->active_user = &$active_user;
57 | $this->id = $id;
58 | $this->data = array();
59 | foreach($preload_data as $field => $value) {
60 | $this->data[$field] = $value;
61 | }
62 | if(is_null($this->id)) $this->dirty = true;
63 | }
64 |
65 | /**
66 | * Magic getter method - return the value of the specified field. Retrieve the row from the
67 | * database if we do not have data for that field yet.
68 | * @param string $field name of field to retrieve
69 | * @return mixed data stored in field
70 | * @throws Exception if the row or the field does not exist in the database
71 | */
72 | public function &__get($field) {
73 | if(!array_key_exists($field, $this->data)) {
74 | // We don't have a value for this field yet
75 | if(is_null($this->id)) {
76 | // Record is not yet in the database - nothing to retrieve
77 | $result = null;
78 | return $result;
79 | }
80 | // Attempt to get data from database
81 | $stmt = $this->database->prepare("SELECT * FROM `$this->table` WHERE {$this->idfield} = ?");
82 | $stmt->bind_param('d', $this->id);
83 | $stmt->execute();
84 |
85 | $result = $stmt->get_result();
86 | if($result->num_rows != 1) {
87 | throw new Exception("Unexpected number of rows returned ({$result->num_rows}), expected exactly 1. Table:{$this->table}, ID field: {$this->idfield}, ID: {$this->id}");
88 | }
89 | $data = $result->fetch_assoc();
90 | // Populate data array for fields we do not already have a value for
91 | foreach($data as $f => $v) {
92 | if(!isset($this->data[$f])) {
93 | $this->data[$f] = $v;
94 | }
95 | }
96 | $stmt->close();
97 | if(!array_key_exists($field, $this->data)) {
98 | // We still don't have a value, so this field doesn't exist in the database
99 | throw new Exception("Field $field does not exist in {$this->table} table.");
100 | }
101 | }
102 | return $this->data[$field];
103 | }
104 |
105 | /**
106 | * Magic setter method - store the updated value and set the record as dirty.
107 | * @param string $field name of field
108 | * @param mixed $value data to store in field
109 | */
110 | public function __set($field, $value) {
111 | $this->data[$field] = $value;
112 | $this->dirty = true;
113 | if($field == $this->idfield) $this->id = $value;
114 | }
115 |
116 | /**
117 | * Update the database with all fields that have been modified.
118 | * @return array of StdClass detailing actual updates that were applied
119 | * @throws UniqueKeyViolationException if the update violated a unique key on the table
120 | */
121 | public function update() {
122 | $stmt = $this->database->prepare("SELECT * FROM `$this->table` WHERE {$this->idfield} = ?");
123 | $stmt->bind_param('d', $this->id);
124 | $stmt->execute();
125 | $result = $stmt->get_result();
126 | if(!($row = $result->fetch_assoc())) {
127 | throw new Exception("Record not found in database");
128 | }
129 | $stmt->close();
130 | $updates = array();
131 | $fields = array();
132 | $values = array();
133 | $types = '';
134 | foreach($row as $field => $value) {
135 | if(array_key_exists($field, $this->data) && $this->data[$field] != $value) {
136 | $update = new StdClass;
137 | $update->field = $field;
138 | $update->old_value = $value;
139 | $update->new_value = $this->data[$field];
140 | $updates[] = $update;
141 | $fields[] = "`$field` = ?";
142 | $values[] =& $this->data[$field];
143 | $types .= 's';
144 | }
145 | }
146 | if(!empty($updates)) {
147 | try {
148 | $stmt = $this->database->prepare("UPDATE `$this->table` SET ".implode(', ', $fields)." WHERE {$this->idfield} = ?");
149 | $values[] =& $this->id;
150 | $types .= 'd';
151 | array_unshift($values, $types);
152 | $reflection = new ReflectionClass('mysqli_stmt');
153 | $method = $reflection->getMethod("bind_param");
154 | $method->invokeArgs($stmt, $values);
155 | $stmt->execute();
156 | } catch(mysqli_sql_exception $e) {
157 | if($e->getCode() == 1062) {
158 | // Duplicate entry
159 | $message = $e->getMessage();
160 | if(preg_match("/^Duplicate entry '(.*)' for key '(.*)'$/", $message, $matches)) {
161 | $ne = new UniqueKeyViolationException($e->getMessage());
162 | $ne->fields = explode(',', $matches[2]);
163 | $ne->values = explode(',', $matches[1]);
164 | throw $ne;
165 | }
166 | }
167 | throw $e;
168 | }
169 | }
170 | $this->dirty = false;
171 | return $updates;
172 | }
173 | }
174 |
175 | class UniqueKeyViolationException extends Exception {
176 | /**
177 | * Fields involved in the unique key conflict
178 | */
179 | public $fields;
180 | /**
181 | * Values that conflicted
182 | */
183 | public $values;
184 | }
185 |
--------------------------------------------------------------------------------
/templates/pubkey.php:
--------------------------------------------------------------------------------
1 | get('pubkey')->owner;
18 | ?>
19 |
20 | Public key 'get('pubkey')->comment)?>' for
21 | name;
25 | ?>
26 |
27 | name.'@'.$owner->server->hostname;
31 | ?>
32 |
33 |
37 |
38 | get('user_is_owner') || $this->get('admin')) { ?>
39 |
44 |
45 |
46 |
47 |
Information
48 |
49 | Key data
50 | get('pubkey')->export())?>
51 | Key size
52 | get('pubkey')->keysize)?>
53 | Fingerprint (MD5)
54 | get('pubkey')->fingerprint_md5)?>
55 | Randomart (MD5)
56 | get('pubkey')->randomart_md5)?>
57 | Fingerprint (SHA256)
58 | get('pubkey')->fingerprint_sha256)?>
59 | Randomart (SHA256)
60 | get('pubkey')->randomart_sha256)?>
61 |
62 |
63 | get('user_is_owner') || $this->get('admin')) { ?>
64 |
65 |
Key signing
66 |
67 | get('signatures')) == 0) { ?>
68 | No signatures have been uploaded for this key yet.
69 |
70 |
71 |
72 |
73 | Signing key
74 | Signed on
75 | Uploaded on
76 | Actions
77 |
78 |
79 |
80 | get('signatures') as $sig) { ?>
81 |
82 | fingerprint)?>
83 | sign_date)?>
84 | upload_date)?>
85 | Delete signature
86 |
87 |
88 |
89 |
90 |
91 | Add signature
92 | get('active_user')->get_csrf_field(), ESC_NONE) ?>
93 |
94 |
95 | Signature file
96 |
97 |
98 |
99 |
100 | Upload signature
101 |
102 |
103 |
104 |
105 |
Destination restrictions
106 | get('dest_rules')) == 0) { ?>
107 |
This key will currently be synced to all accounts and servers that is granted access to. To restrict this key to a subset of that list, add rules below.
108 |
109 |
This key will only be synced to accounts and servers that is granted access to that also match the following rules:
110 |
111 | get('active_user')->get_csrf_field(), ESC_NONE) ?>
112 |
113 |
114 |
115 | Account name
116 | Hostname
117 | Actions
118 |
119 |
120 |
121 | get('dest_rules') as $rule) { ?>
122 |
123 | account_name_filter)?>
124 | hostname_filter)?>
125 | Delete rule
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 | get('active_user')->get_csrf_field(), ESC_NONE) ?>
134 | Add new rule
135 | You can make use of wildcards (* ) in each field below.
136 |
137 | Account name
138 |
139 |
140 |
141 | Hostname
142 |
143 |
144 |
145 | Add rule
146 |
147 |
148 |
149 |
150 |
151 |
--------------------------------------------------------------------------------
/templates/access_options.php:
--------------------------------------------------------------------------------
1 | get('entity');
18 | switch(get_class($entity)) {
19 | case 'ServerAccount': $account = $entity; $server = $entity->server; break;
20 | case 'Group': $group = $entity; break;
21 | }
22 | $remote_entity = $this->get('remote_entity');
23 | $mode = $this->get('mode');
24 | $options = $this->get('options');
25 | switch(get_class($remote_entity)) {
26 | case 'User': $remote_entity_name = $remote_entity->uid; break;
27 | case 'ServerAccount': $remote_entity_name = $remote_entity->name.'@'.$remote_entity->server->hostname; break;
28 | case 'Group': $remote_entity_name = $remote_entity->name; break;
29 | }
30 | ?>
31 | access
32 |
33 | get('active_user')->get_csrf_field(), ESC_NONE) ?>
34 | uid);
38 | ?>
39 |
40 | server->hostname).'/accounts/'.urlencode($remote_entity->name);
44 | ?>
45 |
46 |
47 | name);
51 | ?>
52 |
53 |
57 |
58 | You are SSH access to
59 |
60 | name.'@'.$server->hostname)?>
61 |
62 | resources in the name)?> group
63 |
64 | for
65 | .
66 |
67 |
68 |
114 |
115 |
127 |
128 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | SKA - SSH Key Authority
2 | =======================
3 |
4 | *Please see the [Security Advisories](#security-advisories) section below for a recently addressed security issue*
5 |
6 | A tool for managing user and server SSH access to any number of servers.
7 |
8 | Features
9 | --------
10 |
11 | * Easily manage SSH key access for all accounts on your servers.
12 | * Manage user access and server-to-server access rules.
13 | * Integrate with your LDAP directory service for user authorization.
14 | * Automatically remove server access from people when they leave your team.
15 | * Provides an easy interface for your users to upload their public keys.
16 | * Designate server administrators and let them manage access to their own server.
17 | * Create group-based access rules for easier management.
18 | * Specify SSH access options such as `command=`, `nopty` etc on your access rules.
19 | * All access changes are logged to the database and to the system logs. Granting of access is also reported by email.
20 | * Be notified when a server becomes orphaned (has no active administrators).
21 |
22 | Demo
23 | ----
24 |
25 | You can view the SSH Key Authority in action on the [demonstration server](https://ska.xiven.com/).
26 |
27 | Use one of the following sets of username / password credentials to log in:
28 |
29 | * testuser / testuser - normal user with admin access granted to a few servers
30 | * testadmin / testadmin - admin user
31 |
32 | All data on this demonstration server is reset nightly at 00:00 UTC.
33 |
34 | Requirements
35 | ------------
36 |
37 | * An LDAP directory service
38 | * Apache 2.2 or higher
39 | * PHP 5.6 or higher
40 | * PHP JSON extension
41 | * PHP LDAP extension
42 | * PHP mbstring (Multibyte String) extension
43 | * PHP MySQL extension
44 | * PHP ssh2 extension
45 | * MySQL (5.5+), Percona Server (5.5+) or MariaDB database
46 |
47 | Installation
48 | ------------
49 |
50 | 1. Clone the repo somewhere outside of your default Apache document root.
51 |
52 | 2. Add the following directives to your Apache configuration (eg. virtual host config):
53 |
54 | DocumentRoot /path/to/ska/public_html
55 | DirectoryIndex init.php
56 | FallbackResource /init.php
57 |
58 | 3. Create a MySQL user and database (run in MySQL shell):
59 |
60 | CREATE USER 'ska-user'@'localhost' IDENTIFIED BY 'password';
61 | CREATE DATABASE `ska-db` DEFAULT CHARACTER SET utf8mb4;
62 | GRANT ALL ON `ska-db`.* to 'ska-user'@'localhost';
63 |
64 | 4. Copy the file `config/config-sample.ini` to `config/config.ini` and edit the settings as required.
65 |
66 | 5. Set up authnz_ldap for your virtual host (or any other authentication module that will pass on an Auth-user
67 | variable to the application).
68 |
69 | 6. Set `scripts/ldap_update.php` to run on a regular cron job.
70 |
71 | 7. Generate an SSH key pair to synchronize with. SSH Key Authority will expect to find the files as `config/keys-sync` and `config/keys-sync.pub`
72 | for the private and public keys respectively. The key must be in `pem` format. The following command will generate the key in the required format:
73 |
74 | ssh-keygen -t rsa -b 4096 -m PEM -C 'comment' -f config/keys-sync
75 |
76 | 8. Install the SSH key synchronization daemon. For systemd:
77 |
78 | 1. Copy `services/systemd/keys-sync.service` to `/etc/systemd/system/`
79 | 2. Modify `ExecStart` path and `User` as necessary. If SSH Key Authority is installed under `/home`, disable `ProtectHome`.
80 | 3. `systemctl daemon-reload`
81 | 4. `systemctl enable keys-sync.service`
82 |
83 | for sysv-init:
84 |
85 | 1. Copy `services/init.d/keys-sync` to `/etc/init.d/`
86 | 2. Modify `SCRIPT` path and `USER` as necessary.
87 | 3. `update-rc.d keys-sync defaults`
88 |
89 | Usage
90 | -----
91 |
92 | Anyone in the LDAP group defined under `admin_group_cn` in `config/config.ini` will be able to manage accounts and servers.
93 |
94 | Key distribution
95 | ----------------
96 |
97 | SSH Key Authority distributes authorized keys to your servers via SSH. It does this by:
98 |
99 | 1. Connecting to the server with SSH, authorizing as the `keys-sync` user.
100 | 2. Writing the appropriate authorized keys to named user files in `/var/local/keys-sync/` (eg. all authorized keys for the root user will be written to `/var/local/keys-sync/root`).
101 |
102 | This means that your SSH installation will need to be reconfigured to read authorized keys from `/var/local/keys-sync/`.
103 |
104 | Please note that doing so will deny access to any existing SSH public key authorized in the default `~/.ssh` directories.
105 |
106 | Under OpenSSH, the configuration changes needed are:
107 |
108 | AuthorizedKeysFile /var/local/keys-sync/%u
109 | StrictModes no
110 |
111 | StrictModes must be disabled because the files will all be owned by the keys-sync user.
112 |
113 | The file `/var/local/keys-sync/keys-sync` must exist, with the same contents as the `config/keys-sync.pub` file in order for the synchronization daemon to authenticate.
114 |
115 | Screenshots
116 | -----------
117 |
118 | ### Homepage overview
119 | 
120 |
121 | ### Server listing
122 | 
123 |
124 | ### Server account access management
125 | 
126 |
127 | ### Activity log
128 | 
129 |
130 | ### Getting started guide for new users
131 | 
132 |
133 | Security advisories
134 | -------------------
135 | * [SKA security advisory: SSH port redirection attack](https://github.com/operasoftware/ssh-key-authority/wiki/SKA-security-advisory%3A-SSH-port-redirection-attack)
136 | * [SKA security advisory: insufficient validation of group access rule edit privileges](https://github.com/operasoftware/ssh-key-authority/wiki/SKA-security-advisory%3A-insufficient-validation-of-group-access-rule-edit-privileges)
137 |
138 | License
139 | -------
140 |
141 | Copyright 2013-2017 Opera Software
142 |
143 | Licensed under the Apache License, Version 2.0 (the "License");
144 | you may not use this file except in compliance with the License.
145 | You may obtain a copy of the License at
146 |
147 | http://www.apache.org/licenses/LICENSE-2.0
148 |
149 | Unless required by applicable law or agreed to in writing, software
150 | distributed under the License is distributed on an "AS IS" BASIS,
151 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
152 | See the License for the specific language governing permissions and
153 | limitations under the License.
154 |
--------------------------------------------------------------------------------
/views/group.php:
--------------------------------------------------------------------------------
1 | get_group_by_name($router->vars['group']);
20 | } catch(GroupNotFoundException $e) {
21 | require('views/error404.php');
22 | die;
23 | }
24 | $all_users = $user_dir->list_users();
25 | $all_groups = $group_dir->list_groups();
26 | $all_servers = $server_dir->list_servers();
27 | $admined_servers = $active_user->list_admined_servers();
28 | $group_members = $group->list_members();
29 | $group_access = $group->list_access();
30 | $group_remote_access = $group->list_remote_access();
31 | $group_admins = $group->list_admins();
32 | $group_admin = $active_user->admin_of($group);
33 |
34 | if(isset($_POST['add_admin']) && ($active_user->admin)) {
35 | try {
36 | $user = $user_dir->get_user_by_uid($_POST['user_name']);
37 | } catch(UserNotFoundException $e) {
38 | $content = new PageSection('user_not_found');
39 | }
40 | if(isset($user)) {
41 | $group->add_admin($user);
42 | redirect('#admins');
43 | }
44 | } elseif(isset($_POST['delete_admin']) && ($active_user->admin)) {
45 | foreach($group_admins as $admin) {
46 | if($admin->id == $_POST['delete_admin']) {
47 | $admin_to_delete = $admin;
48 | }
49 | }
50 | if(isset($admin_to_delete)) {
51 | $group->delete_admin($admin_to_delete);
52 | }
53 | redirect('#admins');
54 | } elseif(isset($_POST['add_member']) && ($group_admin || $active_user->admin)) {
55 | if(isset($_POST['username'])) {
56 | try {
57 | $entity = $user_dir->get_user_by_uid(trim($_POST['username']));
58 | } catch(UserNotFoundException $e) {
59 | $content = new PageSection('user_not_found');
60 | }
61 | } elseif(isset($_POST['account'])) {
62 | try {
63 | $server = $server_dir->get_server_by_hostname(trim($_POST['hostname']));
64 | $entity = $server->get_account_by_name(trim($_POST['account']));
65 | } catch(ServerNotFoundException $e) {
66 | $content = new PageSection('server_not_found');
67 | } catch(ServerAccountNotFoundException $e) {
68 | $content = new PageSection('server_account_not_found');
69 | }
70 | }
71 | if(isset($entity) && !$group->system) {
72 | try {
73 | $group->add_member($entity);
74 | redirect('#members');
75 | } catch(InvalidArgumentException $e) {
76 | $content = new PageSection('not_admin');
77 | }
78 | }
79 | } elseif(isset($_POST['delete_member']) && ($group_admin || $active_user->admin)) {
80 | foreach($group_members as $member) {
81 | if($member->entity_id == $_POST['delete_member']) {
82 | $member_to_delete = $member;
83 | }
84 | }
85 | if(isset($member_to_delete) && !$group->system) {
86 | $group->delete_member($member_to_delete);
87 | }
88 | redirect('#members');
89 | } elseif(isset($_POST['add_access']) && ($group_admin || $active_user->admin)) {
90 | if(isset($_POST['username'])) {
91 | try {
92 | $entity = $user_dir->get_user_by_uid(trim($_POST['username']));
93 | } catch(UserNotFoundException $e) {
94 | $content = new PageSection('user_not_found');
95 | }
96 | } elseif(isset($_POST['account'])) {
97 | try {
98 | $server = $server_dir->get_server_by_hostname(trim($_POST['hostname']));
99 | $entity = $server->get_account_by_name(trim($_POST['account']));
100 | } catch(ServerNotFoundException $e) {
101 | $content = new PageSection('server_not_found');
102 | } catch(ServerAccountNotFoundException $e) {
103 | $content = new PageSection('server_account_not_found');
104 | }
105 | } elseif(isset($_POST['group'])) {
106 | try {
107 | $entity = $group_dir->get_group_by_name(trim($_POST['group']));
108 | } catch(GroupNotFoundException $e) {
109 | $content = new PageSection('group_not_found');
110 | }
111 | }
112 | if(isset($entity)) {
113 | if($_POST['add_access'] == '2') {
114 | $options = array();
115 | if(isset($_POST['access_option'])) {
116 | foreach($_POST['access_option'] as $k => $v) {
117 | if(isset($v['enabled'])) {
118 | $option = new AccessOption();
119 | $option->option = $k;
120 | if(isset($v['value'])) {
121 | $option->value = $v['value'];
122 | } else {
123 | $option->value = null;
124 | }
125 | $options[] = $option;
126 | }
127 | }
128 | }
129 | $group->add_access($entity, $options);
130 | redirect('#access');
131 | } else {
132 | $content = new PageSection('access_options');
133 | $content->set('entity', $group);
134 | $content->set('remote_entity', $entity);
135 | $content->set('mode', 'create');
136 | }
137 | }
138 | } elseif(isset($_POST['delete_access']) && ($group_admin || $active_user->admin)) {
139 | foreach($group_access as $access) {
140 | if($access->id == $_POST['delete_access']) {
141 | $access_to_delete = $access;
142 | }
143 | }
144 | if(isset($access_to_delete)) {
145 | $group->delete_access($access_to_delete);
146 | }
147 | redirect('#access');
148 | } elseif(isset($_POST['edit_group']) && ($active_user->admin)) {
149 | $name = trim($_POST['name']);
150 | $group->name = $name;
151 | $group->active = $_POST['active'];
152 | try {
153 | $group->update();
154 | $alert = new UserAlert;
155 | $alert->content = "Settings saved.";
156 | $active_user->add_alert($alert);
157 | redirect('/groups/'.urlencode($name).'#settings'); // Must specify, since the name may have changed
158 | } catch(UniqueKeyViolationException $e) {
159 | $content = new PageSection('unique_key_violation');
160 | $content->set('exception', $e);
161 | }
162 | } else {
163 | if(isset($router->vars['format']) && $router->vars['format'] == 'json') {
164 | $page = new PageSection('group_json');
165 | $page->set('group_members', $group_members);
166 | header('Content-type: application/json; charset=utf-8');
167 | echo $page->generate();
168 | exit;
169 | } else {
170 | $content = new PageSection('group');
171 | $content->set('group', $group);
172 | $content->set('admin', $active_user->admin);
173 | $content->set('group_admin', $group_admin);
174 | $content->set('group_admins', $group_admins);
175 | $content->set('group_members', $group_members);
176 | $content->set('group_access', $group_access);
177 | $content->set('group_remote_access', $group_remote_access);
178 | $content->set('group_log', $group->get_log());
179 | $content->set('all_users', $all_users);
180 | $content->set('all_groups', $all_groups);
181 | $content->set('all_servers', $all_servers);
182 | $content->set('admined_servers', $admined_servers);
183 | }
184 | }
185 |
186 | $page = new PageSection('base');
187 | $page->set('title', $group->name);
188 | $page->set('content', $content);
189 | $page->set('alerts', $active_user->pop_alerts());
190 | echo $page->generate();
191 |
--------------------------------------------------------------------------------
/model/serverdirectory.php:
--------------------------------------------------------------------------------
1 | hostname;
29 | $port = $server->port;
30 | try {
31 | $stmt = $this->database->prepare("INSERT INTO server SET hostname = ?, port = ?");
32 | $stmt->bind_param('sd', $hostname, $port);
33 | $stmt->execute();
34 | $server->id = $stmt->insert_id;
35 | $stmt->close();
36 | $server->log(array('action' => 'Server add'));
37 | $server->add_standard_accounts();
38 | $server->sync_access();
39 | } catch(mysqli_sql_exception $e) {
40 | if($e->getCode() == 1062) {
41 | // Duplicate entry
42 | throw new ServerAlreadyExistsException("Server {$server->hostname} already exists");
43 | } else {
44 | throw $e;
45 | }
46 | }
47 | }
48 |
49 | /**
50 | * Get a server from the database by its ID.
51 | * @param int $id of server
52 | * @return Server with specified ID
53 | * @throws ServerNotFoundException if no server with that ID exists
54 | */
55 | public function get_server_by_id($server_id) {
56 | $stmt = $this->database->prepare("SELECT * FROM server WHERE id = ?");
57 | $stmt->bind_param('d', $server_id);
58 | $stmt->execute();
59 | $result = $stmt->get_result();
60 | if($row = $result->fetch_assoc()) {
61 | $server = new Server($row['id'], $row);
62 | } else {
63 | throw new ServerNotFoundException('Server does not exist.');
64 | }
65 | $stmt->close();
66 | return $server;
67 | }
68 |
69 | /**
70 | * Get a server from the database by its hostname.
71 | * @param string $hostname of server
72 | * @return Server with specified hostname
73 | * @throws ServerNotFoundException if no server with that hostname exists
74 | */
75 | public function get_server_by_hostname($hostname) {
76 | $stmt = $this->database->prepare("SELECT * FROM server WHERE hostname = ?");
77 | $stmt->bind_param('s', $hostname);
78 | $stmt->execute();
79 | $result = $stmt->get_result();
80 | if($row = $result->fetch_assoc()) {
81 | $server = new Server($row['id'], $row);
82 | } else {
83 | throw new ServerNotFoundException('Server does not exist');
84 | }
85 | $stmt->close();
86 | return $server;
87 | }
88 |
89 | /**
90 | * Get a server from the database by its uuid.
91 | * @param string $uuid of server
92 | * @return Server with specified uuid
93 | * @throws ServerNotFoundException if no server with that uuid exists
94 | */
95 | public function get_server_by_uuid($uuid) {
96 | $stmt = $this->database->prepare("SELECT * FROM server WHERE uuid = ?");
97 | $stmt->bind_param('s', $uuid);
98 | $stmt->execute();
99 | $result = $stmt->get_result();
100 | if($row = $result->fetch_assoc()) {
101 | $server = new Server($row['id'], $row);
102 | } else {
103 | throw new ServerNotFoundException('Server does not exist');
104 | }
105 | $stmt->close();
106 | return $server;
107 | }
108 |
109 | /**
110 | * List all servers in the database.
111 | * @param array $include list of extra data to include in response
112 | * @param array $filter list of field/value pairs to filter results on
113 | * @return array of Server objects
114 | */
115 | public function list_servers($include = array(), $filter = array()) {
116 | // WARNING: The search query is not parameterized - be sure to properly escape all input
117 | $fields = array("server.*");
118 | $joins = array();
119 | $where = array('!server.deleted');
120 | foreach($filter as $field => $value) {
121 | if($value) {
122 | switch($field) {
123 | case 'hostname':
124 | $where[] = "hostname REGEXP '".$this->database->escape_string($value)."'";
125 | break;
126 | case 'ip_address':
127 | case 'rsa_key_fingerprint':
128 | $where[] = "server.$field = '".$this->database->escape_string($value)."'";
129 | break;
130 | case 'port':
131 | $where[] = "server.$field = ".intval($value);
132 | break;
133 | case 'admin':
134 | $where[] = "admin_search.entity_id = ".intval($value)." OR admin_search_members.entity_id = ".intval($value);
135 | $joins['adminsearch'] = "LEFT JOIN server_admin AS admin_search ON admin_search.server_id = server.id";
136 | $joins['adminsearchmembers'] = "LEFT JOIN group_member AS admin_search_members ON admin_search_members.group = admin_search.entity_id";
137 | break;
138 | case 'authorization':
139 | case 'key_management':
140 | case 'sync_status':
141 | $where[] = "server.$field IN ('".implode("', '", array_map(array($this->database, 'escape_string'), $value))."')";
142 | break;
143 | }
144 | }
145 | }
146 | foreach($include as $inc) {
147 | switch($inc) {
148 | case 'pending_requests':
149 | $fields[] = "COUNT(DISTINCT access_request.source_entity_id) AS pending_requests";
150 | $joins['accounts'] = "LEFT JOIN server_account ON server_account.server_id = server.id";
151 | $joins['requests'] = "LEFT JOIN access_request ON access_request.dest_entity_id = server_account.entity_id";
152 | break;
153 | case 'admins':
154 | $fields[] = "GROUP_CONCAT(DISTINCT IF(user.uid IS NULL, CONCAT('G:', group.name), CONCAT('U:', user.uid)) SEPARATOR ',') AS admins";
155 | $joins['admins'] = "LEFT JOIN server_admin ON server_admin.server_id = server.id";
156 | $joins['adminusers'] = "LEFT JOIN user ON user.entity_id = server_admin.entity_id AND user.active";
157 | $joins['admingroups'] = "LEFT JOIN `group` ON group.entity_id = server_admin.entity_id";
158 | break;
159 | }
160 | }
161 | try {
162 | $stmt = $this->database->prepare("
163 | SELECT ".implode(", ", $fields)."
164 | FROM server ".implode(" ", $joins)."
165 | WHERE (".implode(") AND (", $where).")
166 | GROUP BY server.id
167 | ORDER BY server.hostname
168 | ");
169 | } catch(mysqli_sql_exception $e) {
170 | if($e->getCode() == 1139) {
171 | throw new ServerSearchInvalidRegexpException;
172 | } else {
173 | throw $e;
174 | }
175 | }
176 |
177 | $stmt->execute();
178 | $result = $stmt->get_result();
179 | $servers = array();
180 | while($row = $result->fetch_assoc()) {
181 | $servers[] = new Server($row['id'], $row);
182 | }
183 | $stmt->close();
184 | usort($servers, function($a, $b) {return strnatcasecmp($a->hostname, $b->hostname);});
185 | # Reverse domain level sort
186 | #usort($servers, function($a, $b) {return strnatcasecmp(implode('.', array_reverse(explode('.', $a->hostname))), implode('.', array_reverse(explode('.', $b->hostname))));});
187 | return $servers;
188 | }
189 | }
190 |
191 | class ServerNotFoundException extends Exception {}
192 | class ServerAlreadyExistsException extends Exception {}
193 | class ServerSearchInvalidRegexpException extends Exception {}
194 |
--------------------------------------------------------------------------------