IPA-EPN: First version.

EPN stands for Expiring Password Notification. It is a standalone
tool designed to build a list of users whose password would expire
in the near future, and either display the list in a machine-readable
format, or send email notifications to these users.

EPN provides command-line options to display the list of affected users.
This provides data introspection and helps understand how many emails
would be sent for a given day, or a given date range.
The command-line options can also be used by a monitoring system to alert
whenever a number of emails over the SMTP quota would be sent.

EPN is meant to be launched once a day from an IPA client (preferred)
or replica from a systemd timer.

EPN does not keep state. The list of affected users is built at runtime
but never kept.

TLS/STARTTLS SMTP code is untested and unlikely to work as-is.

Parts of code contributed by Rob Crittenden.
Ideas and feedback contributed by Christian Heimes and Michal Polovka.

Fixes: https://pagure.io/freeipa/issue/3687
Signed-off-by: François Cami <fcami@redhat.com>
Signed-off-by: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: Michal Polovka <mpolovka@redhat.com>
Reviewed-By: Christian Heimes <cheimes@redhat.com>
This commit is contained in:
François Cami
2020-05-05 15:59:11 +02:00
parent 8f8c560ffd
commit b8886c3e97
11 changed files with 990 additions and 5 deletions

View File

@@ -44,6 +44,7 @@ sbin_SCRIPTS = \
ipa-client-automount \
ipa-client-install \
ipa-client-samba \
ipa-epn \
$(NULL)
ipa_getkeytab_SOURCES = \
@@ -91,10 +92,12 @@ ipa_join_LDADD = \
$(NULL)
SUBDIRS = \
share \
share \
man \
sysconfig \
sysconfig \
$(NULL)
# init
noinst_HEADERS = \
ipa-client-common.h
@@ -104,6 +107,7 @@ EXTRA_DIST = \
ipa-client-automount.in \
ipa-client-install.in \
ipa-client-samba.in \
ipa-epn.in \
$(NULL)
install-data-hook:

25
client/ipa-epn.in Normal file
View File

