Loading LICENSE-3RD-PARTY.md +9 −2 Changes for LICENSE-3RD-PARTY.md: 9 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -19,10 +19,10 @@ ## Redoc - Files: `schwalbe/static/lib/redoc/*` - Files: `portal/static/lib/redoc/*` - Author: Rebilly, Inc. - URL: https://github.com/Redocly/redoc - License: [MIT License](schwalbe/static/lib/redoc/LICENSE) - License: [MIT License](portal/static/lib/redoc/LICENSE) ## Mithril.js Loading @@ -31,6 +31,13 @@ - URL: https://github.com/MithrilJS/mithril.js - License: [MIT License](portal/static/lib/mithril/LICENSE) ## base64.js - Files: `portal/static/lib/base64-*` - Author: Dan Kogai - URL: https://github.com/dankogai/js-base64 - License: [BSD 3-Clause License](portal/static/lib/base64-3.7.5/LICENSE.md) ## flaskr (Flask Tutorial App) Some files in this project are more or less modified versions of the files from the Flask Tutorial App `flaskr`, which Loading alembic/versions/ec81173dbe48_add_ldap_directory.py 0 → 100644 +33 −0 Changes for alembic/versions/ec81173dbe48_add_ldap_directory.py: 33 added lines, 0 removed lines. Original line number Diff line number Diff line """Add ldap directory Revision ID: ec81173dbe48 Revises: 6c174457829c Create Date: 2024-06-06 23:50:54.368404 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa from portal.model import Uuid # revision identifiers, used by Alembic. revision: str = "ec81173dbe48" down_revision: Union[str, None] = "6c174457829c" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: op.create_table( "ldap_directory", sa.Column("url", sa.String(), nullable=False), sa.Column("base_dn", sa.String(), nullable=False), sa.Column("uuid", Uuid(), nullable=False), sa.PrimaryKeyConstraint("uuid"), ) def downgrade() -> None: op.drop_table("ldap_directory") ldap/Directory.py 0 → 100644 +99 −0 Changes for ldap/Directory.py: 99 added lines, 0 removed lines. Original line number Diff line number Diff line import ssl from ldap3 import Tls, Server, Connection from ldap3.core.exceptions import LDAPBindError, LDAPPasswordIsMandatoryError from ldap3.utils.dn import escape_rdn def is_valid_server_url(url: str) -> bool: return url.startswith("ldaps://") or url.startswith("ldap://") TLS_CONFIGURATION = Tls(validate=ssl.CERT_REQUIRED) class Directory: """ A (user information) directory (= database), accessible via the Lightweight Directory Access Protocol (LDAP). Provides a few methods to do things you probably want to do with user information. For a detailed security consideration, please see: https://stackoverflow.com/questions/73681632/how-to-safely-authenticate-a-user-using-ldap/ Taken from an older (internal) project of mine, codex: https://gitlab.cl.uni-heidelberg.de/fachschaft/codex/-/blob/master/ldap/LdapDatabase.py?ref_type=heads """ def __init__(self, url: str, base_dn: str): """ Creates the LdapDatabase instance. Does not establish any connections whatsoever. :param url: A ldap server url (must use either ldaps:// or ldap:// scheme) :param base_dn: The distinguished name below which to look for users (e.g. "ou=employees,dc=example,dc=com") """ if not is_valid_server_url(url): raise ValueError("ldap_server_url must begin with ldaps:// or ldap://") self.ldap_server_url = url self.base_dn = base_dn # Note: The URL scheme (ldap:// or ldaps://) has precedence over the parameter "use_ssl" # (see https://ldap3.readthedocs.io/en/latest/server.html). As we always require a scheme to be present, this # parameter is superfluous. However, setting it to true means that if there ever is a case where no scheme is # specified (e.g. if someone ever deletes the scheme-enforcing code above), the connection will fall back to # use SSL, and I believe that is a good way for things to be. self.__server = Server( self.ldap_server_url, use_ssl=True, tls=TLS_CONFIGURATION ) def is_valid(self, username: str, password: str) -> bool: """ Return if the given username and password are valid credentials. Iff true is returned, the user that provided these credentials can be treated as successfully authenticated. :param username: An (untrusted!) username string :param password: An (untrusted!) password string :return: If username and password are valid """ user_dn = self._get_user_dn(username) try: with Connection(self.__server, user=user_dn, password=password): # This will implicitly perform a bind (as, by default, lazy=False). If it succeeds, the user was # authenticated against the LDAP server, meaning the credentials are valid. # Binding actually seems to be enough, see e.g. https://auth0.com/blog/using-ldap-with-c-sharp/ # (section "Validating user credentials using bind") and https://stackoverflow.com/q/28575359. return True except LDAPBindError: return False except LDAPPasswordIsMandatoryError: # There is a (I believe deprecated) bind method with a username, but without a password # (see https://stackoverflow.com/a/3515564/). This is not permissible by the library, which will throw # this exception. We then return False. return False def _get_user_dn(self, common_name: str) -> str: """ Get the distinguished name of a user, given only its common name (= username, in our case) :param common_name: An (untrusted!) username string :return: The distinguished name of the user (by interpreting "cn=<common_name>" as RDN below self.base_dn) """ # The common_name provided here is untrusted, meaning it can contain arbitrary (malicious) input. # Is this a problem? I don't know. # # When building a filter or search query, it definitely would be: # - https://cheatsheetseries.owasp.org/cheatsheets/LDAP_Injection_Prevention_Cheat_Sheet.html # - https://stackoverflow.com/q/47397341 # This is a problem because untrusted input is processed in a trusted context. # # However, when binding, one could argue that the untrusted input is also only processed in an untrusted # context - anyone could go and make a bind request with a malicious username directly to the ldap server. # So we can probably expect the LDAP server to deal with malicious usernames of any kind. # # Or can we? The documentation (https://ldap3.readthedocs.io/en/latest/connection.html) recommends using # escape_rdn() for the username, so we do this, better safe than sorry. return f"cn={escape_rdn(common_name)},{self.base_dn}" ldap/__init__.py 0 → 100644 +6 −0 Changes for ldap/__init__.py: 6 added lines, 0 removed lines. Original line number Diff line number Diff line """ Tools to authenticate via LDAP. Has no ties whatsoever to the rest of the application's source code, should therefore be freely copyable and reusable in other projects. """ from .Directory import Directory portal/api/agenda.py +3 −0 Changes for portal/api/agenda.py: 3 added lines, 0 removed lines. Original line number Diff line number Diff line from portal.model import TaigaUserStoryProvider from .blueprint import bp from .auth import require_valid from ..model import Key @bp.get("/agendas/-/items") @require_valid(Key) def the_agenda_items(): tasks = TaigaUserStoryProvider.get_the_one().fetch_user_stories() Loading Loading
LICENSE-3RD-PARTY.md +9 −2 Changes for LICENSE-3RD-PARTY.md: 9 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -19,10 +19,10 @@ ## Redoc - Files: `schwalbe/static/lib/redoc/*` - Files: `portal/static/lib/redoc/*` - Author: Rebilly, Inc. - URL: https://github.com/Redocly/redoc - License: [MIT License](schwalbe/static/lib/redoc/LICENSE) - License: [MIT License](portal/static/lib/redoc/LICENSE) ## Mithril.js Loading @@ -31,6 +31,13 @@ - URL: https://github.com/MithrilJS/mithril.js - License: [MIT License](portal/static/lib/mithril/LICENSE) ## base64.js - Files: `portal/static/lib/base64-*` - Author: Dan Kogai - URL: https://github.com/dankogai/js-base64 - License: [BSD 3-Clause License](portal/static/lib/base64-3.7.5/LICENSE.md) ## flaskr (Flask Tutorial App) Some files in this project are more or less modified versions of the files from the Flask Tutorial App `flaskr`, which Loading
alembic/versions/ec81173dbe48_add_ldap_directory.py 0 → 100644 +33 −0 Changes for alembic/versions/ec81173dbe48_add_ldap_directory.py: 33 added lines, 0 removed lines. Original line number Diff line number Diff line """Add ldap directory Revision ID: ec81173dbe48 Revises: 6c174457829c Create Date: 2024-06-06 23:50:54.368404 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa from portal.model import Uuid # revision identifiers, used by Alembic. revision: str = "ec81173dbe48" down_revision: Union[str, None] = "6c174457829c" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: op.create_table( "ldap_directory", sa.Column("url", sa.String(), nullable=False), sa.Column("base_dn", sa.String(), nullable=False), sa.Column("uuid", Uuid(), nullable=False), sa.PrimaryKeyConstraint("uuid"), ) def downgrade() -> None: op.drop_table("ldap_directory")
ldap/Directory.py 0 → 100644 +99 −0 Changes for ldap/Directory.py: 99 added lines, 0 removed lines. Original line number Diff line number Diff line import ssl from ldap3 import Tls, Server, Connection from ldap3.core.exceptions import LDAPBindError, LDAPPasswordIsMandatoryError from ldap3.utils.dn import escape_rdn def is_valid_server_url(url: str) -> bool: return url.startswith("ldaps://") or url.startswith("ldap://") TLS_CONFIGURATION = Tls(validate=ssl.CERT_REQUIRED) class Directory: """ A (user information) directory (= database), accessible via the Lightweight Directory Access Protocol (LDAP). Provides a few methods to do things you probably want to do with user information. For a detailed security consideration, please see: https://stackoverflow.com/questions/73681632/how-to-safely-authenticate-a-user-using-ldap/ Taken from an older (internal) project of mine, codex: https://gitlab.cl.uni-heidelberg.de/fachschaft/codex/-/blob/master/ldap/LdapDatabase.py?ref_type=heads """ def __init__(self, url: str, base_dn: str): """ Creates the LdapDatabase instance. Does not establish any connections whatsoever. :param url: A ldap server url (must use either ldaps:// or ldap:// scheme) :param base_dn: The distinguished name below which to look for users (e.g. "ou=employees,dc=example,dc=com") """ if not is_valid_server_url(url): raise ValueError("ldap_server_url must begin with ldaps:// or ldap://") self.ldap_server_url = url self.base_dn = base_dn # Note: The URL scheme (ldap:// or ldaps://) has precedence over the parameter "use_ssl" # (see https://ldap3.readthedocs.io/en/latest/server.html). As we always require a scheme to be present, this # parameter is superfluous. However, setting it to true means that if there ever is a case where no scheme is # specified (e.g. if someone ever deletes the scheme-enforcing code above), the connection will fall back to # use SSL, and I believe that is a good way for things to be. self.__server = Server( self.ldap_server_url, use_ssl=True, tls=TLS_CONFIGURATION ) def is_valid(self, username: str, password: str) -> bool: """ Return if the given username and password are valid credentials. Iff true is returned, the user that provided these credentials can be treated as successfully authenticated. :param username: An (untrusted!) username string :param password: An (untrusted!) password string :return: If username and password are valid """ user_dn = self._get_user_dn(username) try: with Connection(self.__server, user=user_dn, password=password): # This will implicitly perform a bind (as, by default, lazy=False). If it succeeds, the user was # authenticated against the LDAP server, meaning the credentials are valid. # Binding actually seems to be enough, see e.g. https://auth0.com/blog/using-ldap-with-c-sharp/ # (section "Validating user credentials using bind") and https://stackoverflow.com/q/28575359. return True except LDAPBindError: return False except LDAPPasswordIsMandatoryError: # There is a (I believe deprecated) bind method with a username, but without a password # (see https://stackoverflow.com/a/3515564/). This is not permissible by the library, which will throw # this exception. We then return False. return False def _get_user_dn(self, common_name: str) -> str: """ Get the distinguished name of a user, given only its common name (= username, in our case) :param common_name: An (untrusted!) username string :return: The distinguished name of the user (by interpreting "cn=<common_name>" as RDN below self.base_dn) """ # The common_name provided here is untrusted, meaning it can contain arbitrary (malicious) input. # Is this a problem? I don't know. # # When building a filter or search query, it definitely would be: # - https://cheatsheetseries.owasp.org/cheatsheets/LDAP_Injection_Prevention_Cheat_Sheet.html # - https://stackoverflow.com/q/47397341 # This is a problem because untrusted input is processed in a trusted context. # # However, when binding, one could argue that the untrusted input is also only processed in an untrusted # context - anyone could go and make a bind request with a malicious username directly to the ldap server. # So we can probably expect the LDAP server to deal with malicious usernames of any kind. # # Or can we? The documentation (https://ldap3.readthedocs.io/en/latest/connection.html) recommends using # escape_rdn() for the username, so we do this, better safe than sorry. return f"cn={escape_rdn(common_name)},{self.base_dn}"
ldap/__init__.py 0 → 100644 +6 −0 Changes for ldap/__init__.py: 6 added lines, 0 removed lines. Original line number Diff line number Diff line """ Tools to authenticate via LDAP. Has no ties whatsoever to the rest of the application's source code, should therefore be freely copyable and reusable in other projects. """ from .Directory import Directory
portal/api/agenda.py +3 −0 Changes for portal/api/agenda.py: 3 added lines, 0 removed lines. Original line number Diff line number Diff line from portal.model import TaigaUserStoryProvider from .blueprint import bp from .auth import require_valid from ..model import Key @bp.get("/agendas/-/items") @require_valid(Key) def the_agenda_items(): tasks = TaigaUserStoryProvider.get_the_one().fetch_user_stories() Loading