Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix relay rule credential handling #142

Merged
merged 8 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions imageroot/actions/add-relay-rule/20add-relay-rule
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,14 @@ try:
agent.set_status('validation-failed')
json.dump([{'field':'rule_subject', 'parameter':'rule_subject','value': rule_subject, 'error':'rule_subject_must_be_*'}], fp=sys.stdout)
sys.exit(5)
if rule_type != 'sender':
# Copy auth and transport settings to existing rules with the same host:port key
cur.execute("""UPDATE relayrules SET transport=:transport, tls=:mandatory_tls, username=:username, password=:password WHERE host=:host AND port=:port AND rule_type=:rule_type""", values)
cur.execute("""INSERT INTO relayrules (rule_type, rule_subject, transport, host, port, tls, username, password, enabled) VALUES (:rule_type, :rule_subject, :transport, :host, :port, :mandatory_tls, :username, :password, :enabled)""", values)

except Exception as err:
sdb.rollback()
agent.set_status('validation-failed')
json.dump([{'field':'rule_subject', 'parameter':'rule_subject','value': str(err), 'error':'sender_data_cannot_be_saved_in_db'}], fp=sys.stdout)
sys.exit(3)
raise
else:
sdb.commit()

Expand Down
88 changes: 27 additions & 61 deletions imageroot/actions/alter-relay-rule/20alter_relay
Original file line number Diff line number Diff line change
Expand Up @@ -9,77 +9,43 @@ import json
import sys
import mail
import agent
from sqlite3 import IntegrityError

class RelayNotFoundError(Exception):
pass

request = json.load(sys.stdin)
updatedict = request.copy() # values for SQL statements
updatedict['tls'] = 'encrypt' if request['mandatory_tls'] else 'may'
updatedict['enabled'] = 1 if request['enabled'] else 0

sdb = mail.pcdb_connect()
rule_type = request['rule_type']
rule_subject = request['rule_subject']
username = request.get("username", "")
password = request.get("password", "")
host = request['host']
port = request['port']
mandatory_tls = request.get("mandatory_tls", False)

def get_password_from_database(rule_subject):
# Function to query the database for the password based on sender or recipient
sdb = mail.pcdb_connect()
cur = sdb.cursor()
cur.execute("SELECT password FROM relayrules WHERE rule_subject=?", (rule_subject,))
result = cur.fetchone()
sdb.close()
if result:
return result[0]
else:
agent.set_status('validation-failed')
json.dump([{'field':'test_rule_credentials','parameter':'test_rule_credentials','value':'cannot_retrieve_password_user_from_sqlite','error':'cannot_retrieve_password_user_from_sqlite'}],fp=sys.stdout, default=str)
sys.exit(3)
rulerecord = sdb.execute("SELECT * FROM relayrules WHERE rule_subject=? AND rule_type=?", (
request["rule_subject"],
request["rule_type"],
)).fetchone()
if rulerecord is None:
# Current rule was not found
agent.set_status('validation-failed')
json.dump([{'field': 'rule_subject', 'parameter': 'subject', 'value': request["rule_subject"], 'error':'relay_not_found'}], fp=sys.stdout)
sys.exit(2)

# password not changed, we need to retrieve it from the database
if request['username'] and password == '':
password = get_password_from_database(rule_subject)
# Authentication is enabled but the password was not changed: retrieve it
# from the database to retain the current value.
if request["username"] and request["password"] == '':
updatedict["password"] = rulerecord['password']

# probe we could connect to the smtp server and retrieve if we use smtp or smtps
probe_credentials = mail.probe_and_validate_smtp_credentials(username, password, host, port, mandatory_tls)
transport_is_smtps = probe_credentials['transport_smtps']
# validate and obtain the server transport:
probe_credentials = mail.probe_and_validate_smtp_credentials(request["username"], updatedict["password"], request["host"], request["port"], request["mandatory_tls"])
updatedict['transport'] = 'smtps' if probe_credentials['transport_smtps'] else 'smtp'

try:
cur = sdb.cursor()

if 'host' in request:
cur.execute("""UPDATE relayrules SET host=? WHERE rule_subject=?""", (request['host'], rule_subject))

if 'port' in request:
cur.execute("""UPDATE relayrules SET port=? WHERE rule_subject=?""", (request['port'], rule_subject))

if 'mandatory_tls' in request:
cur.execute("""UPDATE relayrules SET tls=? WHERE rule_subject=?""", ('encrypt' if request['mandatory_tls'] else 'may', rule_subject))
cur.execute("""UPDATE relayrules SET transport=? WHERE rule_subject=?""", ('smtps' if transport_is_smtps else 'smtp', rule_subject))

if 'username' in request:
cur.execute("""UPDATE relayrules SET username=? WHERE rule_subject=?""", (request['username'], rule_subject))

if 'password' in request:
cur.execute("""UPDATE relayrules SET password=? WHERE rule_subject=?""", (password, rule_subject))

if 'enabled' in request:
cur.execute("""UPDATE relayrules SET enabled=? WHERE rule_subject=?""", (1 if request['enabled'] else 0, rule_subject))

if cur.rowcount == 0:
raise RelayNotFoundError()

