diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2c1dc88..57f1b5d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@ Note: Breaking changes between versions are indicated by "💥".
## Unreleased
+- [Feature] Add `encrypt` template filter to conveniently add htpasswd-based authentication to nginx
- [Bugfix] Fix "missing tty" during init in cron jobs
## v3.11.7 (2020-04-01)
diff --git a/docs/plugins/api.rst b/docs/plugins/api.rst
index ca5f2af..048a92e 100644
--- a/docs/plugins/api.rst
+++ b/docs/plugins/api.rst
@@ -147,6 +147,7 @@ With the above declaration, you can store plugin-specific templates in the ``tem
In Tutor, templates are `Jinja2 `__-formatted files that will be rendered in the Tutor environment (the ``$(tutor config printroot)/env`` folder) when running ``tutor config save``. The environment files are overwritten every time the environment is saved. Plugin developers can create templates that make use of the built-in `Jinja2 API `__. In addition, a couple additional filters are added by Tutor:
* ``common_domain``: Return the longest common name between two domain names. Example: ``{{ "studio.demo.myopenedx.com"|common_domain("lms.demo.myopenedx.com") }}`` is equal to "demo.myopenedx.com".
+* ``encrypt``: Encrypt an arbitrary string. The encryption process is compatible with `htpasswd `__ verification.
* ``list_if``: In a list of ``(value, condition)`` tuples, return the list of ``value`` for which the ``condition`` is true.
* ``patch``: See :ref:`patches `.
* ``random_string``: Return a random string of the given length composed of ASCII letters and digits. Example: ``{{ 8|random_string }}``.
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 2716a96..c056b3d 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -22,3 +22,11 @@ class UtilsTests(unittest.TestCase):
def test_list_if(self):
self.assertEqual('["cms"]', utils.list_if([("lms", False), ("cms", True)]))
+
+ def test_encrypt_decrypt(self):
+ password = "passw0rd"
+ encrypted1 = utils.encrypt(password)
+ encrypted2 = utils.encrypt(password)
+ self.assertNotEqual(encrypted1, encrypted2)
+ self.assertTrue(utils.verify_encrypted(encrypted1, password))
+ self.assertTrue(utils.verify_encrypted(encrypted2, password))
diff --git a/tutor/env.py b/tutor/env.py
index a3af5c8..62e3bc9 100644
--- a/tutor/env.py
+++ b/tutor/env.py
@@ -48,9 +48,10 @@ class Renderer:
loader=jinja2.FileSystemLoader(template_roots),
undefined=jinja2.StrictUndefined,
)
- environment.filters["random_string"] = utils.random_string
environment.filters["common_domain"] = utils.common_domain
+ environment.filters["encrypt"] = utils.encrypt
environment.filters["list_if"] = utils.list_if
+ environment.filters["random_string"] = utils.random_string
environment.filters["reverse_host"] = utils.reverse_host
environment.filters["walk_templates"] = self.walk_templates
environment.globals["patch"] = self.patch
diff --git a/tutor/utils.py b/tutor/utils.py
index 6abcaf3..8f928c9 100644
--- a/tutor/utils.py
+++ b/tutor/utils.py
@@ -1,3 +1,5 @@
+from crypt import crypt
+from hmac import compare_digest
import json
import os
import random
@@ -12,6 +14,24 @@ from . import exceptions
from . import fmt
+def encrypt(text):
+ """
+ Encrypt some textual content. The method employed is the same as suggested in the
+ `python docs `__. The
+ encryption process is compatible with the password verification performed by
+ `htpasswd `__.
+ """
+ hashed = crypt(text)
+ return crypt(text, hashed)
+
+
+def verify_encrypted(encrypted, text):
+ """
+ Return True/False if the encrypted content corresponds to the unencrypted text.
+ """
+ return compare_digest(crypt(text, encrypted), encrypted)
+
+
def ensure_file_directory_exists(path):
"""
Create file's base directory if it does not exist.