mirror of
https://github.com/chillout2k/ExOTA-Milter.git
synced 2025-12-15 18:59:45 +00:00
Policy validation
This commit is contained in:
parent
61da6219c0
commit
0b4849a726
@ -9,6 +9,10 @@ import re
|
|||||||
import email.utils
|
import email.utils
|
||||||
import authres
|
import authres
|
||||||
import json
|
import json
|
||||||
|
from policy import (
|
||||||
|
ExOTAPolicyException, ExOTAPolicyNotFoundException,
|
||||||
|
ExOTAPolicyBackendJSON, ExOTAPolicy
|
||||||
|
)
|
||||||
|
|
||||||
# Globals with mostly senseless defaults ;)
|
# Globals with mostly senseless defaults ;)
|
||||||
g_milter_name = 'exota-milter'
|
g_milter_name = 'exota-milter'
|
||||||
@ -21,7 +25,7 @@ g_milter_dkim_enabled = False
|
|||||||
g_milter_dkim_authservid = 'invalid'
|
g_milter_dkim_authservid = 'invalid'
|
||||||
g_milter_policy_source = 'file' # file, ldap, etc.
|
g_milter_policy_source = 'file' # file, ldap, etc.
|
||||||
g_milter_policy_file = 'invalid'
|
g_milter_policy_file = 'invalid'
|
||||||
g_milter_policy = {}
|
g_milter_policy_backend = None
|
||||||
|
|
||||||
class ExOTAMilter(Milter.Base):
|
class ExOTAMilter(Milter.Base):
|
||||||
# Each new connection is handled in an own thread
|
# Each new connection is handled in an own thread
|
||||||
@ -34,6 +38,7 @@ class ExOTAMilter(Milter.Base):
|
|||||||
self.hdr_from = None
|
self.hdr_from = None
|
||||||
self.hdr_from_domain = None
|
self.hdr_from_domain = None
|
||||||
self.hdr_tenant_id = None
|
self.hdr_tenant_id = None
|
||||||
|
self.hdr_tenant_id_count = 0
|
||||||
self.dkim_results = []
|
self.dkim_results = []
|
||||||
self.dkim_valid = False
|
self.dkim_valid = False
|
||||||
# https://stackoverflow.com/a/2257449
|
# https://stackoverflow.com/a/2257449
|
||||||
@ -79,6 +84,8 @@ class ExOTAMilter(Milter.Base):
|
|||||||
logging.debug(self.mconn_id + "/" + str(self.queue_id) +
|
logging.debug(self.mconn_id + "/" + str(self.queue_id) +
|
||||||
"/HEADER Header: {0}, Value: {1}".format(name, hval)
|
"/HEADER Header: {0}, Value: {1}".format(name, hval)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Parse RFC-5322-From header
|
||||||
if(name == "From"):
|
if(name == "From"):
|
||||||
hdr_5322_from = email.utils.parseaddr(hval)
|
hdr_5322_from = email.utils.parseaddr(hval)
|
||||||
self.hdr_from = hdr_5322_from[1].lower()
|
self.hdr_from = hdr_5322_from[1].lower()
|
||||||
@ -93,11 +100,16 @@ class ExOTAMilter(Milter.Base):
|
|||||||
logging.debug(self.mconn_id + "/" + str(self.queue_id) +
|
logging.debug(self.mconn_id + "/" + str(self.queue_id) +
|
||||||
"/HEADER 5322.from: {0}, 5322.from_domain: {1}".format(self.hdr_from, self.hdr_from_domain)
|
"/HEADER 5322.from: {0}, 5322.from_domain: {1}".format(self.hdr_from, self.hdr_from_domain)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Parse non-standardized X-MS-Exchange-CrossTenant-Id header
|
||||||
elif(name == "X-MS-Exchange-CrossTenant-Id"):
|
elif(name == "X-MS-Exchange-CrossTenant-Id"):
|
||||||
|
self.hdr_tenant_id_count += 1
|
||||||
self.hdr_tenant_id = hval.lower()
|
self.hdr_tenant_id = hval.lower()
|
||||||
logging.debug(self.mconn_id + "/" + str(self.queue_id) +
|
logging.debug(self.mconn_id + "/" + str(self.queue_id) +
|
||||||
"/HEADER Tenant-ID: {0}".format(self.hdr_tenant_id)
|
"/HEADER Tenant-ID: {0}".format(self.hdr_tenant_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Parse RFC-7601 Authentication-Results header
|
||||||
elif(name == "Authentication-Results"):
|
elif(name == "Authentication-Results"):
|
||||||
if g_milter_dkim_enabled == True:
|
if g_milter_dkim_enabled == True:
|
||||||
ar = None
|
ar = None
|
||||||
@ -141,11 +153,19 @@ class ExOTAMilter(Milter.Base):
|
|||||||
)
|
)
|
||||||
self.setreply('550','5.7.1', g_milter_tmpfail_message)
|
self.setreply('550','5.7.1', g_milter_tmpfail_message)
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
if self.hdr_from_domain not in g_milter_policy:
|
|
||||||
logging.error(self.mconn_id + "/" + str(self.queue_id) + "/EOM " +
|
# Get policy for 5322.from_domain
|
||||||
"Could not find 5322.from_domain {0} in policy!".format(self.hdr_from_domain)
|
policy = None
|
||||||
|
try:
|
||||||
|
policy = g_milter_policy_backend.get(self.hdr_from_domain)
|
||||||
|
logging.debug(self.mconn_id + "/" + self.queue_id +
|
||||||
|
"/EOM Policy for 5322.from_domain={0} fetched from backend".format(self.hdr_from_domain)
|
||||||
)
|
)
|
||||||
self.setreply('550','5.7.1', g_milter_reject_message)
|
except (ExOTAPolicyException, ExOTAPolicyNotFoundException) as e:
|
||||||
|
logging.info(self.mconn_id + "/" + self.queue_id +
|
||||||
|
"/EOM {0}".format(e.message)
|
||||||
|
)
|
||||||
|
self.setreply('550','5.7.1', g_milter_tmpfail_message)
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
|
|
||||||
if self.hdr_tenant_id is None:
|
if self.hdr_tenant_id is None:
|
||||||
@ -154,18 +174,28 @@ class ExOTAMilter(Milter.Base):
|
|||||||
)
|
)
|
||||||
self.setreply('550','5.7.1', g_milter_reject_message)
|
self.setreply('550','5.7.1', g_milter_reject_message)
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
if self.hdr_tenant_id == g_milter_policy[self.hdr_from_domain]['tenant_id'].lower():
|
if self.hdr_tenant_id_count > 1:
|
||||||
logging.info(self.mconn_id + "/" + self.queue_id +
|
logging.info(self.mconn_id + "/" + self.queue_id +
|
||||||
"/EOM: 5322.from_domain={1} tenant_id={0} status=match".format(self.hdr_tenant_id, self.hdr_from_domain)
|
"/EOM: More than one tenant-IDs for {0} found!".format(self.hdr_from_domain)
|
||||||
|
)
|
||||||
|
self.setreply('550','5.7.1', g_milter_reject_message)
|
||||||
|
return Milter.REJECT
|
||||||
|
if self.hdr_tenant_id == policy.get_tenant_id():
|
||||||
|
logging.info(self.mconn_id + "/" + self.queue_id +
|
||||||
|
"/EOM: 5322.from_domain={0} tenant_id={1} status=match".format(
|
||||||
|
self.hdr_from_domain, self.hdr_tenant_id
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logging.error(self.mconn_id + "/" + self.queue_id +
|
logging.error(self.mconn_id + "/" + self.queue_id +
|
||||||
"/EOM: 5322.from_domain={1} tenant_id={0} status=no_match".format(self.hdr_tenant_id, self.hdr_from_domain)
|
"/EOM: 5322.from_domain={0} tenant_id={1} status=no_match".format(
|
||||||
|
self.hdr_from_domain, self.hdr_tenant_id
|
||||||
|
)
|
||||||
)
|
)
|
||||||
self.setreply('550','5.7.1', g_milter_reject_message)
|
self.setreply('550','5.7.1', g_milter_reject_message)
|
||||||
return Milter.REJECT
|
return Milter.REJECT
|
||||||
|
|
||||||
if g_milter_dkim_enabled == True and g_milter_policy[self.hdr_from_domain]['dkim'] == True:
|
if g_milter_dkim_enabled and policy.is_dkim_enabled():
|
||||||
logging.debug(self.mconn_id + "/" + self.queue_id +
|
logging.debug(self.mconn_id + "/" + self.queue_id +
|
||||||
"/EOM: 5322.from_domain={0} dkim_auth=enabled".format(self.hdr_from_domain)
|
"/EOM: 5322.from_domain={0} dkim_auth=enabled".format(self.hdr_from_domain)
|
||||||
)
|
)
|
||||||
@ -206,7 +236,7 @@ class ExOTAMilter(Milter.Base):
|
|||||||
|
|
||||||
logging.info(self.mconn_id + "/" + self.queue_id +
|
logging.info(self.mconn_id + "/" + self.queue_id +
|
||||||
"/EOM: Authentication successful (dkim_enabled={0})".format(
|
"/EOM: Authentication successful (dkim_enabled={0})".format(
|
||||||
str(g_milter_policy[self.hdr_from_domain]['dkim'])
|
str(policy.is_dkim_enabled())
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
@ -260,12 +290,10 @@ if __name__ == "__main__":
|
|||||||
if 'MILTER_POLICY_FILE' in os.environ:
|
if 'MILTER_POLICY_FILE' in os.environ:
|
||||||
g_milter_policy_file = os.environ['MILTER_POLICY_FILE']
|
g_milter_policy_file = os.environ['MILTER_POLICY_FILE']
|
||||||
try:
|
try:
|
||||||
with open(g_milter_policy_file, 'r') as policy_file:
|
g_milter_policy_backend = ExOTAPolicyBackendJSON(g_milter_policy_file)
|
||||||
g_milter_policy = json.load(policy_file)
|
logging.info("JSON policy backend initialized")
|
||||||
policy_file.close()
|
except ExOTAPolicyException as e:
|
||||||
logging.info("Successfully slurped policy file: {0}".format(g_milter_policy_file))
|
logging.error("Policy backend error: {0}".format(e.message))
|
||||||
except:
|
|
||||||
logging.error("Error reading policy file: " + traceback.format_exc())
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
logging.error("ENV[MILTER_POLICY_FILE] is mandatory!")
|
logging.error("ENV[MILTER_POLICY_FILE] is mandatory!")
|
||||||
|
|||||||
101
app/policy.py
Normal file
101
app/policy.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import json
|
||||||
|
import traceback
|
||||||
|
import re
|
||||||
|
|
||||||
|
class ExOTAPolicyException(Exception):
|
||||||
|
def __init__(self, message):
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
class ExOTAPolicyNotFoundException(ExOTAPolicyException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ExOTAPolicyInvalidException(ExOTAPolicyException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ExOTAPolicy():
|
||||||
|
def __init__(self, policy_dict):
|
||||||
|
self.tenant_id = policy_dict['tenant_id']
|
||||||
|
self.dkim = policy_dict['dkim']
|
||||||
|
|
||||||
|
def get_tenant_id(self):
|
||||||
|
return self.tenant_id
|
||||||
|
|
||||||
|
def is_dkim_enabled(self):
|
||||||
|
return self.dkim
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_policy(policy_dict):
|
||||||
|
if 'tenant_id' not in policy_dict:
|
||||||
|
raise ExOTAPolicyInvalidException(
|
||||||
|
"Policy must have a 'tenant_id' attribute!"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if policy_dict['tenant_id'] == '':
|
||||||
|
raise ExOTAPolicyInvalidException(
|
||||||
|
"'tenant_id' must not be empty!"
|
||||||
|
)
|
||||||
|
if re.match(r'^.*\s+.*$', policy_dict['tenant_id']):
|
||||||
|
raise ExOTAPolicyInvalidException(
|
||||||
|
"'tenant_id' must not contain whitespace characters!"
|
||||||
|
)
|
||||||
|
if 'dkim' not in policy_dict:
|
||||||
|
raise ExOTAPolicyInvalidException(
|
||||||
|
"Policy must have a 'dkim' attribute!"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if not isinstance(policy_dict['dkim'], bool):
|
||||||
|
raise ExOTAPolicyInvalidException(
|
||||||
|
"'dkim'({0}) must be boolean!".format(policy_dict['dkim'])
|
||||||
|
)
|
||||||
|
|
||||||
|
class ExOTAPolicyBackend():
|
||||||
|
type = None
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
def get(self, from_domain):
|
||||||
|
pass
|
||||||
|
|
||||||
|
########## JSON file
|
||||||
|
class ExOTAPolicyBackendJSON(ExOTAPolicyBackend):
|
||||||
|
type = 'json'
|
||||||
|
def __init__(self, file_path):
|
||||||
|
self.policies = None
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r') as policy_file:
|
||||||
|
self.policies = json.load(policy_file)
|
||||||
|
policy_file.close()
|
||||||
|
# validate policy
|
||||||
|
for policy in self.policies:
|
||||||
|
try:
|
||||||
|
ExOTAPolicy.check_policy(self.policies[policy])
|
||||||
|
except ExOTAPolicyInvalidException as e:
|
||||||
|
raise ExOTAPolicyException(
|
||||||
|
"Policy {0} is invalid: {1}".format(policy, e.message)
|
||||||
|
) from e
|
||||||
|
except json.decoder.JSONDecodeError as e:
|
||||||
|
raise ExOTAPolicyException(
|
||||||
|
"JSON-error in policy file: " + str(e)
|
||||||
|
) from e
|
||||||
|
except Exception as e:
|
||||||
|
raise ExOTAPolicyException(
|
||||||
|
"Error reading policy file: " + traceback.format_exc()
|
||||||
|
) from e
|
||||||
|
|
||||||
|
def get(self, from_domain):
|
||||||
|
try:
|
||||||
|
return ExOTAPolicy(self.policies[from_domain])
|
||||||
|
except KeyError as e:
|
||||||
|
raise ExOTAPolicyNotFoundException(
|
||||||
|
"Policy for from_domain={0} not found".format(from_domain)
|
||||||
|
) from e
|
||||||
|
except Exception as e:
|
||||||
|
raise ExOTAPolicyException(
|
||||||
|
"Error fetching policy for {0}: {1}".format(
|
||||||
|
from_domain, traceback.format_exc()
|
||||||
|
)
|
||||||
|
) from e
|
||||||
|
|
||||||
|
########## LDAP
|
||||||
|
class ExOTAPolicyBackendLDAP(ExOTAPolicyBackendJSON):
|
||||||
|
type = 'ldap'
|
||||||
|
pass
|
||||||
@ -33,6 +33,9 @@ end
|
|||||||
if mt.header(conn, "X-MS-Exchange-CrossTenant-Id", "1234abcd-18c5-45e8-88de-123456789abc") ~= nil then
|
if mt.header(conn, "X-MS-Exchange-CrossTenant-Id", "1234abcd-18c5-45e8-88de-123456789abc") ~= nil then
|
||||||
error "mt.header(Subject) failed"
|
error "mt.header(Subject) failed"
|
||||||
end
|
end
|
||||||
|
--if mt.header(conn, "X-MS-Exchange-CrossTenant-Id", "4321abcd-18c5-45e8-88de-blahblubb") ~= nil then
|
||||||
|
-- error "mt.header(Subject) failed"
|
||||||
|
--end
|
||||||
if mt.header(conn, "Authentication-Results", "another-wrong-auth-serv-id;\n dkim=fail header.d=lalalulu.onmicrosoft.com header.s=selector1-lalalulu-onmicrosoft-com header.b=mmmjFpv8") ~= nil then
|
if mt.header(conn, "Authentication-Results", "another-wrong-auth-serv-id;\n dkim=fail header.d=lalalulu.onmicrosoft.com header.s=selector1-lalalulu-onmicrosoft-com header.b=mmmjFpv8") ~= nil then
|
||||||
error "mt.header(Subject) failed"
|
error "mt.header(Subject) failed"
|
||||||
end
|
end
|
||||||
|
|||||||
@ -2,5 +2,9 @@
|
|||||||
"lalalulu.onmicrosoft.com": {
|
"lalalulu.onmicrosoft.com": {
|
||||||
"tenant_id": "1234abcd-18c5-45e8-88de-123456789abc",
|
"tenant_id": "1234abcd-18c5-45e8-88de-123456789abc",
|
||||||
"dkim": true
|
"dkim": true
|
||||||
|
},
|
||||||
|
"asdf2.onmicrosoft.com": {
|
||||||
|
"tenant_id": "asdftasdfa",
|
||||||
|
"dkim": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user