0
0
mirror of https://github.com/postfixadmin/postfixadmin.git synced 2024-09-19 11:12:15 +02:00

Add Domain Key handling

This commit is contained in:
Fredrik Falk 2022-06-21 01:55:36 +02:00 committed by Freddo
parent a349c75f53
commit 9e73025058
18 changed files with 505 additions and 16 deletions

8
.gitignore vendored
View File

@ -5,3 +5,11 @@
/.idea
/vendor
/composer.lock
/coverage
/composer.phar
/composer_2.phar
/.php-cs-fixer.cache
/.phpunit.result.cache
/build
/tests/postfixadmin.sqlite.*.test
/phpstorm-docker-dev/

37
DOCUMENTS/OPENDKIM.txt Normal file
View File

@ -0,0 +1,37 @@
#
# OpenDKIM configuration for Postfix Admin
# Originally written by: Fredrik <Freddo> Falk
#
More complete OpenDKIM documentation:
http://www.opendkim.org/opendkim.8.html
http://www.opendkim.org/opendkim.conf.5.html
http://www.opendkim.org/opendkim-genkey.8.html
https://github.com/trusteddomainproject/OpenDKIM/blob/master/opendkim/README.SQL
Here are the relevant parts of OpenDKIM v2.11.x configuration for Postfixadmin setup.
Please refer to OpenDKIM documentation for complete information.
The setup gets KeyTable and SigningTable info from MySQL, allowing domain admins to edit and add domain keys as well as
assign which authors will use them.
1. PostfixAdmin Setup
-----------------
Add `$CONF['dkim'] = 'YES';` to your `config.local.php`, and optionally ``$CONF['dkim_all_admins'] = 'YES';` to allow
non-super-admins to add domain keys and signtable entries.
2. OpenDKIM setup
-----------------
Ensure that the version of OpenDKIM supports databases/OpenDBX.
After that, simply add the following to your /etc/opendkim.conf:
```
SigningTable dsn:mysql://{USER}:{PASSWORD}@{HOST}/{DATABASE}/table=dkim_signing?keycol=author?datacol=dkim_id
KeyTable dsn:mysql://{USER}:{PASSWORD}@{HOST}/{DATABASE}/table=dkim?keycol=id?datacol=domain_name,selector,private_key
```
Replace {USER}, {PASSWORD}, {HOST}, and {DATABASE} with your values.

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -142,6 +142,8 @@ $CONF['database_tables'] = array (
'vacation_notification' => 'vacation_notification',
'quota' => 'quota',
'quota2' => 'quota2',
'dkim' => 'dkim',
'dkim_signing' => 'dkim_signing',
);
// Site Admin
@ -357,6 +359,8 @@ $CONF['alias_struct_hook'] = '';
$CONF['mailbox_struct_hook'] = '';
$CONF['alias_domain_struct_hook'] = '';
$CONF['fetchmail_struct_hook'] = '';
$CONF['dkim_struct_hook'] = '';
$CONF['dkim_signing_struct_hook'] = '';
// Default Domain Values
@ -524,6 +528,21 @@ EOM;
// address is legal by performing a name server look-up.
$CONF['emailcheck_resolve_domain']='YES';
//
//
// OpenDKIM stuff
//
//
// Enable the dkim database component
$CONF['dkim'] = 'NO';
// Allow regular admins to add/edit/remove dkim entries
$CONF['dkim_all_admins'] = 'NO';
//
// End OpenDKIM stuff
//
// Optional:
// Analyze alias gotos and display a colored block in the first column

View File

@ -17,6 +17,11 @@ url_fetchmail_new_entry = edit.php?table=fetchmail
# sendmail
url_sendmail = sendmail.php
url_broadcast_message = broadcast-message.php
# dkim
url_dkim = list.php?table=dkim
url_dkim_signing = list.php?table=dkimsigning
url_dkim_newkey = edit.php?table=dkim
url_dkim_newsign = edit.php?table=dkimsigning
# password
url_password = edit.php?table=adminpassword
# backup

View File

@ -18,6 +18,7 @@ $PALANG['confirm_delete_admin'] = 'Do you really want to delete the admin %s?';
$PALANG['confirm_delete_alias'] = 'Do you really want to delete the alias %s?';
$PALANG['confirm_delete_aliasdomain'] = 'Do you really want to delete the alias domain %s?';
$PALANG['confirm_delete_domain'] = 'Do you really want to delete all records for the domain %s? This can not be undone!';
$PALANG['confirm_delete_dkim'] = 'Do you really want to delete the domain key entry %s?';
$PALANG['confirm_delete_fetchmail'] = 'Do you really want to delete the fetchmail job %s?';
$PALANG['confirm_delete_mailbox'] = 'Do you really want to delete the mailbox %s?';
$PALANG['confirm_delete_vacation'] = 'Do you really want to delete the vacation message for %s?';
@ -54,6 +55,8 @@ $PALANG['add_alias_domain'] = 'Add Alias Domain';
$PALANG['add_mailbox'] = 'Add Mailbox';
$PALANG['pMenu_fetchmail'] = 'Fetch Email';
$PALANG['pMenu_sendmail'] = 'Send Email';
$PALANG['pMenu_dkim'] = 'Domain Keys';
$PALANG['pMenu_dkim_signing'] = 'Signing Table';
$PALANG['pMenu_password'] = 'Password';
$PALANG['pMenu_viewlog'] = 'View Log';
$PALANG['pMenu_logout'] = 'Logout';
@ -249,6 +252,33 @@ $PALANG['pSendmail_button'] = 'Send Message';
$PALANG['pSendmail_result_error'] = 'Unable to send email to %s!';
$PALANG['pSendmail_result_success'] = 'Email sent to %s.';
$PALANG['pDkim_new_key'] = 'Add Domain Key';
$PALANG['pDkim_new_sign'] = 'Add Sign Table Entry';
$PALANG['pDkim_edit_key'] = 'Edit Domain Key';
$PALANG['pDkim_edit_sign'] = 'Edit Sign Table Entry';
$PALANG['pDkim_field_selector'] = 'Selector';
$PALANG['pDkim_field_pkey'] = 'Private Key';
$PALANG['pDkim_field_pub'] = 'Public Key';
$PALANG['pDkim_field_author'] = 'Author';
$PALANG['pDkim_field_dkim_id'] = 'Domain Key';
$PALANG['pDkim_field_selector_desc'] = 'Defines the name of the selector to be used when signing messages';
$PALANG['pDkim_field_domain_desc'] = 'Defines the domain which will use this key for signing';
$PALANG['pDkim_field_pkey_desc'] = 'PEM-formatted private key to be used for signing all messages';
$PALANG['pDkim_field_pub_desc'] = 'PEM-formatted public key';
$PALANG['pDkim_field_author_desc'] = 'Author that will use this key for signing. Can be either mailbox or domain, with mailbox taking precedence.';
$PALANG['pDkim_field_dkim_id_desc'] = 'Domain Key that this author will use';
$PALANG['dkim_already_exists'] = 'This Domain Key has already been added!';
$PALANG['dkim_does_not_exist'] = 'This Domain Key does not exist!';
$PALANG['dkim_signing_already_exists'] = 'This Domain Signing Entry already exsits!';
$PALANG['dkim_signing_does_not_exist'] = 'This Domain Signing Entry does not exist!';
$PALANG['pViewlog_action_create_dkim_entry'] = 'create domain key entry';
$PALANG['pViewlog_action_edit_dkim_entry'] = 'edit domain key entry';
$PALANG['pViewlog_action_delete_dkim_entry'] = 'delete domain key entry';
$PALANG['pViewlog_action_create_dkim_signing_entry'] = 'create domain signing entry';
$PALANG['pViewlog_action_edit_dkim_signing_entry'] = 'edit domain signing entry';
$PALANG['pViewlog_action_delete_dkim_signing_entry'] = 'delete domain signing entry';
$PALANG['pMain_dkim'] = 'Add a Domain Key for use with OpenDKIM.';
$PALANG['pAdminMenu_list_admin'] = 'Admin List';
$PALANG['pAdminMenu_list_domain'] = 'Domain List';
$PALANG['pAdminMenu_list_virtual'] = 'Virtual List';

102
model/DkimHandler.php Normal file
View File

@ -0,0 +1,102 @@
<?php
# $Id$
/**
* Handler for domain keys
*/
class DkimHandler extends PFAHandler
{
protected $db_table = 'dkim';
protected $id_field = 'id';
protected $label_field = 'description';
protected $domain_field = 'domain_name';
protected $order_by = 'domain_name, selector';
protected function initStruct()
{
$this->struct=array(
# field name allow display in... type $PALANG label $PALANG description default / options / ...
# editing? form list
'id' => pacol(0, 0, 1, 'num' , 'pFetchmail_field_id' , '' ,
'', array(), array('dont_write_to_db' => 1) ),
'description' => pacol(1, 1, 1, 'text' , 'description' , '' ),
'selector' => pacol(1, 1, 1, 'text' , 'pDkim_field_selector','pDkim_field_selector_desc'),
'domain_name' => pacol(1, 1, 1, 'enum' , 'domain' , 'pDkim_field_domain_desc' ,
'', $this->allowed_domains),
'private_key' => pacol(1, 1, 0, 'txtlarge', 'pDkim_field_pkey' , 'pDkim_field_pkey_desc' ),
'public_key' => pacol(1, 1, 0, 'txtlarge', 'pDkim_field_pub' , 'pDkim_field_pub_desc' ),
);
}
protected function initMsg()
{
$this->msg['error_already_exists'] = 'dkim_already_exists';
$this->msg['error_does_not_exist'] = 'dkim_does_not_exist';
$this->msg['confirm_delete'] = 'confirm_delete_dkim';
if ($this->new) {
$this->msg['logname'] = 'create_dkim_entry';
$this->msg['store_error'] = 'pFetchmail_database_save_error';
$this->msg['successmessage'] = 'pFetchmail_database_save_success';
} else {
$this->msg['logname'] = 'edit_dkim_entry';
$this->msg['store_error'] = 'pFetchmail_database_save_error';
$this->msg['successmessage'] = 'pFetchmail_database_save_success';
}
}
public function webformConfig()
{
$required_role = 'global-admin';
if (Config::bool('dkim_all_admins'))
$required_role = 'admin';
return array(
# $PALANG labels
'formtitle_create' => 'pDkim_new_key',
'formtitle_edit' => 'pDkim_edit_key',
'create_button' => 'pFetchmail_new_entry',
# various settings
'required_role' => $required_role,
'listview' => 'list.php?table=dkim',
'early_init' => 0,
);
}
protected function validate_new_id()
{
# auto_increment - any non-empty ID is an error
if ($this->id != '') {
$this->errormsg[$this->id_field] = 'auto_increment value, you must pass an empty string!';
return false;
}
return true;
}
/**
* @return boolean
*/
public function delete()
{
if (! $this->view()) {
$this->errormsg[] = Config::Lang($this->msg['error_does_not_exist']);
return false;
}
db_delete('dkim', $this->id_field, $this->id);
db_delete($this->db_table, $this->id_field, $this->id);
db_log($this->result['domain_name'], 'delete_dkim_entry', $this->result['label']);
$this->infomsg[] = Config::Lang_f('pDelete_delete_success', $this->result['label']);
return true;
}
public function domain_from_id()
{
return '';
}
}
/* vim: set expandtab softtabstop=4 tabstop=4 shiftwidth=4: */

View File

@ -0,0 +1,140 @@
<?php
# $Id$
/**
* Handler for domain key signing table
*/
class DkimsigningHandler extends PFAHandler
{
protected $db_table = 'dkim_signing';
protected $id_field = 'id';
protected $order_by = 'dkim_id, author';
protected function initStruct()
{
// Get Domains as options for authors
$domain_handler = new DomainHandler(0, $this->admin_username);
$domain_handler->getList('1=1');
// Get Mailboxes as options for authors
$mail_handler = new MailboxHandler(0, $this->admin_username);
$mail_handler->getList('1=1');
// Get Domain Keys
$dkim_handler = new DkimHandler(0, $this->admin_username);
$dkim_handler->getList('1=1');
$authors = array_merge(
array_keys($domain_handler->result()),
array_keys($mail_handler->result())
);
$this->struct=array(
# field name allow display in... type $PALANG label $PALANG description default / options / ...
# editing? form list
'id' => pacol(0, 0, 1, 'num' , 'pFetchmail_field_id' , '' ,
'', array(), array('dont_write_to_db' => 1) ),
'dkim_id' => pacol(1, 1, 1, 'enum' , 'pDkim_field_dkim_id' , 'pDkim_field_dkim_id_desc' ,
'', array_keys($dkim_handler->result)),
'author' => pacol(1, 1, 1, 'enum' , 'pDkim_field_author' , 'pDkim_field_author_desc' ,
'', $authors),
);
}
protected function initMsg()
{
$this->msg['error_already_exists'] = 'dkim_signing_already_exists';
$this->msg['error_does_not_exist'] = 'dkim_signing_does_not_exist';
$this->msg['confirm_delete'] = 'confirm_delete_dkim';
if ($this->new) {
$this->msg['logname'] = 'create_dkim_signing_entry';
$this->msg['store_error'] = 'pFetchmail_database_save_error';
$this->msg['successmessage'] = 'pFetchmail_database_save_success';
} else {
$this->msg['logname'] = 'edit_dkim_entry';
$this->msg['store_error'] = 'pFetchmail_database_save_error';
$this->msg['successmessage'] = 'pFetchmail_database_save_success';
}
}
public function webformConfig()
{
$required_role = 'global-admin';
if (Config::bool('dkim_all_admins'))
$required_role = 'admin';
return array(
# $PALANG labels
'formtitle_create' => 'pDkim_new_sign',
'formtitle_edit' => 'pDkim_edit_sign',
'create_button' => 'pFetchmail_new_entry',
# various settings
'required_role' => $required_role,
'listview' => 'list.php?table=dkimsigning',
'early_init' => 0,
);
}
protected function validate_new_id()
{
# auto_increment - any non-empty ID is an error
if ($this->id != '') {
$this->errormsg[$this->id_field] = 'auto_increment value, you must pass an empty string!';
return false;
}
return true;
}
/**
* @return boolean
*/
public function delete()
{
if (! $this->view()) {
$this->errormsg[] = Config::Lang($this->msg['error_does_not_exist']);
return false;
}
db_delete('dkim_signing', $this->id_field, $this->id);
db_delete($this->db_table, $this->id_field, $this->id);
db_log($this->result['author'], 'delete_dkim_signing_entry', $this->result['id']);
$this->infomsg[] = Config::Lang_f('pDelete_delete_success', $this->result['author']);
return true;
}
public function domain_from_id()
{
return '';
}
protected function no_domain_field() {
$domain_handler = new DomainHandler(0, $this->admin_username);
$domain_handler->getList('1=1');
$this->allowed_domains = array_keys($domain_handler->result());
}
/**
* Filters to only allowed domains, as author can be either a mailbox or a domain
* @param $db_result
* @return array
*/
protected function read_from_db_postprocess($db_result)
{
return array_filter($db_result, function($row) {
$domain = $row['author'];
$at_pos = strpos($domain, '@');
if ($at_pos)
$domain = preg_split('/@/', $domain)[1];
return in_array($domain, $this->allowed_domains);
});
}
}
/* vim: set expandtab softtabstop=4 tabstop=4 shiftwidth=4: */

View File

@ -286,6 +286,7 @@ abstract class PFAHandler
* pass password (will be encrypted with pacrypt())
* b64p password (will be stored with base64_encode() - but will NOT be decoded automatically)
* num number
* txtlarge Large text input field
* txtl text "list" - array of one line texts
* *vnum "virtual" number, coming from JOINs etc.
* bool boolean (converted to 0/1, additional column _$field with yes/no)

View File

@ -113,6 +113,8 @@ class PFASmarty
$this->assign('url_domain', '');
$this->assign('version', $CONF['version'] ?? 'unknown');
$this->assign('boolconf_alias_domain', Config::bool('alias_domain'));
$this->assign('boolconf_dkim', Config::bool('dkim'));
$this->assign('boolconf_dkim_all_admins', Config::bool('dkim_all_admins'));
$this->assign('authentication_has_role', array('global_admin' => authentication_has_role('global-admin'), 'admin' => authentication_has_role('admin'), 'user' => authentication_has_role('user')));
header("Expires: Sun, 16 Mar 2003 05:00:00 GMT");

View File

@ -98,7 +98,9 @@ if ($_SERVER['REQUEST_METHOD'] == "GET") {
'quota',
'quota2',
'vacation',
'vacation_notification'
'vacation_notification',
'dkim',
'dkim_signing'
);
for ($i = 0 ; $i < sizeof($tables) ; ++$i) {

View File

@ -2130,3 +2130,81 @@ function upgrade_1846_mysql()
db_query("ALTER TABLE $domain_admins MODIFY `domain` varchar(255) COLLATE latin1_general_ci NOT NULL");
db_query("ALTER TABLE $domain_admins MODIFY username varchar(255) COLLATE latin1_general_ci NOT NULL");
}
/**
* Add DKIM tables
* @return void
*/
function upgrade_1847_mysql_pgsql()
{
$dkim_key_table = table_by_key('dkim');
$dkim_signing_table = table_by_key('dkim_signing');
$domain_table = table_by_key('domain');
db_query_parsed("
CREATE TABLE {IF_NOT_EXISTS} $dkim_key_table (
`id` {AUTOINCREMENT} {PRIMARY},
`domain_name` varchar(255) NOT NULL,
`description` varchar(255) DEFAULT '',
`selector` varchar(63) NOT NULL DEFAULT 'default',
`private_key` text,
`public_key` text,
`created` {DATETIME},
`modified` {DATETIME},
INDEX(domain_name, description),
FOREIGN KEY (`domain_name`)
REFERENCES $domain_table(`domain`)
ON DELETE CASCADE) {COLLATE} COMMENT='Postfix Admin - OpenDKIM Key Table';
");
db_query_parsed("
CREATE TABLE {IF_NOT_EXISTS} $dkim_signing_table (
`id` {AUTOINCREMENT} {PRIMARY},
`author` varchar(255) NOT NULL DEFAULT '',
`dkim_id` integer NOT NULL,
`created` {DATETIME},
`modified` {DATETIME},
INDEX(author),
FOREIGN KEY (`dkim_id`)
REFERENCES $dkim_key_table(`id`)
ON DELETE CASCADE) {COLLATE} COMMENT='Postfix Admin - OpenDKIM Signing Table';
");
}
/**
* Add DKIM tables
* @return void
*/
function upgrade_1847_sqlite()
{
$dkim_key_table = table_by_key('dkim');
$dkim_signing_table = table_by_key('dkim_signing');
$domain_table = table_by_key('domain');
db_query_parsed("
CREATE TABLE {IF_NOT_EXISTS} $dkim_key_table (
`id` {AUTOINCREMENT},
`domain_name` varchar(255) NOT NULL,
`description` varchar(255) DEFAULT '',
`selector` varchar(63) NOT NULL DEFAULT 'default',
`private_key` text,
`public_key` text,
`created` {DATETIME},
`modified` {DATETIME},
FOREIGN KEY (`domain_name`)
REFERENCES $domain_table(`domain`)
ON DELETE CASCADE) {COLLATE};
");
db_query_parsed("
CREATE TABLE {IF_NOT_EXISTS} $dkim_signing_table (
`id` {AUTOINCREMENT},
`author` varchar(255) NOT NULL DEFAULT '',
`dkim_id` integer NOT NULL,
`created` {DATETIME},
`modified` {DATETIME},
FOREIGN KEY (`dkim_id`)
REFERENCES $dkim_key_table(`id`)
ON DELETE CASCADE) {COLLATE};
");
}

View File

@ -62,6 +62,8 @@
<input class="form-control" type="password" name="value[{$key}]" {if $key == 'password' || $key == 'password2'}autocomplete="new-password"{/if}/>
{elseif $field.type == 'txtl'}
<textarea class="form-control" rows="10" cols="35" name="value[{$key}]">{foreach key=key2 item=field2 from=$value_{$key}}{$field2}&#10;{/foreach}</textarea>
{elseif $field.type == 'txtlarge'}
<textarea class="form-control" rows="10" cols="35" name="value[{$key}]">{$value_{$key}}</textarea>
{else}
<input class="form-control" type="text" name="value[{$key}]"
value="{$value_{$key}}"/>

View File

@ -24,6 +24,16 @@
<td style="padding-top: 15px;">{$PALANG.pMain_sendmail}</td>
</tr>
{/if}
{if $CONF.dkim==='YES' && (
$authentication_has_role.global_admin ||
(isset($CONF.dkim_all_admins) && $CONF.dkim_all_admins === 'YES') )
}
<tr>
<td nowrap="nowrap"><a style="text-align:left; padding-left:15px" class="btn btn-primary btn-block" href="{#url_dkim#}"><span class="glyphicon glyphicon-certificate"
aria-hidden="true"></span> {$PALANG.pMenu_dkim}</a></td>
<td style="padding-top: 15px;">{$PALANG.pMain_dkim}</td>
</tr>
{/if}
<tr>
<td nowrap="nowrap"><a style="text-align:left; padding-left:15px" class="btn btn-primary btn-block" href="{#url_password#}"><span class="glyphicon glyphicon-lock"
aria-hidden="true"></span> {$PALANG.pMenu_password}</a></td>

View File

@ -106,6 +106,25 @@
</li>
{/strip}
{/if}
{* dkim *}
{if $CONF.dkim==='YES' && (
$authentication_has_role.global_admin ||
(isset($CONF.dkim_all_admins) && $CONF.dkim_all_admins === 'YES') )
}
{strip}
<li class="dropdown">
<a class="btn navbar-btn dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
aria-expanded="false" href="{#url_dkim#}"><span class="glyphicon glyphicon-certificate" aria-hidden="true"></span> {$PALANG.pMenu_dkim} <span
class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="{#url_dkim#}"><span class="glyphicon glyphicon-certificate" aria-hidden="true"></span> {$PALANG.pMenu_dkim}</a></li>
<li><a href="{#url_dkim_signing#}"><span class="glyphicon glyphicon-list" aria-hidden="true"></span> {$PALANG.pMenu_dkim_signing}</a></li>
<li><a href="{#url_dkim_newkey#}"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> {$PALANG.pDkim_new_key}</a></li>
<li><a href="{#url_dkim_newsign#}"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> {$PALANG.pDkim_new_sign}</a></li>
</ul>
</li>
{/strip}
{/if}
</ul>
<ul class="nav navbar-nav navbar-right">
{* password *}

17
tests/DkimHandlerTest.php Normal file
View File

@ -0,0 +1,17 @@
<?php
class DkimHandlerTest extends \PHPUnit\Framework\TestCase
{
public function testBasic()
{
$x = new DkimHandler();
$list = $x->getList("");
$this->assertTrue($list);
$results = $x->result();
$this->assertEmpty($results);
}
}

View File

@ -0,0 +1,17 @@
<?php
class DkimsigningHandlerTest extends \PHPUnit\Framework\TestCase
{
public function testBasic()
{
$x = new DkimsigningHandler();
$list = $x->getList("");
$this->assertTrue($list);
$results = $x->result();
$this->assertEmpty($results);
}
}