except RelayNotFoundError:
# Update the record with rule_subject key:
sdb.execute("""UPDATE relayrules SET host=:host, port=:port, tls=:tls, transport=:transport, username=:username, password=:password, enabled=:enabled WHERE rule_subject=:rule_subject""", updatedict)
# Copy auth and transport settings to existing rules with the same host:port key:
if rulerecord["rule_type"] != "sender":
sdb.execute("""UPDATE relayrules SET tls=:tls, transport=:transport, username=:username, password=:password WHERE (rule_type != 'sender' AND host=:host AND port=:port)""", updatedict)
except Exception as ex:
print(agent.SD_ERR+"Failed to update relayrules table.", ex, file=sys.stderr)
sdb.rollback()
agent.set_status('validation-failed')
json.dump([{'field': 'rule_subject', 'parameter': 'subject', 'value': rule_subject, 'error':'relay_not_found'}], fp=sys.stdout)
sys.exit(2)

raise
else:
sdb.commit()

finally:
sdb.close()
4 changes: 4 additions & 0 deletions imageroot/pypkg/mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ def pcdb_connect(readonly=False):
con = sqlite3.connect(uri, uri=True)
con.row_factory = sqlite3.Row
con.execute("""PRAGMA foreign_keys = ON""")
def dbg(statement):
print(agent.SD_DEBUG+statement, file=sys.stderr)
if os.getenv("DEBUG"):
con.set_trace_callback(dbg)
return con

def get_user_domains(rdb):
Expand Down
2 changes: 2 additions & 0 deletions postfix/etc/postfix/relaycredentials.cf
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ query = SELECT FORMAT('%%s:%%s', username, password) AS credentials
(rule_subject = '%s' AND rule_type = 'sender')
OR '%s' IN (FORMAT('[%%s]:%%s', host, port), FORMAT('[%%s]', host))
)
LIMIT 1
expansion_limit = 1
7 changes: 5 additions & 2 deletions ui/public/i18n/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@
"enabled": "Status",
"host": "Hostname",
"port": "Port",
"port_placeholder": "e.g. 25, 587 or 465",
DavidePrincipi marked this conversation as resolved.
Show resolved Hide resolved
"authentication": "Authentication",
"username": "Username",
"password": "Password",
Expand All @@ -402,9 +403,11 @@
"no_relay_rules_description": "There is no relay rules configured",
"col_has_password": "Authentication",
"col_rule_subject": "Rule subject",
"col_host": "Hostname",
"col_host": "Hostname and port",
"col_enabled": "Status",
"unchanged": "Unchanged",
"shared_credentials_info_title": "Shared host settings",
"shared_credentials_info_description": "Authentication and TLS settings for Default and Recipient rules are shared and applied collectively to all rules with the same Hostname and port.",
"settings": {
"networks_label": "Allow relay from these IP addresses",
"networks_helper": "One address per line, in IPv4/6 CIDR format",
Expand All @@ -415,7 +418,7 @@
"bcc": "Always BCC address",
"bcc_string_lte": "Invalid email format",
"is_bcc_enabled": "Always BCC",
"bcc_placeholder": "e.g. [email protected]",
"bcc_placeholder": "e.g. [email protected]",
"is_bcc_enabled_tooltips": "When enabled, the specified address will receive a copy of every message sent or received through this server. This setting is useful for mail archiving solutions."
},
"error": {
Expand Down
35 changes: 29 additions & 6 deletions ui/src/views/Relay.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@
</div>
</cv-column>
</cv-row>
<cv-row>
<cv-column>
<NsInlineNotification
v-if="hasWildcard || hasRulesByRecipient"
kind="info"
:title="$t('relay.shared_credentials_info_title')"
:description="$t('relay.shared_credentials_info_description')"
:showCloseButton="false"
/>
</cv-column>
</cv-row>
<cv-row>
<cv-column>
<NsInlineNotification
Expand Down Expand Up @@ -127,14 +138,17 @@
</p>
</cv-data-table-cell>
<cv-data-table-cell>
{{ row.host }}
{{ row.host }}:{{ row.port }}
</cv-data-table-cell>
<cv-data-table-cell>
{{
row.has_password
? $t("common.enabled")
: $t("common.disabled")
}}
<div v-if="row.has_password">
{{ row.username }}
</div>
<NsTag
v-else
kind="high-contrast"
:label="$t('common.disabled')"
></NsTag>
</cv-data-table-cell>
<cv-data-table-cell>
<NsTag
Expand Down Expand Up @@ -270,6 +284,7 @@
/>
<NsTextInput
v-model="form.port"
:placeholder="$t('relay.port_placeholder')"
:label="$t('relay.port')"
:invalid-message="error.form.port"
ref="form.port"
Expand Down Expand Up @@ -452,6 +467,14 @@ export default {
ruleTypeTranslation: function () {
return this.$t("relay." + this.form.rule_type);
},
hasRulesByRecipient: function () {
for (const row of this.relayRules) {
if (row.rule_type == "recipient") {
return true;
}
}
return false;
},
},
beforeRouteEnter(to, from, next) {
next((vm) => {
Expand Down
Loading