@@ -0,0 +1,25 @@
#!/usr/bin/python3
#
# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""This tool prepares then sends email notifications to users
whose passwords are expiring in the near future.
"""
from ipaclient.install.ipa_epn import EPN
EPN.run_cli()

View File

@@ -10,7 +10,9 @@ dist_man1_MANS = \
ipa-client-samba.1 \
ipa-certupdate.1 \
ipa-join.1 \
ipa-epn.1 \
ipa.1
dist_man5_MANS = \
default.conf.5
default.conf.5 \
epn.conf.5

76
client/man/epn.conf.5 Normal file
View File

@@ -0,0 +1,76 @@
.\" A man page for epn.conf
.\" Copyright (C) 2020 Red Hat, Inc.
.\"
.\" This program is free software; you can redistribute it and/or modify
.\" it under the terms of the GNU General Public License as published by
.\" the Free Software Foundation, either version 3 of the License, or
.\" (at your option) any later version.
.\"
.\" This program is distributed in the hope that it will be useful, but
.\" WITHOUT ANY WARRANTY; without even the implied warranty of
.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
.\" General Public License for more details.
.\"
.\" You should have received a copy of the GNU General Public License
.\" along with this program. If not, see <http://www.gnu.org/licenses/>.
.\"
.\" Author: Rob Crittenden <rcritten@@redhat.com>
.\"
.TH "epn.conf" "5" "Apr 28 2020" "FreeIPA" "FreeIPA Manual Pages"
.SH "NAME"
epn.conf \- Expiring Password Notification configuration file
.SH "SYNOPSIS"
/etc/ipa/epn.conf
.SH "DESCRIPTION"
The \fIepn.conf \fRconfiguration file is used to set the options for the ipa-epn tool to notify users of upcoming password expiration.
.SH "SYNTAX"
The configuration options are not case sensitive. The values may be case sensitive, depending on the option.
Blank lines are ignored.
Lines beginning with # are comments and are ignored.
Valid lines consist of an option name, an equals sign and a value. Spaces surrounding equals sign are ignored. An option terminates at the end of a line.
Values should not be quoted, the quotes will not be stripped.
.RS L
# Wrong \- don't include quotes
verbose = "True"
# Right \- Properly formatted options
verbose = True
verbose=True
.RE
Options must appear in the section named [global]. There are no other sections defined or used currently.
Options may be defined that are not used by IPA. Be careful of misspellings, they will not be rejected.
.SH "OPTIONS"
.TP
.B smtp_server\fR <fqdn>
Specifies the SMTP server to use. The default is localhost.
.TP
.B smtp_port <port>
Specifies the SMTP port. The default is 25.
.TP
.B smtp_user <user>
Specifies the id of the user to authenticate with the SMTP server. Default None.
.TP
.B smtp_password <password>
Specifies the password for the authorized user. Default None.
.TP
.B smtp_timeout <seconds>
Specifies the number of seconds to wait for SMTP to respond. Default 60.
.TP
.B smtp_security <security>
Specifies the type of secure connection to make. Options are: none, starttls and ssl. The default is none.
.TP
.B notify_ttls <list of days>
This is the list of days before a password expiration when ipa-epn shoould notify a user that their password will soon require a reset. If this value is not specified then the default list will be used: 28, 14, 7, 3, 1.
.SH "FILES"
.TP
.I /etc/ipa/epn.conf
Configuration file
.SH "SEE ALSO"
.BR ipa-epn (1)

119
client/man/ipa-epn.1 Normal file
View File

@@ -0,0 +1,119 @@
.\" A man page for ipa-epn
.\" Copyright (C) 2020 Red Hat, Inc.
.\"
.\" This program is free software; you can redistribute it and/or modify
.\" it under the terms of the GNU General Public License as published by
.\" the Free Software Foundation, either version 3 of the License, or
.\" (at your option) any later version.
.\"
.\" This program is distributed in the hope that it will be useful, but
.\" WITHOUT ANY WARRANTY; without even the implied warranty of
.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
.\" General Public License for more details.
.\"
.\" You should have received a copy of the GNU General Public License
.\" along with this program. If not, see <http://www.gnu.org/licenses/>.
.\"
.\"
.TH "ipa-epn" "1" "Apr 24 2020" "FreeIPA" "FreeIPA Manual Pages"
.SH "NAME"
ipa\-epn \- Send expiring password nofications
.SH "SYNOPSIS"
ipa\-epn \[options\]
.SH "DESCRIPTION"
ipa\-epn provides a method to warn users via email that their IPA account password is about to expire.
It can be used in dry\-run mode which is recommmended during setup. The output is always JSON in this case.
It can also be launched daily by its systemd timer.
In this case it will parse its configuration file epn.conf(5) and send an email to users whose passwords are expiring within the defined future date ranges.
See the OPTIONS section below and the epn.conf(5) man page on how to configure the tool.
.SH "OPTIONS"
.TP
\fB\-\-to-nbdays\fR \fI<number of days>\fR
The \-\-to\-nbdays CLI option can be used to determine the number of notifications that would be sent in a given timeframe.
If \fB\-\-from\-nbdays\fR is not specified, ipa\-epn will look within a 24\-hour long time range in <number of days> days.
if \fB\-\-from\-nbdays\fR is specified, the date range starts at \fB\-\-from\-nbdays\fR days in the future and ends at \fB\-\-to\-nbdays\fR in the future.
Together, these two CLI options can be used to determine how many emails would be sent in a specific time in the future.
The \fB\-\-to\-nbdays\fR CLI option implies \fB\-\-dry\-run\fR.
.TP
\fB\-\-from\-nbdays\fR \fI<number of days>\fR
See \fB\-\-to\-nbdays\fR for an explanation. This option must be used in conjonction with \fB\-\-to\-nbdays\fR.
.TP
\fB\-\-dry\-run\fR
The \fB\-\-dry\-run\fR CLI option is intented to test ipa\-epn's configuration.
For instance, if notify_ttls is set to 21, 14, 3, \fB\-\-dry-run\fR would display the list of users whose passwords would expire in 21, 14, and 3 days in the future.
.SH "EXAMPLES"
.nf
# date
Sun 12 Apr 2020 06:23:08 AM CEST
# ipa\-epn \-\-dry\-run
[
{
"uid": "user5",
"cn": "user 5",
"krbpasswordexpiration": "2020\-04\-17 15:51:53",
"mail": "['user5@ipa.test']"
}
]
The IPA\-EPN command was successful
# ipa\-epn \-\-to\-nbdays 6 \-\-dry-run
[
{
"uid": "user5",
"cn": "user 5",
"krbpasswordexpiration": "2020\-04\-17 15:51:53",
"mail": "['user5@ipa.test']"
}
]
The IPA\-EPN command was successful
# ipa\-epn \-\-from-nbdays 2 \-\-to-nbdays 6 \-\-dry\-run
[
{
"uid": "user5",
"cn": "user 5",
"krbpasswordexpiration": "2020\-04\-17 15:51:53",
"mail": "['user5@ipa.test']"
}
]
The IPA\-EPN command was successful
# ipa\-epn \-\-from\-nbdays 8 \-\-to\-nbdays 12 \-\-dry\-run
[
{
"uid": "user3",
"cn": "user 5",
"krbpasswordexpiration": "2020\-04\-21 00:00:08",
"mail": "['user3@ipa.test']"
}
]
The IPA\-EPN command was successful
.SH "EXIT STATUS"
The exit status is 0 on success, nonzero on error.
.SH "SEE ALSO"
RFE: https://pagure.io/freeipa/issue/3687
Design document: https://github.com/freeipa/freeipa/blob/master/doc/designs/expiring-password-notification.md
.SH "KNOWN BUGS"
None yet.
.SH "REPORTING BUGS AND ENHANCEMENT IDEAS"
.nf
Please make sure first the issue is not already reported by searching at https://pagure.io/freeipa/issues. If it is not, file a new issue at https://pagure.io/freeipa/new_issue.

View File

@@ -588,6 +588,15 @@ Requires: cifs-utils
This package provides command-line tools to deploy Samba domain member
on the machine enrolled into a FreeIPA environment
%package client-epn
Summary: Tools to configure Expiring Password Notification in IPA
Group: System Environment/Base
Requires: %{name}-client = %{version}-%{release}
%description client-epn
This package provides a service to collect and send expiring password
notifications via email (SMTP).
%package -n python3-ipaclient
Summary: Python libraries used by IPA client
Group: System Environment/Libraries
@@ -1345,6 +1354,16 @@ fi
%{_sbindir}/ipa-client-samba
%{_mandir}/man1/ipa-client-samba.1*
%files client-epn
%doc README.md Contributors.txt
%license COPYING
%{_sbindir}/ipa-epn
%{_mandir}/man1/ipa-epn.1*
%{_mandir}/man5/epn.conf.5*
%attr(644,root,root) %{_unitdir}/ipa-epn.service
%attr(644,root,root) %{_unitdir}/ipa-epn.timer
%files -n python3-ipaclient
%doc README.md Contributors.txt
%license COPYING

View File

@@ -4,11 +4,15 @@ AUTOMAKE_OPTIONS = 1.7
dist_noinst_DATA = \
ipa-custodia.service.in \
ipa.service.in
ipa.service.in \
ipa-epn.service.in \
ipa-epn.timer.in
systemdsystemunit_DATA = \
ipa-custodia.service \
ipa.service
ipa.service \
ipa-epn.service \
ipa-epn.timer
CLEANFILES = $(systemdsystemunit_DATA)

View File

@@ -0,0 +1,9 @@
[Unit]
Description=Execute IPA Expiring Password Notification (EPN)
[Service]
Type=simple
ExecStart=@sbindir@/ipa-epn
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,9 @@
[Unit]
Description=Execute IPA Expiring Password Notification (EPN) every day at 1AM
[Timer]
OnCalendar=*-*-* 01:00:00
Unit=ipa-epn.service
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,717 @@
#
# Copyright (C) 2020 FreeIPA Contributors see COPYING for license
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""This tool prepares then sends email notifications to users
whose passwords are expiring in the near future.
"""
from __future__ import absolute_import, print_function
import ast
import grp
import json
import os
import pwd
import logging
import smtplib
from collections import deque
from datetime import datetime, timedelta
from email.utils import formataddr, formatdate
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.header import Header
from ipaclient.install.client import is_ipa_client_installed
from ipaplatform.paths import paths
from ipalib import api, errors
from ipalib.install import sysrestore
from ipapython import admintool, ipaldap
from ipapython.dn import DN
EPN_CONF = "/etc/ipa/epn.conf"
EPN_CONFIG = {
"smtp_server": "localhost",
"smtp_port": 25,
"smtp_user": None,
"smtp_password": None,
"smtp_timeout": 60,
"smtp_security": "none",
"smtp_admin": "root@localhost",
"notify_ttls": "28,14,7,3,1",
}
logger = logging.getLogger(__name__)
def drop_privileges(new_username="daemon", new_groupname="daemon"):
"""Drop privileges, defaults to daemon:daemon.
"""
try:
if os.getuid() != 0:
return
os.setgroups([])
os.setgid(pwd.getpwnam(new_username).pw_uid)
os.setuid(grp.getgrnam(new_groupname).gr_gid)
if os.getuid() == 0:
raise Exception()
logger.debug(
"Dropped privileges to user=%s, group=%s",
new_username,
new_groupname,
)
except Exception as e:
logger.error(
"Failed to drop privileges to %s, %s: %s",
new_username,
new_groupname,
e,
)
class EPNUserList:
"""Maintains a list of users whose passwords are expiring.
Provides add(), check(), pop(), and json_print().
From the outside, the list is considered always sorted:
* displaying the list results in a sorted JSON representation thereof
* pop() returns the "most urgent" item from the list.
Internal implementation notes:
* Uses a deque instead of a list for efficiency reasons
* all add()-style methods MUST set _sorted to False.
* all print() and pop-like methods MUST call _sort() first.
"""
def __init__(self):
self._sorted = False
self._expiring_password_user_dq = deque()
def __bool__(self):
"""If it quacks like a container...
"""
return bool(self._expiring_password_user_dq)
def __len__(self):
"""Return len(self)."""
return len(self._expiring_password_user_dq)
def add(self, entry):
"""Parses and appends an LDAP user entry with the uid, cn,
krbpasswordexpiration and mail attributes.
"""
try:
self._sorted = False
self._expiring_password_user_dq.append(
dict(
uid=str(entry["uid"].pop(0)),
cn=str(entry["cn"].pop(0)),
krbpasswordexpiration=str(
entry["krbpasswordexpiration"].pop(0)
),
mail=str(entry["mail"]),
)
)
except IndexError as e:
logger.info("IPA-EPN: Could not parse entry: %s", e)
def pop(self):
"""Returns the "most urgent" user to notify.
In fact: popleft()
"""
self._sort()
try:
return self._expiring_password_user_dq.popleft()
except IndexError:
return False
def check(self):
self.json_print(really_print=False)
def json_print(self, really_print=True):
"""Dump self._expiring_password_user_dq to JSON.
Check that the result can be re-rencoded to UTF-8.
If really_print, print the result.
"""
try:
self._sort()
temp_str = json.dumps(
list(self._expiring_password_user_dq),
indent=4,
ensure_ascii=False,
)
temp_str.encode("utf8")
if really_print:
print(temp_str)
except Exception as e:
logger.error("IPA-EPN: unexpected error: %s", e)
def _sort(self):
if not self._sorted:
if isinstance(self._expiring_password_user_dq, deque):
self._expiring_password_user_dq = deque(
sorted(
self._expiring_password_user_dq,
key=lambda item: item["krbpasswordexpiration"],
)
)
self._sorted = True
class EPN(admintool.AdminTool):
command_name = "IPA-EPN"
log_file_name = paths.IPAEPN_LOG
usage = "%prog [options]"
description = "Expiring Password Notifications (EPN)"
def __init__(self, options, args):
super(EPN, self).__init__(options, args)
self._conn = None
self._expiring_password_user_list = EPNUserList()
self._ldap_data = []
self._date_ranges = []
self._mailer = None
@classmethod
def add_options(cls, parser):
super(EPN, cls).add_options(parser, debug_option=True)
parser.add_option(
"--from-nbdays",
dest="from_nbdays",
action="store",
default=None,
help="minimal number of days",
)
parser.add_option(
"--to-nbdays",
dest="to_nbdays",
action="store",
default=None,
help="maximal number of days",
)
parser.add_option(
"--dry-run",
dest="dry_run",
action="store_true",
default=False,
help="Dry run mode. JSON ouput only.",
)
def validate_options(self):
super(EPN, self).validate_options(needs_root=True)
if self.options.to_nbdays:
self.options.dry_run = True
if self.options.from_nbdays and not self.options.to_nbdays:
self.option_parser.error(
"You cannot specify --from-nbdays without --to-nbdays"
)
def setup_logging(self, log_file_mode="a"):
super(EPN, self).setup_logging(log_file_mode="a")
def run(self):
super(EPN, self).run()
fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)
if not is_ipa_client_installed(fstore):
logger.error("IPA client is not configured on this system.")
raise admintool.ScriptError()
self._get_krb5_ticket()
self._read_configuration()
self._validate_configuration()
self._parse_configuration()
self._get_connection()
drop_privileges()
if self.options.to_nbdays:
self._build_cli_date_ranges()
for date_range in self._date_ranges:
self._fetch_data_from_ldap(date_range)
self._parse_ldap_data()
if self.options.dry_run:
self._pretty_print_data()
else:
self._mailer = MailUserAgent(
security_protocol=api.env.smtp_security,
smtp_hostname=api.env.smtp_server,
smtp_port=api.env.smtp_port,
smtp_timeout=api.env.smtp_timeout,
smtp_username=api.env.smtp_user,
smtp_password=api.env.smtp_password,
x_mailer=self.command_name,
)
self._send_emails()
def _get_date_range_from_nbdays(self, nbdays_end, nbdays_start=None):
"""Detects current time and returns a date range, given a number
of days in the future.
If only nbdays_end is specified, the range is 1d long.
"""
now = datetime.utcnow()
today_at_midnight = datetime.combine(now, datetime.min.time())
range_end = today_at_midnight + timedelta(days=nbdays_end)
if nbdays_start is not None:
range_start = today_at_midnight + timedelta(days=nbdays_start)
else:
range_start = range_end - timedelta(days=1)
logger.debug(
"IPA-EPN: Current date: %s \n"
"IPA-EPN: Date & time, today at midnight: %s \n"
"IPA-EPN: Date range start: %s \n"
"IPA-EPN: Date range end: %s \n",
now,
today_at_midnight,
range_start,
range_end,
)
return (range_start, range_end)
def _datetime_to_generalized_time(self, dt):
"""Convert datetime to LDAP_GENERALIZED_TIME_FORMAT
Note: Consider moving into ipalib.
"""
dt = dt.timetuple()
generalized_time_str = str(dt.tm_year) + "".join(
"0" * (2 - len(str(item))) + str(item)
for item in (
dt.tm_mon,
dt.tm_mday,
dt.tm_hour,
dt.tm_min,
dt.tm_sec,
)
)
return generalized_time_str + "Z"
def _get_krb5_ticket(self):
"""Setup the environment to obtain a krb5 ticket for us using the
system keytab.
Uses CCACHE = MEMORY (limited to the current process).
"""
os.environ.setdefault("KRB5_CLIENT_KTNAME", "/etc/krb5.keytab")
os.environ["KRB5CCNAME"] = "MEMORY:"
def _read_configuration(self):
"""Merge in the EPN configuration from /etc/ipa/epn.conf"""
base_config = dict(
context="epn", confdir=paths.ETC_IPA, in_server=False,
)
api.bootstrap(**base_config)
api.env._merge(**EPN_CONFIG)
if not api.isdone("finalize"):
api.finalize()
def _validate_configuration(self):
"""Examine the user-provided configuration.
"""
if api.env.smtp_security.lower() not in ("none", "starttls", "ssl"):
raise RuntimeError(
"smtp_security must be one of: " "none, starttls or ssl"
)
if api.env.smtp_user is not None and api.env.smtp_password is None:
raise RuntimeError("smtp_user set and smtp_password is not")
if api.env.notify_ttls is None:
raise RuntimeError("notify_ttls must be set in %s" % EPN_CONF)
try:
[int(k) for k in str(api.env.notify_ttls).split(',')]
except ValueError as e:
raise RuntimeError('Failed to parse notify_ttls: \'%s\': %s' %
(api.env.notify_ttls, e))
def _parse_configuration(self):
"""
"""
daylist = [int(k) for k in str(api.env.notify_ttls).split(',')]
daylist.sort()
for day in daylist:
self._date_ranges.append(
self._get_date_range_from_nbdays(
nbdays_start=None, nbdays_end=day + 1
)
)
def _get_connection(self):
"""Create a connection to LDAP and bind to it.
"""
if self._conn is not None:
return self._conn
try:
# LDAPI
self._conn = ipaldap.LDAPClient.from_realm(api.env.realm)
self._conn.external_bind()
except Exception:
try:
# LDAP + GSSAPI
self._conn = ipaldap.LDAPClient.from_hostname_secure(
api.env.server
)
self._conn.gssapi_bind()
except Exception as e:
logger.error(
"Unable to bind to LDAP server %s: %s",
self._conn.ldap_uri,
e,
)
return self._conn
def _fetch_data_from_ldap(self, date_range):
"""Run a LDAP query to fetch a list of user entries whose passwords
would expire in the near future. Store in self._ldap_data.
"""
if self._conn is None:
logger.error(
"IPA-EPN: Connection to LDAP not established. Exiting."
)
search_base = DN(api.env.container_user, api.env.basedn)
attrs_list = ["uid", "krbpasswordexpiration", "mail", "cn"]
search_filter = (
"(&(!(nsaccountlock=TRUE)) \
(krbpasswordexpiration<=%s) \
(krbpasswordexpiration>=%s))"
% (
self._datetime_to_generalized_time(date_range[1]),
self._datetime_to_generalized_time(date_range[0]),
)
)
try:
self._ldap_data = self._conn.get_entries(
search_base,
filter=search_filter,
attrs_list=attrs_list,
scope=self._conn.SCOPE_SUBTREE,
)
except errors.EmptyResult:
logger.debug("Empty Result.")
finally:
logger.debug("%d entries found", len(self._ldap_data))
def _parse_ldap_data(self):
"""Fill out self._expiring_password_user_list from data from ldap.
"""
if self._ldap_data:
for entry in self._ldap_data:
self._expiring_password_user_list.add(entry)
# Validate json.
try:
self._pretty_print_data(really_print=False)
except Exception as e:
logger.error("IPA-EPN: Could not create JSON: %s", e)
finally:
self._ldap_data = []
def _pretty_print_data(self, really_print=True):
"""Dump self._expiring_password_user_list to JSON.
"""
self._expiring_password_user_list.json_print(
really_print=really_print
)
def _send_emails(self):
if self._mailer is None:
logger.error("IPA-EPN: mailer was not configured.")
return
else:
while self._expiring_password_user_list:
entry = self._expiring_password_user_list.pop()
self._mailer.send_message(
mail_subject="Your password is expiring.",
mail_body=os.linesep.join(
[
"Hi %s, Your password will expire on %s."
% (entry["cn"], entry["krbpasswordexpiration"]),
"Please change it as soon as possible.",
]
),
subscribers=ast.literal_eval(entry["mail"]),
)
now = datetime.utcnow()
expdate = datetime.strptime(
entry["krbpasswordexpiration"],
'%Y-%m-%d %H:%M:%S')
logger.debug(
"Notified %s (%s). Password expiring in %d days at %s.",
entry["mail"], entry["uid"], (expdate - now).days,
expdate)
self._mailer.cleanup()
def _build_cli_date_ranges(self):
"""When self.options.to_nbdays is set, override the date ranges read
from the configuration file and build the date ranges from the CLI
options.
"""
self._date_ranges = []
logger.debug("IPA-EPN: Ignoring configuration file ranges.")
if self.options.from_nbdays is not None:
self._date_ranges.append(
self._get_date_range_from_nbdays(
nbdays_start=int(self.options.from_nbdays),
nbdays_end=int(self.options.to_nbdays),
)
)
elif self.options.to_nbdays is not None:
self._date_ranges.append(
self._get_date_range_from_nbdays(
nbdays_start=None, nbdays_end=int(self.options.to_nbdays)
)
)
class MTAClient:
"""MTA Client class. Originally done for EPN.
"""
def __init__(
self,
security_protocol="none",
smtp_hostname="localhost",
smtp_port=25,
smtp_timeout=60,
smtp_username=None,
smtp_password=None,
):
# We only support "none" (cleartext) for now.
# Future values: "ssl", "starttls"
self._security_protocol = security_protocol
self._smtp_hostname = smtp_hostname
self._smtp_port = smtp_port
self._smtp_timeout = smtp_timeout
self._username = smtp_username
self._password = smtp_password
# This should not be touched
self._conn = None
if (
self._security_protocol == "none"
and "localhost" not in self._smtp_hostname
):
logger.error(
"IPA-EPN: using cleartext for non-localhost SMTPd "
"is not supported."
)
self._connect()
def cleanup(self):
self._disconnect()
def send_message(self, message_str=None, subscribers=None):
try:
result = self._conn.sendmail(
api.env.smtp_admin, subscribers, message_str,
)
except Exception as e:
logger.info("IPA-EPN: Failed to send mail: %s", e)
finally:
if result:
for key in result:
logger.info(
"IPA-EPN: Failed to send mail to '%s': %s %s",
key,
result[key][0],
result[key][1],
)
logger.info(
"IPA-EPN: Failed to send mail to at least one recipient"
)
def _connect(self):
try:
if self._security_protocol == "none":
self._conn = smtplib.SMTP(
host=self._smtp_hostname,
port=self._smtp_port,
timeout=self._smtp_timeout,
)
else:
self._conn = smtplib.SMTP_SSL(
host=self._smtp_hostname,
port=self._smtp_port,
timeout=self._smtp_timeout,
)
except smtplib.SMTPException as e:
logger.error(
"IPA-EPN: Unable to connect to %s:%s: %s",
self._smtp_hostname,
self._smtp_port,
e,
)
try:
self._conn.ehlo()
except smtplib.SMTPException as e:
logger.error(
"IPA-EPN: EHLO failed for host %s:%s: %s",
self._smtp_hostname,
self._smtp_port,
e,
)
if (
self._conn.has_extn("STARTTLS")
and self._security_protocol == "starttls"
):
try:
self._conn.starttls()
self._conn.ehlo()
except smtplib.SMTPException as e:
logger.error(
"IPA-EPN: Unable to create an encrypted session to "
"%s:%s: %s",
self._smtp_hostname,
self._smtp_port,
e,
)
if self._username and self._password:
if self._conn.has_extn("AUTH"):
try:
self._conn.login(self._username, self._password)
if self._security_protocol == "none":
logger.info(
"IPA-EPN: Username and Password "
"were sent in the clear."
)
except smtplib.SMTPAuthenticationError:
logger.error(
"IPA-EPN: Authentication to %s:%s failed, "
"please check your username and/or password:",
self._smtp_hostname,
self._smtp_port,
)
except smtplib.SMTPException as e:
logger.error(
"IPA-EPN: SMTP Error at %s:%s:%s",
self._smtp_hostname,
self._smtp_port,
e,
)
else:
err_str = (
"IPA-EPN: Server at %s:%s "
"does not support authentication.",
self._smtp_hostname,
self._smtp_port,
)
logger.error(err_str)
def _disconnect(self):
self._conn.quit()
class MailUserAgent:
"""The MUA class for EPN.
"""
def __init__(
self,
security_protocol="none",
smtp_hostname="localhost",
smtp_port=25,
smtp_timeout=60,
smtp_username=None,
smtp_password=None,
x_mailer=None,
):
self._x_mailer = x_mailer
self._subject = None
self._body = None
self._subscribers = None
self._subtype = None
# Let it be plain or html?
self._subtype = "plain"
# UTF-8 only for now
self._charset = "utf-8"
self._msg = None
self._message_str = None
self._mta_client = MTAClient(
security_protocol=security_protocol,
smtp_hostname=smtp_hostname,
smtp_port=smtp_port,
smtp_timeout=smtp_timeout,
smtp_username=smtp_username,
smtp_password=smtp_password,
)
def cleanup(self):
self._mta_client.cleanup()
def send_message(
self, mail_subject=None, mail_body=None, subscribers=None
):
"""Given mail_subject, mail_body, and subscribers, composes
the message and sends it.
"""
if None in [mail_subject, mail_body, subscribers]:
logger.error("IPA-EPN: Tried to send an empty message.")
return False
self._compose_message(
mail_subject=mail_subject,
mail_body=mail_body,
subscribers=subscribers,
)
self._mta_client.send_message(
message_str=self._message_str, subscribers=subscribers
)
return True
def _compose_message(
self, mail_subject=None, mail_body=None, subscribers=None
):
"""The composer creates a MIME multipart message.
"""
self._subject = mail_subject
self._body = mail_body
self._subscribers = subscribers
self._msg = MIMEMultipart(_charset=self._charset)
self._msg["From"] = formataddr(
("IPA-EPN", "noreply@%s" % socket.getfqdn())
)
self._msg["To"] = ", ".join(self._subscribers)
self._msg["Date"] = formatdate(localtime=True)
self._msg["Subject"] = Header(self._subject, self._charset)
self._msg.preamble = "Multipart message"
if "X-Mailer" not in self._msg and self._x_mailer:
self._msg.add_header("X-Mailer", self._x_mailer)
self._msg.attach(
MIMEText(
self._body + "\n\n",
_subtype=self._subtype,
_charset=self._charset,
)
)
self._message_str = self._msg.as_string()

View File

@@ -342,6 +342,7 @@ class BasePathNamespace:
IPASERVER_UNINSTALL_LOG = "/var/log/ipaserver-uninstall.log"
IPAUPGRADE_LOG = "/var/log/ipaupgrade.log"
IPATRUSTENABLEAGENT_LOG = "/var/log/ipatrust-enable-agent.log"
IPAEPN_LOG = "/var/log/ipaepn.log"
KADMIND_LOG = "/var/log/kadmind.log"
KRB5KDC_LOG = "/var/log/krb5kdc.log"
MESSAGES = "/var/log/messages"