From d84d8935eaeb252da8f442f4679bedb45c514aa7 Mon Sep 17 00:00:00 2001 From: Isman Firmansyah Date: Tue, 5 Nov 2024 00:56:16 +0700 Subject: [PATCH 1/2] feat(cloud-native): add support for legacy and simple JSON data (#9936) * feat(cloud-native): add support for legacy and simple JSON data Signed-off-by: iromli --------- Signed-off-by: iromli Co-authored-by: Mohammad Abudayyeh <47318409+moabu@users.noreply.github.com> --- docker-jans-all-in-one/Dockerfile | 2 +- docker-jans-auth-server/Dockerfile | 4 +-- docker-jans-auth-server/scripts/bootstrap.py | 2 ++ docker-jans-auth-server/scripts/upgrade.py | 8 ++--- .../templates/jans-mysql.properties | 8 +++++ docker-jans-casa/Dockerfile | 4 +-- docker-jans-casa/scripts/bootstrap.py | 2 ++ docker-jans-casa/scripts/upgrade.py | 12 ++++---- .../templates/jans-mysql.properties | 8 +++++ docker-jans-certmanager/Dockerfile | 4 +-- docker-jans-config-api/Dockerfile | 16 ++++------ docker-jans-config-api/scripts/bootstrap.py | 2 ++ docker-jans-config-api/scripts/upgrade.py | 21 ++++++-------- .../templates/jans-mysql.properties | 8 +++++ docker-jans-configurator/Dockerfile | 4 +-- docker-jans-fido2/Dockerfile | 4 +-- docker-jans-fido2/scripts/bootstrap.py | 2 ++ .../templates/jans-mysql.properties | 8 +++++ docker-jans-kc-scheduler/Dockerfile | 4 +-- docker-jans-keycloak-link/Dockerfile | 4 +-- .../scripts/bootstrap.py | 2 ++ .../templates/jans-mysql.properties | 8 +++++ docker-jans-link/Dockerfile | 4 +-- docker-jans-link/scripts/bootstrap.py | 2 ++ .../templates/jans-mysql.properties | 8 +++++ docker-jans-monolith/Dockerfile | 2 +- docker-jans-persistence-loader/Dockerfile | 2 +- .../scripts/hooks.py | 7 ++++- .../scripts/sql_setup.py | 15 +++++----- .../scripts/upgrade.py | 29 +++++++++---------- docker-jans-saml/Dockerfile | 4 +-- docker-jans-saml/scripts/bootstrap.py | 2 ++ .../templates/jans-mysql.properties | 8 +++++ docker-jans-scim/Dockerfile | 4 +-- docker-jans-scim/scripts/bootstrap.py | 2 ++ docker-jans-scim/scripts/upgrade.py | 8 ++--- .../templates/jans-mysql.properties | 8 +++++ 37 files changed, 155 insertions(+), 87 deletions(-) diff --git a/docker-jans-all-in-one/Dockerfile b/docker-jans-all-in-one/Dockerfile index b078ace9aa8..389181380d6 100644 --- a/docker-jans-all-in-one/Dockerfile +++ b/docker-jans-all-in-one/Dockerfile @@ -58,7 +58,7 @@ RUN apk update \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=5f3b30e4b565601ccb31cd985920c09071cd1b54 +ENV JANS_SOURCE_VERSION=4f155cfe9e197b15d65be6aa938276862fe36a06 # note that as we're pulling from a monorepo (with multiple project in it) # we are using partial-clone and sparse-checkout to get the assets diff --git a/docker-jans-auth-server/Dockerfile b/docker-jans-auth-server/Dockerfile index 5b613cba629..3acb6ac4c01 100644 --- a/docker-jans-auth-server/Dockerfile +++ b/docker-jans-auth-server/Dockerfile @@ -51,7 +51,7 @@ RUN /opt/jython/bin/pip uninstall -y pip setuptools # =========== ENV CN_VERSION=1.1.6-SNAPSHOT -ENV CN_BUILD_DATE='2024-10-02 13:40' +ENV CN_BUILD_DATE='2024-10-29 17:28' ENV CN_SOURCE_URL=https://jenkins.jans.io/maven/io/jans/jans-auth-server/${CN_VERSION}/jans-auth-server-${CN_VERSION}.war @@ -103,7 +103,7 @@ RUN mkdir -p ${JETTY_BASE}/jans-auth/agama/fl \ /app/static/rdbm \ /app/schema -ENV JANS_SOURCE_VERSION=5f3b30e4b565601ccb31cd985920c09071cd1b54 +ENV JANS_SOURCE_VERSION=4f155cfe9e197b15d65be6aa938276862fe36a06 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup # note that as we're pulling from a monorepo (with multiple project in it) diff --git a/docker-jans-auth-server/scripts/bootstrap.py b/docker-jans-auth-server/scripts/bootstrap.py index 4301072b8ef..64be3586b4d 100644 --- a/docker-jans-auth-server/scripts/bootstrap.py +++ b/docker-jans-auth-server/scripts/bootstrap.py @@ -16,6 +16,7 @@ from jans.pycloudlib.persistence.spanner import sync_google_credentials from jans.pycloudlib.persistence.sql import render_sql_properties from jans.pycloudlib.persistence.sql import sync_sql_password +from jans.pycloudlib.persistence.sql import override_simple_json_property from jans.pycloudlib.persistence.utils import render_base_properties from jans.pycloudlib.persistence.utils import render_salt from jans.pycloudlib.persistence.utils import PersistenceMapper @@ -78,6 +79,7 @@ def main(): sync_google_credentials(manager) wait_for_persistence(manager) + override_simple_json_property("/etc/jans/conf/jans-sql.properties") if not os.path.isfile("/etc/certs/web_https.crt"): if as_boolean(os.environ.get("CN_SSL_CERT_FROM_SECRETS", "true")): diff --git a/docker-jans-auth-server/scripts/upgrade.py b/docker-jans-auth-server/scripts/upgrade.py index 3e2f52dcf88..ca55f337aed 100644 --- a/docker-jans-auth-server/scripts/upgrade.py +++ b/docker-jans-auth-server/scripts/upgrade.py @@ -324,7 +324,7 @@ def update_lock_client_scopes(self): if not entry: return - if self.backend.type == "sql" and self.backend.client.dialect == "mysql": + if not self.backend.client.use_simple_json: client_scopes = entry.attrs["jansScope"]["v"] else: client_scopes = entry.attrs.get("jansScope") or [] @@ -348,10 +348,8 @@ def update_lock_client_scopes(self): new_client_scopes += lock_scopes # find missing scopes from the client - diff = list(set(new_client_scopes).difference(client_scopes)) - - if diff: - if self.backend.type == "sql" and self.backend.client.dialect == "mysql": + if diff := list(set(new_client_scopes).difference(client_scopes)): + if not self.backend.client.use_simple_json: entry.attrs["jansScope"]["v"] = client_scopes + diff else: entry.attrs["jansScope"] = client_scopes + diff diff --git a/docker-jans-auth-server/templates/jans-mysql.properties b/docker-jans-auth-server/templates/jans-mysql.properties index ae85a0595f3..fce6cd47bb7 100644 --- a/docker-jans-auth-server/templates/jans-mysql.properties +++ b/docker-jans-auth-server/templates/jans-mysql.properties @@ -33,5 +33,13 @@ connection.pool.max-wait-time-millis=20000 # Allow to evict connection in pool after 30 minutes connection.pool.min-evictable-idle-time-millis=1800000 +# Sets whether objects created for the pool will be validated before being returned from it +#connection.pool.test-on-create=true + +# Sets whether objects borrowed from the pool will be validated when they are returned to the pool +#connection.pool.test-on-return=true + binaryAttributes=objectGUID certificateAttributes=userCertificate + +mysql.simple-json=true diff --git a/docker-jans-casa/Dockerfile b/docker-jans-casa/Dockerfile index 5c7cbf4bd67..a4c07802c00 100644 --- a/docker-jans-casa/Dockerfile +++ b/docker-jans-casa/Dockerfile @@ -30,7 +30,7 @@ RUN wget -q https://repo1.maven.org/maven2/org/eclipse/jetty/jetty-home/${JETTY_ # ==== ENV CN_VERSION=1.1.6-SNAPSHOT -ENV CN_BUILD_DATE='2024-10-02 09:31' +ENV CN_BUILD_DATE='2024-10-27 08:51' ENV CN_SOURCE_URL=https://jenkins.jans.io/maven/io/jans/casa/${CN_VERSION}/casa-${CN_VERSION}.war @@ -60,7 +60,7 @@ RUN mkdir -p /usr/share/java \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=5f3b30e4b565601ccb31cd985920c09071cd1b54 +ENV JANS_SOURCE_VERSION=4f155cfe9e197b15d65be6aa938276862fe36a06 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup # note that as we're pulling from a monorepo (with multiple project in it) diff --git a/docker-jans-casa/scripts/bootstrap.py b/docker-jans-casa/scripts/bootstrap.py index 26e6c965c41..b80a74c013d 100644 --- a/docker-jans-casa/scripts/bootstrap.py +++ b/docker-jans-casa/scripts/bootstrap.py @@ -23,6 +23,7 @@ from jans.pycloudlib.persistence.sql import render_sql_properties from jans.pycloudlib.persistence.sql import SqlClient from jans.pycloudlib.persistence.sql import sync_sql_password +from jans.pycloudlib.persistence.sql import override_simple_json_property from jans.pycloudlib.persistence.utils import PersistenceMapper from jans.pycloudlib.persistence.utils import render_base_properties from jans.pycloudlib.persistence.utils import render_salt @@ -159,6 +160,7 @@ def main(): sync_google_credentials(manager) wait_for_persistence(manager) + override_simple_json_property("/etc/jans/conf/jans-sql.properties") if not os.path.isfile("/etc/certs/web_https.crt"): if as_boolean(os.environ.get("CN_SSL_CERT_FROM_SECRETS", "true")): diff --git a/docker-jans-casa/scripts/upgrade.py b/docker-jans-casa/scripts/upgrade.py index 4f11a498683..4ba83dc84b1 100644 --- a/docker-jans-casa/scripts/upgrade.py +++ b/docker-jans-casa/scripts/upgrade.py @@ -172,7 +172,7 @@ def update_client_scopes(self): if not entry: return - if self.backend.type == "sql" and self.backend.client.dialect == "mysql": + if not self.backend.client.use_simple_json: client_scopes = entry.attrs["jansScope"]["v"] else: client_scopes = entry.attrs["jansScope"] @@ -188,10 +188,8 @@ def update_client_scopes(self): ] # find missing scopes from the client - diff = list(set(new_client_scopes).difference(client_scopes)) - - if diff: - if self.backend.type == "sql" and self.backend.client.dialect == "mysql": + if diff := list(set(new_client_scopes).difference(client_scopes)): + if not self.backend.client.use_simple_json: entry.attrs["jansScope"]["v"] = client_scopes + diff else: entry.attrs["jansScope"] = client_scopes + diff @@ -264,7 +262,7 @@ def update_client_uris(self): } for key, uri in uri_mapping.items(): - if self.backend.type == "sql" and self.backend.client.dialect == "mysql": + if not self.backend.client.use_simple_json: client_uris = entry.attrs[key]["v"] else: client_uris = entry.attrs[key] @@ -275,7 +273,7 @@ def update_client_uris(self): if uri not in client_uris: client_uris.append(uri) - if self.backend.type == "sql" and self.backend.client.dialect == "mysql": + if not self.backend.client.use_simple_json: entry.attrs[key]["v"] = client_uris else: entry.attrs[key] = client_uris diff --git a/docker-jans-casa/templates/jans-mysql.properties b/docker-jans-casa/templates/jans-mysql.properties index ae85a0595f3..fce6cd47bb7 100644 --- a/docker-jans-casa/templates/jans-mysql.properties +++ b/docker-jans-casa/templates/jans-mysql.properties @@ -33,5 +33,13 @@ connection.pool.max-wait-time-millis=20000 # Allow to evict connection in pool after 30 minutes connection.pool.min-evictable-idle-time-millis=1800000 +# Sets whether objects created for the pool will be validated before being returned from it +#connection.pool.test-on-create=true + +# Sets whether objects borrowed from the pool will be validated when they are returned to the pool +#connection.pool.test-on-return=true + binaryAttributes=objectGUID certificateAttributes=userCertificate + +mysql.simple-json=true diff --git a/docker-jans-certmanager/Dockerfile b/docker-jans-certmanager/Dockerfile index 5856622c756..29a450d0e06 100644 --- a/docker-jans-certmanager/Dockerfile +++ b/docker-jans-certmanager/Dockerfile @@ -15,7 +15,7 @@ RUN apk update \ # JAR files required to generate OpenID Connect keys ENV CN_VERSION=1.1.6-SNAPSHOT -ENV CN_BUILD_DATE='2024-10-02 13:34' +ENV CN_BUILD_DATE='2024-10-26 11:35' ENV CN_SOURCE_URL=https://jenkins.jans.io/maven/io/jans/jans-auth-client/${CN_VERSION}/jans-auth-client-${CN_VERSION}-jar-with-dependencies.jar @@ -25,7 +25,7 @@ RUN wget -q ${CN_SOURCE_URL} -P /app/javalibs/ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=5f3b30e4b565601ccb31cd985920c09071cd1b54 +ENV JANS_SOURCE_VERSION=4f155cfe9e197b15d65be6aa938276862fe36a06 # note that as we're pulling from a monorepo (with multiple project in it) # we are using partial-clone and sparse-checkout to get the assets diff --git a/docker-jans-config-api/Dockerfile b/docker-jans-config-api/Dockerfile index 348ba33d0d1..62e06e2535e 100644 --- a/docker-jans-config-api/Dockerfile +++ b/docker-jans-config-api/Dockerfile @@ -41,7 +41,7 @@ RUN wget -q https://maven.jans.io/maven/io/jans/jython-installer/${JYTHON_VERSIO # ========== ENV CN_VERSION=1.1.6-SNAPSHOT -ENV CN_BUILD_DATE='2024-10-02 09:23' +ENV CN_BUILD_DATE='2024-10-26 09:14' ENV CN_SOURCE_URL=https://jenkins.jans.io/maven/io/jans/jans-config-api-server/${CN_VERSION}/jans-config-api-server-${CN_VERSION}.war @@ -56,14 +56,6 @@ RUN mkdir -p ${JETTY_BASE}/jans-config-api/webapps \ && java -jar ${JETTY_HOME}/start.jar jetty.home=${JETTY_HOME} jetty.base=${JETTY_BASE}/jans-config-api --add-module=server,deploy,annotations,resources,http,http-forwarded,threadpool,jsp,websocket,cdi-decorate,jmx,stats,logging-log4j2 --approve-all-licenses \ && rm -rf /tmp/jans-config-api.war /tmp/WEB-INF -# ====== -# Facter -# ====== - -ARG PYFACTER_VERSION=9d8478ee47dc5498a766e010e8d3a3451b46e541 -RUN wget -q https://github.com/GluuFederation/gluu-snap/raw/${PYFACTER_VERSION}/facter/facter -O /usr/bin/facter \ - && chmod +x /usr/bin/facter - # ======= # Plugins # ======= @@ -78,7 +70,7 @@ RUN mkdir -p ${JETTY_BASE}/jans-config-api/_plugins \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=5f3b30e4b565601ccb31cd985920c09071cd1b54 +ENV JANS_SOURCE_VERSION=4f155cfe9e197b15d65be6aa938276862fe36a06 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup ARG JANS_CONFIG_API_RESOURCES=jans-config-api/server/src/main/resources @@ -115,7 +107,9 @@ RUN cd /tmp/jans \ && cp ${JANS_CONFIG_API_RESOURCES}/config-api-rs-protect.json /app/templates/jans-config-api/ \ && mkdir -p org/eclipse/jetty \ && cp ${JANS_SETUP_DIR}/static/favicon.ico org/eclipse/jetty/favicon.ico \ - && zip -r ${JETTY_HOME}/lib/jetty-server-${JETTY_VERSION}.jar org/eclipse/jetty/favicon.ico + && zip -r ${JETTY_HOME}/lib/jetty-server-${JETTY_VERSION}.jar org/eclipse/jetty/favicon.ico \ + && cp ${JANS_SETUP_DIR}/static/scripts/facter /usr/bin/facter \ + && chmod +x /usr/bin/facter # ====== # Python diff --git a/docker-jans-config-api/scripts/bootstrap.py b/docker-jans-config-api/scripts/bootstrap.py index d6e93442127..af98c51bada 100644 --- a/docker-jans-config-api/scripts/bootstrap.py +++ b/docker-jans-config-api/scripts/bootstrap.py @@ -25,6 +25,7 @@ from jans.pycloudlib.persistence.sql import SqlClient from jans.pycloudlib.persistence.sql import render_sql_properties from jans.pycloudlib.persistence.sql import sync_sql_password +from jans.pycloudlib.persistence.sql import override_simple_json_property from jans.pycloudlib.persistence.utils import PersistenceMapper from jans.pycloudlib.persistence.utils import render_base_properties from jans.pycloudlib.persistence.utils import render_salt @@ -89,6 +90,7 @@ def main(): sync_google_credentials(manager) wait_for_persistence(manager) + override_simple_json_property("/etc/jans/conf/jans-sql.properties") if not os.path.isfile("/etc/certs/web_https.crt"): if as_boolean(os.environ.get("CN_SSL_CERT_FROM_SECRETS", "true")): diff --git a/docker-jans-config-api/scripts/upgrade.py b/docker-jans-config-api/scripts/upgrade.py index 0355e1e195b..6093c7f3021 100644 --- a/docker-jans-config-api/scripts/upgrade.py +++ b/docker-jans-config-api/scripts/upgrade.py @@ -356,7 +356,7 @@ def update_client_redirect_uri(self): should_update = False hostname = self.manager.config.get("hostname") - if self.backend.type == "sql" and self.backend.client.dialect == "mysql": + if not self.backend.client.use_simple_json: if f"https://{hostname}/admin" not in entry.attrs["jansRedirectURI"]["v"]: entry.attrs["jansRedirectURI"]["v"].append(f"https://{hostname}/admin") should_update = True @@ -414,7 +414,7 @@ def update_client_scopes(self): if not entry: return - if self.backend.type == "sql" and self.backend.client.dialect == "mysql": + if not self.backend.client.use_simple_json: client_scopes = entry.attrs["jansScope"]["v"] else: client_scopes = entry.attrs["jansScope"] @@ -427,10 +427,8 @@ def update_client_scopes(self): new_client_scopes = [f"inum={inum},ou=scopes,o=jans" for inum in scope_mapping.keys()] # find missing scopes from the client - diff = list(set(new_client_scopes).difference(client_scopes)) - - if diff: - if self.backend.type == "sql" and self.backend.client.dialect == "mysql": + if diff := list(set(new_client_scopes).difference(client_scopes)): + if not self.backend.client.use_simple_json: entry.attrs["jansScope"]["v"] = client_scopes + diff else: entry.attrs["jansScope"] = client_scopes + diff @@ -454,7 +452,7 @@ def update_test_client_scopes(self): if not entry: return - if self.backend.type == "sql" and self.backend.client.dialect == "mysql": + if not self.backend.client.use_simple_json: client_scopes = entry.attrs["jansScope"]["v"] else: client_scopes = entry.attrs["jansScope"] @@ -480,9 +478,8 @@ def update_test_client_scopes(self): ] # find missing scopes from the client - diff = list(set(scopes).difference(client_scopes)) - if diff: - if self.backend.type == "sql" and self.backend.client.dialect == "mysql": + if diff := list(set(scopes).difference(client_scopes)): + if not self.backend.client.use_simple_json: entry.attrs["jansScope"]["v"] = client_scopes + diff else: entry.attrs["jansScope"] = client_scopes + diff @@ -498,7 +495,7 @@ def update_scope_creator_attrs(self): entries = self.backend.search_entries("", **kwargs) for entry in entries: - if self.backend.type == "sql" and self.backend.client.dialect == "mysql": + if not self.backend.client.use_simple_json: creator_attrs = (entry.attrs.get("creatorAttrs") or {}).get("v") or [] else: creator_attrs = entry.attrs.get("creatorAttrs") or [] @@ -520,7 +517,7 @@ def update_scope_creator_attrs(self): new_creator_attrs.append(attr) if new_creator_attrs != creator_attrs: - if self.backend.type == "sql" and self.backend.client.dialect == "mysql": + if not self.backend.client.use_simple_json: entry.attrs["creatorAttrs"]["v"] = new_creator_attrs else: entry.attrs["creatorAttrs"] = new_creator_attrs diff --git a/docker-jans-config-api/templates/jans-mysql.properties b/docker-jans-config-api/templates/jans-mysql.properties index ae85a0595f3..fce6cd47bb7 100644 --- a/docker-jans-config-api/templates/jans-mysql.properties +++ b/docker-jans-config-api/templates/jans-mysql.properties @@ -33,5 +33,13 @@ connection.pool.max-wait-time-millis=20000 # Allow to evict connection in pool after 30 minutes connection.pool.min-evictable-idle-time-millis=1800000 +# Sets whether objects created for the pool will be validated before being returned from it +#connection.pool.test-on-create=true + +# Sets whether objects borrowed from the pool will be validated when they are returned to the pool +#connection.pool.test-on-return=true + binaryAttributes=objectGUID certificateAttributes=userCertificate + +mysql.simple-json=true diff --git a/docker-jans-configurator/Dockerfile b/docker-jans-configurator/Dockerfile index b2b526fbf24..2eb5e1601b3 100644 --- a/docker-jans-configurator/Dockerfile +++ b/docker-jans-configurator/Dockerfile @@ -16,7 +16,7 @@ RUN apk update \ # JAR files required to generate OpenID Connect keys ENV CN_VERSION=1.1.6-SNAPSHOT -ENV CN_BUILD_DATE='2024-10-02 13:34' +ENV CN_BUILD_DATE='2024-10-26 11:35' ENV CN_SOURCE_URL=https://jenkins.jans.io/maven/io/jans/jans-auth-client/${CN_VERSION}/jans-auth-client-${CN_VERSION}-jar-with-dependencies.jar @@ -27,7 +27,7 @@ RUN mkdir -p /opt/jans/configurator/javalibs \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=5f3b30e4b565601ccb31cd985920c09071cd1b54 +ENV JANS_SOURCE_VERSION=4f155cfe9e197b15d65be6aa938276862fe36a06 RUN git clone --depth 500 --filter blob:none --no-checkout https://github.com/janssenproject/jans /tmp/jans \ && cd /tmp/jans \ diff --git a/docker-jans-fido2/Dockerfile b/docker-jans-fido2/Dockerfile index 0ad9916540c..dfaa52c8b02 100644 --- a/docker-jans-fido2/Dockerfile +++ b/docker-jans-fido2/Dockerfile @@ -42,7 +42,7 @@ RUN wget -q https://maven.jans.io/maven/io/jans/jython-installer/${JYTHON_VERSIO ENV CN_VERSION=1.1.6-SNAPSHOT -ENV CN_BUILD_DATE='2024-10-02 14:39' +ENV CN_BUILD_DATE='2024-10-27 08:58' ENV CN_SOURCE_URL=https://jenkins.jans.io/maven/io/jans/jans-fido2-server/${CN_VERSION}/jans-fido2-server-${CN_VERSION}.war @@ -61,7 +61,7 @@ RUN mkdir -p ${JETTY_BASE}/jans-fido2/webapps \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=5f3b30e4b565601ccb31cd985920c09071cd1b54 +ENV JANS_SOURCE_VERSION=4f155cfe9e197b15d65be6aa938276862fe36a06 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup # note that as we're pulling from a monorepo (with multiple project in it) diff --git a/docker-jans-fido2/scripts/bootstrap.py b/docker-jans-fido2/scripts/bootstrap.py index 2760f3c2d14..3cfbe02a500 100644 --- a/docker-jans-fido2/scripts/bootstrap.py +++ b/docker-jans-fido2/scripts/bootstrap.py @@ -19,6 +19,7 @@ from jans.pycloudlib.persistence.sql import SqlClient from jans.pycloudlib.persistence.sql import render_sql_properties from jans.pycloudlib.persistence.sql import sync_sql_password +from jans.pycloudlib.persistence.sql import override_simple_json_property from jans.pycloudlib.persistence.utils import PersistenceMapper from jans.pycloudlib.persistence.utils import render_base_properties from jans.pycloudlib.persistence.utils import render_salt @@ -79,6 +80,7 @@ def main(): sync_google_credentials(manager) wait_for_persistence(manager) + override_simple_json_property("/etc/jans/conf/jans-sql.properties") if not os.path.isfile("/etc/certs/web_https.crt"): if as_boolean(os.environ.get("CN_SSL_CERT_FROM_SECRETS", "true")): diff --git a/docker-jans-fido2/templates/jans-mysql.properties b/docker-jans-fido2/templates/jans-mysql.properties index ae85a0595f3..fce6cd47bb7 100644 --- a/docker-jans-fido2/templates/jans-mysql.properties +++ b/docker-jans-fido2/templates/jans-mysql.properties @@ -33,5 +33,13 @@ connection.pool.max-wait-time-millis=20000 # Allow to evict connection in pool after 30 minutes connection.pool.min-evictable-idle-time-millis=1800000 +# Sets whether objects created for the pool will be validated before being returned from it +#connection.pool.test-on-create=true + +# Sets whether objects borrowed from the pool will be validated when they are returned to the pool +#connection.pool.test-on-return=true + binaryAttributes=objectGUID certificateAttributes=userCertificate + +mysql.simple-json=true diff --git a/docker-jans-kc-scheduler/Dockerfile b/docker-jans-kc-scheduler/Dockerfile index adb92c51098..1e57603e62f 100644 --- a/docker-jans-kc-scheduler/Dockerfile +++ b/docker-jans-kc-scheduler/Dockerfile @@ -14,7 +14,7 @@ RUN apk update \ # ============ ENV CN_VERSION=1.1.6-SNAPSHOT -ENV CN_BUILD_DATE='2024-10-16 20:17' +ENV CN_BUILD_DATE='2024-10-27 08:54' ENV SCHEDULER_HOME=/opt/kc-scheduler RUN mkdir -p ${SCHEDULER_HOME}/bin \ @@ -38,7 +38,7 @@ RUN wget -q https://repo1.maven.org/maven2/org/codehaus/janino/janino/3.1.9/jani # Assets sync # =========== -ENV JANS_SOURCE_VERSION=5f3b30e4b565601ccb31cd985920c09071cd1b54 +ENV JANS_SOURCE_VERSION=4f155cfe9e197b15d65be6aa938276862fe36a06 # note that as we're pulling from a monorepo (with multiple project in it) # we are using partial-clone and sparse-checkout to get the assets diff --git a/docker-jans-keycloak-link/Dockerfile b/docker-jans-keycloak-link/Dockerfile index 65781ebe4cf..2c6ff9d702b 100644 --- a/docker-jans-keycloak-link/Dockerfile +++ b/docker-jans-keycloak-link/Dockerfile @@ -42,7 +42,7 @@ RUN wget -q https://maven.jans.io/maven/io/jans/jython-installer/${JYTHON_VERSIO ENV CN_VERSION=1.1.6-SNAPSHOT -ENV CN_BUILD_DATE='2024-10-02 09:20' +ENV CN_BUILD_DATE='2024-10-27 08:02' ENV CN_SOURCE_URL=https://jenkins.jans.io/maven/io/jans/jans-keycloak-link-server/${CN_VERSION}/jans-keycloak-link-server-${CN_VERSION}.war @@ -61,7 +61,7 @@ RUN mkdir -p ${JETTY_BASE}/jans-keycloak-link/webapps \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=5f3b30e4b565601ccb31cd985920c09071cd1b54 +ENV JANS_SOURCE_VERSION=4f155cfe9e197b15d65be6aa938276862fe36a06 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup # note that as we're pulling from a monorepo (with multiple project in it) diff --git a/docker-jans-keycloak-link/scripts/bootstrap.py b/docker-jans-keycloak-link/scripts/bootstrap.py index c0fe359a54b..22f627b250d 100644 --- a/docker-jans-keycloak-link/scripts/bootstrap.py +++ b/docker-jans-keycloak-link/scripts/bootstrap.py @@ -21,6 +21,7 @@ from jans.pycloudlib.persistence.sql import SqlClient from jans.pycloudlib.persistence.sql import render_sql_properties from jans.pycloudlib.persistence.sql import sync_sql_password +from jans.pycloudlib.persistence.sql import override_simple_json_property from jans.pycloudlib.persistence.utils import PersistenceMapper from jans.pycloudlib.persistence.utils import render_base_properties from jans.pycloudlib.persistence.utils import render_salt @@ -95,6 +96,7 @@ def main(): get_server_certificate(hostname, 443, "/etc/certs/web_https.crt") wait_for_persistence(manager) + override_simple_json_property("/etc/jans/conf/jans-sql.properties") cert_to_truststore( "web_https", diff --git a/docker-jans-keycloak-link/templates/jans-mysql.properties b/docker-jans-keycloak-link/templates/jans-mysql.properties index ae85a0595f3..fce6cd47bb7 100644 --- a/docker-jans-keycloak-link/templates/jans-mysql.properties +++ b/docker-jans-keycloak-link/templates/jans-mysql.properties @@ -33,5 +33,13 @@ connection.pool.max-wait-time-millis=20000 # Allow to evict connection in pool after 30 minutes connection.pool.min-evictable-idle-time-millis=1800000 +# Sets whether objects created for the pool will be validated before being returned from it +#connection.pool.test-on-create=true + +# Sets whether objects borrowed from the pool will be validated when they are returned to the pool +#connection.pool.test-on-return=true + binaryAttributes=objectGUID certificateAttributes=userCertificate + +mysql.simple-json=true diff --git a/docker-jans-link/Dockerfile b/docker-jans-link/Dockerfile index bf5cc30f197..ac030f30e4e 100644 --- a/docker-jans-link/Dockerfile +++ b/docker-jans-link/Dockerfile @@ -42,7 +42,7 @@ RUN wget -q https://maven.jans.io/maven/io/jans/jython-installer/${JYTHON_VERSIO ENV CN_VERSION=1.1.6-SNAPSHOT -ENV CN_BUILD_DATE='2024-10-02 09:13' +ENV CN_BUILD_DATE='2024-10-27 08:05' ENV CN_SOURCE_URL=https://jenkins.jans.io/maven/io/jans/jans-link-server/${CN_VERSION}/jans-link-server-${CN_VERSION}.war @@ -61,7 +61,7 @@ RUN mkdir -p ${JETTY_BASE}/jans-link/webapps \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=5f3b30e4b565601ccb31cd985920c09071cd1b54 +ENV JANS_SOURCE_VERSION=4f155cfe9e197b15d65be6aa938276862fe36a06 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup # note that as we're pulling from a monorepo (with multiple project in it) diff --git a/docker-jans-link/scripts/bootstrap.py b/docker-jans-link/scripts/bootstrap.py index bc2b30f9d74..252adf795ce 100644 --- a/docker-jans-link/scripts/bootstrap.py +++ b/docker-jans-link/scripts/bootstrap.py @@ -21,6 +21,7 @@ from jans.pycloudlib.persistence.sql import render_sql_properties from jans.pycloudlib.persistence.sql import SqlClient from jans.pycloudlib.persistence.sql import sync_sql_password +from jans.pycloudlib.persistence.sql import override_simple_json_property from jans.pycloudlib.persistence.utils import PersistenceMapper from jans.pycloudlib.persistence.utils import render_base_properties from jans.pycloudlib.persistence.utils import render_salt @@ -88,6 +89,7 @@ def main(): sync_google_credentials(manager) wait_for_persistence(manager) + override_simple_json_property("/etc/jans/conf/jans-sql.properties") if not os.path.isfile("/etc/certs/web_https.crt"): if as_boolean(os.environ.get("CN_SSL_CERT_FROM_SECRETS", "true")): diff --git a/docker-jans-link/templates/jans-mysql.properties b/docker-jans-link/templates/jans-mysql.properties index ae85a0595f3..fce6cd47bb7 100644 --- a/docker-jans-link/templates/jans-mysql.properties +++ b/docker-jans-link/templates/jans-mysql.properties @@ -33,5 +33,13 @@ connection.pool.max-wait-time-millis=20000 # Allow to evict connection in pool after 30 minutes connection.pool.min-evictable-idle-time-millis=1800000 +# Sets whether objects created for the pool will be validated before being returned from it +#connection.pool.test-on-create=true + +# Sets whether objects borrowed from the pool will be validated when they are returned to the pool +#connection.pool.test-on-return=true + binaryAttributes=objectGUID certificateAttributes=userCertificate + +mysql.simple-json=true diff --git a/docker-jans-monolith/Dockerfile b/docker-jans-monolith/Dockerfile index f4ff4f55513..ea6f73b3137 100644 --- a/docker-jans-monolith/Dockerfile +++ b/docker-jans-monolith/Dockerfile @@ -45,7 +45,7 @@ EXPOSE 443 8080 1636 # jans-linux-setup # ===================== -ENV JANS_SOURCE_VERSION=5f3b30e4b565601ccb31cd985920c09071cd1b54 +ENV JANS_SOURCE_VERSION=4f155cfe9e197b15d65be6aa938276862fe36a06 # cleanup RUN rm -rf /tmp/jans diff --git a/docker-jans-persistence-loader/Dockerfile b/docker-jans-persistence-loader/Dockerfile index d3d7c1a293d..9f0239b588f 100644 --- a/docker-jans-persistence-loader/Dockerfile +++ b/docker-jans-persistence-loader/Dockerfile @@ -16,7 +16,7 @@ RUN apk update \ # =========== # janssenproject/jans SHA commit -ENV JANS_SOURCE_VERSION=5f3b30e4b565601ccb31cd985920c09071cd1b54 +ENV JANS_SOURCE_VERSION=4f155cfe9e197b15d65be6aa938276862fe36a06 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup ARG JANS_SCRIPT_CATALOG_DIR=docs/script-catalog ARG JANS_CONFIG_API_RESOURCES=jans-config-api/server/src/main/resources diff --git a/docker-jans-persistence-loader/scripts/hooks.py b/docker-jans-persistence-loader/scripts/hooks.py index 29d478a5d3e..8f2a0a60d77 100644 --- a/docker-jans-persistence-loader/scripts/hooks.py +++ b/docker-jans-persistence-loader/scripts/hooks.py @@ -20,6 +20,7 @@ def merge_auth_keystore_ctx_hook(manager, ctx: dict[str, _t.Any]) -> dict[str, _ def transform_auth_dynamic_config_hook(conf, manager): should_update = False hostname = manager.config.get("hostname") + auth_challenge_endpoint = f"https://{hostname}/jans-auth/restv1/authorize-challenge" # add missing top-level keys for missing_key, value in [ @@ -66,7 +67,7 @@ def transform_auth_dynamic_config_hook(conf, manager): "Cache-Control": "max-age=0, no-store", }, }), - ("authorizationChallengeEndpoint", f"https://{hostname}/jans-auth/restv1/authorization_challenge"), + ("authorizationChallengeEndpoint", auth_challenge_endpoint), ("archivedJwksUri", f"https://{hostname}/jans-auth/restv1/jwks/archived"), ("featureFlags", []), ("lockMessageConfig", { @@ -301,6 +302,10 @@ def transform_auth_dynamic_config_hook(conf, manager): conf[attr].remove("tx_token") should_update = True + if conf["authorizationChallengeEndpoint"] != auth_challenge_endpoint: + conf["authorizationChallengeEndpoint"] = auth_challenge_endpoint + should_update = True + # return the conf and flag to determine whether it needs update or not return conf, should_update diff --git a/docker-jans-persistence-loader/scripts/sql_setup.py b/docker-jans-persistence-loader/scripts/sql_setup.py index e6ac88ee665..8351ca711e7 100644 --- a/docker-jans-persistence-loader/scripts/sql_setup.py +++ b/docker-jans-persistence-loader/scripts/sql_setup.py @@ -117,14 +117,14 @@ def create_mysql_indexes(self, table_name: str, column_mapping: dict): index_name = f"{table_name}_{FIELD_RE.sub('_', column_name)}" if column_type == "TEXT": - # set key length to 768 to accomodate charset utf8mb4 - query = f"CREATE INDEX {self.client.quoted_id(index_name)} ON {self.client.quoted_id(table_name)} ({self.client.quoted_id(column_name)} (768))" + # set key length to 255 + query = f"CREATE INDEX {self.client.quoted_id(index_name)} ON {self.client.quoted_id(table_name)} ({self.client.quoted_id(column_name)} (255))" self.client.create_index(query) elif column_type.lower() != "json": query = f"CREATE INDEX {self.client.quoted_id(index_name)} ON {self.client.quoted_id(table_name)} ({self.client.quoted_id(column_name)})" self.client.create_index(query) else: - if self.client.server_version < "8.0": + if self.client.get_server_version() < (8, 0): # prior to MySQL 8.0, CASTing on index creation will raise SQL syntax error; # switch to virtual column instead for i in range(4): @@ -152,7 +152,6 @@ def create_mysql_indexes(self, table_name: str, column_mapping: dict): for i, custom in enumerate(self.sql_indexes.get(table_name, {}).get("custom", []), start=1): # jansPerson table has unsupported custom index expressions that need to be skipped if mysql < 8.0 - # if table_name == "jansPerson" and self.client.server_version < "8.0": if table_name == "jansPerson" and self.client.get_server_version() < (8, 0): continue name = f"{table_name}_CustomIdx{i}" @@ -277,7 +276,7 @@ def column_to_multivalued(table_name, col_name): else: new_value = [value] - if self.client.dialect == "mysql": + if not self.client.use_simple_json: new_value = {"v": new_value} self.client.update(table_name, doc_id, {col_name: new_value}) @@ -363,9 +362,11 @@ def column_from_multivalued(table_name, col_name): # pre-populate the modified column for doc_id, value in values.items(): - if self.client.dialect == "mysql" and value and value.get("v", []): + simple_json = self.client.use_simple_json + + if not simple_json and value and value.get("v", []): new_value = value["v"][0] - elif self.client.dialect == "pgsql" and value: + elif simple_json and value: new_value = value[0] else: new_value = "" diff --git a/docker-jans-persistence-loader/scripts/upgrade.py b/docker-jans-persistence-loader/scripts/upgrade.py index 6d95455c8ca..eaae19c7466 100644 --- a/docker-jans-persistence-loader/scripts/upgrade.py +++ b/docker-jans-persistence-loader/scripts/upgrade.py @@ -313,7 +313,7 @@ def update_scripts_entries(self): agama_entry = self.backend.get_entry(agama_id, **kwargs) if agama_entry: - if self.backend.type == "sql" and self.backend.client.dialect == "mysql": + if not self.backend.client.use_simple_json: props = agama_entry.attrs["jansConfProperty"]["v"] else: props = agama_entry.attrs["jansConfProperty"] @@ -332,7 +332,7 @@ def update_scripts_entries(self): if new_props != props: new_props = [json.dumps(prop) for prop in new_props] - if self.backend.type == "sql" and self.backend.client.dialect == "mysql": + if not self.backend.client.use_simple_json: agama_entry.attrs["jansConfProperty"]["v"] = new_props else: agama_entry.attrs["jansConfProperty"] = new_props @@ -483,19 +483,16 @@ def update_people_entries(self): ("jansAdminUIRole", "api-admin"), ("role", "CasaAdmin"), ]: - if self.user_backend.type == "sql" and self.user_backend.client.dialect == "mysql" and not entry.attrs[attr_name]["v"]: - entry.attrs[attr_name] = {"v": [role_name]} - should_update = True - if self.user_backend.type == "sql" and self.user_backend.client.dialect == "pgsql" and not entry.attrs[attr_name]: - entry.attrs[attr_name] = [role_name] - should_update = True - elif self.user_backend.type == "spanner" and not entry.attrs[attr_name]: - entry.attrs[attr_name] = [role_name] + if not self.user_backend.client.use_simple_json: + old_roles = entry.attrs.get(attr_name, {}).get("v", []) + new_roles = {"v": [role_name]} + else: + old_roles = entry.attrs.get(attr_name, []) + new_roles = [role_name] + + if not old_roles: + entry.attrs[attr_name] = roles should_update = True - else: # couchbase - if attr_name not in entry.attrs: - entry.attrs[attr_name] = [role_name] - should_update = True # set lowercased jansStatus if entry.attrs["jansStatus"] == "ACTIVE": @@ -669,7 +666,7 @@ def update_tui_client(self): # add SSA scope inum=B9D2-D6E5,ou=scopes,o=jans to tui client ssa_scope = "inum=B9D2-D6E5,ou=scopes,o=jans" - if isinstance(entry.attrs["jansScope"], dict): # likely mysql + if not self.backend.client.use_simple_json: if ssa_scope not in entry.attrs["jansScope"]["v"]: entry.attrs["jansScope"]["v"].append(ssa_scope) should_update = True @@ -732,7 +729,7 @@ def update_config(self): # set jansSmtpConf if still empty smtp_conf = entry.attrs.get("jansSmtpConf") - if isinstance(smtp_conf, dict): # likely mysql + if not self.backend.client.use_simple_json: if not smtp_conf["v"]: entry.attrs["jansSmtpConf"]["v"].append(json.dumps(default_smtp_conf)) should_update = True diff --git a/docker-jans-saml/Dockerfile b/docker-jans-saml/Dockerfile index 7975edcedee..e3a97447d47 100644 --- a/docker-jans-saml/Dockerfile +++ b/docker-jans-saml/Dockerfile @@ -24,7 +24,7 @@ RUN mkdir -p /opt/keycloak/logs \ # ============== ENV CN_VERSION=1.1.6-SNAPSHOT -ENV CN_BUILD_DATE='2024-10-16 20:18' +ENV CN_BUILD_DATE='2024-10-27 08:54' RUN wget -q https://jenkins.jans.io/maven/io/jans/kc-jans-spi/${CN_VERSION}/kc-jans-spi-${CN_VERSION}.jar -P /opt/keycloak/providers \ && wget -q https://jenkins.jans.io/maven/io/jans/kc-jans-spi/${CN_VERSION}/kc-jans-spi-${CN_VERSION}-deps.zip -O /tmp/kc-jans-spi.zip \ @@ -35,7 +35,7 @@ RUN wget -q https://jenkins.jans.io/maven/io/jans/kc-jans-spi/${CN_VERSION}/kc-j # Assets sync # =========== -ENV JANS_SOURCE_VERSION=5f3b30e4b565601ccb31cd985920c09071cd1b54 +ENV JANS_SOURCE_VERSION=4f155cfe9e197b15d65be6aa938276862fe36a06 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup # note that as we're pulling from a monorepo (with multiple project in it) diff --git a/docker-jans-saml/scripts/bootstrap.py b/docker-jans-saml/scripts/bootstrap.py index 8dc7d9e0b0f..3d196539388 100644 --- a/docker-jans-saml/scripts/bootstrap.py +++ b/docker-jans-saml/scripts/bootstrap.py @@ -27,6 +27,7 @@ from jans.pycloudlib.persistence.sql import SqlClient from jans.pycloudlib.persistence.sql import render_sql_properties from jans.pycloudlib.persistence.sql import sync_sql_password +from jans.pycloudlib.persistence.sql import override_simple_json_property from jans.pycloudlib.persistence.utils import PersistenceMapper from jans.pycloudlib.persistence.utils import render_base_properties from jans.pycloudlib.persistence.utils import render_salt @@ -118,6 +119,7 @@ def main(): sync_google_credentials(manager) wait_for_persistence(manager) + override_simple_json_property("/etc/jans/conf/jans-sql.properties") shutil.copyfile( "/app/templates/jans-saml/quarkus.properties", diff --git a/docker-jans-saml/templates/jans-mysql.properties b/docker-jans-saml/templates/jans-mysql.properties index ae85a0595f3..fce6cd47bb7 100644 --- a/docker-jans-saml/templates/jans-mysql.properties +++ b/docker-jans-saml/templates/jans-mysql.properties @@ -33,5 +33,13 @@ connection.pool.max-wait-time-millis=20000 # Allow to evict connection in pool after 30 minutes connection.pool.min-evictable-idle-time-millis=1800000 +# Sets whether objects created for the pool will be validated before being returned from it +#connection.pool.test-on-create=true + +# Sets whether objects borrowed from the pool will be validated when they are returned to the pool +#connection.pool.test-on-return=true + binaryAttributes=objectGUID certificateAttributes=userCertificate + +mysql.simple-json=true diff --git a/docker-jans-scim/Dockerfile b/docker-jans-scim/Dockerfile index 3f52b7feaba..bbd2161a358 100644 --- a/docker-jans-scim/Dockerfile +++ b/docker-jans-scim/Dockerfile @@ -41,7 +41,7 @@ RUN wget -q https://maven.jans.io/maven/io/jans/jython-installer/${JYTHON_VERSIO # ==== ENV CN_VERSION=1.1.6-SNAPSHOT -ENV CN_BUILD_DATE='2024-10-02 14:33' +ENV CN_BUILD_DATE='2024-10-27 08:48' ENV CN_SOURCE_URL=https://jenkins.jans.io/maven/io/jans/jans-scim-server/${CN_VERSION}/jans-scim-server-${CN_VERSION}.war @@ -60,7 +60,7 @@ RUN mkdir -p ${JETTY_BASE}/jans-scim/webapps \ # Assets sync # =========== -ENV JANS_SOURCE_VERSION=5f3b30e4b565601ccb31cd985920c09071cd1b54 +ENV JANS_SOURCE_VERSION=4f155cfe9e197b15d65be6aa938276862fe36a06 ARG JANS_SETUP_DIR=jans-linux-setup/jans_setup ARG JANS_SCIM_RESOURCE_DIR=jans-scim/server/src/main/resources diff --git a/docker-jans-scim/scripts/bootstrap.py b/docker-jans-scim/scripts/bootstrap.py index 592a3cf5fae..1b6538006ca 100644 --- a/docker-jans-scim/scripts/bootstrap.py +++ b/docker-jans-scim/scripts/bootstrap.py @@ -24,6 +24,7 @@ from jans.pycloudlib.persistence.sql import render_sql_properties from jans.pycloudlib.persistence.sql import SqlClient from jans.pycloudlib.persistence.sql import sync_sql_password +from jans.pycloudlib.persistence.sql import override_simple_json_property from jans.pycloudlib.persistence.utils import PersistenceMapper from jans.pycloudlib.persistence.utils import render_base_properties from jans.pycloudlib.persistence.utils import render_salt @@ -93,6 +94,7 @@ def main(): sync_google_credentials(manager) wait_for_persistence(manager) + override_simple_json_property("/etc/jans/conf/jans-sql.properties") if not os.path.isfile("/etc/certs/web_https.crt"): if as_boolean(os.environ.get("CN_SSL_CERT_FROM_SECRETS", "true")): diff --git a/docker-jans-scim/scripts/upgrade.py b/docker-jans-scim/scripts/upgrade.py index ab9dc124e8e..3a429168efa 100644 --- a/docker-jans-scim/scripts/upgrade.py +++ b/docker-jans-scim/scripts/upgrade.py @@ -194,7 +194,7 @@ def update_client_scopes(self): if not entry: return - if self.backend.type == "sql" and self.backend.client.dialect == "mysql": + if not self.backend.client.use_simple_json: client_scopes = entry.attrs["jansScope"]["v"] else: client_scopes = entry.attrs.get("jansScope") or [] @@ -218,10 +218,8 @@ def update_client_scopes(self): new_client_scopes += scim_scopes # find missing scopes from the client - diff = list(set(new_client_scopes).difference(client_scopes)) - - if diff: - if self.backend.type == "sql" and self.backend.client.dialect == "mysql": + if diff := list(set(new_client_scopes).difference(client_scopes)): + if not self.backend.client.use_simple_json: entry.attrs["jansScope"]["v"] = client_scopes + diff else: entry.attrs["jansScope"] = client_scopes + diff diff --git a/docker-jans-scim/templates/jans-mysql.properties b/docker-jans-scim/templates/jans-mysql.properties index ae85a0595f3..fce6cd47bb7 100644 --- a/docker-jans-scim/templates/jans-mysql.properties +++ b/docker-jans-scim/templates/jans-mysql.properties @@ -33,5 +33,13 @@ connection.pool.max-wait-time-millis=20000 # Allow to evict connection in pool after 30 minutes connection.pool.min-evictable-idle-time-millis=1800000 +# Sets whether objects created for the pool will be validated before being returned from it +#connection.pool.test-on-create=true + +# Sets whether objects borrowed from the pool will be validated when they are returned to the pool +#connection.pool.test-on-return=true + binaryAttributes=objectGUID certificateAttributes=userCertificate + +mysql.simple-json=true From 8e3cc21b5f9453c83717a817d3862eba8ed28f19 Mon Sep 17 00:00:00 2001 From: Oleh Date: Mon, 4 Nov 2024 23:16:43 +0200 Subject: [PATCH 2/2] =?UTF-8?q?feat(jans-cedarling):=20add=20build=C2=A0`R?= =?UTF-8?q?ole`=C2=A0entity=20(#10016)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(jans-cedarling): remove #[allow(unused)] in key_service Signed-off-by: Oleh Bohzok * chore(jans-cedarling): refactor initialization of KeyService to be more readable Signed-off-by: Oleh Bohzok * chore(jans-cedarling): add getting trusted issuer when decode JWT tokens Signed-off-by: Oleh Bohzok * chore(jans-cedarling): remove transaction token Signed-off-by: Oleh Bohzok * test(jans-cedarling): fix test case after deleting `transaction_token` Signed-off-by: Oleh Bohzok * feat(jans-cedarling): add entity Jans::Role to entity store Signed-off-by: Oleh Bohzok * test(jans-cedarling): fix unit tests and add some refactor, extract function `build_entity_attributes` Signed-off-by: Oleh Bohzok * chore(jans-cedarling): fix copy-paste error. Signed-off-by: Oleh Bohzok * feat(jans-cedarling): add to authorize check `execute_authorize` with principal `Jans::Role` Signed-off-by: Oleh Bohzok * test(jans-cedarling): fix python unit tests Signed-off-by: Oleh Bohzok * feat(jans-cedarling): add to python bindings `AuthorizeResult` field person and role Signed-off-by: Oleh Bohzok * feat(jans-cedarling): add parsing `Jans::Role` only if field present in JWT token Signed-off-by: Oleh Bohzok * chore(jans-cedarling): change default search Role to the Userinfo token Signed-off-by: Oleh Bohzok * chore(jans-cedarling): add #[allow(dead_code)] in test case Signed-off-by: Oleh Bohzok * chore(jans-cedarling): add parse yaml using config Signed-off-by: Oleh Bohzok * test(jans-cedarling): refactor current unit test `success_test_json` to be more readable Signed-off-by: Oleh Bohzok * test(jans-cedarling): move `success_test_json` to own file Signed-off-by: Oleh Bohzok * chore(jans-cedarling): add config yaml file for testing `policy-store_ok_2.yaml` Signed-off-by: Oleh Bohzok * test(jans-cedarling): add test case on check authorization request, positive and negative Signed-off-by: Oleh Bohzok * chore(jans-cedarling): fix python binding result of authorize for role Signed-off-by: Oleh Bohzok * docs(jans-cedarling): add update to documentation related to adding role check on authorization request Signed-off-by: Oleh Bohzok * test(jans-cedarling): fix python tests Signed-off-by: Oleh Bohzok * chore(jans-cedarling): fix copy-paste comment about YAML usage also added text `Mostly used only for testing purposes.` Signed-off-by: Oleh Bohzok * chore(jans-cedarling): refactor code to be more readable, add match statement in function `create_role_entities` Signed-off-by: Oleh Bohzok * chore(jans-cedarling): remove cloning the entity_uid in function `create_entity` Signed-off-by: Oleh Bohzok * chore(jans-cedarling): fix the markdown file using linter Signed-off-by: Oleh Bohzok * docs(jans-cedarling): add information about minimum supported `cedar-policy schema` Signed-off-by: Oleh Bohzok * chore(jans-cedarling): update pyo3 to latest Signed-off-by: Oleh Bohzok * chore(jans-cedarling): remove outdated comment Signed-off-by: Oleh Bohzok --------- Signed-off-by: Oleh Bohzok --- docs/cedarling/cedarling-authz.md | 4 +- docs/cedarling/cedarling-policy-store.md | 61 +- .../bindings/cedarling_python/Cargo.toml | 2 +- .../cedarling_python/cedarling_python.pyi | 4 + .../bindings/cedarling_python/example.py | 69 +- .../example_files/policy-store.json | 2 +- .../policy-store_human_readable/cedar.schema | 5 +- .../src/authorize/authorize_result.rs | 10 + .../cedarling_python/src/authorize/errors.rs | 16 + .../cedarling_python/tests/test_authorize.py | 27 +- .../tests/test_policy_store.py | 5 +- jans-cedarling/cedarling/Cargo.toml | 9 +- .../authorize_without_jwt_validation.rs | 2 +- .../cedarling/src/authz/authorize_result.rs | 25 +- .../cedarling/src/authz/entities/create.rs | 132 ++- .../cedarling/src/authz/entities/mod.rs | 131 ++- .../src/authz/entities/test_create.rs | 22 +- jans-cedarling/cedarling/src/authz/mod.rs | 131 ++- .../cedarling/src/authz/token_data.rs | 5 - .../bootstrap_config/policy_store_config.rs | 5 + .../cedarling/src/common/cedar_schema.rs | 128 ++- .../src/common/cedar_schema/cedar_json.rs | 131 +-- .../test_files/test_data_cedar.json | 3 +- .../test_files/test_data_cedar.schema | 1 + .../cedarling/src/common/policy_store.rs | 48 +- .../cedarling/src/common/policy_store/test.rs | 12 +- .../cedarling/src/init/policy_store.rs | 11 +- .../cedarling/src/init/service_config.rs | 23 +- .../cedarling/src/init/service_factory.rs | 6 +- .../cedarling/src/jwt/decoding_strategy.rs | 1 + .../src/jwt/decoding_strategy/key_service.rs | 90 +- .../key_service/openid_config.rs | 31 +- .../jwt/decoding_strategy/open_id_storage.rs | 32 + .../cedarling/src/jwt/jwt_service_config.rs | 33 +- jans-cedarling/cedarling/src/jwt/mod.rs | 69 +- .../cedarling/src/jwt/test/with_validation.rs | 50 +- .../jwt/test/with_validation/access_token.rs | 89 +- .../src/jwt/test/with_validation/id_token.rs | 137 ++- .../jwt/test/with_validation/key_service.rs | 60 +- .../test/with_validation/userinfo_token.rs | 163 ++- .../src/jwt/test/without_validation.rs | 15 +- jans-cedarling/cedarling/src/log/log_entry.rs | 9 + .../cedarling/src/log/memory_logger.rs | 5 + .../cases_authorize_without_check_jwt.rs | 953 ++++++++++++++++++ jans-cedarling/cedarling/src/tests/mod.rs | 135 +-- .../cedarling/src/tests/success_test_json.rs | 111 ++ jans-cedarling/cedarling/src/tests/utils.rs | 95 ++ .../test_files/policy-store_blobby.json | 14 +- .../test_files/policy-store_ok.json | 8 +- .../test_files/policy-store_ok/cedar.schema | 5 +- .../test_files/policy-store_ok/schema.json | 182 +++- .../test_files/policy-store_ok_2.yaml | 62 ++ .../test_files/policy-store_readable.json | 14 +- .../test_files/policy-store_readable.yaml | 4 +- 54 files changed, 2755 insertions(+), 642 deletions(-) create mode 100644 jans-cedarling/cedarling/src/jwt/decoding_strategy/open_id_storage.rs create mode 100644 jans-cedarling/cedarling/src/tests/cases_authorize_without_check_jwt.rs create mode 100644 jans-cedarling/cedarling/src/tests/success_test_json.rs create mode 100644 jans-cedarling/cedarling/src/tests/utils.rs create mode 100644 jans-cedarling/test_files/policy-store_ok_2.yaml diff --git a/docs/cedarling/cedarling-authz.md b/docs/cedarling/cedarling-authz.md index c050d772f7b..19f2666ca9e 100644 --- a/docs/cedarling/cedarling-authz.md +++ b/docs/cedarling/cedarling-authz.md @@ -27,7 +27,7 @@ The JWTs, Resource, Action, and Context are sent in the authz request. Cedar Pri are derived from JWT tokens. The OpenID Connect ("OIDC") JWTs are joined by the Cedarling to create User and Role entities; the OAuth access token is used to create a Workload entity, which is the software that is acting on behalf of the Person (or autonomously). The Cedarling validates that -given its policies, both the Person and Workload are authorized. +given its policies, Role, Person and Workload are authorized. If one of Role or Person and Workload is authorized then the request is allowed to proceed. The Cedarling maps "Roles" out-of-the-box. In Cedar, Roles are a special kind of Principal. Instead of saying "User can perform action", we can say "Role can perform action"--a convenient way to @@ -50,7 +50,7 @@ values of the `scope` claim), or other claims--domains frequently enhance the ac contain business specific data needed for policy evaluation. The Cedarling authorizes a Person using a certain piece of software, which is called a "Workload". -From a logical perspective, `person_allowed` AND `workload_allowed` must be `True`. The JWT's, +From a logical perspective, (`person_allowed` AND `workload_allowed`) OR (`role_allowed` AND `workload_allowed`) must be `True`. The JWT's, Action, Resource and Context is sent by the application in the authorization request. For example, this is a sample request from a hypothetical application: diff --git a/docs/cedarling/cedarling-policy-store.md b/docs/cedarling/cedarling-policy-store.md index 65fc86cf326..17ede25b750 100644 --- a/docs/cedarling/cedarling-policy-store.md +++ b/docs/cedarling/cedarling-policy-store.md @@ -51,6 +51,7 @@ The `cedar_policies` field outlines the Cedar policies that will be used in Ceda ... } ``` + - **unique_policy_id**: (*String*) A uniqe policy ID used to for tracking and auditing purposes. - **description** : (*String*) A brief description of cedar policy - **creation_date** : (*String*) Policy creating date in `YYYY-MM-DDTHH:MM:SS.ssssss` @@ -98,8 +99,7 @@ This record contains the information needed to validate tokens from this issuer: ### Token Metadata Schema - -```json +```json { "type": "access_token" "user_id": "some_user123", @@ -109,7 +109,7 @@ This record contains the information needed to validate tokens from this issuer: - **type** : (String, no spaces) The type of token (e.g., Access, ID, Userinfo, Transaction). - **user_id** : (String) The claim used to create the Cedar entity associated with this token. -- **role_mapping** : (String, *optional*) The claim used to create a role for the token. The default value of `role_mapping` is `role`. +- **role_mapping** : (String, *optional*) The claim used to create a role for the token. The default value of `role_mapping` is `role`. The claim can be string or array of string. **Note**: Only one token should include the `role_mapping` field in the list of `token_metadata`. @@ -153,13 +153,58 @@ Here is a non-normative example of a `cedarling_store.json` file: } ``` - ## Policy and Schema Authoring -You can hand create your Cedar policies and schema in -[Visual Studio](https://marketplace.visualstudio.com/items?itemName=cedar-policy.vscode-cedar). -Make sure you run the cedar command line tool to validate both your schema and policies. +You can hand create your Cedar policies and schema in +[Visual Studio](https://marketplace.visualstudio.com/items?itemName=cedar-policy.vscode-cedar). +Make sure you run the cedar command line tool to validate both your schema and policies. -The easiest way to author your policy store is to use the Policy Designer in +The easiest way to author your policy store is to use the Policy Designer in [Agama Lab](https://cloud.gluu.org/agama-lab). This tool helps you define the policies, schema and trusted IDPs and to publish a policy store to a Github repository. + +### Minimum supported `cedar-policy schema` + +Here is example of a minimum supported `cedar-policy schema`: + +```cedar-policy_schema +namespace Jans { +entity id_token = {"aud": String,"iss": String, "sub": String}; +entity Role; +entity User in [Role] = {}; +entity Access_token = {"aud": String,"iss": String, "jti": String, "client_id": String}; +entity Workload = {}; + +entity Issue = {}; +action "Update" appliesTo { + principal: [Workload, User, Role], + resource: [Issue], + context: {} +}; +} +``` + +You can extend all of this entites and add your own. + +Mandatory entities is: `id_token`, `Role`, `User`, `Access_token`, `Workload`. +`Issue` entity and `Update` action are optinal. Is created for example, you can create others for your needs. + +`Context` and `Resource` entities you can pass during authorization request and next entites creating based on the JWT tokens: + +- `id_token` - entity based on the `id` JWT token fields. + - ID for entity based in `jti` field. + +- `Role` - define role of user. + - Mapping defined in `Token Metadata Schema`. + - Claim in JWT usually is string or array of string. + - If many roles present, the `cedarling` will try each to find first permit case. + +- `User` - entity based on the `id`and `userinfo` JWT token fields. + - If `id`and `userinfo` JWT token fields has different `sub` value, `userinfo` JWT token will be ignored. + - ID for entity based in `sub` field. (will be changed in future) + +- `Access_token` - entity based on the `access` JWT token fields. + - ID for entity based in `jti` field. + +- `Workload` - entity based on the `access` JWT token fields. + - ID for entity based in `client_id` field. diff --git a/jans-cedarling/bindings/cedarling_python/Cargo.toml b/jans-cedarling/bindings/cedarling_python/Cargo.toml index 91e89b70917..cddd0d69908 100644 --- a/jans-cedarling/bindings/cedarling_python/Cargo.toml +++ b/jans-cedarling/bindings/cedarling_python/Cargo.toml @@ -8,7 +8,7 @@ name = "cedarling_python" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.22.0", features = ["extension-module"] } +pyo3 = { version = "0.22.5", features = ["extension-module"] } cedarling = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/jans-cedarling/bindings/cedarling_python/cedarling_python.pyi b/jans-cedarling/bindings/cedarling_python/cedarling_python.pyi index 2d6f6fd3b31..abe45af3e0b 100644 --- a/jans-cedarling/bindings/cedarling_python/cedarling_python.pyi +++ b/jans-cedarling/bindings/cedarling_python/cedarling_python.pyi @@ -109,6 +109,10 @@ class AuthorizeResult: def workload(self) -> AuthorizeResultResponse: ... + def person(self) -> AuthorizeResultResponse: ... + + def role(self) -> AuthorizeResultResponse | None: ... + @final class AuthorizeResultResponse: diff --git a/jans-cedarling/bindings/cedarling_python/example.py b/jans-cedarling/bindings/cedarling_python/example.py index 0b00d03ed6e..e8dbd054beb 100644 --- a/jans-cedarling/bindings/cedarling_python/example.py +++ b/jans-cedarling/bindings/cedarling_python/example.py @@ -49,7 +49,7 @@ # show logs print("Logs stored in memory:") -print(*instance.pop_logs(), sep="\n\n") +print(*instance.pop_logs()) # //// Execute authentication request //// @@ -97,7 +97,7 @@ } """ -id_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY3IiOiJiYXNpYyIsImFtciI6IjEwIiwiYXVkIjoiNWI0NDg3YzQtOGRiMS00MDlkLWE2NTMtZjkwN2I4MDk0MDM5IiwiZXhwIjoxNzI0ODM1ODU5LCJpYXQiOjE3MjQ4MzIyNTksInN1YiI6ImJvRzhkZmM1TUtUbjM3bzdnc2RDZXlxTDhMcFdRdGdvTzQxbTFLWndkcTAiLCJpc3MiOiJodHRwczovL2FkbWluLXVpLXRlc3QuZ2x1dS5vcmciLCJqdGkiOiJzazNUNDBOWVNZdWs1c2FIWk5wa1p3Iiwibm9uY2UiOiJjMzg3MmFmOS1hMGY1LTRjM2YtYTFhZi1mOWQwZTg4NDZlODEiLCJzaWQiOiI2YTdmZTUwYS1kODEwLTQ1NGQtYmU1ZC01NDlkMjk1OTVhMDkiLCJqYW5zT3BlbklEQ29ubmVjdFZlcnNpb24iOiJvcGVuaWRjb25uZWN0LTEuMCIsImNfaGFzaCI6InBHb0s2WV9SS2NXSGtVZWNNOXV3NlEiLCJhdXRoX3RpbWUiOjE3MjQ4MzA3NDYsImdyYW50IjoiYXV0aG9yaXphdGlvbl9jb2RlIiwic3RhdHVzIjp7InN0YXR1c19saXN0Ijp7ImlkeCI6MjAyLCJ1cmkiOiJodHRwczovL2FkbWluLXVpLXRlc3QuZ2x1dS5vcmcvamFucy1hdXRoL3Jlc3R2MS9zdGF0dXNfbGlzdCJ9fX0.8BwLLGkFpWGx8wGpvVmNk_Ao8nZrP_WT-zoo-MY4zqY" +id_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY3IiOiJiYXNpYyIsImFtciI6IjEwIiwiYXVkIjoiNWI0NDg3YzQtOGRiMS00MDlkLWE2NTMtZjkwN2I4MDk0MDM5IiwiZXhwIjoxNzI0ODM1ODU5LCJpYXQiOjE3MjQ4MzIyNTksInN1YiI6ImJvRzhkZmM1TUtUbjM3bzdnc2RDZXlxTDhMcFdRdGdvTzQxbTFLWndkcTAiLCJpc3MiOiJodHRwczovL2FkbWluLXVpLXRlc3QuZ2x1dS5vcmciLCJqdGkiOiJzazNUNDBOWVNZdWs1c2FIWk5wa1p3Iiwibm9uY2UiOiJjMzg3MmFmOS1hMGY1LTRjM2YtYTFhZi1mOWQwZTg4NDZlODEiLCJzaWQiOiI2YTdmZTUwYS1kODEwLTQ1NGQtYmU1ZC01NDlkMjk1OTVhMDkiLCJqYW5zT3BlbklEQ29ubmVjdFZlcnNpb24iOiJvcGVuaWRjb25uZWN0LTEuMCIsImNfaGFzaCI6InBHb0s2WV9SS2NXSGtVZWNNOXV3NlEiLCJhdXRoX3RpbWUiOjE3MjQ4MzA3NDYsImdyYW50IjoiYXV0aG9yaXphdGlvbl9jb2RlIiwic3RhdHVzIjp7InN0YXR1c19saXN0Ijp7ImlkeCI6MjAyLCJ1cmkiOiJodHRwczovL2FkbWluLXVpLXRlc3QuZ2x1dS5vcmcvamFucy1hdXRoL3Jlc3R2MS9zdGF0dXNfbGlzdCJ9fSwicm9sZSI6IkFkbWluIn0.pU6-2tleV9OzpIMH4coVzu9kmh6Po6VPMchoRGYFYjQ" """ JSON payload of id token { @@ -120,7 +120,8 @@ "idx": 202, "uri": "https://admin-ui-test.gluu.org/jans-auth/restv1/status_list" } - } + }, + "role": "Admin" } """ @@ -150,6 +151,27 @@ } """ +""" +Policies used: +@840da5d85403f35ea76519ed1a18a33989f855bf1cf8 +permit( + principal is Jans::Workload, + action in [Jans::Action::"Update"], + resource is Jans::Issue +)when{ + principal.org_id == resource.org_id +}; + +@444da5d85403f35ea76519ed1a18a33989f855bf1cf8 +permit( + principal is Jans::User, + action in [Jans::Action::"Update"], + resource is Jans::Issue +)when{ + principal.country == resource.country +}; +""" + # Creating cedarling request request = Request( action_token, @@ -160,23 +182,42 @@ # Authorize call authorize_result = instance.authorize(request) -print(*instance.pop_logs(), sep="\n\n") +print(*instance.pop_logs()) # if you change org_id result will be false assert authorize_result.is_allowed() + # watch on the decision for workload workload_result = authorize_result.workload() -print(workload_result.decision) +print(f"Result of workload authorization: {workload_result.decision}") # show diagnostic information workload_diagnostic = workload_result.diagnostics -for i, reason in enumerate(workload_diagnostic.reason): - if i == 0: - print("reason policies:") - print(reason) - -for i, error in enumerate(workload_diagnostic.errors): - if i == 0: - print("errors:") - print("id:", error.id, "error:", error.error) +print("Policy ID(s) used:") +for diagnostic in workload_diagnostic.reason: + print(diagnostic) + +print("Errors during authorization:") +for diagnostic in workload_diagnostic.errors: + print(diagnostic) + +print() + +# watch on the decision for person +person_result = authorize_result.person() +print(f"Result of person authorization: {person_result.decision}") +person_diagnostic = person_result.diagnostics +print("Policy ID(s) used:") +for diagnostic in person_diagnostic.reason: + print(diagnostic) + +print("Errors during authorization:") +for diagnostic in person_diagnostic.errors: + print(diagnostic) + + +# watch on the decision for role if present +role_result = authorize_result.role() +if role_result is not None: + print(authorize_result.role().decision) diff --git a/jans-cedarling/bindings/cedarling_python/example_files/policy-store.json b/jans-cedarling/bindings/cedarling_python/example_files/policy-store.json index 43bf7ee30a3..87cac6a8212 100644 --- a/jans-cedarling/bindings/cedarling_python/example_files/policy-store.json +++ b/jans-cedarling/bindings/cedarling_python/example_files/policy-store.json @@ -12,5 +12,5 @@ "policy_content": "cGVybWl0KAogICAgcHJpbmNpcGFsIGlzIEphbnM6OlVzZXIsCiAgICBhY3Rpb24gaW4gW0phbnM6OkFjdGlvbjo6IlVwZGF0ZSJdLAogICAgcmVzb3VyY2UgaXMgSmFuczo6SXNzdWUKKXdoZW57CiAgICBwcmluY2lwYWwuY291bnRyeSA9PSByZXNvdXJjZS5jb3VudHJ5Cn07" } }, - "cedar_schema": "eyJKYW5zIjp7ImNvbW1vblR5cGVzIjp7IlVybCI6eyJ0eXBlIjoiUmVjb3JkIiwiYXR0cmlidXRlcyI6eyJob3N0Ijp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJTdHJpbmcifSwicGF0aCI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiU3RyaW5nIn0sInByb3RvY29sIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJTdHJpbmcifX19fSwiZW50aXR5VHlwZXMiOnsiV29ya2xvYWQiOnsic2hhcGUiOnsidHlwZSI6IlJlY29yZCIsImF0dHJpYnV0ZXMiOnsiY2xpZW50X2lkIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJTdHJpbmcifSwiaXNzIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJUcnVzdGVkSXNzdWVyIn0sIm5hbWUiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJvcmdfaWQiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9fX19LCJVc2VyIjp7InNoYXBlIjp7InR5cGUiOiJSZWNvcmQiLCJhdHRyaWJ1dGVzIjp7ImNvdW50cnkiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJlbWFpbCI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiU3RyaW5nIn0sInN1YiI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiU3RyaW5nIn0sInVzZXJuYW1lIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJTdHJpbmcifX19fSwiQWNjZXNzX3Rva2VuIjp7InNoYXBlIjp7InR5cGUiOiJSZWNvcmQiLCJhdHRyaWJ1dGVzIjp7ImF1ZCI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiU3RyaW5nIn0sImV4cCI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiTG9uZyJ9LCJpYXQiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IkxvbmcifSwiaXNzIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJUcnVzdGVkSXNzdWVyIn0sImp0aSI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiU3RyaW5nIn19fX0sIklzc3VlIjp7InNoYXBlIjp7InR5cGUiOiJSZWNvcmQiLCJhdHRyaWJ1dGVzIjp7ImNvdW50cnkiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJvcmdfaWQiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9fX19LCJpZF90b2tlbiI6eyJzaGFwZSI6eyJ0eXBlIjoiUmVjb3JkIiwiYXR0cmlidXRlcyI6eyJhY3IiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJhbXIiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJhdWQiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJleHAiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IkxvbmcifSwiaWF0Ijp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJMb25nIn0sImlzcyI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiVHJ1c3RlZElzc3VlciJ9LCJqdGkiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJzdWIiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9fX19LCJUcnVzdGVkSXNzdWVyIjp7InNoYXBlIjp7InR5cGUiOiJSZWNvcmQiLCJhdHRyaWJ1dGVzIjp7Imlzc3Vlcl9lbnRpdHlfaWQiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlVybCJ9fX19fSwiYWN0aW9ucyI6eyJVcGRhdGUiOnsiYXBwbGllc1RvIjp7InJlc291cmNlVHlwZXMiOlsiSXNzdWUiXSwicHJpbmNpcGFsVHlwZXMiOlsiV29ya2xvYWQiLCJVc2VyIl19fX19fQo=" + "cedar_schema": "eyJKYW5zIjp7ImNvbW1vblR5cGVzIjp7IlVybCI6eyJ0eXBlIjoiUmVjb3JkIiwiYXR0cmlidXRlcyI6eyJob3N0Ijp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJTdHJpbmcifSwicGF0aCI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiU3RyaW5nIn0sInByb3RvY29sIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJTdHJpbmcifX19fSwiZW50aXR5VHlwZXMiOnsiVHJ1c3RlZElzc3VlciI6eyJzaGFwZSI6eyJ0eXBlIjoiUmVjb3JkIiwiYXR0cmlidXRlcyI6eyJpc3N1ZXJfZW50aXR5X2lkIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJVcmwifX19fSwiV29ya2xvYWQiOnsic2hhcGUiOnsidHlwZSI6IlJlY29yZCIsImF0dHJpYnV0ZXMiOnsiY2xpZW50X2lkIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJTdHJpbmcifSwiaXNzIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJUcnVzdGVkSXNzdWVyIn0sIm5hbWUiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJvcmdfaWQiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9fX19LCJVc2VyIjp7Im1lbWJlck9mVHlwZXMiOlsiUm9sZSJdLCJzaGFwZSI6eyJ0eXBlIjoiUmVjb3JkIiwiYXR0cmlidXRlcyI6eyJjb3VudHJ5Ijp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJTdHJpbmcifSwiZW1haWwiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJzdWIiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJ1c2VybmFtZSI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiU3RyaW5nIn19fX0sIkFjY2Vzc190b2tlbiI6eyJzaGFwZSI6eyJ0eXBlIjoiUmVjb3JkIiwiYXR0cmlidXRlcyI6eyJhdWQiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJleHAiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IkxvbmcifSwiaWF0Ijp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJMb25nIn0sImlzcyI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiVHJ1c3RlZElzc3VlciJ9LCJqdGkiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9fX19LCJpZF90b2tlbiI6eyJzaGFwZSI6eyJ0eXBlIjoiUmVjb3JkIiwiYXR0cmlidXRlcyI6eyJhY3IiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJhbXIiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJhdWQiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJleHAiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IkxvbmcifSwiaWF0Ijp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJMb25nIn0sImlzcyI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiVHJ1c3RlZElzc3VlciJ9LCJqdGkiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJzdWIiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9fX19LCJJc3N1ZSI6eyJzaGFwZSI6eyJ0eXBlIjoiUmVjb3JkIiwiYXR0cmlidXRlcyI6eyJjb3VudHJ5Ijp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJTdHJpbmcifSwib3JnX2lkIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJTdHJpbmcifX19fSwiUm9sZSI6e319LCJhY3Rpb25zIjp7IlVwZGF0ZSI6eyJhcHBsaWVzVG8iOnsicmVzb3VyY2VUeXBlcyI6WyJJc3N1ZSJdLCJwcmluY2lwYWxUeXBlcyI6WyJXb3JrbG9hZCIsIlVzZXIiLCJSb2xlIl19fX19fQ==" } \ No newline at end of file diff --git a/jans-cedarling/bindings/cedarling_python/example_files/policy-store_human_readable/cedar.schema b/jans-cedarling/bindings/cedarling_python/example_files/policy-store_human_readable/cedar.schema index 6d494061f28..37db7a56166 100644 --- a/jans-cedarling/bindings/cedarling_python/example_files/policy-store_human_readable/cedar.schema +++ b/jans-cedarling/bindings/cedarling_python/example_files/policy-store_human_readable/cedar.schema @@ -3,11 +3,12 @@ type Url = {"host": String, "path": String, "protocol": String}; entity TrustedIssuer = {"issuer_entity_id": Url}; entity Issue = {"country": String, "org_id": String}; entity id_token = {"acr": String, "amr": String, "aud": String, "exp": Long, "iat": Long, "iss": TrustedIssuer, "jti": String, "sub": String}; -entity User = {"country": String, "email": String, "sub": String, "username": String}; +entity Role; +entity User in [Role] = {"country": String, "email": String, "sub": String, "username": String}; entity Workload = {"client_id": String, "iss": TrustedIssuer, "name": String, "org_id": String}; entity Access_token = {"aud": String, "exp": Long, "iat": Long, "iss": TrustedIssuer, "jti": String}; action "Update" appliesTo { - principal: [Workload, User], + principal: [Workload, User, Role], resource: [Issue], context: {} }; diff --git a/jans-cedarling/bindings/cedarling_python/src/authorize/authorize_result.rs b/jans-cedarling/bindings/cedarling_python/src/authorize/authorize_result.rs index c4a9a8aa88f..41b944616d4 100644 --- a/jans-cedarling/bindings/cedarling_python/src/authorize/authorize_result.rs +++ b/jans-cedarling/bindings/cedarling_python/src/authorize/authorize_result.rs @@ -37,6 +37,16 @@ impl AuthorizeResult { fn workload(&self) -> AuthorizeResultResponse { self.inner.workload.clone().into() } + + /// Get the decision value for person/user + fn person(&self) -> AuthorizeResultResponse { + self.inner.person.clone().into() + } + + /// Get the decision value for role + fn role(&self) -> Option { + self.inner.role.clone().map(|response| response.into()) + } } impl From for AuthorizeResult { diff --git a/jans-cedarling/bindings/cedarling_python/src/authorize/errors.rs b/jans-cedarling/bindings/cedarling_python/src/authorize/errors.rs index 8fbeb4db658..5a723758aa1 100644 --- a/jans-cedarling/bindings/cedarling_python/src/authorize/errors.rs +++ b/jans-cedarling/bindings/cedarling_python/src/authorize/errors.rs @@ -51,6 +51,13 @@ create_exception!( "Error encountered while creating resource entity" ); +create_exception!( + authorize_errors, + RoleEntityError, + AuthorizeError, + "Error encountered while creating role entity" +); + create_exception!( authorize_errors, ActionError, @@ -79,6 +86,13 @@ create_exception!( "Error encountered while creating cedar_policy::Request for user entity principal" ); +create_exception!( + authorize_errors, + CreateRequestRoleEntityError, + AuthorizeError, + "Error encountered while creating cedar_policy::Request for role entity principal" +); + create_exception!( authorize_errors, EntitiesError, @@ -129,10 +143,12 @@ errors_functions! { CreateIdTokenEntity => CreateIdTokenEntityError, CreateUserEntity => CreateUserEntityError, ResourceEntity => ResourceEntityError, + RoleEntity => RoleEntityError, Action => ActionError, CreateContext => CreateContextError, CreateRequestWorkloadEntity => CreateRequestWorkloadEntityError, CreateRequestUserEntity => CreateRequestUserEntityError, + CreateRequestRoleEntity => CreateRequestRoleEntityError, Entities => EntitiesError } diff --git a/jans-cedarling/bindings/cedarling_python/tests/test_authorize.py b/jans-cedarling/bindings/cedarling_python/tests/test_authorize.py index 7d7c131bcda..e48a194c07a 100644 --- a/jans-cedarling/bindings/cedarling_python/tests/test_authorize.py +++ b/jans-cedarling/bindings/cedarling_python/tests/test_authorize.py @@ -39,8 +39,31 @@ # } ACCESS_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJib0c4ZGZjNU1LVG4zN283Z3NkQ2V5cUw4THBXUXRnb080MW0xS1p3ZHEwIiwiY29kZSI6ImJmMTkzNGY2LTM5MDUtNDIwYS04Mjk5LTZiMmUzZmZkZGQ2ZSIsImlzcyI6Imh0dHBzOi8vYWRtaW4tdWktdGVzdC5nbHV1Lm9yZyIsInRva2VuX3R5cGUiOiJCZWFyZXIiLCJjbGllbnRfaWQiOiI1YjQ0ODdjNC04ZGIxLTQwOWQtYTY1My1mOTA3YjgwOTQwMzkiLCJhdWQiOiI1YjQ0ODdjNC04ZGIxLTQwOWQtYTY1My1mOTA3YjgwOTQwMzkiLCJhY3IiOiJiYXNpYyIsIng1dCNTMjU2IjoiIiwic2NvcGUiOlsib3BlbmlkIiwicHJvZmlsZSJdLCJvcmdfaWQiOiJzb21lX2xvbmdfaWQiLCJhdXRoX3RpbWUiOjE3MjQ4MzA3NDYsImV4cCI6MTcyNDk0NTk3OCwiaWF0IjoxNzI0ODMyMjU5LCJqdGkiOiJseFRtQ1ZSRlR4T2pKZ3ZFRXBvek1RIiwibmFtZSI6IkRlZmF1bHQgQWRtaW4gVXNlciIsInN0YXR1cyI6eyJzdGF0dXNfbGlzdCI6eyJpZHgiOjIwMSwidXJpIjoiaHR0cHM6Ly9hZG1pbi11aS10ZXN0LmdsdXUub3JnL2phbnMtYXV0aC9yZXN0djEvc3RhdHVzX2xpc3QifX19._eQT-DsfE_kgdhA0YOyFxxPEMNw44iwoelWa5iU1n9s" -# currently id_token is unused and it just valid JWT file -ID_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY3IiOiJiYXNpYyIsImFtciI6IjEwIiwiYXVkIjoiNWI0NDg3YzQtOGRiMS00MDlkLWE2NTMtZjkwN2I4MDk0MDM5IiwiZXhwIjoxNzI0ODM1ODU5LCJpYXQiOjE3MjQ4MzIyNTksInN1YiI6ImJvRzhkZmM1TUtUbjM3bzdnc2RDZXlxTDhMcFdRdGdvTzQxbTFLWndkcTAiLCJpc3MiOiJodHRwczovL2FkbWluLXVpLXRlc3QuZ2x1dS5vcmciLCJqdGkiOiJzazNUNDBOWVNZdWs1c2FIWk5wa1p3Iiwibm9uY2UiOiJjMzg3MmFmOS1hMGY1LTRjM2YtYTFhZi1mOWQwZTg4NDZlODEiLCJzaWQiOiI2YTdmZTUwYS1kODEwLTQ1NGQtYmU1ZC01NDlkMjk1OTVhMDkiLCJqYW5zT3BlbklEQ29ubmVjdFZlcnNpb24iOiJvcGVuaWRjb25uZWN0LTEuMCIsImNfaGFzaCI6InBHb0s2WV9SS2NXSGtVZWNNOXV3NlEiLCJhdXRoX3RpbWUiOjE3MjQ4MzA3NDYsImdyYW50IjoiYXV0aG9yaXphdGlvbl9jb2RlIiwic3RhdHVzIjp7InN0YXR1c19saXN0Ijp7ImlkeCI6MjAyLCJ1cmkiOiJodHRwczovL2FkbWluLXVpLXRlc3QuZ2x1dS5vcmcvamFucy1hdXRoL3Jlc3R2MS9zdGF0dXNfbGlzdCJ9fX0.8BwLLGkFpWGx8wGpvVmNk_Ao8nZrP_WT-zoo-MY4zqY" +# JSON payload of id token +# { +# "acr": "basic", +# "amr": "10", +# "aud": "5b4487c4-8db1-409d-a653-f907b8094039", +# "exp": 1724835859, +# "iat": 1724832259, +# "sub": "boG8dfc5MKTn37o7gsdCeyqL8LpWQtgoO41m1KZwdq0", +# "iss": "https://admin-ui-test.gluu.org", +# "jti": "sk3T40NYSYuk5saHZNpkZw", +# "nonce": "c3872af9-a0f5-4c3f-a1af-f9d0e8846e81", +# "sid": "6a7fe50a-d810-454d-be5d-549d29595a09", +# "jansOpenIDConnectVersion": "openidconnect-1.0", +# "c_hash": "pGoK6Y_RKcWHkUecM9uw6Q", +# "auth_time": 1724830746, +# "grant": "authorization_code", +# "status": { +# "status_list": { +# "idx": 202, +# "uri": "https://admin-ui-test.gluu.org/jans-auth/restv1/status_list" +# } +# }, +# "role": "Admin" +# } +ID_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY3IiOiJiYXNpYyIsImFtciI6IjEwIiwiYXVkIjoiNWI0NDg3YzQtOGRiMS00MDlkLWE2NTMtZjkwN2I4MDk0MDM5IiwiZXhwIjoxNzI0ODM1ODU5LCJpYXQiOjE3MjQ4MzIyNTksInN1YiI6ImJvRzhkZmM1TUtUbjM3bzdnc2RDZXlxTDhMcFdRdGdvTzQxbTFLWndkcTAiLCJpc3MiOiJodHRwczovL2FkbWluLXVpLXRlc3QuZ2x1dS5vcmciLCJqdGkiOiJzazNUNDBOWVNZdWs1c2FIWk5wa1p3Iiwibm9uY2UiOiJjMzg3MmFmOS1hMGY1LTRjM2YtYTFhZi1mOWQwZTg4NDZlODEiLCJzaWQiOiI2YTdmZTUwYS1kODEwLTQ1NGQtYmU1ZC01NDlkMjk1OTVhMDkiLCJqYW5zT3BlbklEQ29ubmVjdFZlcnNpb24iOiJvcGVuaWRjb25uZWN0LTEuMCIsImNfaGFzaCI6InBHb0s2WV9SS2NXSGtVZWNNOXV3NlEiLCJhdXRoX3RpbWUiOjE3MjQ4MzA3NDYsImdyYW50IjoiYXV0aG9yaXphdGlvbl9jb2RlIiwic3RhdHVzIjp7InN0YXR1c19saXN0Ijp7ImlkeCI6MjAyLCJ1cmkiOiJodHRwczovL2FkbWluLXVpLXRlc3QuZ2x1dS5vcmcvamFucy1hdXRoL3Jlc3R2MS9zdGF0dXNfbGlzdCJ9fSwicm9sZSI6IkFkbWluIn0.pU6-2tleV9OzpIMH4coVzu9kmh6Po6VPMchoRGYFYjQ" # JSON payload of userinfo token # { diff --git a/jans-cedarling/bindings/cedarling_python/tests/test_policy_store.py b/jans-cedarling/bindings/cedarling_python/tests/test_policy_store.py index d34dff41504..a611ed799a0 100644 --- a/jans-cedarling/bindings/cedarling_python/tests/test_policy_store.py +++ b/jans-cedarling/bindings/cedarling_python/tests/test_policy_store.py @@ -27,7 +27,7 @@ ("policy-store_schema_err_json.json", "unable to unmarshal cedar policy schema json to the structure"), ("policy-store_schema_err_cedar_mistake.json", - "Could not load policy: failed to parse the policy store from `policy_store.json`: unable to parse cedar policy schema json: failed to resolve type: User_TypeNotExist at line 32 column 1"), + "Could not load policy: failed to parse the policy store from policy_store json: unable to parse cedar policy schema: failed to resolve type: User_TypeNotExist at line 32 column 1"), ] @@ -56,7 +56,8 @@ def test_load_policy_store(sample_bootstrap_config, policy_file_name, expected_e error_message = str(e) assert error_message != "", "error message should not be empty" - assert expected_error in error_message, "expected error message not found" + assert expected_error in error_message, "expected error message not found, but: {}".format( + error_message) else: raise Exception("expected error not found") diff --git a/jans-cedarling/cedarling/Cargo.toml b/jans-cedarling/cedarling/Cargo.toml index 02aeca9f6d5..7281dbbf297 100644 --- a/jans-cedarling/cedarling/Cargo.toml +++ b/jans-cedarling/cedarling/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] serde = { workspace = true } serde_json = { workspace = true } +serde_yml = "0.0.12" thiserror = { workspace = true } sparkv = { workspace = true } uuid7 = { version = "1.1.0", features = ["serde", "uuid"] } @@ -18,7 +19,12 @@ reqwest = { version = "0.12.8", features = ["blocking", "json"] } bytes = "1.7.2" typed-builder = "0.20.0" semver = "1.0.23" -derive_more = { version = "1.0.0", features = ["deref"] } +derive_more = { version = "1.0.0", features = [ + "deref", + "from", + "display", + "error", +] } [dev-dependencies] # is used in testing @@ -26,4 +32,3 @@ test_utils = { workspace = true } rand = "0.8.5" jsonwebkey = { version = "0.3.5", features = ["generate", "jwt-convert"] } mockito = "1.5.0" -serde_yml = "0.0.12" diff --git a/jans-cedarling/cedarling/examples/authorize_without_jwt_validation.rs b/jans-cedarling/cedarling/examples/authorize_without_jwt_validation.rs index 0a706c99a0f..fd8c61bf649 100644 --- a/jans-cedarling/cedarling/examples/authorize_without_jwt_validation.rs +++ b/jans-cedarling/cedarling/examples/authorize_without_jwt_validation.rs @@ -128,7 +128,7 @@ fn main() -> Result<(), Box> { Ok(result) => { println!("\n\nis allowed: {}", result.is_allowed()); }, - Err(e) => eprintln!("Error while authorizing: {}\n {:?}\n\n", e.to_string(), e), + Err(e) => eprintln!("Error while authorizing: {}\n {:?}\n\n", e, e), } Ok(()) diff --git a/jans-cedarling/cedarling/src/authz/authorize_result.rs b/jans-cedarling/cedarling/src/authz/authorize_result.rs index 09b88baed4a..906a92b252d 100644 --- a/jans-cedarling/cedarling/src/authz/authorize_result.rs +++ b/jans-cedarling/cedarling/src/authz/authorize_result.rs @@ -7,12 +7,31 @@ pub struct AuthorizeResult { pub workload: cedar_policy::Response, /// Result of authorization where principal is `Jans::User` pub person: cedar_policy::Response, + /// Result of authorization where principal is `Jans::Role` + pub role: Option, } impl AuthorizeResult { - /// Returns true if request is allowed + /// Evaluates the authorization result to determine if the request is allowed. + /// + /// This function checks the decision based on the following rule: + /// - The `workload` must allow the request (PRINCIPAL). + /// - Either the `person` or `role` must also allow the request (USER OR ROLE). + /// + /// This approach represents a hierarchical decision-making model, where the + /// `workload` (i.e., primary principal) needs to permit the request and + /// additional conditions (either `person` or `role`) must also indicate allowance. pub fn is_allowed(&self) -> bool { - // in future we should also check if the person is allowed - self.workload.decision() == Decision::Allow + let role_decision = self + .role + .as_ref() + .map(|result| result.decision()) + .unwrap_or(Decision::Deny); + + let workload_allowed = self.workload.decision() == Decision::Allow; + let person_or_role_allowed = + self.person.decision() == Decision::Allow || role_decision == Decision::Allow; + + workload_allowed && person_or_role_allowed } } diff --git a/jans-cedarling/cedarling/src/authz/entities/create.rs b/jans-cedarling/cedarling/src/authz/entities/create.rs index e0ea43068b5..2b9a6eac496 100644 --- a/jans-cedarling/cedarling/src/authz/entities/create.rs +++ b/jans-cedarling/cedarling/src/authz/entities/create.rs @@ -10,11 +10,14 @@ use std::{ str::FromStr, }; -use crate::authz::token_data::{GetTokenClaimValue, Payload, TokenPayload}; use crate::common::cedar_schema::{ cedar_json::{CedarSchemaRecord, CedarType, GetCedarTypeError}, CedarSchemaJson, }; +use crate::{ + authz::token_data::{GetTokenClaimValue, Payload, TokenPayload}, + common::cedar_schema::cedar_json::CedarSchemaEntityShape, +}; use cedar_policy::{EntityId, EntityTypeName, EntityUid, RestrictedExpression}; @@ -45,31 +48,33 @@ impl<'a> EntityMetadata<'a> { &'a self, schema: &'a CedarSchemaJson, data: &'a TokenPayload, + parents: HashSet, ) -> Result { - let entity_uid = EntityUid::from_type_name_and_id( - EntityTypeName::from_str(self.entity_type).map_err(|err| { - CedarPolicyCreateTypeError::EntityTypeName(self.entity_type.to_string(), err) - })?, - EntityId::new(data.get_payload(self.entity_id_data_key)?.as_str()?), - ); - - let parsed_typename = parse_namespace_and_typename(self.entity_type); - // fetch the schema record from the json-schema. - let entity_schema_record = fetch_schema_record( - &parsed_typename.namespace(), - parsed_typename.typename, - schema, + let entity_uid = build_entity_uid( + self.entity_type, + data.get_payload(self.entity_id_data_key)?.as_str()?, )?; - create_entity( - entity_uid, - &parsed_typename.namespace(), - entity_schema_record, - data, - ) + let parsed_typename = parse_namespace_and_typename(self.entity_type); + create_entity(entity_uid, &parsed_typename, schema, data, parents) } } +/// build [`EntityUid`] based on input parameters +pub(crate) fn build_entity_uid( + entity_type: &str, + entity_id: &str, +) -> Result { + let entity_uid = EntityUid::from_type_name_and_id( + EntityTypeName::from_str(entity_type).map_err(|err| { + CedarPolicyCreateTypeError::EntityTypeName(entity_type.to_string(), err) + })?, + EntityId::new(entity_id), + ); + + Ok(entity_uid) +} + /// Parsed result of entity type name and namespace. /// Analog to the internal cedar_policy type `InternalName` pub(crate) struct EntityParsedTypeName<'a> { @@ -97,20 +102,24 @@ fn fetch_schema_record<'a>( entity_namespace: &str, entity_typename: &str, schema: &'a CedarSchemaJson, -) -> Result<&'a CedarSchemaRecord, CedarPolicyCreateTypeError> { - let entity_record = schema - .entity_schema_record(entity_namespace, entity_typename) +) -> Result<&'a CedarSchemaEntityShape, CedarPolicyCreateTypeError> { + let entity_shape = schema + .entity_schema(entity_namespace, entity_typename) .ok_or(CedarPolicyCreateTypeError::CouldNotFindEntity( entity_typename.to_string(), ))?; // just to check if the entity is a record to be sure - if !entity_record.is_record() { - return Err(CedarPolicyCreateTypeError::NotRecord( - entity_typename.to_string(), - )); - }; - Ok(entity_record) + // if shape not empty + if let Some(entity_record) = &entity_shape.shape { + if !entity_record.is_record() { + return Err(CedarPolicyCreateTypeError::NotRecord( + entity_typename.to_string(), + )); + }; + } + + Ok(entity_shape) } /// get mapping of the entity attributes @@ -132,36 +141,55 @@ fn entity_meta_attributes( .collect::, _>>() } +/// Build attributes for the entity +fn build_entity_attributes( + schema_shape: &CedarSchemaEntityShape, + data: &TokenPayload, + entity_namespace: &str, +) -> Result, CedarPolicyCreateTypeError> { + if let Some(schema_record) = &schema_shape.shape { + let attr_vec = entity_meta_attributes(schema_record)? + .into_iter() + .filter_map(|attr: EntityAttributeMetadata| { + let attr_name = attr.attribute_name; + let cedar_exp_result = token_attribute_to_cedar_exp(&attr, data, entity_namespace); + match (cedar_exp_result, attr.is_required) { + (Ok(cedar_exp), _) => Some(Ok((attr_name.to_string(), cedar_exp))), + ( + Err(CedarPolicyCreateTypeError::GetTokenClaimValue( + GetTokenClaimValue::KeyNotFound(_), + )), + false, + // when the attribute is not required and not found in token data we skip it + ) => None, + (Err(err), _) => Some(Err(err)), + } + }) + .collect::, CedarPolicyCreateTypeError>>()?; + Ok(HashMap::from_iter(attr_vec)) + } else { + Ok(HashMap::new()) + } +} + /// Create entity from token payload data. pub fn create_entity<'a>( entity_uid: EntityUid, - entity_namespace: &str, - schema_record: &'a CedarSchemaRecord, + parsed_typename: &EntityParsedTypeName, + schema: &'a CedarSchemaJson, data: &'a TokenPayload, + parents: HashSet, ) -> Result { - let attr_vec = entity_meta_attributes(schema_record)? - .into_iter() - .filter_map(|attr: EntityAttributeMetadata<'a>| { - let attr_name = attr.attribute_name; - let cedar_exp_result = token_attribute_to_cedar_exp(&attr, data, entity_namespace); - match (cedar_exp_result, attr.is_required) { - (Ok(cedar_exp), _) => Some(Ok((attr_name.to_string(), cedar_exp))), - ( - Err(CedarPolicyCreateTypeError::GetTokenClaimValue( - GetTokenClaimValue::KeyNotFound(_), - )), - false, - // when the attribute is not required and not found in token data we skip it - ) => None, - (Err(err), _) => Some(Err(err)), - } - }) - .collect::, CedarPolicyCreateTypeError>>()?; + let entity_namespace = parsed_typename.namespace(); + + // fetch the schema entity shape from the json-schema. + let schema_shape = fetch_schema_record(&entity_namespace, parsed_typename.typename, schema)?; - let attrs: HashMap = HashMap::from_iter(attr_vec); + let attrs = build_entity_attributes(schema_shape, data, &entity_namespace)?; - cedar_policy::Entity::new(entity_uid.clone(), attrs, HashSet::new()) - .map_err(|err| CedarPolicyCreateTypeError::CreateEntity(entity_uid.to_string(), err)) + let entity_uid_string = entity_uid.to_string(); + cedar_policy::Entity::new(entity_uid, attrs, parents) + .map_err(|err| CedarPolicyCreateTypeError::CreateEntity(entity_uid_string, err)) } /// Meta information about an attribute for cedar policy. diff --git a/jans-cedarling/cedarling/src/authz/entities/mod.rs b/jans-cedarling/cedarling/src/authz/entities/mod.rs index 6687281d487..b5e1aeec9d9 100644 --- a/jans-cedarling/cedarling/src/authz/entities/mod.rs +++ b/jans-cedarling/cedarling/src/authz/entities/mod.rs @@ -14,13 +14,22 @@ mod trait_as_expression; #[cfg(test)] mod test_create; +use std::collections::HashSet; + use crate::common::cedar_schema::CedarSchemaJson; use crate::authz::token_data::{AccessTokenData, IdTokenData, UserInfoTokenData}; +use crate::common::policy_store::TokenKind; +use crate::jwt; +use cedar_policy::EntityUid; pub use create::CedarPolicyCreateTypeError; -use create::{create_entity, parse_namespace_and_typename}; +use create::{build_entity_uid, create_entity, parse_namespace_and_typename}; use super::request::ResourceData; +use super::token_data::TokenPayload; + +pub(crate) type DecodeTokensResult<'a> = + jwt::DecodeTokensResult<'a, AccessTokenData, IdTokenData, UserInfoTokenData>; /// Access token entities pub(crate) struct AccessTokenEntities { @@ -48,8 +57,8 @@ pub fn create_access_token_entities( data: &AccessTokenData, ) -> Result { Ok(AccessTokenEntities { - access_token_entity: meta::AccessTokenMeta.create_entity(schema, data)?, - workload_entity: meta::WorkloadEntityMeta.create_entity(schema, data)?, + access_token_entity: meta::AccessTokenMeta.create_entity(schema, data, HashSet::new())?, + workload_entity: meta::WorkloadEntityMeta.create_entity(schema, data, HashSet::new())?, }) } @@ -58,7 +67,7 @@ pub fn create_id_token_entity( schema: &CedarSchemaJson, data: &IdTokenData, ) -> Result { - meta::IdToken.create_entity(schema, data) + meta::IdToken.create_entity(schema, data, HashSet::new()) } /// Create user entity @@ -66,6 +75,7 @@ pub fn create_user_entity( schema: &CedarSchemaJson, id_token_data: &IdTokenData, userinfo_token_data: &UserInfoTokenData, + parents: HashSet, ) -> Result { const SUB_KEY: &str = "sub"; @@ -77,7 +87,7 @@ pub fn create_user_entity( id_token_data }; - meta::User.create_entity(schema, payload) + meta::User.create_entity(schema, payload, parents) } /// Describe errors on creating resource entity @@ -97,17 +107,112 @@ pub fn create_resource_entity( })?; let parsed_typename = parse_namespace_and_typename(&resource.resource_type); - // fetch the schema record from the json-schema. - let schema_record = schema - .entity_schema_record(&parsed_typename.namespace(), parsed_typename.typename) - .ok_or(CedarPolicyCreateTypeError::CouldNotFindEntity( - entity_uid.to_string(), - ))?; Ok(create_entity( entity_uid, - &parsed_typename.namespace(), - schema_record, + &parsed_typename, + schema, &resource.payload.into(), + HashSet::new(), )?) } + +/// Describe errors on creating role entity +#[derive(thiserror::Error, Debug)] +pub enum RoleEntityError { + #[error("could not create Jans::Role entity from {token_kind} token: {error}")] + Create { + error: CedarPolicyCreateTypeError, + token_kind: TokenKind, + }, +} + +/// Create `Role` entity from based on `TrustedIssuer` or default value of `RoleMapping` +pub fn create_role_entities( + schema: &CedarSchemaJson, + tokens: &DecodeTokensResult, +) -> Result, RoleEntityError> { + // get role mapping or default value + let role_mapping = tokens + .trusted_issuer + .map(|trusted_issuer| trusted_issuer.get_role_mapping().unwrap_or_default()) + .unwrap_or_default(); + + let role_entity_type: &str = "Jans::Role"; + let parsed_typename = parse_namespace_and_typename(role_entity_type); + + // map payload from token + let token_data: &'_ TokenPayload = match role_mapping.kind { + TokenKind::Access => &tokens.access_token, + TokenKind::Id => &tokens.id_token, + TokenKind::Userinfo => &tokens.userinfo_token, + }; + + // get payload of role id in JWT token data + let Ok(payload) = token_data.get_payload(role_mapping.role_mapping_field) else { + // if key not found we return empty vector + return Ok(Vec::new()); + }; + + // it can be 2 scenario when field is array or field is string + let entity_uid_vec: Vec = if let Ok(payload_str) = payload.as_str() { + // case if it string + let entity_uid = build_entity_uid(role_entity_type, payload_str).map_err(|err| { + RoleEntityError::Create { + error: err, + token_kind: role_mapping.kind, + } + })?; + vec![entity_uid] + } else { + // case if it array of string + match payload + // get as array + .as_array() + { + Ok(payload_vec) => { + payload_vec + .iter() + .map(|payload_el| { + // get each element of array as `str` + payload_el.as_str().map_err(|err| RoleEntityError::Create { + error: err.into(), + token_kind: role_mapping.kind, + }) + // build entity uid + .and_then(|name| build_entity_uid(role_entity_type, name) + .map_err(|err| RoleEntityError::Create { + error: err, + token_kind: role_mapping.kind, + })) + }) + .collect::, _>>()? + }, + Err(err) => { + // Handle the case where the payload is neither a string nor an array + return Err(RoleEntityError::Create { + error: err.into(), + token_kind: role_mapping.kind, + }); + }, + } + }; + + // create role entity for each entity uid + entity_uid_vec + .into_iter() + .map(|entity_uid| { + create_entity( + entity_uid, + &parsed_typename, + schema, + token_data, + HashSet::new(), + ) + .map_err(|err| RoleEntityError::Create { + error: err, + token_kind: role_mapping.kind, + }) + }) + .collect::, _>>() +} diff --git a/jans-cedarling/cedarling/src/authz/entities/test_create.rs b/jans-cedarling/cedarling/src/authz/entities/test_create.rs index 67c280fa08e..603a941b88b 100644 --- a/jans-cedarling/cedarling/src/authz/entities/test_create.rs +++ b/jans-cedarling/cedarling/src/authz/entities/test_create.rs @@ -7,6 +7,8 @@ //! Testing the creating entities +use std::collections::HashSet; + use test_utils::assert_eq; use crate::authz::token_data::{GetTokenClaimValue, TokenPayload}; @@ -37,7 +39,7 @@ fn successful_scenario_empty_namespace() { let payload: TokenPayload = serde_json::from_value(json).unwrap(); let entity = metadata - .create_entity(&schema, &payload) + .create_entity(&schema, &payload, HashSet::new()) .expect("entity should be created"); let entity_json = entity.to_json_value().expect("should serialize to json"); @@ -86,7 +88,7 @@ fn successful_scenario_not_empty_namespace() { let payload: TokenPayload = serde_json::from_value(json).unwrap(); let entity = metadata - .create_entity(&schema, &payload) + .create_entity(&schema, &payload, HashSet::new()) .expect("entity should be created"); let entity_json = entity.to_json_value().expect("should serialize to json"); @@ -136,7 +138,7 @@ fn get_token_claim_type_string_error() { let payload: TokenPayload = serde_json::from_value(json.clone()).unwrap(); let entity_creation_error = metadata - .create_entity(&schema, &payload) + .create_entity(&schema, &payload, HashSet::new()) .expect_err("entity creating should throw error"); if let CedarPolicyCreateTypeError::GetTokenClaimValue(GetTokenClaimValue::KeyNotCorrectType { @@ -181,7 +183,7 @@ fn get_token_claim_type_long_error() { let payload: TokenPayload = serde_json::from_value(json.clone()).unwrap(); let entity_creation_error = metadata - .create_entity(&schema, &payload) + .create_entity(&schema, &payload, HashSet::new()) .expect_err("entity creating should throw error"); if let CedarPolicyCreateTypeError::GetTokenClaimValue(GetTokenClaimValue::KeyNotCorrectType { @@ -226,7 +228,7 @@ fn get_token_claim_type_entity_uid_error() { let payload: TokenPayload = serde_json::from_value(json.clone()).unwrap(); let entity_creation_error = metadata - .create_entity(&schema, &payload) + .create_entity(&schema, &payload, HashSet::new()) .expect_err("entity creating should throw error"); if let CedarPolicyCreateTypeError::GetTokenClaimValue(GetTokenClaimValue::KeyNotCorrectType { @@ -271,7 +273,7 @@ fn get_token_claim_type_boolean_error() { let payload: TokenPayload = serde_json::from_value(json.clone()).unwrap(); let entity_creation_error = metadata - .create_entity(&schema, &payload) + .create_entity(&schema, &payload, HashSet::new()) .expect_err("entity creating should throw error"); if let CedarPolicyCreateTypeError::GetTokenClaimValue(GetTokenClaimValue::KeyNotCorrectType { @@ -319,7 +321,7 @@ fn get_token_claim_type_set_error() { let payload: TokenPayload = serde_json::from_value(json.clone()).unwrap(); let entity_creation_error = metadata - .create_entity(&schema, &payload) + .create_entity(&schema, &payload, HashSet::new()) .expect_err("entity creating should throw error"); if let CedarPolicyCreateTypeError::GetTokenClaimValue(GetTokenClaimValue::KeyNotCorrectType { @@ -366,7 +368,7 @@ fn get_token_claim_type_set_of_set_error() { let payload: TokenPayload = serde_json::from_value(json.clone()).unwrap(); let entity_creation_error = metadata - .create_entity(&schema, &payload) + .create_entity(&schema, &payload, HashSet::new()) .expect_err("entity creating should throw error"); if let CedarPolicyCreateTypeError::GetTokenClaimValue(GetTokenClaimValue::KeyNotCorrectType { @@ -415,7 +417,7 @@ fn get_token_claim_cedar_typename_error() { let payload: TokenPayload = serde_json::from_value(json).unwrap(); let entity_creation_error = metadata - .create_entity(&schema, &payload) + .create_entity(&schema, &payload, HashSet::new()) .expect_err("entity creating should throw error"); if let CedarPolicyCreateTypeError::EntityTypeName(typename, _) = &entity_creation_error { @@ -455,7 +457,7 @@ fn get_token_claim_cedar_typename_in_attr_error() { let payload: TokenPayload = serde_json::from_value(json).unwrap(); let entity_creation_error = metadata - .create_entity(&schema, &payload) + .create_entity(&schema, &payload, HashSet::new()) .expect_err("entity creating should throw error"); if let CedarPolicyCreateTypeError::EntityTypeName(typename, _) = &entity_creation_error { diff --git a/jans-cedarling/cedarling/src/authz/mod.rs b/jans-cedarling/cedarling/src/authz/mod.rs index 6cc340259ee..d1db002da7b 100644 --- a/jans-cedarling/cedarling/src/authz/mod.rs +++ b/jans-cedarling/cedarling/src/authz/mod.rs @@ -9,6 +9,7 @@ //! - evaluate if authorization is granted for *user* //! - evaluate if authorization is granted for *client* / *workload * +use std::collections::HashSet; use std::str::FromStr; use std::sync::Arc; @@ -26,10 +27,11 @@ mod token_data; pub use authorize_result::AuthorizeResult; use cedar_policy::{Entities, Entity, EntityUid}; use entities::CedarPolicyCreateTypeError; +use entities::DecodeTokensResult; use entities::ResourceEntityError; use entities::{ - create_access_token_entities, create_id_token_entity, create_user_entity, - AccessTokenEntitiesError, + create_access_token_entities, create_id_token_entity, create_role_entities, create_user_entity, + AccessTokenEntitiesError, RoleEntityError, }; use entities::{create_resource_entity, AccessTokenEntities}; use request::Request; @@ -92,6 +94,11 @@ impl Authz { let principal_workload_uid = entities_data.access_token_entities.workload_entity.uid(); let resource_uid = entities_data.resource_entity.uid(); let principal_user_entity_uid = entities_data.user_entity.uid(); + let principal_role_entity_uids = entities_data + .role_entities + .iter() + .map(|e| e.uid()) + .collect::>(); // Convert [`AuthorizeEntitiesData`] to [`cedar_policy::Entities`] structure, // hold all entities that will be used on authorize check. @@ -114,12 +121,57 @@ impl Authz { .execute_authorize(ExecuteAuthorizeParameters { entities: &entities, principal: principal_user_entity_uid.clone(), - action, + action: action.clone(), resource: resource_uid.clone(), - context, + context: context.clone(), }) .map_err(AuthorizeError::CreateRequestUserEntity)?; + // Variable holds last used `EntityUid`role or None + let mut principal_role_entity_uid = None; + + // Check authorize for each principal `"Jans::Role"` from cedar-policy schema. + // Return last used or None if vector empty + let role_result = if !principal_role_entity_uids.is_empty() { + let mut result = None; + + // iterate over list of role uids + for role_uid in principal_role_entity_uids { + let tmp_result = self + .execute_authorize(ExecuteAuthorizeParameters { + entities: &entities, + principal: role_uid.clone(), + action: action.clone(), + resource: resource_uid.clone(), + context: context.clone(), + }) + .map_err(|err| { + AuthorizeError::CreateRequestRoleEntity(CreateRequestRoleError { + uid: role_uid.clone(), + err, + }) + })?; + + let decision = tmp_result.decision(); + principal_role_entity_uid = Some(role_uid); + result = Some(tmp_result); + + // if succeed then we no need iterate to next + if decision == cedar_policy::Decision::Allow { + break; + } + } + result + } else { + None + }; + + let result = AuthorizeResult { + workload: workload_result, + person: person_result, + role: role_result, + }; + // Log all result information about both authorize checks. // Where principal is `"Jans::Workload"` and where principal is `"Jans::User"`. self.config.log_service.as_ref().log( @@ -135,20 +187,25 @@ impl Authz { person_principal: principal_user_entity_uid.to_string(), workload_principal: principal_workload_uid.to_string(), + role_principal: principal_role_entity_uid.map(|v| v.to_string()), - person_diagnostics: person_result.diagnostics().into(), - workload_diagnostics: workload_result.diagnostics().into(), + person_diagnostics: result.person.diagnostics().into(), + workload_diagnostics: result.workload.diagnostics().into(), + role_diagnostics: result + .role + .as_ref() + .map(|result| result.diagnostics().into()), - person_decision: person_result.decision().into(), - workload_decision: workload_result.decision().into(), + person_decision: result.person.decision().into(), + workload_decision: result.workload.decision().into(), + role_decision: result.role.as_ref().map(|result| result.decision().into()), + + authorized: result.is_allowed(), }) .set_message("Result of authorize.".to_string()), ); - Ok(AuthorizeResult { - workload: workload_result, - person: person_result, - }) + Ok(result) } /// Execute cedar policy is_authorized method to check @@ -179,8 +236,10 @@ impl Authz { &self, request: &Request, ) -> Result { + let schema = &self.config.policy_store.cedar_schema.json; + // decode JWT tokens to structs AccessTokenData, IdTokenData, UserInfoTokenData using jwt service - let (access_token, id_token, userinfo_token) = self + let decode_result: DecodeTokensResult = self .config .jwt_service .decode_tokens::( @@ -189,24 +248,28 @@ impl Authz { &request.userinfo_token, )?; + let role_entities = create_role_entities(schema, &decode_result)?; + // Populate the `AuthorizeEntitiesData` structure using the builder pattern let data = AuthorizeEntitiesData::builder() // Populate the structure with entities derived from the access token .access_token_entities(create_access_token_entities( - &self.config.policy_store.cedar_schema.json, - &access_token, + schema, + &decode_result.access_token, )?) // Add an entity created from the ID token .id_token_entity( - create_id_token_entity(&self.config.policy_store.cedar_schema.json, &id_token) + create_id_token_entity(schema, &decode_result.id_token) .map_err(AuthorizeError::CreateIdTokenEntity)?, ) // Add an entity created from the userinfo token .user_entity( create_user_entity( - &self.config.policy_store.cedar_schema.json, - &id_token, - &userinfo_token, + schema, + &decode_result.id_token, + &decode_result.userinfo_token, + // parents for Jans::User entity + HashSet::from_iter(role_entities.iter().map(|e|e.uid())), ) .map_err(AuthorizeError::CreateUserEntity)?, ) @@ -214,7 +277,9 @@ impl Authz { .resource_entity(create_resource_entity( request.resource.clone(), &self.config.policy_store.cedar_schema.json, - )?); + )?) + // Add Jans::Role entities + .role_entities(role_entities); Ok(data.build()) } @@ -239,14 +304,16 @@ struct AuthorizeEntitiesData { id_token_entity: Entity, user_entity: Entity, resource_entity: Entity, + role_entities: Vec, } impl AuthorizeEntitiesData { /// Create iterator to get all entities fn into_iter(self) -> impl Iterator { - let iter = vec![self.id_token_entity, self.user_entity, self.resource_entity].into_iter(); - - self.access_token_entities.into_iter().chain(iter) + vec![self.id_token_entity, self.user_entity, self.resource_entity] + .into_iter() + .chain(self.access_token_entities.into_iter()) + .chain(self.role_entities) } /// Collect all entities to [`cedar_policy::Entities`] @@ -276,6 +343,9 @@ pub enum AuthorizeError { /// Error encountered while creating resource entity #[error("{0}")] ResourceEntity(#[from] ResourceEntityError), + /// Error encountered while creating role entity + #[error(transparent)] + RoleEntity(#[from] RoleEntityError), /// Error encountered while parsing Action to EntityUid #[error("could not parse action: {0}")] Action(cedar_policy::ParseErrors), @@ -288,7 +358,22 @@ pub enum AuthorizeError { /// Error encountered while creating [`cedar_policy::Request`] for user entity principal #[error("could not create request user entity principal: {0}")] CreateRequestUserEntity(cedar_policy::RequestValidationError), + /// Error encountered while creating [`cedar_policy::Request`] for role entity principal + // + // Additional error was created to use only one placeholder. + // It allows to use macro for error mapping in python binding + #[error(transparent)] + CreateRequestRoleEntity(CreateRequestRoleError), /// Error encountered while collecting all entities #[error("could not collect all entities: {0}")] Entities(#[from] cedar_policy::entities_errors::EntitiesError), } + +#[derive(Debug, derive_more::Error, derive_more::Display)] +#[display("could not create request user entity principal for {uid}: {err}")] +pub struct CreateRequestRoleError { + /// Error value + err: cedar_policy::RequestValidationError, + /// Role ID [`EntityUid`] value used for authorization request + uid: EntityUid, +} diff --git a/jans-cedarling/cedarling/src/authz/token_data.rs b/jans-cedarling/cedarling/src/authz/token_data.rs index 08cc3d3339f..5ce7af8a9e1 100644 --- a/jans-cedarling/cedarling/src/authz/token_data.rs +++ b/jans-cedarling/cedarling/src/authz/token_data.rs @@ -77,11 +77,6 @@ pub enum GetTokenClaimValue { expected_type: String, got_type: String, }, - #[error("could not convert json value to: {expected_type}, got: {got_type}")] - NotCorrectType { - expected_type: String, - got_type: String, - }, } impl GetTokenClaimValue { diff --git a/jans-cedarling/cedarling/src/bootstrap_config/policy_store_config.rs b/jans-cedarling/cedarling/src/bootstrap_config/policy_store_config.rs index 5d6b44f64ab..d6702a5a405 100644 --- a/jans-cedarling/cedarling/src/bootstrap_config/policy_store_config.rs +++ b/jans-cedarling/cedarling/src/bootstrap_config/policy_store_config.rs @@ -20,6 +20,11 @@ pub enum PolicyStoreSource { /// /// The string contains the raw JSON data representing the policy. Json(String), + /// Read the policy directly from a raw YAML string. + /// + /// The string contains the raw YAML data representing the policy. + /// Mostly used only for testing purposes. + Yaml(String), /// Fetch the policies from the Lock Master service using a specified identifier. /// diff --git a/jans-cedarling/cedarling/src/common/cedar_schema.rs b/jans-cedarling/cedarling/src/common/cedar_schema.rs index 58b0d6afd99..8cb65ecb527 100644 --- a/jans-cedarling/cedarling/src/common/cedar_schema.rs +++ b/jans-cedarling/cedarling/src/common/cedar_schema.rs @@ -14,9 +14,9 @@ pub(crate) mod cedar_json; /// content_type is one of cedar or cedar-json#[derive(Debug, Clone, serde::Deserialize)] #[derive(Debug, Clone, serde::Deserialize)] struct EncodedSchema { - pub encoding : super::Encoding, - pub content_type : super::ContentType, - pub body : String, + pub encoding: super::Encoding, + pub content_type: super::ContentType, + pub body: String, } /// Intermediate struct to handle both kinds of cedar_schema values. @@ -29,7 +29,7 @@ struct EncodedSchema { #[serde(untagged)] enum MaybeEncoded { Plain(String), - Tagged(EncodedSchema) + Tagged(EncodedSchema), } /// Box that holds the [`cedar_policy::Schema`] and @@ -50,31 +50,31 @@ impl PartialEq for CedarSchema { let self_principals = self.schema.principals().collect::>(); let other_principals = other.schema.principals().collect::>(); if self_principals != other_principals { - return false + return false; } let self_resources = self.schema.resources().collect::>(); let other_resources = other.schema.resources().collect::>(); if self_resources != other_resources { - return false + return false; } let self_action_groups = self.schema.action_groups().collect::>(); let other_action_groups = other.schema.action_groups().collect::>(); if self_action_groups != other_action_groups { - return false + return false; } let self_entity_types = self.schema.entity_types().collect::>(); let other_entity_types = other.schema.entity_types().collect::>(); if self_entity_types != other_entity_types { - return false + return false; } let self_actions = self.schema.actions().collect::>(); let other_actions = other.schema.actions().collect::>(); if self_actions != other_actions { - return false + return false; } // and this only checks the schema anyway @@ -83,15 +83,15 @@ impl PartialEq for CedarSchema { } impl<'de> serde::Deserialize<'de> for CedarSchema { - fn deserialize>(deserializer: D) -> Result - { + fn deserialize>(deserializer: D) -> Result { // Read the next thing as either a String or a Map, using the MaybeEncoded enum to distinguish - let encoded_schema = match ::deserialize(deserializer)? { - MaybeEncoded::Plain(body) => EncodedSchema{ + let encoded_schema = match ::deserialize(deserializer)? + { + MaybeEncoded::Plain(body) => EncodedSchema { // These are the default if the encoding is not specified. encoding: super::Encoding::Base64, content_type: super::ContentType::CedarJson, - body + body, }, MaybeEncoded::Tagged(encoded_schema) => encoded_schema, }; @@ -101,12 +101,20 @@ impl<'de> serde::Deserialize<'de> for CedarSchema { super::Encoding::Base64 => { use base64::prelude::*; let buf = BASE64_STANDARD.decode(encoded_schema.body).map_err(|err| { - serde::de::Error::custom(format!("{}: {}", deserialize::ParseCedarSchemaSetMessage::Base64, err)) + serde::de::Error::custom(format!( + "{}: {}", + deserialize::ParseCedarSchemaSetMessage::Base64, + err + )) })?; String::from_utf8(buf).map_err(|err| { - serde::de::Error::custom(format!("{}: {}", deserialize::ParseCedarSchemaSetMessage::Utf8, err)) + serde::de::Error::custom(format!( + "{}: {}", + deserialize::ParseCedarSchemaSetMessage::Utf8, + err + )) })? - } + }, }; // Need both of these because CedarSchema wants both. @@ -114,42 +122,62 @@ impl<'de> serde::Deserialize<'de> for CedarSchema { super::ContentType::Cedar => { // parse cedar policy from the cedar representation // TODO must log warnings or something - let (schema_fragment, _warning) = cedar_policy::SchemaFragment::from_cedarschema_str(&decoded_body) - .map_err(|err| { - serde::de::Error::custom(format!("{}: {}", deserialize::ParseCedarSchemaSetMessage::Parse, err)) - })?; + let (schema_fragment, _warning) = + cedar_policy::SchemaFragment::from_cedarschema_str(&decoded_body).map_err( + |err| { + serde::de::Error::custom(format!( + "{}: {}", + deserialize::ParseCedarSchemaSetMessage::Parse, + err + )) + }, + )?; // urgh now recreate the json representation - let json_string = schema_fragment.to_json_string() - .map_err(|err| { - serde::de::Error::custom(format!("{}: {}", deserialize::ParseCedarSchemaSetMessage::CedarSchemaJsonFormat, err)) - })?; + let json_string = schema_fragment.to_json_string().map_err(|err| { + serde::de::Error::custom(format!( + "{}: {}", + deserialize::ParseCedarSchemaSetMessage::CedarSchemaJsonFormat, + err + )) + })?; (schema_fragment, json_string) - } + }, super::ContentType::CedarJson => { // parse cedar policy from the json representation let schema_fragment = cedar_policy::SchemaFragment::from_json_str(&decoded_body) .map_err(|err| { - serde::de::Error::custom(format!("{}: {}", deserialize::ParseCedarSchemaSetMessage::CedarSchemaJsonFormat, err)) + serde::de::Error::custom(format!( + "{}: {}", + deserialize::ParseCedarSchemaSetMessage::CedarSchemaJsonFormat, + err + )) })?; (schema_fragment, decoded_body) - } + }, }; // create the schema let fragment_iter = std::iter::once(schema_fragment); let schema = cedar_policy::Schema::from_schema_fragments(fragment_iter.into_iter()) .map_err(|err| { - serde::de::Error::custom(format!("{}: {}", deserialize::ParseCedarSchemaSetMessage::Parse, err)) + serde::de::Error::custom(format!( + "{}: {}", + deserialize::ParseCedarSchemaSetMessage::Parse, + err + )) })?; - let json = serde_json::from_str(&json_string) - .map_err(|err| { - serde::de::Error::custom(format!("{}: {}", deserialize::ParseCedarSchemaSetMessage::CedarSchemaJsonFormat, err )) - })?; + let json = serde_json::from_str(&json_string).map_err(|err| { + serde::de::Error::custom(format!( + "{}: {}", + deserialize::ParseCedarSchemaSetMessage::CedarSchemaJsonFormat, + err + )) + })?; - Ok(CedarSchema{schema, json}) + Ok(CedarSchema { schema, json }) } } @@ -160,7 +188,7 @@ mod deserialize { Base64, #[error("unable to unmarshal cedar policy schema json to the structure")] CedarSchemaJsonFormat, - #[error("unable to parse cedar policy schema json")] + #[error("unable to parse cedar policy schema")] Parse, #[error("invalid utf8 detected while decoding cedar policy")] Utf8, @@ -168,6 +196,8 @@ mod deserialize { #[cfg(test)] mod tests { + use test_utils::assert_eq; + use crate::common::policy_store::PolicyStore; use super::*; @@ -194,17 +224,24 @@ mod deserialize { #[test] fn test_readable_yaml_ok() { - static YAML_POLICY_STORE: &str = include_str!("../../../test_files/policy-store_readable.yaml"); + static YAML_POLICY_STORE: &str = + include_str!("../../../test_files/policy-store_readable.yaml"); let yaml_policy_result = serde_yml::from_str::(YAML_POLICY_STORE); - assert!(yaml_policy_result.is_ok(), "{:?}", yaml_policy_result.unwrap_err()); + assert!( + yaml_policy_result.is_ok(), + "{:?}", + yaml_policy_result.unwrap_err() + ); } #[test] fn test_readable_yaml_identical_readable_json() { - static YAML_POLICY_STORE: &str = include_str!("../../../test_files/policy-store_readable.yaml"); + static YAML_POLICY_STORE: &str = + include_str!("../../../test_files/policy-store_readable.yaml"); let yaml_policy_result = serde_yml::from_str::(YAML_POLICY_STORE); - static JSON_POLICY_STORE: &str = include_str!("../../../test_files/policy-store_readable.json"); + static JSON_POLICY_STORE: &str = + include_str!("../../../test_files/policy-store_readable.json"); let json_policy_result = serde_yml::from_str::(JSON_POLICY_STORE); assert_eq!(yaml_policy_result.unwrap(), json_policy_result.unwrap()); @@ -212,6 +249,7 @@ mod deserialize { // In fact this fails because of limitations in cedar_policy::Policy::from_json // see PolicyContentType + #[allow(dead_code)] fn test_both_ok() { static POLICY_STORE_RAW: &str = include_str!("../../../test_files/policy-store_blobby.json"); @@ -230,7 +268,10 @@ mod deserialize { let policy_result = serde_json::from_str::(POLICY_STORE_RAW); let err = policy_result.unwrap_err(); let msg = err.to_string(); - assert!(msg.contains(&ParseCedarSchemaSetMessage::Base64.to_string()), "{err:?}"); + assert!( + msg.contains(&ParseCedarSchemaSetMessage::Base64.to_string()), + "{err:?}" + ); } #[test] @@ -241,7 +282,10 @@ mod deserialize { let policy_result = serde_json::from_str::(POLICY_STORE_RAW); let err = policy_result.unwrap_err(); let msg = err.to_string(); - assert!(msg.contains("unable to unmarshal cedar policy schema json"), "{err:?}"); + assert!( + msg.contains("unable to unmarshal cedar policy schema json"), + "{err:?}" + ); } #[test] @@ -253,7 +297,7 @@ mod deserialize { let err_msg = policy_result.unwrap_err().to_string(); assert_eq!( err_msg, - "unable to parse cedar policy schema json: failed to resolve type: User_TypeNotExist at line 32 column 1" + "unable to parse cedar policy schema: failed to resolve type: User_TypeNotExist at line 32 column 1" ); } } diff --git a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json.rs b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json.rs index 05ad714cc80..7f39f0d052f 100644 --- a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json.rs +++ b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json.rs @@ -32,15 +32,13 @@ pub(crate) struct CedarSchemaJson { impl CedarSchemaJson { /// Get schema record by namespace name and entity type name - pub fn entity_schema_record( + pub fn entity_schema( &self, namespace: &str, typename: &str, - ) -> Option<&CedarSchemaRecord> { + ) -> Option<&CedarSchemaEntityShape> { let namespace = self.namespace.get(namespace)?; - let entity_shape = namespace.entity_types.get(typename)?; - - entity_shape.shape.as_ref() + namespace.entity_types.get(typename) } } @@ -290,64 +288,71 @@ mod tests { namespace: HashMap::from_iter(vec![( "Jans".to_string(), CedarSchemaEntities { - entity_types: HashMap::from_iter(vec![( - "Access_token".to_string(), - CedarSchemaEntityShape { - shape: Some(CedarSchemaRecord { - entity_type: "Record".to_string(), - attributes: HashMap::from_iter(vec![ - ( - "aud".to_string(), - CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "String".to_string(), - }), - required: true, - }, - ), - ( - "exp".to_string(), - CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "Long".to_string(), - }), - required: true, - }, - ), - ( - "iat".to_string(), - CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Primitive( - PrimitiveType { - kind: PrimitiveTypeKind::Long, - }, - ), - required: true, - }, - ), - ( - "scope".to_string(), - CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Set(Box::new( - SetEntityType { - element: CedarSchemaEntityType::Typed( - EntityType { - kind: "EntityOrCommon".to_string(), - name: "String".to_string(), - }, - ), - }, - )), - - required: false, - }, - ), - ]), - }), - }, - )]), + entity_types: HashMap::from_iter(vec![ + ( + "Access_token".to_string(), + CedarSchemaEntityShape { + shape: Some(CedarSchemaRecord { + entity_type: "Record".to_string(), + attributes: HashMap::from_iter(vec![ + ( + "aud".to_string(), + CedarSchemaEntityAttribute { + cedar_type: CedarSchemaEntityType::Typed( + EntityType { + kind: "EntityOrCommon".to_string(), + name: "String".to_string(), + }, + ), + required: true, + }, + ), + ( + "exp".to_string(), + CedarSchemaEntityAttribute { + cedar_type: CedarSchemaEntityType::Typed( + EntityType { + kind: "EntityOrCommon".to_string(), + name: "Long".to_string(), + }, + ), + required: true, + }, + ), + ( + "iat".to_string(), + CedarSchemaEntityAttribute { + cedar_type: CedarSchemaEntityType::Primitive( + PrimitiveType { + kind: PrimitiveTypeKind::Long, + }, + ), + required: true, + }, + ), + ( + "scope".to_string(), + CedarSchemaEntityAttribute { + cedar_type: CedarSchemaEntityType::Set(Box::new( + SetEntityType { + element: CedarSchemaEntityType::Typed( + EntityType { + kind: "EntityOrCommon".to_string(), + name: "String".to_string(), + }, + ), + }, + )), + + required: false, + }, + ), + ]), + }), + }, + ), + ("Role".to_string(), CedarSchemaEntityShape { shape: None }), + ]), }, )]), }; diff --git a/jans-cedarling/cedarling/src/common/cedar_schema/test_files/test_data_cedar.json b/jans-cedarling/cedarling/src/common/cedar_schema/test_files/test_data_cedar.json index 2f8bafb61ba..b69f606a216 100644 --- a/jans-cedarling/cedarling/src/common/cedar_schema/test_files/test_data_cedar.json +++ b/jans-cedarling/cedarling/src/common/cedar_schema/test_files/test_data_cedar.json @@ -26,7 +26,8 @@ } } } - } + }, + "Role": {} }, "actions": {} } diff --git a/jans-cedarling/cedarling/src/common/cedar_schema/test_files/test_data_cedar.schema b/jans-cedarling/cedarling/src/common/cedar_schema/test_files/test_data_cedar.schema index fe3bd28d850..0c6674f6de8 100644 --- a/jans-cedarling/cedarling/src/common/cedar_schema/test_files/test_data_cedar.schema +++ b/jans-cedarling/cedarling/src/common/cedar_schema/test_files/test_data_cedar.schema @@ -1,3 +1,4 @@ namespace Jans { entity Access_token = {"aud": String, "exp": Long, "iat": Long, scope?: Set }; +entity Role; } \ No newline at end of file diff --git a/jans-cedarling/cedarling/src/common/policy_store.rs b/jans-cedarling/cedarling/src/common/policy_store.rs index 1c3d5f5afc3..c05db280336 100644 --- a/jans-cedarling/cedarling/src/common/policy_store.rs +++ b/jans-cedarling/cedarling/src/common/policy_store.rs @@ -67,6 +67,40 @@ pub struct TrustedIssuer { pub token_metadata: Option>, } +/// Structure define the source from which role mappings are retrieved. +pub struct RoleMapping<'a> { + pub kind: TokenKind, + pub role_mapping_field: &'a str, +} + +// By default we will search role in the User token +impl Default for RoleMapping<'_> { + fn default() -> Self { + Self { + kind: TokenKind::Userinfo, + role_mapping_field: "role", + } + } +} + +impl TrustedIssuer { + /// Retrieves the available `RoleMapping` from the token metadata. + // + // in `token_metadata` list only one element with mapping + // it is maximum 3 elements in list so iterating is efficient enouf + pub fn get_role_mapping(&self) -> Option { + for metadata in self.token_metadata.as_ref()? { + if let Some(role_mapping_field) = &metadata.role_mapping { + return Some(RoleMapping { + kind: metadata.kind, + role_mapping_field, + }); + } + } + None + } +} + /// Parses and validates the `token_metadata` field. /// /// This function ensures that the metadata contains at most one `TokenMetadata` with a `role_mapping` @@ -113,7 +147,7 @@ pub struct TokenMetadata { pub role_mapping: Option, } -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] #[allow(dead_code)] pub enum TokenKind { /// Access token used for granting access to resources. @@ -124,9 +158,6 @@ pub enum TokenKind { /// Userinfo token containing user-specific information. Userinfo, - - /// Transaction token used for tracking transactions. - Transaction, } /// Enum representing the different kinds of tokens used by Cedarling. @@ -136,7 +167,6 @@ impl fmt::Display for TokenKind { TokenKind::Access => "access", TokenKind::Id => "id", TokenKind::Userinfo => "userinfo", - TokenKind::Transaction => "transaction", }; write!(f, "{}", kind_str) } @@ -153,15 +183,9 @@ impl<'de> Deserialize<'de> for TokenKind { "id_token" => Ok(TokenKind::Id), "userinfo_token" => Ok(TokenKind::Userinfo), "access_token" => Ok(TokenKind::Access), - "transaction_token" => Ok(TokenKind::Transaction), _ => Err(serde::de::Error::unknown_variant( &token_kind, - &[ - "access_token", - "id_token", - "userinfo_token", - "transaction_token", - ], + &["access_token", "id_token", "userinfo_token"], )), } } diff --git a/jans-cedarling/cedarling/src/common/policy_store/test.rs b/jans-cedarling/cedarling/src/common/policy_store/test.rs index 4ce431f0e47..c9cd40a33c9 100644 --- a/jans-cedarling/cedarling/src/common/policy_store/test.rs +++ b/jans-cedarling/cedarling/src/common/policy_store/test.rs @@ -63,7 +63,7 @@ fn test_base64_decoding_error_in_policy_store() { "#; // check if the string is a valid policy cedar_policy::Policy::from_str(policy).expect("invalid cedar policy"); - let mut encoded_policy = BASE64_STANDARD.encode(&policy); + let mut encoded_policy = BASE64_STANDARD.encode(policy); // simulate an invalid base64 encoding by adding an invalid character encoded_policy.push('!'); @@ -107,7 +107,7 @@ fn test_policy_parsing_error_in_policy_store() { cedar_policy::Policy::from_str(policy).expect("invalid cedar policy"); // base64 encode the policy - let mut encoded_policy = BASE64_STANDARD.encode(&policy); + let mut encoded_policy = BASE64_STANDARD.encode(policy); // Simulate invalid UTF-8 by manually inserting invalid byte sequences let mut invalid_utf8_bytes = BASE64_STANDARD @@ -230,9 +230,11 @@ fn test_error_on_invalid_token_type() { { "type": "userinfo", "user_id": "email" } ]); - let result = parse_and_check_token_metadata(invalid_token_metadata); + let result = + parse_and_check_token_metadata(invalid_token_metadata).expect_err("should throw error"); - assert!( - matches!(result, Err(e) if e.to_string() == "unknown variant `Access`, expected one of `access_token`, `id_token`, `userinfo_token`, `transaction_token`") + assert_eq!( + result.to_string(), + "unknown variant `Access`, expected one of `access_token`, `id_token`, `userinfo_token`" ); } diff --git a/jans-cedarling/cedarling/src/init/policy_store.rs b/jans-cedarling/cedarling/src/init/policy_store.rs index 746e923c316..bf8b63bc266 100644 --- a/jans-cedarling/cedarling/src/init/policy_store.rs +++ b/jans-cedarling/cedarling/src/init/policy_store.rs @@ -11,8 +11,10 @@ use crate::common::policy_store::PolicyStore; /// Errors that can occur when loading a policy store. #[derive(Debug, thiserror::Error)] pub enum PolicyStoreLoadError { - #[error("failed to parse the policy store from `policy_store.json`: {0}")] - Parse(#[from] serde_json::Error), + #[error("failed to parse the policy store from policy_store json: {0}")] + ParseJson(#[from] serde_json::Error), + #[error("failed to parse the policy store from policy_store yaml: {0}")] + ParseYaml(#[from] serde_yml::Error), #[error("failed to fetch the policy store from the lock server")] FetchFromLockServer, } @@ -25,7 +27,10 @@ pub(crate) fn load_policy_store( ) -> Result { let policy_store = match &config.source { PolicyStoreSource::Json(policy_json) => { - load_policy_store_from_json(policy_json).map_err(PolicyStoreLoadError::Parse)? + load_policy_store_from_json(policy_json).map_err(PolicyStoreLoadError::ParseJson)? + }, + PolicyStoreSource::Yaml(policy_yaml) => { + serde_yml::from_str(policy_yaml).map_err(PolicyStoreLoadError::ParseYaml)? }, PolicyStoreSource::LockMaster(policy_store_id) => { load_policy_store_from_lock_master(policy_store_id)? diff --git a/jans-cedarling/cedarling/src/init/service_config.rs b/jans-cedarling/cedarling/src/init/service_config.rs index 207f4ab66f6..4aaac39cb74 100644 --- a/jans-cedarling/cedarling/src/init/service_config.rs +++ b/jans-cedarling/cedarling/src/init/service_config.rs @@ -8,6 +8,7 @@ use super::jwt_algorithm::parse_jwt_algorithms; use super::policy_store::{load_policy_store, PolicyStoreLoadError}; use crate::common::policy_store::PolicyStore; +use crate::jwt::TrustedIssuerAndOpenIdConfig; use crate::{bootstrap_config, jwt}; use bootstrap_config::BootstrapConfig; @@ -16,6 +17,7 @@ use bootstrap_config::BootstrapConfig; pub(crate) struct ServiceConfig { pub policy_store: PolicyStore, pub jwt_algorithms: Vec, + pub trusted_issuers_and_openid: Vec, } #[derive(thiserror::Error, Debug)] @@ -26,13 +28,32 @@ pub enum ServiceConfigError { /// Error that may occur during loading the policy store. #[error("Could not load policy: {0}")] PolicyStore(#[from] PolicyStoreLoadError), + #[error("Could not load openid config: {0}")] + // TODO: refactor error when remove panicking on init JWT server + OpenIdConfig(#[from] jwt::decoding_strategy::key_service::KeyServiceError), } impl ServiceConfig { pub fn new(bootstrap: &BootstrapConfig) -> Result { + let client = reqwest::blocking::Client::new(); + let policy_store = load_policy_store(&bootstrap.policy_store_config)?; + + // We fetch `OpenidConfig` using `TrustedIssuer` + // and store both in the `TrustedIssuerAndOpenIdConfig` structure. + let trusted_issuers_and_openid = policy_store + .trusted_issuers + .clone() // we need clone to avoid borrowing + .unwrap_or_default() + .iter() + .map(|trusted_issuer| { + TrustedIssuerAndOpenIdConfig::fetch(trusted_issuer.clone(), &client) + }) + .collect::, _>>()?; + let builder = ServiceConfig::builder() .jwt_algorithms(parse_jwt_algorithms(bootstrap)?) - .policy_store(load_policy_store(&bootstrap.policy_store_config)?); + .policy_store(policy_store) + .trusted_issuers_and_openid(trusted_issuers_and_openid); Ok(builder.build()) } diff --git a/jans-cedarling/cedarling/src/init/service_factory.rs b/jans-cedarling/cedarling/src/init/service_factory.rs index 40b99a2af1f..e61c392fd76 100644 --- a/jans-cedarling/cedarling/src/init/service_factory.rs +++ b/jans-cedarling/cedarling/src/init/service_factory.rs @@ -79,10 +79,12 @@ impl<'a> ServiceFactory<'a> { jwt_service.clone() } else { let config = match self.bootstrap_config.jwt_config { - crate::JwtConfig::Disabled => JwtServiceConfig::WithoutValidation, + crate::JwtConfig::Disabled => JwtServiceConfig::WithoutValidation { + trusted_idps: self.service_config.trusted_issuers_and_openid.clone(), + }, crate::JwtConfig::Enabled { .. } => JwtServiceConfig::WithValidation { supported_algs: self.service_config.jwt_algorithms.clone(), - trusted_idps: self.policy_store().trusted_issuers.expect("Expected trusted issuers to be present for JWT validation, but found None. Ensure that the policy store is properly initialized with trusted issuers before using JWT validation.").clone(), + trusted_idps: self.service_config.trusted_issuers_and_openid.clone(), }, }; diff --git a/jans-cedarling/cedarling/src/jwt/decoding_strategy.rs b/jans-cedarling/cedarling/src/jwt/decoding_strategy.rs index 45310f4fca1..c27a4c245c8 100644 --- a/jans-cedarling/cedarling/src/jwt/decoding_strategy.rs +++ b/jans-cedarling/cedarling/src/jwt/decoding_strategy.rs @@ -8,6 +8,7 @@ pub mod error; pub mod key_service; +pub mod open_id_storage; use crate::common::policy_store::TrustedIssuer; pub use error::JwtDecodingError; use jsonwebtoken as jwt; diff --git a/jans-cedarling/cedarling/src/jwt/decoding_strategy/key_service.rs b/jans-cedarling/cedarling/src/jwt/decoding_strategy/key_service.rs index c813e0fae51..343fbace7a9 100644 --- a/jans-cedarling/cedarling/src/jwt/decoding_strategy/key_service.rs +++ b/jans-cedarling/cedarling/src/jwt/decoding_strategy/key_service.rs @@ -11,17 +11,59 @@ mod openid_config; pub use error::KeyServiceError; use jsonwebtoken::jwk::JwkSet; use jsonwebtoken::DecodingKey; -use openid_config::*; +pub(crate) use openid_config::*; use reqwest::blocking::Client; use std::collections::HashMap; use std::sync::Arc; +/// Retrieves a [`DecodingKey`]-s based on the provided jwks_uri. +fn fetch_decoding_keys( + jwks_uri: &str, + http_client: &Client, +) -> Result, DecodingKey>, KeyServiceError> { + let jwks: JwkSet = http_client + .get(jwks_uri) + .send() + .map_err(KeyServiceError::Http)? + .error_for_status() + .map_err(KeyServiceError::Http)? + .json() + .map_err(KeyServiceError::RequestDeserialization)?; + + let mut decoding_keys = HashMap::new(); + for jwk in jwks.keys { + let decoding_key = DecodingKey::from_jwk(&jwk).map_err(KeyServiceError::KeyParsing)?; + let key_id = jwk.common.key_id.ok_or(KeyServiceError::MissingKeyId)?; + decoding_keys.insert(key_id.into_boxed_str(), decoding_key); + } + + Ok(decoding_keys) +} + +/// Retrieves a [`OpenIdConfig`] based on the provided openid uri endpoint. +pub(crate) fn fetch_openid_config( + openid_endpoint: &str, + http_client: &Client, +) -> Result { + let conf_src: OpenIdConfigSource = http_client + .get(openid_endpoint) + .send() + .map_err(KeyServiceError::Http)? + .error_for_status() + .map_err(KeyServiceError::Http)? + .json() + .map_err(KeyServiceError::RequestDeserialization)?; + + let decoding_keys = fetch_decoding_keys(&conf_src.jwks_uri, http_client)?; + + Ok(OpenIdConfig::from_source(conf_src, decoding_keys)) +} + pub struct KeyService { idp_configs: HashMap, OpenIdConfig>, // http_client: Client, } -#[allow(unused)] impl KeyService { /// initializes a new `KeyService` with the provided OpenID configuration endpoints. /// @@ -34,42 +76,8 @@ impl KeyService { // fetch IDP configs for endpoint in openid_conf_endpoints { - let conf_src: OpenIdConfigSource = http_client - .get(endpoint) - .send() - .map_err(KeyServiceError::Http)? - .error_for_status() - .map_err(KeyServiceError::Http)? - .json() - .map_err(KeyServiceError::RequestDeserialization)?; - let (issuer, conf) = OpenIdConfig::from_source(conf_src); - idp_configs.insert(issuer, conf); - } - - /// retrieves a decoding key based on the provided key ID (`kid`). - /// - /// this method first attempts to retrieve the key from the local key store. if the key - /// is not found, it will refresh the JWKS and try again. if the key is still not found, - /// an error of type `KeyNotFound` is returned. - for (iss, conf) in &mut idp_configs { - let jwks: JwkSet = http_client - .get(&*conf.jwks_uri) - .send() - .map_err(KeyServiceError::Http)? - .error_for_status() - .map_err(KeyServiceError::Http)? - .json() - .map_err(KeyServiceError::RequestDeserialization)?; - let mut decoding_keys = conf - .decoding_keys - .write() - .map_err(|_| KeyServiceError::Lock)?; - for jwk in jwks.keys { - let decoding_key = - DecodingKey::from_jwk(&jwk).map_err(KeyServiceError::KeyParsing)?; - let key_id = jwk.common.key_id.ok_or(KeyServiceError::MissingKeyId)?; - decoding_keys.insert(key_id.into(), Arc::new(decoding_key)); - } + let conf = fetch_openid_config(endpoint, &http_client)?; + idp_configs.insert(conf.issuer.clone(), conf); } Ok(Self { @@ -84,7 +92,7 @@ impl KeyService { /// is not found, it will refresh the JWKS and try again. if the key is still not found, /// an error of type `KeyNotFound` is returned. pub fn get_key(&self, kid: &str) -> Result, KeyServiceError> { - for (iss, config) in &self.idp_configs { + for iss in self.idp_configs.keys() { // first try to get the key from the local keystore if let Some(key) = self.get_key_from_iss(iss, kid)? { return Ok(key.clone()); @@ -93,7 +101,9 @@ impl KeyService { eprintln!("could not find {}, updating jwks", kid); // if the key is not found in the local keystore, update // the local keystore and try again - self.update_jwks(iss); + + // TODO: handle result + _ = self.update_jwks(iss); if let Some(key) = self.get_key_from_iss(iss, kid)? { return Ok(key.clone()); } diff --git a/jans-cedarling/cedarling/src/jwt/decoding_strategy/key_service/openid_config.rs b/jans-cedarling/cedarling/src/jwt/decoding_strategy/key_service/openid_config.rs index 315763fc700..c13518f5d96 100644 --- a/jans-cedarling/cedarling/src/jwt/decoding_strategy/key_service/openid_config.rs +++ b/jans-cedarling/cedarling/src/jwt/decoding_strategy/key_service/openid_config.rs @@ -14,9 +14,9 @@ use std::{ /// represents the source data for OpenID configuration. #[derive(Deserialize)] -pub struct OpenIdConfigSource { - issuer: Box, - jwks_uri: Box, +pub(crate) struct OpenIdConfigSource { + pub issuer: Box, + pub jwks_uri: Box, // The following values are also normally returned when sending // a GET request to the `openid_configuration_endpoint` but are // not currently being used. @@ -34,7 +34,9 @@ pub struct OpenIdConfigSource { } /// represents the OpenID configuration for an identity provider. +#[derive(Clone)] pub struct OpenIdConfig { + pub issuer: Box, pub jwks_uri: Box, // pub decoding_keys: Arc, Arc>>>, @@ -45,14 +47,19 @@ impl OpenIdConfig { /// /// this method extracts the issuer and constructs a new `OpenIdConfig` /// instance, initializing the decoding keys storage. - pub fn from_source(src: OpenIdConfigSource) -> (Box, OpenIdConfig) { - let issuer = src.issuer; - ( - issuer, - OpenIdConfig { - jwks_uri: src.jwks_uri, - decoding_keys: Arc::new(RwLock::new(HashMap::new())), - }, - ) + pub fn from_source( + src: OpenIdConfigSource, + decoding_keys: HashMap, DecodingKey>, + ) -> OpenIdConfig { + OpenIdConfig { + issuer: src.issuer, + jwks_uri: src.jwks_uri, + decoding_keys: Arc::new(RwLock::new( + decoding_keys + .into_iter() + .map(|(k, v)| (k, Arc::new(v))) + .collect(), + )), + } } } diff --git a/jans-cedarling/cedarling/src/jwt/decoding_strategy/open_id_storage.rs b/jans-cedarling/cedarling/src/jwt/decoding_strategy/open_id_storage.rs new file mode 100644 index 00000000000..6bfe7adf00e --- /dev/null +++ b/jans-cedarling/cedarling/src/jwt/decoding_strategy/open_id_storage.rs @@ -0,0 +1,32 @@ +/* + * This software is available under the Apache-2.0 license. + * See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. + * + * Copyright (c) 2024, Gluu, Inc. + */ + +use std::collections::HashMap; + +use crate::jwt::TrustedIssuerAndOpenIdConfig; + +/// Storage to hold mapping issuer to trusted_issuer and openid config +/// ang get this values whe it is needed +#[derive(Default)] +pub struct OpenIdStorage { + issuers_map: HashMap, TrustedIssuerAndOpenIdConfig>, // issuer => TrustedIssuerAndOpenIdConfig +} + +impl OpenIdStorage { + pub fn new(trusted_idps: Vec) -> OpenIdStorage { + Self { + issuers_map: trusted_idps + .into_iter() + .map(|config| (config.openid_config.issuer.clone(), config)) + .collect(), + } + } + + pub fn get(&self, issuer: &str) -> Option<&TrustedIssuerAndOpenIdConfig> { + self.issuers_map.get(issuer) + } +} diff --git a/jans-cedarling/cedarling/src/jwt/jwt_service_config.rs b/jans-cedarling/cedarling/src/jwt/jwt_service_config.rs index cb9fb33f1ae..035c88dacad 100644 --- a/jans-cedarling/cedarling/src/jwt/jwt_service_config.rs +++ b/jans-cedarling/cedarling/src/jwt/jwt_service_config.rs @@ -11,14 +11,43 @@ use crate::common::policy_store::TrustedIssuer; use crate::jwt; +use super::decoding_strategy::key_service::{fetch_openid_config, OpenIdConfig}; + /// Configuration for JWT service pub enum JwtServiceConfig { /// Decoding strategy that does not perform validation. - WithoutValidation, + WithoutValidation { + trusted_idps: Vec, + }, /// Decoding strategy that performs validation using a key service and supported algorithms. WithValidation { supported_algs: Vec, - trusted_idps: Vec, + trusted_idps: Vec, }, } + +/// Structure to store `TrustedIssuer` and `OpenIdConfig` in one place. +#[derive(Clone)] +pub struct TrustedIssuerAndOpenIdConfig { + pub trusted_issuer: TrustedIssuer, + pub openid_config: OpenIdConfig, +} + +impl TrustedIssuerAndOpenIdConfig { + /// Fetch openid configuration based on the `TrustedIssuer` and return config + pub fn fetch( + trusted_issuer: TrustedIssuer, + client: &reqwest::blocking::Client, + ) -> Result { + let openid_config = fetch_openid_config( + trusted_issuer.openid_configuration_endpoint.as_str(), + client, + )?; + + Ok(Self { + trusted_issuer, + openid_config, + }) + } +} diff --git a/jans-cedarling/cedarling/src/jwt/mod.rs b/jans-cedarling/cedarling/src/jwt/mod.rs index ec21e4045cd..082c4aefe70 100644 --- a/jans-cedarling/cedarling/src/jwt/mod.rs +++ b/jans-cedarling/cedarling/src/jwt/mod.rs @@ -15,27 +15,27 @@ #[cfg(test)] mod test; -#[cfg(test)] -pub use decoding_strategy::{ - key_service::{KeyService, KeyServiceError}, - JwtDecodingError, -}; -mod decoding_strategy; +pub use decoding_strategy::key_service::KeyServiceError; + +pub(crate) mod decoding_strategy; mod error; mod jwt_service_config; mod token; +use decoding_strategy::{open_id_storage::OpenIdStorage, DecodingArgs, DecodingStrategy}; pub use decoding_strategy::{string_to_alg, ParseAlgorithmError}; -use decoding_strategy::{DecodingArgs, DecodingStrategy}; pub use error::JwtServiceError; pub use jsonwebtoken::Algorithm; pub use jwt_service_config::*; use serde::de::DeserializeOwned; use token::*; +use crate::common::policy_store::TrustedIssuer; + pub struct JwtService { decoding_strategy: DecodingStrategy, + open_id_storage: OpenIdStorage, } /// A service for handling JSON Web Tokens (JWT). @@ -48,18 +48,32 @@ impl JwtService { /// Initializes a new `JwtService` instance based on the provided configuration. pub(crate) fn new_with_config(config: JwtServiceConfig) -> Self { match config { - JwtServiceConfig::WithoutValidation => { + JwtServiceConfig::WithoutValidation { trusted_idps } => { let decoding_strategy = DecodingStrategy::new_without_validation(); - Self { decoding_strategy } + Self { + decoding_strategy, + open_id_storage: OpenIdStorage::new(trusted_idps), + } }, JwtServiceConfig::WithValidation { supported_algs, trusted_idps, } => { - let decoding_strategy = - DecodingStrategy::new_with_validation(supported_algs, trusted_idps) - .expect("could not initialize decoding strategy with validation"); - Self { decoding_strategy } + let decoding_strategy = DecodingStrategy::new_with_validation( + supported_algs, + // TODO: found the way to use `OpenIdStorage` in the decoding strategy. + // Or use more suitable structure + trusted_idps + .iter() + .map(|v| v.trusted_issuer.clone()) + .collect(), + ) + // TODO: remove expect here and all data should be already in the `JwtServiceConfig` + .expect("could not initialize decoding strategy with validation"); + Self { + decoding_strategy, + open_id_storage: OpenIdStorage::new(trusted_idps), + } }, } } @@ -88,7 +102,7 @@ impl JwtService { access_token: &str, id_token: &str, userinfo_token: &str, - ) -> Result<(A, I, U), JwtServiceError> + ) -> Result, JwtServiceError> where A: DeserializeOwned, I: DeserializeOwned, @@ -150,6 +164,10 @@ impl JwtService { self.decoding_strategy .decode::(DecodingArgs { jwt: userinfo_token, + // Getting next values from access token looks little strange for me + // TODO: add comment here why we are doing in this way + // We also need to check if `Userinfo token` not associated with a sub from the `id_token` + // https://github.com/JanssenProject/jans/wiki/Cedarling-Nativity-Plan#cedarling-token-validation iss: Some(&access_token.iss), aud: Some(&access_token.aud), sub: Some(&id_token.sub), @@ -158,6 +176,27 @@ impl JwtService { }) .map_err(JwtServiceError::InvalidUserinfoToken)?; - Ok((access_token_claims, id_token_claims, userinfo_token_claims)) + // assume that all tokens has the same `iss` (issuer) so we get config only for one JWT token + // this behavior can be changed in future + let trusted_issuer = self + .open_id_storage + .get(access_token.iss.as_str()) + .map(|config| &config.trusted_issuer); + + Ok(DecodeTokensResult { + access_token: access_token_claims, + id_token: id_token_claims, + userinfo_token: userinfo_token_claims, + trusted_issuer, + }) } } + +#[derive(Debug)] +pub struct DecodeTokensResult<'a, A, I, U> { + pub access_token: A, + pub id_token: I, + pub userinfo_token: U, + + pub trusted_issuer: Option<&'a TrustedIssuer>, +} diff --git a/jans-cedarling/cedarling/src/jwt/test/with_validation.rs b/jans-cedarling/cedarling/src/jwt/test/with_validation.rs index 1d8d09ba803..dd97d7c7ad6 100644 --- a/jans-cedarling/cedarling/src/jwt/test/with_validation.rs +++ b/jans-cedarling/cedarling/src/jwt/test/with_validation.rs @@ -14,7 +14,7 @@ mod userinfo_token; use super::*; use crate::common::policy_store::TrustedIssuer; -use crate::jwt::{self, JwtService, JwtServiceError}; +use crate::jwt::{self, JwtService, JwtServiceError, TrustedIssuerAndOpenIdConfig}; use jsonwebtoken::Algorithm; use serde_json::json; @@ -73,18 +73,20 @@ fn can_decode_claims_with_validation() { .with_status(200) .with_header("content-type", "application/json") .with_body(openid_config_response.to_string()) + .expect_at_least(1) + .expect_at_most(2) .create(); let jwks_uri_mock = server .mock("GET", "/jwks") .with_status(200) .with_header("content-type", "application/json") .with_body(jwks) + .expect_at_least(1) + .expect_at_most(2) .create(); - // initialize JwtService with validation enabled and ES256 as the supported algorithm - let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { - supported_algs: vec![Algorithm::ES256], - trusted_idps: vec![TrustedIssuer { + let trusted_idp = TrustedIssuerAndOpenIdConfig::fetch( + TrustedIssuer { name: "some_idp".to_string(), description: "some_desc".to_string(), openid_configuration_endpoint: format!( @@ -92,7 +94,15 @@ fn can_decode_claims_with_validation() { server.url() ), token_metadata: None, - }], + }, + &reqwest::blocking::Client::new(), + ) + .expect("openid config should be fetched successfully"); + + // initialize JwtService with validation enabled and ES256 as the supported algorithm + let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { + supported_algs: vec![Algorithm::ES256], + trusted_idps: vec![trusted_idp], }); // key service should fetch the jwks_uri on init @@ -101,7 +111,7 @@ fn can_decode_claims_with_validation() { jwks_uri_mock.assert(); // decode and validate the tokens - let (access_token_result, id_token_result, userinfo_token_result) = jwt_service + let result = jwt_service .decode_tokens::( &access_token, &id_token, @@ -111,15 +121,15 @@ fn can_decode_claims_with_validation() { // assert that the decoded token claims match the input claims assert_eq!( - access_token_result, access_token_claims, + result.access_token, access_token_claims, "decoded access_token claims did not match the input claims" ); assert_eq!( - id_token_result, id_token_claims, + result.id_token, id_token_claims, "decoded id_token claims did not match the input claims" ); assert_eq!( - userinfo_token_result, userinfo_token_claims, + result.userinfo_token, userinfo_token_claims, "decoded id_token claims did not match the input claims" ); @@ -180,18 +190,20 @@ fn errors_on_unsupported_alg() { .with_status(200) .with_header("content-type", "application/json") .with_body(openid_config_response.to_string()) + .expect_at_least(1) + .expect_at_most(2) .create(); let jwks_uri_mock = server .mock("GET", "/jwks") .with_status(200) .with_header("content-type", "application/json") .with_body(jwks) + .expect_at_least(1) + .expect_at_most(2) .create(); - // initialize JwtService with validation enabled and ES256 as the supported algorithm - let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { - supported_algs: vec![Algorithm::HS256], - trusted_idps: vec![TrustedIssuer { + let trusted_idp = TrustedIssuerAndOpenIdConfig::fetch( + TrustedIssuer { name: "some_idp".to_string(), description: "some_desc".to_string(), openid_configuration_endpoint: format!( @@ -199,7 +211,15 @@ fn errors_on_unsupported_alg() { server.url() ), token_metadata: None, - }], + }, + &reqwest::blocking::Client::new(), + ) + .expect("openid config should be fetched successfully"); + + // initialize JwtService with validation enabled and ES256 as the supported algorithm + let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { + supported_algs: vec![Algorithm::HS256], + trusted_idps: vec![trusted_idp], }); // assert that the validation fails due to the tokens being signed with an diff --git a/jans-cedarling/cedarling/src/jwt/test/with_validation/access_token.rs b/jans-cedarling/cedarling/src/jwt/test/with_validation/access_token.rs index 155230fca84..862920c9856 100644 --- a/jans-cedarling/cedarling/src/jwt/test/with_validation/access_token.rs +++ b/jans-cedarling/cedarling/src/jwt/test/with_validation/access_token.rs @@ -22,7 +22,8 @@ use super::super::*; use crate::common::policy_store::TrustedIssuer; -use crate::jwt::{self, JwtService}; +use crate::jwt::decoding_strategy::JwtDecodingError; +use crate::jwt::{self, JwtService, TrustedIssuerAndOpenIdConfig}; use jsonwebtoken::Algorithm; use serde_json::json; @@ -118,18 +119,20 @@ fn test_missing_claim(missing_claim: &'static str) { .with_status(200) .with_header("content-type", "application/json") .with_body(openid_config_response.to_string()) + .expect_at_least(1) + .expect_at_most(2) .create(); let jwks_uri_mock = server .mock("GET", "/jwks") .with_status(200) .with_header("content-type", "application/json") .with_body(jwks) + .expect_at_least(1) + .expect_at_most(2) .create(); - // initialize JwtService with validation enabled and ES256 as the supported algorithm - let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { - supported_algs: vec![Algorithm::ES256], - trusted_idps: vec![TrustedIssuer { + let trusted_idp = TrustedIssuerAndOpenIdConfig::fetch( + TrustedIssuer { name: "some_idp".to_string(), description: "some_desc".to_string(), openid_configuration_endpoint: format!( @@ -137,7 +140,15 @@ fn test_missing_claim(missing_claim: &'static str) { server.url() ), token_metadata: None, - }], + }, + &reqwest::blocking::Client::new(), + ) + .expect("openid config should be fetched successfully"); + + // initialize JwtService with validation enabled and ES256 as the supported algorithm + let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { + supported_algs: vec![Algorithm::ES256], + trusted_idps: vec![trusted_idp], }); // key service should fetch the jwks_uri on init @@ -169,7 +180,7 @@ fn test_missing_claim(missing_claim: &'static str) { matches!( decode_result, Err(jwt::JwtServiceError::InvalidAccessToken( - jwt::JwtDecodingError::Validation(ref e) + JwtDecodingError::Validation(ref e) )) if matches!(e.kind(), jsonwebtoken::errors::ErrorKind::Json(json_err) if json_err.to_string().contains(&err_string)) ), @@ -259,18 +270,20 @@ fn errors_on_invalid_signature() { .with_status(200) .with_header("content-type", "application/json") .with_body(openid_config_response.to_string()) + .expect_at_least(1) + .expect_at_most(2) .create(); let jwks_uri_mock = server .mock("GET", "/jwks") .with_status(200) .with_header("content-type", "application/json") .with_body(jwks) + .expect_at_least(1) + .expect_at_most(2) .create(); - // initialize JwtService with validation enabled and ES256 as the supported algorithm - let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { - supported_algs: vec![Algorithm::ES256], - trusted_idps: vec![TrustedIssuer { + let trusted_idp = TrustedIssuerAndOpenIdConfig::fetch( + TrustedIssuer { name: "some_idp".to_string(), description: "some_desc".to_string(), openid_configuration_endpoint: format!( @@ -278,7 +291,15 @@ fn errors_on_invalid_signature() { server.url() ), token_metadata: None, - }], + }, + &reqwest::blocking::Client::new(), + ) + .expect("openid config should be fetched successfully"); + + // initialize JwtService with validation enabled and ES256 as the supported algorithm + let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { + supported_algs: vec![Algorithm::ES256], + trusted_idps: vec![trusted_idp], }); // key service should fetch the jwks_uri on init @@ -364,18 +385,20 @@ fn errors_on_expired_token() { .with_status(200) .with_header("content-type", "application/json") .with_body(openid_config_response.to_string()) + .expect_at_least(1) + .expect_at_most(2) .create(); let jwks_uri_mock = server .mock("GET", "/jwks") .with_status(200) .with_header("content-type", "application/json") .with_body(jwks) + .expect_at_least(1) + .expect_at_most(2) .create(); - // initialize JwtService with validation enabled and ES256 as the supported algorithm - let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { - supported_algs: vec![Algorithm::ES256], - trusted_idps: vec![TrustedIssuer { + let trusted_idp = TrustedIssuerAndOpenIdConfig::fetch( + TrustedIssuer { name: "some_idp".to_string(), description: "some_desc".to_string(), openid_configuration_endpoint: format!( @@ -383,7 +406,15 @@ fn errors_on_expired_token() { server.url() ), token_metadata: None, - }], + }, + &reqwest::blocking::Client::new(), + ) + .expect("openid config should be fetched successfully"); + + // initialize JwtService with validation enabled and ES256 as the supported algorithm + let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { + supported_algs: vec![Algorithm::ES256], + trusted_idps: vec![trusted_idp], }); // key service should fetch the jwks_uri on init @@ -403,7 +434,7 @@ fn errors_on_expired_token() { matches!( decode_result, Err(jwt::JwtServiceError::InvalidAccessToken( - jwt::JwtDecodingError::Validation(ref e) + JwtDecodingError::Validation(ref e) )) if *e.kind() == jsonwebtoken::errors::ErrorKind::ExpiredSignature, ), "Expected decoding to fail due to `access_token` being expired: {:?}", @@ -471,18 +502,20 @@ fn errors_on_token_used_before_nbf() { .with_status(200) .with_header("content-type", "application/json") .with_body(openid_config_response.to_string()) + .expect_at_least(1) + .expect_at_most(2) .create(); let jwks_uri_mock = server .mock("GET", "/jwks") .with_status(200) .with_header("content-type", "application/json") .with_body(jwks) + .expect_at_least(1) + .expect_at_most(2) .create(); - // initialize JwtService with validation enabled and ES256 as the supported algorithm - let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { - supported_algs: vec![Algorithm::ES256], - trusted_idps: vec![TrustedIssuer { + let trusted_idp = TrustedIssuerAndOpenIdConfig::fetch( + TrustedIssuer { name: "some_idp".to_string(), description: "some_desc".to_string(), openid_configuration_endpoint: format!( @@ -490,7 +523,15 @@ fn errors_on_token_used_before_nbf() { server.url() ), token_metadata: None, - }], + }, + &reqwest::blocking::Client::new(), + ) + .expect("openid config should be fetched successfully"); + + // initialize JwtService with validation enabled and ES256 as the supported algorithm + let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { + supported_algs: vec![Algorithm::ES256], + trusted_idps: vec![trusted_idp], }); // key service should fetch the jwks_uri on init @@ -510,7 +551,7 @@ fn errors_on_token_used_before_nbf() { matches!( decode_result, Err(jwt::JwtServiceError::InvalidAccessToken( - jwt::JwtDecodingError::Validation(ref e) + JwtDecodingError::Validation(ref e) )) if *e.kind() == jsonwebtoken::errors::ErrorKind::ImmatureSignature, ), "Expected decoding to fail due to `access_token` being used before the `nbf` timestamp: {:?}", diff --git a/jans-cedarling/cedarling/src/jwt/test/with_validation/id_token.rs b/jans-cedarling/cedarling/src/jwt/test/with_validation/id_token.rs index 8ab1873919d..e06aac7694b 100644 --- a/jans-cedarling/cedarling/src/jwt/test/with_validation/id_token.rs +++ b/jans-cedarling/cedarling/src/jwt/test/with_validation/id_token.rs @@ -24,7 +24,8 @@ use super::super::*; use crate::common::policy_store::TrustedIssuer; -use crate::jwt::{self, JwtService}; +use crate::jwt::decoding_strategy::JwtDecodingError; +use crate::jwt::{self, JwtService, TrustedIssuerAndOpenIdConfig}; use jsonwebtoken::Algorithm; use serde_json::json; @@ -119,18 +120,20 @@ fn test_missing_claim(missing_claim: &str) { .with_status(200) .with_header("content-type", "application/json") .with_body(openid_config_response.to_string()) + .expect_at_least(1) + .expect_at_most(2) .create(); let jwks_uri_mock = server .mock("GET", "/jwks") .with_status(200) .with_header("content-type", "application/json") .with_body(jwks) + .expect_at_least(1) + .expect_at_most(2) .create(); - // initialize JwtService with validation enabled and ES256 as the supported algorithm - let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { - supported_algs: vec![Algorithm::ES256], - trusted_idps: vec![TrustedIssuer { + let trusted_idp = TrustedIssuerAndOpenIdConfig::fetch( + TrustedIssuer { name: "some_idp".to_string(), description: "some_desc".to_string(), openid_configuration_endpoint: format!( @@ -138,7 +141,15 @@ fn test_missing_claim(missing_claim: &str) { server.url() ), token_metadata: None, - }], + }, + &reqwest::blocking::Client::new(), + ) + .expect("openid config should be fetched successfully"); + + // initialize JwtService with validation enabled and ES256 as the supported algorithm + let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { + supported_algs: vec![Algorithm::ES256], + trusted_idps: vec![trusted_idp], }); // key service should fetch the jwks_uri on init @@ -162,7 +173,7 @@ fn test_missing_claim(missing_claim: &str) { matches!( decode_result, Err(jwt::JwtServiceError::InvalidIdToken( - jwt::JwtDecodingError::Validation(ref e) + JwtDecodingError::Validation(ref e) )) if matches!(e.kind(), jsonwebtoken::errors::ErrorKind::Json(json_err) if json_err.to_string().contains(&err_string)) ), @@ -175,7 +186,7 @@ fn test_missing_claim(missing_claim: &str) { matches!( decode_result, Err(jwt::JwtServiceError::InvalidIdToken( - jwt::JwtDecodingError::Validation(ref e) + JwtDecodingError::Validation(ref e) )) if matches!( e.kind(), jsonwebtoken::errors::ErrorKind::MissingRequiredClaim(req_claim) if req_claim == missing_claim @@ -253,18 +264,20 @@ fn errors_on_invalid_signature() { .with_status(200) .with_header("content-type", "application/json") .with_body(openid_config_response.to_string()) + .expect_at_least(1) + .expect_at_most(2) .create(); let jwks_uri_mock = server .mock("GET", "/jwks") .with_status(200) .with_header("content-type", "application/json") .with_body(jwks) + .expect_at_least(1) + .expect_at_most(2) .create(); - // initialize JwtService with validation enabled and ES256 as the supported algorithm - let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { - supported_algs: vec![Algorithm::ES256], - trusted_idps: vec![TrustedIssuer { + let trusted_idp = TrustedIssuerAndOpenIdConfig::fetch( + TrustedIssuer { name: "some_idp".to_string(), description: "some_desc".to_string(), openid_configuration_endpoint: format!( @@ -272,7 +285,15 @@ fn errors_on_invalid_signature() { server.url() ), token_metadata: None, - }], + }, + &reqwest::blocking::Client::new(), + ) + .expect("openid config should be fetched successfully"); + + // initialize JwtService with validation enabled and ES256 as the supported algorithm + let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { + supported_algs: vec![Algorithm::ES256], + trusted_idps: vec![trusted_idp], }); // key service should fetch the jwks_uri on init @@ -292,7 +313,7 @@ fn errors_on_invalid_signature() { matches!( decode_result, Err(jwt::JwtServiceError::InvalidIdToken( - jwt::JwtDecodingError::Validation(ref e) + JwtDecodingError::Validation(ref e) )) if *e.kind() == jsonwebtoken::errors::ErrorKind::InvalidSignature, ), "Expected decoding to fail due to `id_token` having an invalid signature: {:?}", @@ -358,18 +379,20 @@ fn errors_on_expired_token() { .with_status(200) .with_header("content-type", "application/json") .with_body(openid_config_response.to_string()) + .expect_at_least(1) + .expect_at_most(2) .create(); let jwks_uri_mock = server .mock("GET", "/jwks") .with_status(200) .with_header("content-type", "application/json") .with_body(jwks) + .expect_at_least(1) + .expect_at_most(2) .create(); - // initialize JwtService with validation enabled and ES256 as the supported algorithm - let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { - supported_algs: vec![Algorithm::ES256], - trusted_idps: vec![TrustedIssuer { + let trusted_idp = TrustedIssuerAndOpenIdConfig::fetch( + TrustedIssuer { name: "some_idp".to_string(), description: "some_desc".to_string(), openid_configuration_endpoint: format!( @@ -377,7 +400,15 @@ fn errors_on_expired_token() { server.url() ), token_metadata: None, - }], + }, + &reqwest::blocking::Client::new(), + ) + .expect("openid config should be fetched successfully"); + + // initialize JwtService with validation enabled and ES256 as the supported algorithm + let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { + supported_algs: vec![Algorithm::ES256], + trusted_idps: vec![trusted_idp], }); // key service should fetch the jwks_uri on init @@ -397,7 +428,7 @@ fn errors_on_expired_token() { matches!( decode_result, Err(jwt::JwtServiceError::InvalidIdToken( - jwt::JwtDecodingError::Validation(ref e) + JwtDecodingError::Validation(ref e) )) if matches!(e.kind(), jsonwebtoken::errors::ErrorKind::ExpiredSignature), ), "Expected decoding to fail due to `id_token` being expired: {:?}", @@ -464,18 +495,20 @@ fn errors_on_invalid_iss() { .with_status(200) .with_header("content-type", "application/json") .with_body(openid_config_response.to_string()) + .expect_at_least(1) + .expect_at_most(2) .create(); let jwks_uri_mock = server .mock("GET", "/jwks") .with_status(200) .with_header("content-type", "application/json") .with_body(jwks) + .expect_at_least(1) + .expect_at_most(2) .create(); - // initialize JwtService with validation enabled and ES256 as the supported algorithm - let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { - supported_algs: vec![Algorithm::ES256], - trusted_idps: vec![TrustedIssuer { + let trusted_idp = TrustedIssuerAndOpenIdConfig::fetch( + TrustedIssuer { name: "some_idp".to_string(), description: "some_desc".to_string(), openid_configuration_endpoint: format!( @@ -483,7 +516,15 @@ fn errors_on_invalid_iss() { server.url() ), token_metadata: None, - }], + }, + &reqwest::blocking::Client::new(), + ) + .expect("openid config should be fetched successfully"); + + // initialize JwtService with validation enabled and ES256 as the supported algorithm + let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { + supported_algs: vec![Algorithm::ES256], + trusted_idps: vec![trusted_idp], }); // key service should fetch the jwks_uri on init @@ -503,7 +544,7 @@ fn errors_on_invalid_iss() { matches!( decode_result, Err(jwt::JwtServiceError::InvalidIdToken( - jwt::JwtDecodingError::Validation(ref e) + JwtDecodingError::Validation(ref e) )) if matches!(e.kind(), jsonwebtoken::errors::ErrorKind::InvalidIssuer), ), "Expected decoding to fail due to `id_token` not having the same `iss` as `access_token`: {:?}", @@ -570,18 +611,20 @@ fn errors_on_invalid_aud() { .with_status(200) .with_header("content-type", "application/json") .with_body(openid_config_response.to_string()) + .expect_at_least(1) + .expect_at_most(2) .create(); let jwks_uri_mock = server .mock("GET", "/jwks") .with_status(200) .with_header("content-type", "application/json") .with_body(jwks) + .expect_at_least(1) + .expect_at_most(2) .create(); - // initialize JwtService with validation enabled and ES256 as the supported algorithm - let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { - supported_algs: vec![Algorithm::ES256], - trusted_idps: vec![TrustedIssuer { + let trusted_idp = TrustedIssuerAndOpenIdConfig::fetch( + TrustedIssuer { name: "some_idp".to_string(), description: "some_desc".to_string(), openid_configuration_endpoint: format!( @@ -589,7 +632,15 @@ fn errors_on_invalid_aud() { server.url() ), token_metadata: None, - }], + }, + &reqwest::blocking::Client::new(), + ) + .expect("openid config should be fetched successfully"); + + // initialize JwtService with validation enabled and ES256 as the supported algorithm + let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { + supported_algs: vec![Algorithm::ES256], + trusted_idps: vec![trusted_idp], }); // key service should fetch the jwks_uri on init @@ -609,7 +660,7 @@ fn errors_on_invalid_aud() { matches!( decode_result, Err(jwt::JwtServiceError::InvalidIdToken( - jwt::JwtDecodingError::Validation(ref e) + JwtDecodingError::Validation(ref e) )) if matches!(e.kind(), jsonwebtoken::errors::ErrorKind::InvalidAudience), ), "Expected decoding to fail due to `id_token` not having the same `aud` as `access_token`: {:?}", @@ -677,18 +728,20 @@ fn errors_on_token_used_before_nbf() { .with_status(200) .with_header("content-type", "application/json") .with_body(openid_config_response.to_string()) + .expect_at_least(1) + .expect_at_most(2) .create(); let jwks_uri_mock = server .mock("GET", "/jwks") .with_status(200) .with_header("content-type", "application/json") .with_body(jwks) + .expect_at_least(1) + .expect_at_most(2) .create(); - // initialize JwtService with validation enabled and ES256 as the supported algorithm - let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { - supported_algs: vec![Algorithm::ES256], - trusted_idps: vec![TrustedIssuer { + let trusted_idp = TrustedIssuerAndOpenIdConfig::fetch( + TrustedIssuer { name: "some_idp".to_string(), description: "some_desc".to_string(), openid_configuration_endpoint: format!( @@ -696,7 +749,15 @@ fn errors_on_token_used_before_nbf() { server.url() ), token_metadata: None, - }], + }, + &reqwest::blocking::Client::new(), + ) + .expect("openid config should be fetched successfully"); + + // initialize JwtService with validation enabled and ES256 as the supported algorithm + let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { + supported_algs: vec![Algorithm::ES256], + trusted_idps: vec![trusted_idp], }); // key service should fetch the jwks_uri on init @@ -716,7 +777,7 @@ fn errors_on_token_used_before_nbf() { matches!( decode_result, Err(jwt::JwtServiceError::InvalidIdToken( - jwt::JwtDecodingError::Validation(ref e) + JwtDecodingError::Validation(ref e) )) if matches!(e.kind(), jsonwebtoken::errors::ErrorKind::ImmatureSignature), ), "Expected decoding to fail due to `id_token` being used before the `nbf` timestamp: {:?}", diff --git a/jans-cedarling/cedarling/src/jwt/test/with_validation/key_service.rs b/jans-cedarling/cedarling/src/jwt/test/with_validation/key_service.rs index 4bc50fea4c5..61bfa1475f1 100644 --- a/jans-cedarling/cedarling/src/jwt/test/with_validation/key_service.rs +++ b/jans-cedarling/cedarling/src/jwt/test/with_validation/key_service.rs @@ -16,7 +16,9 @@ use super::super::*; use crate::common::policy_store::TrustedIssuer; -use crate::jwt::KeyService; +use crate::jwt::decoding_strategy::key_service::KeyService; +use crate::jwt::decoding_strategy::JwtDecodingError; +use crate::jwt::TrustedIssuerAndOpenIdConfig; use crate::jwt::{self, JwtService}; use jsonwebtoken::Algorithm; use serde_json::json; @@ -82,18 +84,20 @@ fn errors_when_no_key_found() { .with_status(200) .with_header("content-type", "application/json") .with_body(openid_config_response.to_string()) + .expect_at_least(1) + .expect_at_most(3) .create(); let jwks_uri_mock = server .mock("GET", "/jwks") .with_status(200) .with_header("content-type", "application/json") .with_body(jwks) + .expect_at_least(2) + .expect_at_most(3) .create(); - // initialize JwtService with validation enabled and ES256 as the supported algorithm - let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { - supported_algs: vec![Algorithm::ES256], - trusted_idps: vec![TrustedIssuer { + let trusted_idp = TrustedIssuerAndOpenIdConfig::fetch( + TrustedIssuer { name: "some_idp".to_string(), description: "some_desc".to_string(), openid_configuration_endpoint: format!( @@ -101,7 +105,15 @@ fn errors_when_no_key_found() { server.url() ), token_metadata: None, - }], + }, + &reqwest::blocking::Client::new(), + ) + .expect("openid config should be fetched successfully"); + + // initialize JwtService with validation enabled and ES256 as the supported algorithm + let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { + supported_algs: vec![Algorithm::ES256], + trusted_idps: vec![trusted_idp], }); // key service should fetch the jwks_uri on init @@ -121,14 +133,13 @@ fn errors_when_no_key_found() { matches!( decode_result, Err(jwt::JwtServiceError::InvalidAccessToken( - jwt::JwtDecodingError::KeyService(jwt::decoding_strategy::key_service::KeyServiceError::KeyNotFound(ref e)) + JwtDecodingError::KeyService(jwt::decoding_strategy::key_service::KeyServiceError::KeyNotFound(ref e)) )) if **e == *"some_key_id_not_in_the_jwks", ), "Expected decoding to fail due to not being able to find a key to validate `access_token`: {:?}", decode_result ); // key service should fetch the jwks again when it cant find the `kid` for the access_token - let jwks_uri_mock = jwks_uri_mock.expect(2); jwks_uri_mock.assert(); // assert that there aren't any additional calls to the openid_config_uri @@ -155,6 +166,7 @@ fn errors_when_cant_fetch_jwks_uri() { .with_status(200) .with_header("content-type", "application/json") .with_body(openid_config_response.to_string()) + .expect_at_least(1) .create(); let openid_conf_endpoint = format!("{}/.well-known/openid-configuration", server.url()); @@ -178,6 +190,7 @@ fn errors_when_cant_fetch_openid_configuration() { let openid_conf_mock = server .mock("GET", "/.well-known/openid-configuration") .with_status(500) + .expect_at_least(1) .create(); let openid_conf_endpoint = format!("{}/.well-known/openid-configuration", server.url()); @@ -246,19 +259,20 @@ fn can_update_local_jwks() { .with_status(200) .with_header("content-type", "application/json") .with_body(openid_config_response.to_string()) + .expect_at_least(1) + .expect_at_most(2) .create(); let jwks_uri_mock = server .mock("GET", "/jwks") .with_status(200) .with_header("content-type", "application/json") .with_body(json!({"keys": Vec::<&str>::new()}).to_string()) // empty JWKS - .expect(2) + .expect_at_least(1) + .expect_at_most(3) .create(); - // initialize JwtService with validation enabled and ES256 as the supported algorithm - let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { - supported_algs: vec![Algorithm::ES256], - trusted_idps: vec![TrustedIssuer { + let trusted_idp = TrustedIssuerAndOpenIdConfig::fetch( + TrustedIssuer { name: "some_idp".to_string(), description: "some_desc".to_string(), openid_configuration_endpoint: format!( @@ -266,7 +280,15 @@ fn can_update_local_jwks() { server.url() ), token_metadata: None, - }], + }, + &reqwest::blocking::Client::new(), + ) + .expect("openid config should be fetched successfully"); + + // initialize JwtService with validation enabled and ES256 as the supported algorithm + let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { + supported_algs: vec![Algorithm::ES256], + trusted_idps: vec![trusted_idp], }); // assert that first call attempt to validate the token fails since a @@ -281,7 +303,7 @@ fn can_update_local_jwks() { matches!( decode_result, Err(jwt::JwtServiceError::InvalidAccessToken( - jwt::JwtDecodingError::KeyService( + JwtDecodingError::KeyService( jwt::decoding_strategy::key_service::KeyServiceError::KeyNotFound(ref key_id) ) )) if key_id == &encoding_keys[0].key_id, @@ -301,7 +323,7 @@ fn can_update_local_jwks() { .create(); // decode and validate the tokens again - let (access_token_result, id_token_result, userinfo_token_result) = jwt_service + let result = jwt_service .decode_tokens::( &access_token, &id_token, @@ -311,9 +333,9 @@ fn can_update_local_jwks() { jwks_uri_mock.assert(); // assert that the decoded token claims match the expected claims - assert_eq!(access_token_result, access_token_claims); - assert_eq!(id_token_result, id_token_claims); - assert_eq!(userinfo_token_result, userinfo_token_claims); + assert_eq!(result.access_token, access_token_claims); + assert_eq!(result.id_token, id_token_claims); + assert_eq!(result.userinfo_token, userinfo_token_claims); // verify that the OpenID configuration endpoints was called exactly once openid_conf_mock.assert(); diff --git a/jans-cedarling/cedarling/src/jwt/test/with_validation/userinfo_token.rs b/jans-cedarling/cedarling/src/jwt/test/with_validation/userinfo_token.rs index 6331ce9696f..c9abf4a1077 100644 --- a/jans-cedarling/cedarling/src/jwt/test/with_validation/userinfo_token.rs +++ b/jans-cedarling/cedarling/src/jwt/test/with_validation/userinfo_token.rs @@ -25,7 +25,8 @@ use super::super::*; use crate::common::policy_store::TrustedIssuer; -use crate::jwt::{self, JwtService}; +use crate::jwt::decoding_strategy::JwtDecodingError; +use crate::jwt::{self, JwtService, TrustedIssuerAndOpenIdConfig}; use jsonwebtoken::Algorithm; use serde_json::json; @@ -123,18 +124,20 @@ fn test_missing_claim(missing_claim: &str) { .with_status(200) .with_header("content-type", "application/json") .with_body(openid_config_response.to_string()) + .expect_at_least(1) + .expect_at_most(2) .create(); let jwks_uri_mock = server .mock("GET", "/jwks") .with_status(200) .with_header("content-type", "application/json") .with_body(jwks) + .expect_at_least(1) + .expect_at_most(2) .create(); - // initialize JwtService with validation enabled and ES256 as the supported algorithm - let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { - supported_algs: vec![Algorithm::ES256], - trusted_idps: vec![TrustedIssuer { + let trusted_idp = TrustedIssuerAndOpenIdConfig::fetch( + TrustedIssuer { name: "some_idp".to_string(), description: "some_desc".to_string(), openid_configuration_endpoint: format!( @@ -142,13 +145,21 @@ fn test_missing_claim(missing_claim: &str) { server.url() ), token_metadata: None, - }], + }, + &reqwest::blocking::Client::new(), + ) + .expect("openid config should be fetched successfully"); + // key service should fetch the jwks_uri on init + + // initialize JwtService with validation enabled and ES256 as the supported algorithm + let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { + supported_algs: vec![Algorithm::ES256], + trusted_idps: vec![trusted_idp], }); + // TODO: jwt service should not call openid config endpoint on init, because all data already has in the config // key service should fetch the jwks_uri on init openid_conf_mock.assert(); - // key service should fetch the jwks on init - jwks_uri_mock.assert(); // decode and validate the tokens let decode_result = jwt_service @@ -170,7 +181,7 @@ fn test_missing_claim(missing_claim: &str) { matches!( decode_result, Err(jwt::JwtServiceError::InvalidUserinfoToken( - jwt::JwtDecodingError::Validation(ref e) + JwtDecodingError::Validation(ref e) )) if matches!(e.kind(), jsonwebtoken::errors::ErrorKind::Json(json_err) if json_err.to_string().contains(&err_string)) ), @@ -183,7 +194,7 @@ fn test_missing_claim(missing_claim: &str) { matches!( decode_result, Err(jwt::JwtServiceError::InvalidUserinfoToken( - jwt::JwtDecodingError::Validation(ref e) + JwtDecodingError::Validation(ref e) )) if matches!( e.kind(), jsonwebtoken::errors::ErrorKind::MissingRequiredClaim(req_claim) if req_claim == missing_claim @@ -262,18 +273,20 @@ fn errors_on_invalid_signature() { .with_status(200) .with_header("content-type", "application/json") .with_body(openid_config_response.to_string()) + .expect_at_least(1) + .expect_at_most(2) .create(); let jwks_uri_mock = server .mock("GET", "/jwks") .with_status(200) .with_header("content-type", "application/json") .with_body(jwks) + .expect_at_least(1) + .expect_at_most(2) .create(); - // initialize JwtService with validation enabled and ES256 as the supported algorithm - let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { - supported_algs: vec![Algorithm::ES256], - trusted_idps: vec![TrustedIssuer { + let trusted_idp = TrustedIssuerAndOpenIdConfig::fetch( + TrustedIssuer { name: "some_idp".to_string(), description: "some_desc".to_string(), openid_configuration_endpoint: format!( @@ -281,7 +294,15 @@ fn errors_on_invalid_signature() { server.url() ), token_metadata: None, - }], + }, + &reqwest::blocking::Client::new(), + ) + .expect("openid config should be fetched successfully"); + + // initialize JwtService with validation enabled and ES256 as the supported algorithm + let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { + supported_algs: vec![Algorithm::ES256], + trusted_idps: vec![trusted_idp], }); // key service should fetch the jwks_uri on init @@ -302,7 +323,7 @@ fn errors_on_invalid_signature() { matches!( decode_result, Err(jwt::JwtServiceError::InvalidUserinfoToken( - jwt::JwtDecodingError::Validation(ref e) + JwtDecodingError::Validation(ref e) )) if matches!( e.kind(), jsonwebtoken::errors::ErrorKind::InvalidSignature @@ -372,18 +393,20 @@ fn errors_on_expired_token() { .with_status(200) .with_header("content-type", "application/json") .with_body(openid_config_response.to_string()) + .expect_at_least(1) + .expect_at_most(2) .create(); let jwks_uri_mock = server .mock("GET", "/jwks") .with_status(200) .with_header("content-type", "application/json") .with_body(jwks) + .expect_at_least(1) + .expect_at_most(2) .create(); - // initialize JwtService with validation enabled and ES256 as the supported algorithm - let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { - supported_algs: vec![Algorithm::ES256], - trusted_idps: vec![TrustedIssuer { + let trusted_idp = TrustedIssuerAndOpenIdConfig::fetch( + TrustedIssuer { name: "some_idp".to_string(), description: "some_desc".to_string(), openid_configuration_endpoint: format!( @@ -391,7 +414,15 @@ fn errors_on_expired_token() { server.url() ), token_metadata: None, - }], + }, + &reqwest::blocking::Client::new(), + ) + .expect("openid config should be fetched successfully"); + + // initialize JwtService with validation enabled and ES256 as the supported algorithm + let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { + supported_algs: vec![Algorithm::ES256], + trusted_idps: vec![trusted_idp], }); // key service should fetch the jwks_uri on init @@ -412,7 +443,7 @@ fn errors_on_expired_token() { matches!( decode_result, Err(jwt::JwtServiceError::InvalidUserinfoToken( - jwt::JwtDecodingError::Validation(ref e) + JwtDecodingError::Validation(ref e) )) if matches!( e.kind(), jsonwebtoken::errors::ErrorKind::ExpiredSignature @@ -483,18 +514,20 @@ fn errors_on_invalid_iss() { .with_status(200) .with_header("content-type", "application/json") .with_body(openid_config_response.to_string()) + .expect_at_least(1) + .expect_at_most(2) .create(); let jwks_uri_mock = server .mock("GET", "/jwks") .with_status(200) .with_header("content-type", "application/json") .with_body(jwks) + .expect_at_least(1) + .expect_at_most(2) .create(); - // initialize JwtService with validation enabled and ES256 as the supported algorithm - let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { - supported_algs: vec![Algorithm::ES256], - trusted_idps: vec![TrustedIssuer { + let trusted_idp = TrustedIssuerAndOpenIdConfig::fetch( + TrustedIssuer { name: "some_idp".to_string(), description: "some_desc".to_string(), openid_configuration_endpoint: format!( @@ -502,7 +535,15 @@ fn errors_on_invalid_iss() { server.url() ), token_metadata: None, - }], + }, + &reqwest::blocking::Client::new(), + ) + .expect("openid config should be fetched successfully"); + + // initialize JwtService with validation enabled and ES256 as the supported algorithm + let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { + supported_algs: vec![Algorithm::ES256], + trusted_idps: vec![trusted_idp], }); // key service should fetch the jwks_uri on init @@ -522,7 +563,7 @@ fn errors_on_invalid_iss() { matches!( decode_result, Err(jwt::JwtServiceError::InvalidUserinfoToken( - jwt::JwtDecodingError::Validation(ref e) + JwtDecodingError::Validation(ref e) )) if matches!( e.kind(), jsonwebtoken::errors::ErrorKind::InvalidIssuer @@ -593,18 +634,20 @@ fn errors_on_invalid_aud() { .with_status(200) .with_header("content-type", "application/json") .with_body(openid_config_response.to_string()) + .expect_at_least(1) + .expect_at_most(2) .create(); let jwks_uri_mock = server .mock("GET", "/jwks") .with_status(200) .with_header("content-type", "application/json") .with_body(jwks) + .expect_at_least(1) + .expect_at_most(2) .create(); - // initialize JwtService with validation enabled and ES256 as the supported algorithm - let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { - supported_algs: vec![Algorithm::ES256], - trusted_idps: vec![TrustedIssuer { + let trusted_idp = TrustedIssuerAndOpenIdConfig::fetch( + TrustedIssuer { name: "some_idp".to_string(), description: "some_desc".to_string(), openid_configuration_endpoint: format!( @@ -612,7 +655,15 @@ fn errors_on_invalid_aud() { server.url() ), token_metadata: None, - }], + }, + &reqwest::blocking::Client::new(), + ) + .expect("openid config should be fetched successfully"); + + // initialize JwtService with validation enabled and ES256 as the supported algorithm + let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { + supported_algs: vec![Algorithm::ES256], + trusted_idps: vec![trusted_idp], }); // key service should fetch the jwks_uri on init @@ -632,7 +683,7 @@ fn errors_on_invalid_aud() { matches!( decode_result, Err(jwt::JwtServiceError::InvalidUserinfoToken( - jwt::JwtDecodingError::Validation(ref e) + JwtDecodingError::Validation(ref e) )) if matches!( e.kind(), jsonwebtoken::errors::ErrorKind::InvalidAudience @@ -703,18 +754,20 @@ fn errors_on_invalid_sub() { .with_status(200) .with_header("content-type", "application/json") .with_body(openid_config_response.to_string()) + .expect_at_least(1) + .expect_at_most(2) .create(); let jwks_uri_mock = server .mock("GET", "/jwks") .with_status(200) .with_header("content-type", "application/json") .with_body(jwks) + .expect_at_least(1) + .expect_at_most(2) .create(); - // initialize JwtService with validation enabled and ES256 as the supported algorithm - let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { - supported_algs: vec![Algorithm::ES256], - trusted_idps: vec![TrustedIssuer { + let trusted_idp = TrustedIssuerAndOpenIdConfig::fetch( + TrustedIssuer { name: "some_idp".to_string(), description: "some_desc".to_string(), openid_configuration_endpoint: format!( @@ -722,7 +775,15 @@ fn errors_on_invalid_sub() { server.url() ), token_metadata: None, - }], + }, + &reqwest::blocking::Client::new(), + ) + .expect("openid config should be fetched successfully"); + + // initialize JwtService with validation enabled and ES256 as the supported algorithm + let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { + supported_algs: vec![Algorithm::ES256], + trusted_idps: vec![trusted_idp], }); // key service should fetch the jwks_uri on init @@ -742,7 +803,7 @@ fn errors_on_invalid_sub() { matches!( decode_result, Err(jwt::JwtServiceError::InvalidUserinfoToken( - jwt::JwtDecodingError::Validation(ref e) + JwtDecodingError::Validation(ref e) )) if matches!( e.kind(), jsonwebtoken::errors::ErrorKind::InvalidSubject, @@ -814,18 +875,20 @@ fn errors_on_token_used_before_nbf() { .with_status(200) .with_header("content-type", "application/json") .with_body(openid_config_response.to_string()) + .expect_at_least(1) + .expect_at_most(2) .create(); let jwks_uri_mock = server .mock("GET", "/jwks") .with_status(200) .with_header("content-type", "application/json") .with_body(jwks) + .expect_at_least(1) + .expect_at_most(2) .create(); - // initialize JwtService with validation enabled and ES256 as the supported algorithm - let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { - supported_algs: vec![Algorithm::ES256], - trusted_idps: vec![TrustedIssuer { + let trusted_idp = TrustedIssuerAndOpenIdConfig::fetch( + TrustedIssuer { name: "some_idp".to_string(), description: "some_desc".to_string(), openid_configuration_endpoint: format!( @@ -833,7 +896,15 @@ fn errors_on_token_used_before_nbf() { server.url() ), token_metadata: None, - }], + }, + &reqwest::blocking::Client::new(), + ) + .expect("openid config should be fetched successfully"); + + // initialize JwtService with validation enabled and ES256 as the supported algorithm + let jwt_service = JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithValidation { + supported_algs: vec![Algorithm::ES256], + trusted_idps: vec![trusted_idp], }); // key service should fetch the jwks_uri on init @@ -853,7 +924,7 @@ fn errors_on_token_used_before_nbf() { matches!( decode_result, Err(jwt::JwtServiceError::InvalidUserinfoToken( - jwt::JwtDecodingError::Validation(ref e) + JwtDecodingError::Validation(ref e) )) if matches!( e.kind(), jsonwebtoken::errors::ErrorKind::ImmatureSignature, diff --git a/jans-cedarling/cedarling/src/jwt/test/without_validation.rs b/jans-cedarling/cedarling/src/jwt/test/without_validation.rs index 4ce3fce4460..d8d0b51c744 100644 --- a/jans-cedarling/cedarling/src/jwt/test/without_validation.rs +++ b/jans-cedarling/cedarling/src/jwt/test/without_validation.rs @@ -5,6 +5,8 @@ * Copyright (c) 2024, Gluu, Inc. */ +use crate::jwt::JwtServiceConfig; + use super::{super::JwtService, *}; use serde_json::json; @@ -16,8 +18,9 @@ use serde_json::json; /// The decoded claims are compared to the expected claims to ensure correctness. fn can_decode_claims_without_validation() { // initialize JwtService with validation disabled - let jwt_service = - JwtService::new_with_config(crate::jwt::JwtServiceConfig::WithoutValidation {}); + let jwt_service = JwtService::new_with_config(JwtServiceConfig::WithoutValidation { + trusted_idps: Vec::new(), + }); // generate keys and setup the encoding keys and JWKS (JSON Web Key Set) let (encoding_keys, _jwks) = generate_keys(); @@ -52,7 +55,7 @@ fn can_decode_claims_without_validation() { let userinfo_token = generate_token_using_claims(&userinfo_token_claims, &encoding_keys[0]); // decode and validate both the access token and the ID token - let (access_token_result, id_token_result, userinfo_token_result) = jwt_service + let result = jwt_service .decode_tokens::( &access_token, &id_token, @@ -61,7 +64,7 @@ fn can_decode_claims_without_validation() { .expect("should decode token"); // assert that the decoded token claims match the expected claims - assert_eq!(access_token_result, access_token_claims); - assert_eq!(id_token_result, id_token_claims); - assert_eq!(userinfo_token_result, userinfo_token_claims); + assert_eq!(result.access_token, access_token_claims); + assert_eq!(result.id_token, id_token_claims); + assert_eq!(result.userinfo_token, userinfo_token_claims); } diff --git a/jans-cedarling/cedarling/src/log/log_entry.rs b/jans-cedarling/cedarling/src/log/log_entry.rs index b318c42ab72..2dd2d4a578f 100644 --- a/jans-cedarling/cedarling/src/log/log_entry.rs +++ b/jans-cedarling/cedarling/src/log/log_entry.rs @@ -107,16 +107,25 @@ pub struct AuthorizationLogInfo { pub person_principal: String, /// cedar-policy workload principal pub workload_principal: String, + /// cedar-policy `role` principal + pub role_principal: Option, /// cedar-policy user/person diagnostics information pub person_diagnostics: Diagnostics, /// cedar-policy workload diagnostics information pub workload_diagnostics: Diagnostics, + /// cedar-policy `role` diagnostics information + pub role_diagnostics: Option, /// cedar-policy user/person decision pub person_decision: Decision, /// cedar-policy workload decision pub workload_decision: Decision, + /// cedar-policy role decision + pub role_decision: Option, + + /// is authorized + pub authorized: bool, } /// Cedar-policy decision of the authorization diff --git a/jans-cedarling/cedarling/src/log/memory_logger.rs b/jans-cedarling/cedarling/src/log/memory_logger.rs index 707b2cd26cf..7a0cb27687f 100644 --- a/jans-cedarling/cedarling/src/log/memory_logger.rs +++ b/jans-cedarling/cedarling/src/log/memory_logger.rs @@ -112,12 +112,17 @@ mod tests { person_principal: "test_person_principal".to_string(), workload_principal: "test_workload_principal".to_string(), + role_principal: Some("test_role_principal".to_string()), person_diagnostics: Default::default(), workload_diagnostics: Default::default(), + role_diagnostics: Default::default(), person_decision: Decision::Allow, workload_decision: Decision::Allow, + role_decision: Some(Decision::Allow), + + authorized: true, }); let entry2 = LogEntry::new_with_data( app_types::PdpID::new(), diff --git a/jans-cedarling/cedarling/src/tests/cases_authorize_without_check_jwt.rs b/jans-cedarling/cedarling/src/tests/cases_authorize_without_check_jwt.rs new file mode 100644 index 00000000000..2bfc41600cb --- /dev/null +++ b/jans-cedarling/cedarling/src/tests/cases_authorize_without_check_jwt.rs @@ -0,0 +1,953 @@ +/* + * This software is available under the Apache-2.0 license. + * See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. + * + * Copyright (c) 2024, Gluu, Inc. + */ + +use super::utils::*; +use test_utils::assert_eq; + +static POLICY_STORE_RAW_YAML: &str = include_str!("../../../test_files/policy-store_ok_2.yaml"); + +/// Success test case where all check a successful +/// role field in the `userinfo_token` because we search here by default +/// role field is string. +/// +/// we check here that field are parsed from JWT tokens +/// and correctly executed using correct cedar-policy id +#[test] +fn success_test_role_string() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); + + // deserialize `Request` from json + let request = Request::deserialize(serde_json::json!( + { + "access_token": generate_token_using_claims(json!({ + "org_id": "some_long_id", + "jti": "some_jti", + "client_id": "some_client_id", + "iss": "some_iss", + "aud": "some_aud", + })), + "id_token": generate_token_using_claims(json!({ + "jti": "some_jti", + "iss": "some_iss", + "aud": "some_aud", + "sub": "some_sub", + })), + "userinfo_token": generate_token_using_claims(json!({ + "country": "US", + "sub": "some_sub", + "iss": "some_iss", + "client_id": "some_client_id", + "role": "Admin", + })), + "action": "Jans::Action::\"Update\"", + "resource": { + "id": "random_id", + "type": "Jans::Issue", + "org_id": "some_long_id", + "country": "US" + }, + "context": {}, + } + )) + .expect("Request should be deserialized from json"); + + let result = cedarling + .authorize(request) + .expect("request should be parsed without errors"); + + assert_eq!( + result.workload.decision(), + Decision::Allow, + "request result should be allowed for workload" + ); + assert_eq!( + result + .workload + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + vec!["1"], + "reason of permit workload should be '1'" + ); + assert_eq!( + result.person.decision(), + Decision::Allow, + "request result should be allowed for person" + ); + assert_eq!( + result + .person + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + vec!["2"], + "reason of permit person should be '2'" + ); + let role_result = result.role.as_ref().expect("role result should present"); + assert_eq!( + role_result.decision(), + Decision::Allow, + "request result should be allowed for role" + ); + assert_eq!( + role_result + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + vec!["3"], + "reason of permit role should be '3'" + ); + assert!(result.is_allowed(), "request result should be allowed"); +} + +/// Success test case where all check a successful +/// role field in the `userinfo_token` because we search here by default +/// role field is array of string. +/// +/// we check here that field are parsed from JWT tokens +/// and correctly executed using correct cedar-policy id +#[test] +fn success_test_role_array() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); + + // deserialize `Request` from json + let request = Request::deserialize(serde_json::json!( + { + "access_token": generate_token_using_claims(json!({ + "org_id": "some_long_id", + "jti": "some_jti", + "client_id": "some_client_id", + "iss": "some_iss", + "aud": "some_aud", + })), + "id_token": generate_token_using_claims(json!({ + "jti": "some_jti", + "iss": "some_iss", + "aud": "some_aud", + "sub": "some_sub", + })), + "userinfo_token": generate_token_using_claims(json!({ + "country": "US", + "sub": "some_sub", + "iss": "some_iss", + "client_id": "some_client_id", + "role": ["Admin"], + })), + "action": "Jans::Action::\"Update\"", + "resource": { + "id": "random_id", + "type": "Jans::Issue", + "org_id": "some_long_id", + "country": "US" + }, + "context": {}, + } + )) + .expect("Request should be deserialized from json"); + + let result = cedarling + .authorize(request) + .expect("request should be parsed without errors"); + + assert_eq!( + result.workload.decision(), + Decision::Allow, + "request result should be allowed for workload" + ); + assert_eq!( + result + .workload + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + vec!["1"], + "reason of permit workload should be '1'" + ); + assert_eq!( + result.person.decision(), + Decision::Allow, + "request result should be allowed for person" + ); + assert_eq!( + result + .person + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + vec!["2"], + "reason of permit person should be '2'" + ); + let role_result = result.role.as_ref().expect("role result should present"); + assert_eq!( + role_result.decision(), + Decision::Allow, + "request result should be allowed for role" + ); + assert_eq!( + role_result + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + vec!["3"], + "reason of permit role should be '3'" + ); + assert!(result.is_allowed(), "request result should be allowed"); +} + +/// Success test case where all check a successful +/// role field is not present +/// +/// we check here that field are parsed from JWT tokens +/// and correctly executed using correct cedar-policy id +/// if role field is not present, just ignore role check +#[test] +fn success_test_no_role() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); + + // deserialize `Request` from json + let request = Request::deserialize(serde_json::json!( + { + "access_token": generate_token_using_claims(json!({ + "org_id": "some_long_id", + "jti": "some_jti", + "client_id": "some_client_id", + "iss": "some_iss", + "aud": "some_aud", + })), + "id_token": generate_token_using_claims(json!({ + "jti": "some_jti", + "iss": "some_iss", + "aud": "some_aud", + "sub": "some_sub", + })), + "userinfo_token": generate_token_using_claims(json!({ + "country": "US", + "sub": "some_sub", + "iss": "some_iss", + "client_id": "some_client_id", + // comment role field (removed) + // "role": ["Admin"], + })), + "action": "Jans::Action::\"Update\"", + "resource": { + "id": "random_id", + "type": "Jans::Issue", + "org_id": "some_long_id", + "country": "US" + }, + "context": {}, + } + )) + .expect("Request should be deserialized from json"); + + let result = cedarling + .authorize(request) + .expect("request should be parsed without errors"); + + assert_eq!( + result.workload.decision(), + Decision::Allow, + "request result should be allowed for workload" + ); + assert_eq!( + result + .workload + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + vec!["1"], + "reason of permit workload should be '1'" + ); + assert_eq!( + result.person.decision(), + Decision::Allow, + "request result should be allowed for person" + ); + assert_eq!( + result + .person + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + vec!["2"], + "reason of permit person should be '2'" + ); + + assert!(result.role.is_none(), "result.role should be none"); + + assert!( + result.is_allowed(), + "request result should be allowed, because workload and user allowed" + ); +} + +/// Success test case where all check a successful +/// +/// we check here that field for `Jans::User` is present in `id_token` +/// it is `country` field of `Jans::User` +#[test] +fn success_test_user_data_in_id_token() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); + + // deserialize `Request` from json + let request = Request::deserialize(serde_json::json!( + { + "access_token": generate_token_using_claims(json!({ + "org_id": "some_long_id", + "jti": "some_jti", + "client_id": "some_client_id", + "iss": "some_iss", + "aud": "some_aud", + })), + "id_token": generate_token_using_claims(json!({ + "jti": "some_jti", + "iss": "some_iss", + "aud": "some_aud", + "sub": "some_sub", + "country": "US", + })), + "userinfo_token": generate_token_using_claims(json!({ + "sub": "some_sub", + "iss": "some_iss", + "client_id": "some_client_id", + "role": ["Admin"], + })), + "action": "Jans::Action::\"Update\"", + "resource": { + "id": "random_id", + "type": "Jans::Issue", + "org_id": "some_long_id", + "country": "US" + }, + "context": {}, + } + )) + .expect("Request should be deserialized from json"); + + let result = cedarling + .authorize(request) + .expect("request should be parsed without errors"); + + assert_eq!( + result.workload.decision(), + Decision::Allow, + "request result should be allowed for workload" + ); + assert_eq!( + result + .workload + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + vec!["1"], + "reason of permit workload should be '1'" + ); + assert_eq!( + result.person.decision(), + Decision::Allow, + "request result should be allowed for person" + ); + assert_eq!( + result + .person + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + vec!["2"], + "reason of permit person should be '2'" + ); + let role_result = result.role.as_ref().expect("role result should present"); + assert_eq!( + role_result.decision(), + Decision::Allow, + "request result should be allowed for role" + ); + assert_eq!( + role_result + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + vec!["3"], + "reason of permit role should be '3'" + ); + assert!(result.is_allowed(), "request result should be allowed"); +} + +// check all forbid +#[test] +fn all_forbid() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); + + // deserialize `Request` from json + let request = Request::deserialize(serde_json::json!( + { + "access_token": generate_token_using_claims(json!({ + // org_id different from resource + "org_id": "some_long_id_2", + "jti": "some_jti", + "client_id": "some_client_id", + "iss": "some_iss", + "aud": "some_aud", + })), + "id_token": generate_token_using_claims(json!({ + "jti": "some_jti", + "iss": "some_iss", + "aud": "some_aud", + "sub": "some_sub", + })), + "userinfo_token": generate_token_using_claims(json!({ + // country different from resource + "country": "UK", + "sub": "some_sub", + "iss": "some_iss", + "client_id": "some_client_id", + // role not Admin + "role": ["Guest"], + })), + "action": "Jans::Action::\"Update\"", + "resource": { + "id": "random_id", + "type": "Jans::Issue", + "org_id": "some_long_id", + "country": "US" + }, + "context": {}, + } + )) + .expect("Request should be deserialized from json"); + + let result = cedarling + .authorize(request) + .expect("request should be parsed without errors"); + + assert_eq!( + result.workload.decision(), + Decision::Deny, + "request result should be forbidden for workload" + ); + assert_eq!( + result + .workload + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + Vec::new() as Vec, + "reason of permit workload should be empty" + ); + assert_eq!( + result.person.decision(), + Decision::Deny, + "request result should be forbidden for person" + ); + assert_eq!( + result + .person + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + Vec::new() as Vec, + "reason of forbid person should empty, no forbid rule" + ); + let role_result = result.role.as_ref().expect("role result should present"); + assert_eq!( + role_result.decision(), + Decision::Deny, + "request result should be forbidden for role" + ); + assert_eq!( + role_result + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + Vec::new() as Vec, + "reason of forbid role should be empty" + ); + assert!(!result.is_allowed(), "request result should be not allowed"); +} + +// check only principal permit and other not +#[test] +fn only_principal_permit() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); + + // deserialize `Request` from json + let request = Request::deserialize(serde_json::json!( + { + "access_token": generate_token_using_claims(json!({ + "org_id": "some_long_id", + "jti": "some_jti", + "client_id": "some_client_id", + "iss": "some_iss", + "aud": "some_aud", + })), + "id_token": generate_token_using_claims(json!({ + "jti": "some_jti", + "iss": "some_iss", + "aud": "some_aud", + "sub": "some_sub", + })), + "userinfo_token": generate_token_using_claims(json!({ + // country different from resource + "country": "UK", + "sub": "some_sub", + "iss": "some_iss", + "client_id": "some_client_id", + // role not Admin + "role": ["Guest"], + })), + "action": "Jans::Action::\"Update\"", + "resource": { + "id": "random_id", + "type": "Jans::Issue", + "org_id": "some_long_id", + "country": "US" + }, + "context": {}, + } + )) + .expect("Request should be deserialized from json"); + + let result = cedarling + .authorize(request) + .expect("request should be parsed without errors"); + + assert_eq!( + result.workload.decision(), + Decision::Allow, + "request result should be allowed for workload" + ); + assert_eq!( + result + .workload + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + vec!["1"], + "reason of permit workload should be '1'" + ); + assert_eq!( + result.person.decision(), + Decision::Deny, + "request result should be forbidden for person" + ); + assert_eq!( + result + .person + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + Vec::new() as Vec, + "reason of forbid person should empty, no forbid rule" + ); + let role_result = result.role.as_ref().expect("role result should present"); + assert_eq!( + role_result.decision(), + Decision::Deny, + "request result should be forbidden for role" + ); + assert_eq!( + role_result + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + Vec::new() as Vec, + "reason of forbid role should be empty" + ); + assert!(!result.is_allowed(), "request result should be not allowed"); +} + +// check only person permit and other not +#[test] +fn only_person_permit() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); + + // deserialize `Request` from json + let request = Request::deserialize(serde_json::json!( + { + "access_token": generate_token_using_claims(json!({ + // org_id different from resource + "org_id": "some_long_id_2", + "jti": "some_jti", + "client_id": "some_client_id", + "iss": "some_iss", + "aud": "some_aud", + })), + "id_token": generate_token_using_claims(json!({ + "jti": "some_jti", + "iss": "some_iss", + "aud": "some_aud", + "sub": "some_sub", + })), + "userinfo_token": generate_token_using_claims(json!({ + // country different from resource + "country": "US", + "sub": "some_sub", + "iss": "some_iss", + "client_id": "some_client_id", + // role not Admin + "role": ["Guest"], + })), + "action": "Jans::Action::\"Update\"", + "resource": { + "id": "random_id", + "type": "Jans::Issue", + "org_id": "some_long_id", + "country": "US" + }, + "context": {}, + } + )) + .expect("Request should be deserialized from json"); + + let result = cedarling + .authorize(request) + .expect("request should be parsed without errors"); + + assert_eq!( + result.workload.decision(), + Decision::Deny, + "request result should be forbidden for workload" + ); + assert_eq!( + result + .workload + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + Vec::new() as Vec, + "reason of permit workload should be empty" + ); + assert_eq!( + result.person.decision(), + Decision::Allow, + "request result should be allowed for person" + ); + assert_eq!( + result + .person + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + vec!["2"], + "reason of forbid person should '2'" + ); + let role_result = result.role.as_ref().expect("role result should present"); + assert_eq!( + role_result.decision(), + Decision::Deny, + "request result should be forbidden for role" + ); + assert_eq!( + role_result + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + Vec::new() as Vec, + "reason of forbid role should be empty" + ); + assert!(!result.is_allowed(), "request result should be not allowed"); +} + +// check only role permit and other not +#[test] +fn only_role_permit() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); + + // deserialize `Request` from json + let request = Request::deserialize(serde_json::json!( + { + "access_token": generate_token_using_claims(json!({ + // org_id different from resource + "org_id": "some_long_id_2", + "jti": "some_jti", + "client_id": "some_client_id", + "iss": "some_iss", + "aud": "some_aud", + })), + "id_token": generate_token_using_claims(json!({ + "jti": "some_jti", + "iss": "some_iss", + "aud": "some_aud", + "sub": "some_sub", + })), + "userinfo_token": generate_token_using_claims(json!({ + // country different from resource + "country": "UK", + "sub": "some_sub", + "iss": "some_iss", + "client_id": "some_client_id", + "role": ["Admin"], + })), + "action": "Jans::Action::\"Update\"", + "resource": { + "id": "random_id", + "type": "Jans::Issue", + "org_id": "some_long_id", + "country": "US" + }, + "context": {}, + } + )) + .expect("Request should be deserialized from json"); + + let result = cedarling + .authorize(request) + .expect("request should be parsed without errors"); + + assert_eq!( + result.workload.decision(), + Decision::Deny, + "request result should be forbidden for workload" + ); + assert_eq!( + result + .workload + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + Vec::new() as Vec, + "reason of permit workload should be empty" + ); + assert_eq!( + result.person.decision(), + Decision::Deny, + "request result should be forbidden for person" + ); + assert_eq!( + result + .person + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + Vec::new() as Vec, + "reason of forbid person should empty, no forbid rule" + ); + let role_result = result.role.as_ref().expect("role result should present"); + assert_eq!( + role_result.decision(), + Decision::Allow, + "request result should be permit for role" + ); + assert_eq!( + role_result + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + vec!["3"], + "reason of permit role should be '3'" + ); + assert!(!result.is_allowed(), "request result should be not allowed"); +} + +// check only workload and person permit and role not +#[test] +fn only_workload_and_person_permit() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); + + // deserialize `Request` from json + let request = Request::deserialize(serde_json::json!( + { + "access_token": generate_token_using_claims(json!({ + "org_id": "some_long_id", + "jti": "some_jti", + "client_id": "some_client_id", + "iss": "some_iss", + "aud": "some_aud", + })), + "id_token": generate_token_using_claims(json!({ + "jti": "some_jti", + "iss": "some_iss", + "aud": "some_aud", + "sub": "some_sub", + })), + "userinfo_token": generate_token_using_claims(json!({ + "country": "US", + "sub": "some_sub", + "iss": "some_iss", + "client_id": "some_client_id", + // role not Admin + "role": ["Guest"], + })), + "action": "Jans::Action::\"Update\"", + "resource": { + "id": "random_id", + "type": "Jans::Issue", + "org_id": "some_long_id", + "country": "US" + }, + "context": {}, + } + )) + .expect("Request should be deserialized from json"); + + let result = cedarling + .authorize(request) + .expect("request should be parsed without errors"); + + assert_eq!( + result.workload.decision(), + Decision::Allow, + "request result should be allowed for workload" + ); + assert_eq!( + result + .workload + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + vec!["1"], + "reason of permit workload should be '1'" + ); + assert_eq!( + result.person.decision(), + Decision::Allow, + "request result should be allowed for person" + ); + assert_eq!( + result + .person + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + vec!["2"], + "reason of permit person should '2'" + ); + let role_result = result.role.as_ref().expect("role result should present"); + assert_eq!( + role_result.decision(), + Decision::Deny, + "request result should be forbidden for role" + ); + assert_eq!( + role_result + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + Vec::new() as Vec, + "reason of forbid role should be empty" + ); + assert!(result.is_allowed(), "request result should be allowed"); +} + +// check only workload and role permit and user not +#[test] +fn only_workload_and_role_permit() { + let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_RAW_YAML.to_string())); + + // deserialize `Request` from json + let request = Request::deserialize(serde_json::json!( + { + "access_token": generate_token_using_claims(json!({ + "org_id": "some_long_id", + "jti": "some_jti", + "client_id": "some_client_id", + "iss": "some_iss", + "aud": "some_aud", + })), + "id_token": generate_token_using_claims(json!({ + "jti": "some_jti", + "iss": "some_iss", + "aud": "some_aud", + "sub": "some_sub", + })), + "userinfo_token": generate_token_using_claims(json!({ + // country different from resource + "country": "UK", + "sub": "some_sub", + "iss": "some_iss", + "client_id": "some_client_id", + "role": ["Admin"], + })), + "action": "Jans::Action::\"Update\"", + "resource": { + "id": "random_id", + "type": "Jans::Issue", + "org_id": "some_long_id", + "country": "US" + }, + "context": {}, + } + )) + .expect("Request should be deserialized from json"); + + let result = cedarling + .authorize(request) + .expect("request should be parsed without errors"); + + assert_eq!( + result.workload.decision(), + Decision::Allow, + "request result should be allowed for workload" + ); + assert_eq!( + result + .workload + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + vec!["1"], + "reason of permit workload should be '1'" + ); + assert_eq!( + result.person.decision(), + Decision::Deny, + "request result should be not allowed for person" + ); + assert_eq!( + result + .person + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + Vec::new() as Vec, + "reason of forbid person should be none" + ); + let role_result = result.role.as_ref().expect("role result should present"); + assert_eq!( + role_result.decision(), + Decision::Allow, + "request result should be allowed for role" + ); + assert_eq!( + role_result + .diagnostics() + .reason() + .map(|policy_id| policy_id.to_string()) + .collect::>(), + vec!["3"], + "reason of permit role should be '3'" + ); + assert!(result.is_allowed(), "request result should be allowed"); +} diff --git a/jans-cedarling/cedarling/src/tests/mod.rs b/jans-cedarling/cedarling/src/tests/mod.rs index 84c4bc49a62..08d39789dda 100644 --- a/jans-cedarling/cedarling/src/tests/mod.rs +++ b/jans-cedarling/cedarling/src/tests/mod.rs @@ -7,136 +7,7 @@ //! This module is used for integration tests. -use serde::Deserialize; - -use crate::{ - BootstrapConfig, Cedarling, JwtConfig, LogConfig, LogTypeConfig, PolicyStoreConfig, - PolicyStoreSource, Request, -}; - -// JSON payload of access token -// { -// "sub": "boG8dfc5MKTn37o7gsdCeyqL8LpWQtgoO41m1KZwdq0", -// "code": "bf1934f6-3905-420a-8299-6b2e3ffddd6e", -// "iss": "https://admin-ui-test.gluu.org", -// "token_type": "Bearer", -// "client_id": "5b4487c4-8db1-409d-a653-f907b8094039", -// "aud": "5b4487c4-8db1-409d-a653-f907b8094039", -// "acr": "basic", -// "x5t#S256": "", -// "scope": [ -// "openid", -// "profile" -// ], -// "org_id": "some_long_id", -// "auth_time": 1724830746, -// "exp": 1724945978, -// "iat": 1724832259, -// "jti": "lxTmCVRFTxOjJgvEEpozMQ", -// "name": "Default Admin User", -// "status": { -// "status_list": { -// "idx": 201, -// "uri": "https://admin-ui-test.gluu.org/jans-auth/restv1/status_list" -// } -// } -// } -const ACCESS_TOKEN: &str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJib0c4ZGZjNU1LVG4zN283Z3NkQ2V5cUw4THBXUXRnb080MW0xS1p3ZHEwIiwiY29kZSI6ImJmMTkzNGY2LTM5MDUtNDIwYS04Mjk5LTZiMmUzZmZkZGQ2ZSIsImlzcyI6Imh0dHBzOi8vYWRtaW4tdWktdGVzdC5nbHV1Lm9yZyIsInRva2VuX3R5cGUiOiJCZWFyZXIiLCJjbGllbnRfaWQiOiI1YjQ0ODdjNC04ZGIxLTQwOWQtYTY1My1mOTA3YjgwOTQwMzkiLCJhdWQiOiI1YjQ0ODdjNC04ZGIxLTQwOWQtYTY1My1mOTA3YjgwOTQwMzkiLCJhY3IiOiJiYXNpYyIsIng1dCNTMjU2IjoiIiwic2NvcGUiOlsib3BlbmlkIiwicHJvZmlsZSJdLCJvcmdfaWQiOiJzb21lX2xvbmdfaWQiLCJhdXRoX3RpbWUiOjE3MjQ4MzA3NDYsImV4cCI6MTcyNDk0NTk3OCwiaWF0IjoxNzI0ODMyMjU5LCJqdGkiOiJseFRtQ1ZSRlR4T2pKZ3ZFRXBvek1RIiwibmFtZSI6IkRlZmF1bHQgQWRtaW4gVXNlciIsInN0YXR1cyI6eyJzdGF0dXNfbGlzdCI6eyJpZHgiOjIwMSwidXJpIjoiaHR0cHM6Ly9hZG1pbi11aS10ZXN0LmdsdXUub3JnL2phbnMtYXV0aC9yZXN0djEvc3RhdHVzX2xpc3QifX19._eQT-DsfE_kgdhA0YOyFxxPEMNw44iwoelWa5iU1n9s"; - -// JSON payload of id token -// { -// "acr": "basic", -// "amr": "10", -// "aud": "5b4487c4-8db1-409d-a653-f907b8094039", -// "exp": 1724835859, -// "iat": 1724832259, -// "sub": "boG8dfc5MKTn37o7gsdCeyqL8LpWQtgoO41m1KZwdq0", -// "iss": "https://admin-ui-test.gluu.org", -// "jti": "sk3T40NYSYuk5saHZNpkZw", -// "nonce": "c3872af9-a0f5-4c3f-a1af-f9d0e8846e81", -// "sid": "6a7fe50a-d810-454d-be5d-549d29595a09", -// "jansOpenIDConnectVersion": "openidconnect-1.0", -// "c_hash": "pGoK6Y_RKcWHkUecM9uw6Q", -// "auth_time": 1724830746, -// "grant": "authorization_code", -// "status": { -// "status_list": { -// "idx": 202, -// "uri": "https://admin-ui-test.gluu.org/jans-auth/restv1/status_list" -// } -// } -// } -const ID_TOKEN: &str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY3IiOiJiYXNpYyIsImFtciI6IjEwIiwiYXVkIjoiNWI0NDg3YzQtOGRiMS00MDlkLWE2NTMtZjkwN2I4MDk0MDM5IiwiZXhwIjoxNzI0ODM1ODU5LCJpYXQiOjE3MjQ4MzIyNTksInN1YiI6ImJvRzhkZmM1TUtUbjM3bzdnc2RDZXlxTDhMcFdRdGdvTzQxbTFLWndkcTAiLCJpc3MiOiJodHRwczovL2FkbWluLXVpLXRlc3QuZ2x1dS5vcmciLCJqdGkiOiJzazNUNDBOWVNZdWs1c2FIWk5wa1p3Iiwibm9uY2UiOiJjMzg3MmFmOS1hMGY1LTRjM2YtYTFhZi1mOWQwZTg4NDZlODEiLCJzaWQiOiI2YTdmZTUwYS1kODEwLTQ1NGQtYmU1ZC01NDlkMjk1OTVhMDkiLCJqYW5zT3BlbklEQ29ubmVjdFZlcnNpb24iOiJvcGVuaWRjb25uZWN0LTEuMCIsImNfaGFzaCI6InBHb0s2WV9SS2NXSGtVZWNNOXV3NlEiLCJhdXRoX3RpbWUiOjE3MjQ4MzA3NDYsImdyYW50IjoiYXV0aG9yaXphdGlvbl9jb2RlIiwic3RhdHVzIjp7InN0YXR1c19saXN0Ijp7ImlkeCI6MjAyLCJ1cmkiOiJodHRwczovL2FkbWluLXVpLXRlc3QuZ2x1dS5vcmcvamFucy1hdXRoL3Jlc3R2MS9zdGF0dXNfbGlzdCJ9fX0.8BwLLGkFpWGx8wGpvVmNk_Ao8nZrP_WT-zoo-MY4zqY"; - -// JSON payload of userinfo token -// { -// "country": "US", -// "email": "user@example.com", -// "username": "UserNameExample", -// "sub": "boG8dfc5MKTn37o7gsdCeyqL8LpWQtgoO41m1KZwdq0", -// "iss": "https://admin-ui-test.gluu.org", -// "given_name": "Admin", -// "middle_name": "Admin", -// "inum": "8d1cde6a-1447-4766-b3c8-16663e13b458", -// "client_id": "5b4487c4-8db1-409d-a653-f907b8094039", -// "aud": "5b4487c4-8db1-409d-a653-f907b8094039", -// "updated_at": 1724778591, -// "name": "Default Admin User", -// "nickname": "Admin", -// "family_name": "User", -// "jti": "faiYvaYIT0cDAT7Fow0pQw", -// "jansAdminUIRole": [ -// "api-admin" -// ], -// "exp": 1724945978 -// } -const USERINFO_TOKEN: &str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb3VudHJ5IjoiVVMiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJ1c2VybmFtZSI6IlVzZXJOYW1lRXhhbXBsZSIsInN1YiI6ImJvRzhkZmM1TUtUbjM3bzdnc2RDZXlxTDhMcFdRdGdvTzQxbTFLWndkcTAiLCJpc3MiOiJodHRwczovL2FkbWluLXVpLXRlc3QuZ2x1dS5vcmciLCJnaXZlbl9uYW1lIjoiQWRtaW4iLCJtaWRkbGVfbmFtZSI6IkFkbWluIiwiaW51bSI6IjhkMWNkZTZhLTE0NDctNDc2Ni1iM2M4LTE2NjYzZTEzYjQ1OCIsImNsaWVudF9pZCI6IjViNDQ4N2M0LThkYjEtNDA5ZC1hNjUzLWY5MDdiODA5NDAzOSIsImF1ZCI6IjViNDQ4N2M0LThkYjEtNDA5ZC1hNjUzLWY5MDdiODA5NDAzOSIsInVwZGF0ZWRfYXQiOjE3MjQ3Nzg1OTEsIm5hbWUiOiJEZWZhdWx0IEFkbWluIFVzZXIiLCJuaWNrbmFtZSI6IkFkbWluIiwiZmFtaWx5X25hbWUiOiJVc2VyIiwianRpIjoiZmFpWXZhWUlUMGNEQVQ3Rm93MHBRdyIsImphbnNBZG1pblVJUm9sZSI6WyJhcGktYWRtaW4iXSwiZXhwIjoxNzI0OTQ1OTc4fQ.3LTc8YLvEeb7ONZp_FKA7yPP7S6e_VTzwhvAWUJrL4M"; - -// The human-readable policy and schema file is located in next folder: -// `test_files\policy-store_ok` -static POLICY_STORE_RAW: &str = include_str!("../../../test_files/policy-store_ok.json"); - -/// Test success scenario without authorization -// test duplicate code of example file `authorize.rs` (authorization without JWT validation) -#[test] -fn success_test() { - let cedarling = Cedarling::new(BootstrapConfig { - application_name: "test_app".to_string(), - log_config: LogConfig { - log_type: LogTypeConfig::StdOut, - }, - policy_store_config: PolicyStoreConfig { - source: PolicyStoreSource::Json(POLICY_STORE_RAW.to_string()), - }, - jwt_config: JwtConfig::Disabled, - }) - .expect("boostrap config should initialize correctly"); - - // deserialize `Request` from json - let request = Request::deserialize(serde_json::json!( - { - "access_token": ACCESS_TOKEN, - "id_token": ID_TOKEN, - "userinfo_token": USERINFO_TOKEN, - "action": "Jans::Action::\"Update\"", - "resource": { - "id": "random_id", - "type": "Jans::Issue", - "org_id": "some_long_id", - "country": "US" - }, - "context": {}, - } - )) - .expect("Request should be deserialized from json"); - - let result = cedarling - .authorize(request) - .expect("request should be parsed without errors"); - - assert!(result.is_allowed(), "request result should be allowed"); -} - -// TODO: add fail test case -// To test all failure scenarios, we need to create a special issue, as there are many possible cases to consider. +mod utils; +mod success_test_json; +mod cases_authorize_without_check_jwt; diff --git a/jans-cedarling/cedarling/src/tests/success_test_json.rs b/jans-cedarling/cedarling/src/tests/success_test_json.rs new file mode 100644 index 00000000000..2344469c93d --- /dev/null +++ b/jans-cedarling/cedarling/src/tests/success_test_json.rs @@ -0,0 +1,111 @@ +/* + * This software is available under the Apache-2.0 license. + * See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. + * + * Copyright (c) 2024, Gluu, Inc. + */ + +use super::utils::*; + +/// Test success scenario wiht authorization +// test duplicate code of example file `authorize.rs` (authorization without JWT validation) +#[test] +fn success_test_json() { + // The human-readable policy and schema file is located in next folder: + // `test_files\policy-store_ok` + // Is used to check that the JSON policy is loaded correctly + static POLICY_STORE_RAW_JSON: &str = include_str!("../../../test_files/policy-store_ok.json"); + + let cedarling = get_cedarling(PolicyStoreSource::Json(POLICY_STORE_RAW_JSON.to_string())); + + // deserialize `Request` from json + let request = Request::deserialize(serde_json::json!( + { + "access_token": generate_token_using_claims(json!({ + "sub": "boG8dfc5MKTn37o7gsdCeyqL8LpWQtgoO41m1KZwdq0", + "code": "bf1934f6-3905-420a-8299-6b2e3ffddd6e", + "iss": "https://admin-ui-test.gluu.org", + "token_type": "Bearer", + "client_id": "5b4487c4-8db1-409d-a653-f907b8094039", + "aud": "5b4487c4-8db1-409d-a653-f907b8094039", + "acr": "basic", + "x5t#S256": "", + "scope": [ + "openid", + "profile" + ], + "org_id": "some_long_id", + "auth_time": 1724830746, + "exp": 1724945978, + "iat": 1724832259, + "jti": "lxTmCVRFTxOjJgvEEpozMQ", + "name": "Default Admin User", + "status": { + "status_list": { + "idx": 201, + "uri": "https://admin-ui-test.gluu.org/jans-auth/restv1/status_list" + } + } + })), + "id_token": generate_token_using_claims(json!({ + "acr": "basic", + "amr": "10", + "aud": "5b4487c4-8db1-409d-a653-f907b8094039", + "exp": 1724835859, + "iat": 1724832259, + "sub": "boG8dfc5MKTn37o7gsdCeyqL8LpWQtgoO41m1KZwdq0", + "iss": "https://admin-ui-test.gluu.org", + "jti": "sk3T40NYSYuk5saHZNpkZw", + "nonce": "c3872af9-a0f5-4c3f-a1af-f9d0e8846e81", + "sid": "6a7fe50a-d810-454d-be5d-549d29595a09", + "jansOpenIDConnectVersion": "openidconnect-1.0", + "c_hash": "pGoK6Y_RKcWHkUecM9uw6Q", + "auth_time": 1724830746, + "grant": "authorization_code", + "status": { + "status_list": { + "idx": 202, + "uri": "https://admin-ui-test.gluu.org/jans-auth/restv1/status_list" + } + }, + "role":"Admin" + })), + "userinfo_token": generate_token_using_claims(json!({ + "country": "US", + "email": "user@example.com", + "username": "UserNameExample", + "sub": "boG8dfc5MKTn37o7gsdCeyqL8LpWQtgoO41m1KZwdq0", + "iss": "https://admin-ui-test.gluu.org", + "given_name": "Admin", + "middle_name": "Admin", + "inum": "8d1cde6a-1447-4766-b3c8-16663e13b458", + "client_id": "5b4487c4-8db1-409d-a653-f907b8094039", + "aud": "5b4487c4-8db1-409d-a653-f907b8094039", + "updated_at": 1724778591, + "name": "Default Admin User", + "nickname": "Admin", + "family_name": "User", + "jti": "faiYvaYIT0cDAT7Fow0pQw", + "jansAdminUIRole": [ + "api-admin" + ], + "exp": 1724945978 + })), + "action": "Jans::Action::\"Update\"", + "resource": { + "id": "random_id", + "type": "Jans::Issue", + "org_id": "some_long_id", + "country": "US" + }, + "context": {}, + } + )) + .expect("Request should be deserialized from json"); + + let result = cedarling + .authorize(request) + .expect("request should be parsed without errors"); + + assert!(result.is_allowed(), "request result should be allowed"); +} diff --git a/jans-cedarling/cedarling/src/tests/utils.rs b/jans-cedarling/cedarling/src/tests/utils.rs new file mode 100644 index 00000000000..2fd9144d0b3 --- /dev/null +++ b/jans-cedarling/cedarling/src/tests/utils.rs @@ -0,0 +1,95 @@ +/* + * This software is available under the Apache-2.0 license. + * See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. + * + * Copyright (c) 2024, Gluu, Inc. + */ + +pub use crate::{ + BootstrapConfig, Cedarling, JwtConfig, LogConfig, LogTypeConfig, PolicyStoreConfig, + PolicyStoreSource, Request, +}; +pub use serde::Deserialize; + +pub use cedar_policy::Decision; +use jsonwebkey as jwk; +use jsonwebtoken as jwt; +pub use serde_json::json; + +use lazy_static::lazy_static; + +// Represent meta information about entity from cedar-policy schema. +lazy_static! { + pub(crate) static ref EncodingKeys: GeneratedKeys = generate_keys(); +} + +pub(crate) struct GeneratedKeys { + pub private_key_id: String, + pub private_encoding_key: jwt::EncodingKey, +} + +/// Generates a set of private and public keys using ES256 +/// +/// Returns a tuple: (Vec<(key_id, private_key)>, jwks) +pub fn generate_keys() -> GeneratedKeys { + let kid = 1; + // Generate a private key + let mut jwk = jwk::JsonWebKey::new(jwk::Key::generate_p256()); + jwk.set_algorithm(jwk::Algorithm::ES256) + .expect("should set encryption algorithm"); + jwk.key_id = Some("some_id".to_string()); + + // Generate public key + let mut public_key = + serde_json::to_value(jwk.key.to_public()).expect("should serialize public key"); + public_key["kid"] = serde_json::Value::String(kid.to_string()); // set `kid` + let public_key: jwt::jwk::Jwk = + serde_json::from_value(public_key).expect("should deserialize public key"); + + let private_key = jwt::EncodingKey::from_ec_pem(jwk.key.to_pem().as_bytes()) + .expect("should generate encoding key"); + + let public_keys = jwt::jwk::JwkSet { + keys: vec![public_key], + }; + let _public_keys = serde_json::to_string(&public_keys).expect("should serialize keyset"); + + GeneratedKeys { + private_key_id: kid.to_string(), + private_encoding_key: private_key, + } +} + +/// Generates a token string signed with ES256 +pub fn generate_token_using_claims(claims: impl serde::Serialize) -> String { + let key_id = EncodingKeys.private_key_id.clone(); + let encoding_key = &EncodingKeys.private_encoding_key; + + // select a key from the keyset + // for simplicity, were just choosing the second one + + // specify the header + let header = jwt::Header { + alg: jwt::Algorithm::ES256, + kid: Some(key_id.to_string()), + ..Default::default() + }; + + // serialize token to a string + jwt::encode(&header, &claims, encoding_key).expect("should generate token") +} + +/// create [`Cedarling`] from [`PolicyStoreSource`] +pub fn get_cedarling(policy_source: PolicyStoreSource) -> Cedarling { + Cedarling::new(BootstrapConfig { + application_name: "test_app".to_string(), + log_config: LogConfig { + log_type: LogTypeConfig::StdOut, + }, + policy_store_config: PolicyStoreConfig { + source: policy_source, + }, + jwt_config: JwtConfig::Disabled, + }) + .expect("boostrap config should initialize correctly") +} diff --git a/jans-cedarling/test_files/policy-store_blobby.json b/jans-cedarling/test_files/policy-store_blobby.json index f4cb7b7dc87..503b4cbb249 100644 --- a/jans-cedarling/test_files/policy-store_blobby.json +++ b/jans-cedarling/test_files/policy-store_blobby.json @@ -2,16 +2,16 @@ "cedar_version": "v2.4.7", "cedar_policies": { "840da5d85403f35ea76519ed1a18a33989f855bf1cf8": { - "description": "simple policy example for pricipal workload", + "description": "simple policy example for principal workload", "creation_date": "2024-09-20T17:22:39.996050", "policy_content": { "encoding": "base64", "content_type": "cedar-json", - "body":"ewogICAgInN0YXRpY1BvbGljaWVzIjogewogICAgICAgICJwb2xpY3kwIjogewogICAgICAgICAgICAiYWN0aW9uIjogewogICAgICAgICAgICAgICAgImVudGl0eSI6IHsKICAgICAgICAgICAgICAgICAgICAiaWQiOiAiVXBkYXRlIiwKICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJKYW5zOjpBY3Rpb24iCiAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICAgIm9wIjogImluIgogICAgICAgICAgICB9LAogICAgICAgICAgICAiY29uZGl0aW9ucyI6IFsKICAgICAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgICAgICAiYm9keSI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgIj09IjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgImxlZnQiOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIi4iOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJhdHRyIjogIm9yZ19pZCIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJsZWZ0IjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIlZhciI6ICJwcmluY2lwYWwiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgInJpZ2h0IjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICIuIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiYXR0ciI6ICJvcmdfaWQiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAibGVmdCI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJWYXIiOiAicmVzb3VyY2UiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICAgICAgICJraW5kIjogIndoZW4iCiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJlZmZlY3QiOiAicGVybWl0IiwKICAgICAgICAgICAgInByaW5jaXBhbCI6IHsKICAgICAgICAgICAgICAgICJlbnRpdHlfdHlwZSI6ICJKYW5zOjpXb3JrbG9hZCIsCiAgICAgICAgICAgICAgICAib3AiOiAiaXMiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJyZXNvdXJjZSI6IHsKICAgICAgICAgICAgICAgICJlbnRpdHlfdHlwZSI6ICJKYW5zOjpJc3N1ZSIsCiAgICAgICAgICAgICAgICAib3AiOiAiaXMiCiAgICAgICAgICAgIH0KICAgICAgICB9CiAgICB9LAogICAgInRlbXBsYXRlTGlua3MiOiBbXSwKICAgICJ0ZW1wbGF0ZXMiOiB7fQp9" + "body": "ewogICAgInN0YXRpY1BvbGljaWVzIjogewogICAgICAgICJwb2xpY3kwIjogewogICAgICAgICAgICAiYWN0aW9uIjogewogICAgICAgICAgICAgICAgImVudGl0eSI6IHsKICAgICAgICAgICAgICAgICAgICAiaWQiOiAiVXBkYXRlIiwKICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJKYW5zOjpBY3Rpb24iCiAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICAgIm9wIjogImluIgogICAgICAgICAgICB9LAogICAgICAgICAgICAiY29uZGl0aW9ucyI6IFsKICAgICAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgICAgICAiYm9keSI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgIj09IjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgImxlZnQiOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIi4iOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJhdHRyIjogIm9yZ19pZCIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJsZWZ0IjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIlZhciI6ICJwcmluY2lwYWwiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgInJpZ2h0IjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICIuIjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiYXR0ciI6ICJvcmdfaWQiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAibGVmdCI6IHsKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJWYXIiOiAicmVzb3VyY2UiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICAgICAgICJraW5kIjogIndoZW4iCiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJlZmZlY3QiOiAicGVybWl0IiwKICAgICAgICAgICAgInByaW5jaXBhbCI6IHsKICAgICAgICAgICAgICAgICJlbnRpdHlfdHlwZSI6ICJKYW5zOjpXb3JrbG9hZCIsCiAgICAgICAgICAgICAgICAib3AiOiAiaXMiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJyZXNvdXJjZSI6IHsKICAgICAgICAgICAgICAgICJlbnRpdHlfdHlwZSI6ICJKYW5zOjpJc3N1ZSIsCiAgICAgICAgICAgICAgICAib3AiOiAiaXMiCiAgICAgICAgICAgIH0KICAgICAgICB9CiAgICB9LAogICAgInRlbXBsYXRlTGlua3MiOiBbXSwKICAgICJ0ZW1wbGF0ZXMiOiB7fQp9" } }, "444da5d85403f35ea76519ed1a18a33989f855bf1cf8": { - "description": "simple policy example for pricipal user", + "description": "simple policy example for principal user", "creation_date": "2024-09-20T17:22:39.996050", "policy_content": { "encoding": "none", @@ -21,8 +21,8 @@ } }, "cedar_schema": { - "encoding": "none", - "content_type": "cedar", - "body": "namespace Jans {\ntype Url = {\"host\": String, \"path\": String, \"protocol\": String};\nentity Access_token = {\"aud\": String, \"exp\": Long, \"iat\": Long, \"iss\": TrustedIssuer, \"jti\": String};\nentity Issue = {\"country\": String, \"org_id\": String};\nentity TrustedIssuer = {\"issuer_entity_id\": Url};\nentity User = {\"country\": String, \"email\": String, \"sub\": String, \"username\": String};\nentity Workload = {\"client_id\": String, \"iss\": TrustedIssuer, \"name\": String, \"org_id\": String};\nentity id_token = {\"acr\": String, \"amr\": String, \"aud\": String, \"exp\": Long, \"iat\": Long, \"iss\": TrustedIssuer, \"jti\": String, \"sub\": String};\naction \"Update\" appliesTo {\n principal: [Workload, User],\n resource: [Issue],\n context: {}\n};\n}\n" + "encoding": "none", + "content_type": "cedar", + "body": "namespace Jans {\ntype Url = {\"host\": String, \"path\": String, \"protocol\": String};\nentity Access_token = {\"aud\": String, \"exp\": Long, \"iat\": Long, \"iss\": TrustedIssuer, \"jti\": String};\nentity Issue = {\"country\": String, \"org_id\": String};\nentity TrustedIssuer = {\"issuer_entity_id\": Url};\nentity User = {\"country\": String, \"email\": String, \"sub\": String, \"username\": String};\nentity Workload = {\"client_id\": String, \"iss\": TrustedIssuer, \"name\": String, \"org_id\": String};\nentity id_token = {\"acr\": String, \"amr\": String, \"aud\": String, \"exp\": Long, \"iat\": Long, \"iss\": TrustedIssuer, \"jti\": String, \"sub\": String};\naction \"Update\" appliesTo {\n principal: [Workload, User],\n resource: [Issue],\n context: {}\n};\n}\n" } -} +} \ No newline at end of file diff --git a/jans-cedarling/test_files/policy-store_ok.json b/jans-cedarling/test_files/policy-store_ok.json index d3a2337a36a..87cac6a8212 100644 --- a/jans-cedarling/test_files/policy-store_ok.json +++ b/jans-cedarling/test_files/policy-store_ok.json @@ -2,15 +2,15 @@ "cedar_version": "v2.4.7", "cedar_policies": { "840da5d85403f35ea76519ed1a18a33989f855bf1cf8": { - "description": "simple policy example for pricipal workload", + "description": "simple policy example for principal workload", "creation_date": "2024-09-20T17:22:39.996050", "policy_content": "cGVybWl0KAogICAgcHJpbmNpcGFsIGlzIEphbnM6Oldvcmtsb2FkLAogICAgYWN0aW9uIGluIFtKYW5zOjpBY3Rpb246OiJVcGRhdGUiXSwKICAgIHJlc291cmNlIGlzIEphbnM6Oklzc3VlCil3aGVuewogICAgcHJpbmNpcGFsLm9yZ19pZCA9PSByZXNvdXJjZS5vcmdfaWQKfTs=" }, "444da5d85403f35ea76519ed1a18a33989f855bf1cf8": { - "description": "simple policy example for pricipal user", + "description": "simple policy example for principal user", "creation_date": "2024-09-20T17:22:39.996050", "policy_content": "cGVybWl0KAogICAgcHJpbmNpcGFsIGlzIEphbnM6OlVzZXIsCiAgICBhY3Rpb24gaW4gW0phbnM6OkFjdGlvbjo6IlVwZGF0ZSJdLAogICAgcmVzb3VyY2UgaXMgSmFuczo6SXNzdWUKKXdoZW57CiAgICBwcmluY2lwYWwuY291bnRyeSA9PSByZXNvdXJjZS5jb3VudHJ5Cn07" } }, - "cedar_schema": "eyJKYW5zIjp7ImNvbW1vblR5cGVzIjp7IlVybCI6eyJ0eXBlIjoiUmVjb3JkIiwiYXR0cmlidXRlcyI6eyJob3N0Ijp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJTdHJpbmcifSwicGF0aCI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiU3RyaW5nIn0sInByb3RvY29sIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJTdHJpbmcifX19fSwiZW50aXR5VHlwZXMiOnsiQWNjZXNzX3Rva2VuIjp7InNoYXBlIjp7InR5cGUiOiJSZWNvcmQiLCJhdHRyaWJ1dGVzIjp7ImF1ZCI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiU3RyaW5nIn0sImV4cCI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiTG9uZyJ9LCJpYXQiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IkxvbmcifSwiaXNzIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJUcnVzdGVkSXNzdWVyIn0sImp0aSI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiU3RyaW5nIn19fX0sIklzc3VlIjp7InNoYXBlIjp7InR5cGUiOiJSZWNvcmQiLCJhdHRyaWJ1dGVzIjp7ImNvdW50cnkiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJvcmdfaWQiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9fX19LCJUcnVzdGVkSXNzdWVyIjp7InNoYXBlIjp7InR5cGUiOiJSZWNvcmQiLCJhdHRyaWJ1dGVzIjp7Imlzc3Vlcl9lbnRpdHlfaWQiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlVybCJ9fX19LCJVc2VyIjp7InNoYXBlIjp7InR5cGUiOiJSZWNvcmQiLCJhdHRyaWJ1dGVzIjp7ImNvdW50cnkiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJlbWFpbCI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiU3RyaW5nIn0sInN1YiI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiU3RyaW5nIn0sInVzZXJuYW1lIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJTdHJpbmcifX19fSwiV29ya2xvYWQiOnsic2hhcGUiOnsidHlwZSI6IlJlY29yZCIsImF0dHJpYnV0ZXMiOnsiY2xpZW50X2lkIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJTdHJpbmcifSwiaXNzIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJUcnVzdGVkSXNzdWVyIn0sIm5hbWUiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJvcmdfaWQiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9fX19LCJpZF90b2tlbiI6eyJzaGFwZSI6eyJ0eXBlIjoiUmVjb3JkIiwiYXR0cmlidXRlcyI6eyJhY3IiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJhbXIiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJhdWQiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJleHAiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IkxvbmcifSwiaWF0Ijp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJMb25nIn0sImlzcyI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiVHJ1c3RlZElzc3VlciJ9LCJqdGkiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJzdWIiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9fX19fSwiYWN0aW9ucyI6eyJVcGRhdGUiOnsiYXBwbGllc1RvIjp7InJlc291cmNlVHlwZXMiOlsiSXNzdWUiXSwicHJpbmNpcGFsVHlwZXMiOlsiV29ya2xvYWQiLCJVc2VyIl19fX19fQo=" -} + "cedar_schema": "eyJKYW5zIjp7ImNvbW1vblR5cGVzIjp7IlVybCI6eyJ0eXBlIjoiUmVjb3JkIiwiYXR0cmlidXRlcyI6eyJob3N0Ijp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJTdHJpbmcifSwicGF0aCI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiU3RyaW5nIn0sInByb3RvY29sIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJTdHJpbmcifX19fSwiZW50aXR5VHlwZXMiOnsiVHJ1c3RlZElzc3VlciI6eyJzaGFwZSI6eyJ0eXBlIjoiUmVjb3JkIiwiYXR0cmlidXRlcyI6eyJpc3N1ZXJfZW50aXR5X2lkIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJVcmwifX19fSwiV29ya2xvYWQiOnsic2hhcGUiOnsidHlwZSI6IlJlY29yZCIsImF0dHJpYnV0ZXMiOnsiY2xpZW50X2lkIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJTdHJpbmcifSwiaXNzIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJUcnVzdGVkSXNzdWVyIn0sIm5hbWUiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJvcmdfaWQiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9fX19LCJVc2VyIjp7Im1lbWJlck9mVHlwZXMiOlsiUm9sZSJdLCJzaGFwZSI6eyJ0eXBlIjoiUmVjb3JkIiwiYXR0cmlidXRlcyI6eyJjb3VudHJ5Ijp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJTdHJpbmcifSwiZW1haWwiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJzdWIiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJ1c2VybmFtZSI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiU3RyaW5nIn19fX0sIkFjY2Vzc190b2tlbiI6eyJzaGFwZSI6eyJ0eXBlIjoiUmVjb3JkIiwiYXR0cmlidXRlcyI6eyJhdWQiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJleHAiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IkxvbmcifSwiaWF0Ijp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJMb25nIn0sImlzcyI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiVHJ1c3RlZElzc3VlciJ9LCJqdGkiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9fX19LCJpZF90b2tlbiI6eyJzaGFwZSI6eyJ0eXBlIjoiUmVjb3JkIiwiYXR0cmlidXRlcyI6eyJhY3IiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJhbXIiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJhdWQiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJleHAiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IkxvbmcifSwiaWF0Ijp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJMb25nIn0sImlzcyI6eyJ0eXBlIjoiRW50aXR5T3JDb21tb24iLCJuYW1lIjoiVHJ1c3RlZElzc3VlciJ9LCJqdGkiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9LCJzdWIiOnsidHlwZSI6IkVudGl0eU9yQ29tbW9uIiwibmFtZSI6IlN0cmluZyJ9fX19LCJJc3N1ZSI6eyJzaGFwZSI6eyJ0eXBlIjoiUmVjb3JkIiwiYXR0cmlidXRlcyI6eyJjb3VudHJ5Ijp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJTdHJpbmcifSwib3JnX2lkIjp7InR5cGUiOiJFbnRpdHlPckNvbW1vbiIsIm5hbWUiOiJTdHJpbmcifX19fSwiUm9sZSI6e319LCJhY3Rpb25zIjp7IlVwZGF0ZSI6eyJhcHBsaWVzVG8iOnsicmVzb3VyY2VUeXBlcyI6WyJJc3N1ZSJdLCJwcmluY2lwYWxUeXBlcyI6WyJXb3JrbG9hZCIsIlVzZXIiLCJSb2xlIl19fX19fQ==" +} \ No newline at end of file diff --git a/jans-cedarling/test_files/policy-store_ok/cedar.schema b/jans-cedarling/test_files/policy-store_ok/cedar.schema index 6d494061f28..37db7a56166 100644 --- a/jans-cedarling/test_files/policy-store_ok/cedar.schema +++ b/jans-cedarling/test_files/policy-store_ok/cedar.schema @@ -3,11 +3,12 @@ type Url = {"host": String, "path": String, "protocol": String}; entity TrustedIssuer = {"issuer_entity_id": Url}; entity Issue = {"country": String, "org_id": String}; entity id_token = {"acr": String, "amr": String, "aud": String, "exp": Long, "iat": Long, "iss": TrustedIssuer, "jti": String, "sub": String}; -entity User = {"country": String, "email": String, "sub": String, "username": String}; +entity Role; +entity User in [Role] = {"country": String, "email": String, "sub": String, "username": String}; entity Workload = {"client_id": String, "iss": TrustedIssuer, "name": String, "org_id": String}; entity Access_token = {"aud": String, "exp": Long, "iat": Long, "iss": TrustedIssuer, "jti": String}; action "Update" appliesTo { - principal: [Workload, User], + principal: [Workload, User, Role], resource: [Issue], context: {} }; diff --git a/jans-cedarling/test_files/policy-store_ok/schema.json b/jans-cedarling/test_files/policy-store_ok/schema.json index 1a9af2c66d0..49d48a512eb 100644 --- a/jans-cedarling/test_files/policy-store_ok/schema.json +++ b/jans-cedarling/test_files/policy-store_ok/schema.json @@ -1 +1,181 @@ -{"Jans":{"commonTypes":{"Url":{"type":"Record","attributes":{"host":{"type":"EntityOrCommon","name":"String"},"path":{"type":"EntityOrCommon","name":"String"},"protocol":{"type":"EntityOrCommon","name":"String"}}}},"entityTypes":{"Access_token":{"shape":{"type":"Record","attributes":{"aud":{"type":"EntityOrCommon","name":"String"},"exp":{"type":"EntityOrCommon","name":"Long"},"iat":{"type":"EntityOrCommon","name":"Long"},"iss":{"type":"EntityOrCommon","name":"TrustedIssuer"},"jti":{"type":"EntityOrCommon","name":"String"}}}},"Issue":{"shape":{"type":"Record","attributes":{"country":{"type":"EntityOrCommon","name":"String"},"org_id":{"type":"EntityOrCommon","name":"String"}}}},"TrustedIssuer":{"shape":{"type":"Record","attributes":{"issuer_entity_id":{"type":"EntityOrCommon","name":"Url"}}}},"User":{"shape":{"type":"Record","attributes":{"country":{"type":"EntityOrCommon","name":"String"},"email":{"type":"EntityOrCommon","name":"String"},"sub":{"type":"EntityOrCommon","name":"String"},"username":{"type":"EntityOrCommon","name":"String"}}}},"Workload":{"shape":{"type":"Record","attributes":{"client_id":{"type":"EntityOrCommon","name":"String"},"iss":{"type":"EntityOrCommon","name":"TrustedIssuer"},"name":{"type":"EntityOrCommon","name":"String"},"org_id":{"type":"EntityOrCommon","name":"String"}}}},"id_token":{"shape":{"type":"Record","attributes":{"acr":{"type":"EntityOrCommon","name":"String"},"amr":{"type":"EntityOrCommon","name":"String"},"aud":{"type":"EntityOrCommon","name":"String"},"exp":{"type":"EntityOrCommon","name":"Long"},"iat":{"type":"EntityOrCommon","name":"Long"},"iss":{"type":"EntityOrCommon","name":"TrustedIssuer"},"jti":{"type":"EntityOrCommon","name":"String"},"sub":{"type":"EntityOrCommon","name":"String"}}}}},"actions":{"Update":{"appliesTo":{"resourceTypes":["Issue"],"principalTypes":["Workload","User"]}}}}} +{ + "Jans": { + "commonTypes": { + "Url": { + "type": "Record", + "attributes": { + "host": { + "type": "EntityOrCommon", + "name": "String" + }, + "path": { + "type": "EntityOrCommon", + "name": "String" + }, + "protocol": { + "type": "EntityOrCommon", + "name": "String" + } + } + } + }, + "entityTypes": { + "TrustedIssuer": { + "shape": { + "type": "Record", + "attributes": { + "issuer_entity_id": { + "type": "EntityOrCommon", + "name": "Url" + } + } + } + }, + "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { + "type": "EntityOrCommon", + "name": "String" + }, + "iss": { + "type": "EntityOrCommon", + "name": "TrustedIssuer" + }, + "name": { + "type": "EntityOrCommon", + "name": "String" + }, + "org_id": { + "type": "EntityOrCommon", + "name": "String" + } + } + } + }, + "User": { + "memberOfTypes": [ + "Role" + ], + "shape": { + "type": "Record", + "attributes": { + "country": { + "type": "EntityOrCommon", + "name": "String" + }, + "email": { + "type": "EntityOrCommon", + "name": "String" + }, + "sub": { + "type": "EntityOrCommon", + "name": "String" + }, + "username": { + "type": "EntityOrCommon", + "name": "String" + } + } + } + }, + "Access_token": { + "shape": { + "type": "Record", + "attributes": { + "aud": { + "type": "EntityOrCommon", + "name": "String" + }, + "exp": { + "type": "EntityOrCommon", + "name": "Long" + }, + "iat": { + "type": "EntityOrCommon", + "name": "Long" + }, + "iss": { + "type": "EntityOrCommon", + "name": "TrustedIssuer" + }, + "jti": { + "type": "EntityOrCommon", + "name": "String" + } + } + } + }, + "id_token": { + "shape": { + "type": "Record", + "attributes": { + "acr": { + "type": "EntityOrCommon", + "name": "String" + }, + "amr": { + "type": "EntityOrCommon", + "name": "String" + }, + "aud": { + "type": "EntityOrCommon", + "name": "String" + }, + "exp": { + "type": "EntityOrCommon", + "name": "Long" + }, + "iat": { + "type": "EntityOrCommon", + "name": "Long" + }, + "iss": { + "type": "EntityOrCommon", + "name": "TrustedIssuer" + }, + "jti": { + "type": "EntityOrCommon", + "name": "String" + }, + "sub": { + "type": "EntityOrCommon", + "name": "String" + } + } + } + }, + "Issue": { + "shape": { + "type": "Record", + "attributes": { + "country": { + "type": "EntityOrCommon", + "name": "String" + }, + "org_id": { + "type": "EntityOrCommon", + "name": "String" + } + } + } + }, + "Role": {} + }, + "actions": { + "Update": { + "appliesTo": { + "resourceTypes": [ + "Issue" + ], + "principalTypes": [ + "Workload", + "User", + "Role" + ] + } + } + } + } +} \ No newline at end of file diff --git a/jans-cedarling/test_files/policy-store_ok_2.yaml b/jans-cedarling/test_files/policy-store_ok_2.yaml new file mode 100644 index 00000000000..c3936634595 --- /dev/null +++ b/jans-cedarling/test_files/policy-store_ok_2.yaml @@ -0,0 +1,62 @@ +cedar_version: v2.4.7 +cedar_policies: + 1: + description: simple policy example for principal workload, permit when workload org_id same with resource + creation_date: '2024-09-20T17:22:39.996050' + policy_content: + encoding: none + content_type: cedar + body: |- + permit( + principal is Jans::Workload, + action in [Jans::Action::"Update"], + resource is Jans::Issue + )when{ + principal.org_id == resource.org_id + }; + 2: + description: simple policy example for principal user, permit if user country same with the issue + creation_date: '2024-09-20T17:22:39.996050' + policy_content: + encoding: none + content_type: cedar + body: |- + permit( + principal is Jans::User, + action in [Jans::Action::"Update"], + resource is Jans::Issue + )when{ + principal.country == resource.country + }; + 3: + description: simple policy example for principal role, permit when Admin + creation_date: '2024-09-20T17:22:39.996050' + policy_content: + encoding: none + content_type: cedar + body: |- + permit( + principal == Jans::Role::"Admin", + action in [Jans::Action::"Update"], + resource is Jans::Issue + ); +cedar_schema: + encoding: none + content_type: cedar + # we minimize amount of field in entites to simplify test cases + body: | + namespace Jans { + type Url = {"host": String, "path": String, "protocol": String}; + entity TrustedIssuer = {"issuer_entity_id": Url}; + entity Issue = {"country": String, "org_id": String}; + entity id_token = {"aud": String,"iss": String, "sub": String}; + entity Role; + entity User in [Role] = {"country": String}; + entity Workload = {"org_id": String}; + entity Access_token = {"aud": String,"iss": String, "jti": String, "client_id": String,"org_id": String}; + action "Update" appliesTo { + principal: [Workload, User, Role], + resource: [Issue], + context: {} + }; + } diff --git a/jans-cedarling/test_files/policy-store_readable.json b/jans-cedarling/test_files/policy-store_readable.json index 291a8b39b93..2f5bacc5e3f 100644 --- a/jans-cedarling/test_files/policy-store_readable.json +++ b/jans-cedarling/test_files/policy-store_readable.json @@ -2,16 +2,16 @@ "cedar_version": "v2.4.7", "cedar_policies": { "840da5d85403f35ea76519ed1a18a33989f855bf1cf8": { - "description": "simple policy example for pricipal workload", + "description": "simple policy example for principal workload", "creation_date": "2024-09-20T17:22:39.996050", "policy_content": { "encoding": "none", "content_type": "cedar", - "body":"permit(\n principal is Jans::Workload,\n action in [Jans::Action::\"Update\"],\n resource is Jans::Issue\n)when{\n principal.org_id == resource.org_id\n};" + "body": "permit(\n principal is Jans::Workload,\n action in [Jans::Action::\"Update\"],\n resource is Jans::Issue\n)when{\n principal.org_id == resource.org_id\n};" } }, "444da5d85403f35ea76519ed1a18a33989f855bf1cf8": { - "description": "simple policy example for pricipal user", + "description": "simple policy example for principal user", "creation_date": "2024-09-20T17:22:39.996050", "policy_content": { "encoding": "none", @@ -21,8 +21,8 @@ } }, "cedar_schema": { - "encoding": "none", - "content_type": "cedar", - "body": "namespace Jans {\ntype Url = {\"host\": String, \"path\": String, \"protocol\": String};\nentity Access_token = {\"aud\": String, \"exp\": Long, \"iat\": Long, \"iss\": TrustedIssuer, \"jti\": String};\nentity Issue = {\"country\": String, \"org_id\": String};\nentity TrustedIssuer = {\"issuer_entity_id\": Url};\nentity User = {\"country\": String, \"email\": String, \"sub\": String, \"username\": String};\nentity Workload = {\"client_id\": String, \"iss\": TrustedIssuer, \"name\": String, \"org_id\": String};\nentity id_token = {\"acr\": String, \"amr\": String, \"aud\": String, \"exp\": Long, \"iat\": Long, \"iss\": TrustedIssuer, \"jti\": String, \"sub\": String};\naction \"Update\" appliesTo {\n principal: [Workload, User],\n resource: [Issue],\n context: {}\n};\n}\n" + "encoding": "none", + "content_type": "cedar", + "body": "namespace Jans {\ntype Url = {\"host\": String, \"path\": String, \"protocol\": String};\nentity Access_token = {\"aud\": String, \"exp\": Long, \"iat\": Long, \"iss\": TrustedIssuer, \"jti\": String};\nentity Issue = {\"country\": String, \"org_id\": String};\nentity TrustedIssuer = {\"issuer_entity_id\": Url};\nentity User = {\"country\": String, \"email\": String, \"sub\": String, \"username\": String};\nentity Workload = {\"client_id\": String, \"iss\": TrustedIssuer, \"name\": String, \"org_id\": String};\nentity id_token = {\"acr\": String, \"amr\": String, \"aud\": String, \"exp\": Long, \"iat\": Long, \"iss\": TrustedIssuer, \"jti\": String, \"sub\": String};\naction \"Update\" appliesTo {\n principal: [Workload, User],\n resource: [Issue],\n context: {}\n};\n}\n" } -} +} \ No newline at end of file diff --git a/jans-cedarling/test_files/policy-store_readable.yaml b/jans-cedarling/test_files/policy-store_readable.yaml index 1c1734dd582..7ed24efac01 100644 --- a/jans-cedarling/test_files/policy-store_readable.yaml +++ b/jans-cedarling/test_files/policy-store_readable.yaml @@ -1,7 +1,7 @@ cedar_version: v2.4.7 cedar_policies: 840da5d85403f35ea76519ed1a18a33989f855bf1cf8: - description: simple policy example for pricipal workload + description: simple policy example for principal workload creation_date: '2024-09-20T17:22:39.996050' policy_content: encoding: none @@ -15,7 +15,7 @@ cedar_policies: principal.org_id == resource.org_id }; 444da5d85403f35ea76519ed1a18a33989f855bf1cf8: - description: simple policy example for pricipal user + description: simple policy example for principal user creation_date: '2024-09-20T17:22:39.996050' policy_content: encoding: none