From 58fd3598777a31b5a4f7b7be3fac31a9f2131268 Mon Sep 17 00:00:00 2001 From: Jose Gonzalez Date: Fri, 3 Jan 2025 15:08:42 -0500 Subject: [PATCH 1/7] feat(agama): modify RRF and RFAC behavior for non-web clients (#10547) * feat: introduce the "native" flavor #10546 Signed-off-by: jgomer2001 * chore: rename engine's default json-based templates #10546 Signed-off-by: jgomer2001 * feat: refactor page modeling & page response logic to account native clients #10546 Signed-off-by: jgomer2001 --------- Signed-off-by: jgomer2001 --- .../misc/{json_crash.ftl => json_crash.ftlh} | 0 .../{json_finished.ftl => json_finished.ftlh} | 0 .../{json_mismatch.ftl => json_mismatch.ftlh} | 0 .../{json_template.ftl => json_template.ftlh} | 0 .../{json_timeout.ftl => json_timeout.ftlh} | 0 .../agama-bridge/AgamaBridge.py | 2 +- .../io/jans/agama/NativeJansFlowBridge.java | 3 +- .../jans/agama/engine/model/FlowStatus.java | 9 +++ .../java/io/jans/agama/engine/page/Page.java | 55 ++++++---------- .../service/AgamaPersistenceService.java | 2 +- .../agama/engine/service/FlowService.java | 5 +- .../agama/engine/servlet/BaseServlet.java | 62 ++++++++++++++++--- .../engine/servlet/ExecutionServlet.java | 3 +- 13 files changed, 90 insertions(+), 51 deletions(-) rename agama/misc/{json_crash.ftl => json_crash.ftlh} (100%) rename agama/misc/{json_finished.ftl => json_finished.ftlh} (100%) rename agama/misc/{json_mismatch.ftl => json_mismatch.ftlh} (100%) rename agama/misc/{json_template.ftl => json_template.ftlh} (100%) rename agama/misc/{json_timeout.ftl => json_timeout.ftlh} (100%) diff --git a/agama/misc/json_crash.ftl b/agama/misc/json_crash.ftlh similarity index 100% rename from agama/misc/json_crash.ftl rename to agama/misc/json_crash.ftlh diff --git a/agama/misc/json_finished.ftl b/agama/misc/json_finished.ftlh similarity index 100% rename from agama/misc/json_finished.ftl rename to agama/misc/json_finished.ftlh diff --git a/agama/misc/json_mismatch.ftl b/agama/misc/json_mismatch.ftlh similarity index 100% rename from agama/misc/json_mismatch.ftl rename to agama/misc/json_mismatch.ftlh diff --git a/agama/misc/json_template.ftl b/agama/misc/json_template.ftlh similarity index 100% rename from agama/misc/json_template.ftl rename to agama/misc/json_template.ftlh diff --git a/agama/misc/json_timeout.ftl b/agama/misc/json_timeout.ftlh similarity index 100% rename from agama/misc/json_timeout.ftl rename to agama/misc/json_timeout.ftlh diff --git a/docs/script-catalog/person_authentication/agama-bridge/AgamaBridge.py b/docs/script-catalog/person_authentication/agama-bridge/AgamaBridge.py index 212c1a2ab5e..ebbfaa433c5 100644 --- a/docs/script-catalog/person_authentication/agama-bridge/AgamaBridge.py +++ b/docs/script-catalog/person_authentication/agama-bridge/AgamaBridge.py @@ -144,7 +144,7 @@ def prepareForStep(self, configurationAttributes, requestParameters, step): try: bridge = CdiUtil.bean(NativeJansFlowBridge) - running = bridge.prepareFlow(session.getId(), qn, ins) + running = bridge.prepareFlow(session.getId(), qn, ins, False) if running == None: print "Agama. Flow '%s' does not exist or cannot be launched from a browser!" % qn diff --git a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/NativeJansFlowBridge.java b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/NativeJansFlowBridge.java index 6df66696dfe..c334e4ce8f2 100644 --- a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/NativeJansFlowBridge.java +++ b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/NativeJansFlowBridge.java @@ -42,7 +42,7 @@ public String getTriggerUrl() { "agama" + ExecutionServlet.URL_SUFFIX; } - public Boolean prepareFlow(String sessionId, String qname, String jsonInput) throws Exception { + public Boolean prepareFlow(String sessionId, String qname, String jsonInput, boolean nativeClient) throws Exception { logger.info("Preparing flow '{}'", qname); Boolean alreadyRunning = null; @@ -68,6 +68,7 @@ public Boolean prepareFlow(String sessionId, String qname, String jsonInput) thr st.setQname(qname); st.setJsonInput(jsonInput); st.setFinishBefore(expireAt); + st.setNativeClient(nativeClient); aps.createFlowRun(sessionId, st, expireAt); LogUtils.log("@w Effective timeout for this flow will be % seconds", timeout); } diff --git a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/model/FlowStatus.java b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/model/FlowStatus.java index dee36df5b4f..6592cf5037a 100644 --- a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/model/FlowStatus.java +++ b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/model/FlowStatus.java @@ -16,6 +16,7 @@ public class FlowStatus { private String templatePath; private long startedAt; private long finishBefore; + private boolean nativeClient; @JsonInclude(JsonInclude.Include.NON_NULL) private Object templateDataModel; @@ -59,6 +60,14 @@ public void setFinishBefore(long finishBefore) { this.finishBefore = finishBefore; } + public boolean isNativeClient() { + return nativeClient; + } + + public void setNativeClient(boolean nativeClient) { + this.nativeClient = nativeClient; + } + public String getQname() { return qname; } diff --git a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/page/Page.java b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/page/Page.java index 81513870f3f..a7956316ccd 100644 --- a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/page/Page.java +++ b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/page/Page.java @@ -6,9 +6,7 @@ import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.RequestScoped; import jakarta.inject.Inject; - -import java.util.HashMap; -import java.util.Map; +import java.util.*; import io.jans.agama.engine.service.*; import io.jans.service.CacheService; @@ -36,7 +34,6 @@ public class Page { private String templatePath; private Map dataModel; - private Object rawModel; public String getTemplatePath() { return templatePath; @@ -47,42 +44,28 @@ public void setTemplatePath(String templatePath) { } public Object getDataModel() { + return dataModel; + } + + public Object getAugmentedDataModel(boolean includeContextualData, Map extra) { - if (rawModel == null) { - if (dataModel != null) { - - dataModel.putIfAbsent(WEB_CTX_KEY, webContext); - dataModel.putIfAbsent(MessagesService.BUNDLE_ID, msgsService); - dataModel.putIfAbsent(LabelsService.METHOD_NAME, labelsService); - dataModel.putIfAbsent(CACHE_KEY, cache); - return dataModel; - - } else return new Object(); - } else return rawModel; + Map model = new HashMap<>(dataModel); + + if (includeContextualData) { + model.putIfAbsent(WEB_CTX_KEY, webContext); + model.putIfAbsent(MessagesService.BUNDLE_ID, msgsService); + model.putIfAbsent(LabelsService.METHOD_NAME, labelsService); + model.putIfAbsent(CACHE_KEY, cache); + } + if (extra != null) { + extra.forEach((k, v) -> model.putIfAbsent(k, v)); + } + return model; - } - - /** - * This call is cheaper than setDataModel, but pages won't have access to any - * contextual data - * @param object - */ - public void setRawDataModel(Object object) { - rawModel = object; - dataModel = null; } public void setDataModel(Object object) { - rawModel = null; - dataModel = mapFromObject(object); - } - - public void appendToDataModel(Object object) { - if (rawModel != null) { - rawModel = null; - dataModel = new HashMap<>(); - } - dataModel.putAll(mapFromObject(object)); + dataModel = object == null ? Map.of() : mapFromObject(object); } private Map mapFromObject(Object object) { @@ -91,7 +74,7 @@ private Map mapFromObject(Object object) { @PostConstruct private void init() { - dataModel = new HashMap<>(); + dataModel = Map.of(); } } diff --git a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/service/AgamaPersistenceService.java b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/service/AgamaPersistenceService.java index d9f3641af58..89e95632222 100644 --- a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/service/AgamaPersistenceService.java +++ b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/service/AgamaPersistenceService.java @@ -189,7 +189,7 @@ public void saveState(String sessionId, FlowStatus fst, NativeContinuation conti logger.debug("Saving state of current flow run"); entryManager.merge(run); - + } public void finishFlow(String sessionId, FlowResult result) throws IOException { diff --git a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/service/FlowService.java b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/service/FlowService.java index dcee5e9e686..1ad8494bfed 100644 --- a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/service/FlowService.java +++ b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/service/FlowService.java @@ -51,7 +51,7 @@ public class FlowService { private static final String SESSION_ID_COOKIE = "session_id"; private static final String SCRIPT_SUFFIX = ".js"; - private static final int TIMEOUT_SKEW = 8000; //millisecons + private static final int TIMEOUT_SKEW = 8000; //milliseconds @Inject private Logger logger; @@ -272,6 +272,9 @@ private FlowStatus processPause(ContinuationPending pending, FlowStatus status) } else if (pending instanceof PendingRedirectException) { + if (status.isNativeClient()) + throw new IOException("RFAC for native clients is not available"); + PendingRedirectException pre = (PendingRedirectException) pending; status.setTemplatePath(null); diff --git a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/servlet/BaseServlet.java b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/servlet/BaseServlet.java index 4393a0870dc..873ba2e6abb 100644 --- a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/servlet/BaseServlet.java +++ b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/servlet/BaseServlet.java @@ -1,5 +1,8 @@ package io.jans.agama.engine.servlet; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + import io.jans.agama.engine.exception.TemplateProcessingException; import io.jans.agama.engine.misc.FlowUtils; import io.jans.agama.engine.page.BasicTemplateModel; @@ -13,13 +16,18 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.ws.rs.core.MediaType; +import java.util.Map; import java.io.IOException; import java.io.StringWriter; import org.slf4j.Logger; +import static java.nio.charset.StandardCharsets.UTF_8; + public abstract class BaseServlet extends HttpServlet { + private static final String TEMPLATE_PATH_KEY = "_template"; + @Inject protected Logger logger; @@ -29,6 +37,9 @@ public abstract class BaseServlet extends HttpServlet { @Inject private TemplatingService templatingService; + @Inject + private ObjectMapper mapper; + @Inject protected EngineConfig engineConf; @@ -49,6 +60,8 @@ protected void sendNotAvailable(HttpServletResponse response) throws IOException protected void sendFlowTimeout(HttpServletResponse response, String message) throws IOException { + response.setStatus(HttpServletResponse.SC_GONE); + String errorPage = engineConf.getInterruptionErrorPage(); page.setTemplatePath(errorPath(errorPage)); page.setDataModel(new BasicTemplateModel(message)); @@ -62,7 +75,7 @@ protected void sendFlowCrashed(HttpServletResponse response, String error) throw String errorPage = engineConf.getCrashErrorPage(); page.setTemplatePath(errorPath(errorPage)); - page.setRawDataModel(new BasicTemplateModel(error)); + page.setDataModel(new BasicTemplateModel(error)); sendPageContents(response); } @@ -79,11 +92,22 @@ protected void sendPageMismatch(HttpServletResponse response, String message, St } - protected void sendPageContents(HttpServletResponse response) throws IOException { + protected void sendPageContents(HttpServletResponse response) throws IOException { + sendPageContents(response, false); + } + + protected void sendPageContents(HttpServletResponse response, boolean nativeClient) throws IOException { try { - processTemplate(response, page.getTemplatePath(), page.getDataModel()); - } catch (TemplateProcessingException e) { + if (nativeClient) { + String simplePath = shortenPath(page.getTemplatePath(), 2); + Object model = page.getAugmentedDataModel(false, Map.of(TEMPLATE_PATH_KEY, simplePath)); + String entity = mapper.writeValueAsString(model); + processResponse(response, UTF_8.toString(), MediaType.APPLICATION_JSON, entity); + } else { + processTemplate(response, page.getTemplatePath(), page.getAugmentedDataModel(true, null)); + } + } catch (TemplateProcessingException | JsonProcessingException e) { try { response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); @@ -100,23 +124,41 @@ protected void sendPageContents(HttpServletResponse response) throws IOException private String errorPath(String page) { return isJsonRequest() ? engineConf.getJsonErrorPage(page) : page; } - + private void processTemplate(HttpServletResponse response, String path, Object dataModel) throws TemplateProcessingException, IOException { StringWriter sw = new StringWriter(); Pair contentType = templatingService.process(path, dataModel, sw, false); - - //encoding MUST be set before calling getWriter - response.setCharacterEncoding(contentType.getSecond()); + processResponse(response, contentType.getSecond(), contentType.getFirst(), sw.toString()); + } + + private void processResponse(HttpServletResponse response, String charset, String mediaType, + String entity) throws IOException { + + //encoding MUST be set before calling getWriter + response.setCharacterEncoding(charset); engineConf.getDefaultResponseHeaders().forEach((h, v) -> response.setHeader(h, v)); - String mediaType = contentType.getFirst(); + if (mediaType != null) { response.setContentType(mediaType); } - response.getWriter().write(sw.toString()); + response.getWriter().write(entity); } + private String shortenPath(String str, int subPaths) { + + int idx = (str.charAt(0) == '/') ? 1 : 0; + + for (int i = 0; i < subPaths; i++) { + int j = str.indexOf("/", idx); + if (j == -1) break; + idx = j + 1; + } + return str.substring(idx); + + } + } diff --git a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/servlet/ExecutionServlet.java b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/servlet/ExecutionServlet.java index 6a874625994..f6a91b240df 100644 --- a/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/servlet/ExecutionServlet.java +++ b/jans-auth-server/agama/engine/src/main/java/io/jans/agama/engine/servlet/ExecutionServlet.java @@ -71,7 +71,7 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) if (path.equals(expectedUrl)) { page.setTemplatePath(engineConf.getTemplatesPath() + "/" + fstatus.getTemplatePath()); page.setDataModel(fstatus.getTemplateDataModel()); - sendPageContents(response); + sendPageContents(response, fstatus.isNativeClient()); } else { //This is an attempt to GET a page which is not the current page of this flow //json-based clients must explicitly pass the content-type in GET requests @@ -194,6 +194,7 @@ private void sendRedirect(HttpServletResponse response, String contextPath, Flow // Local redirection newLocation = contextPath + getExpectedUrl(fls); } + //See https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections and //https://stackoverflow.com/questions/4764297/difference-between-http-redirect-codes if (currentIsGet) { From b8a7e1a493a2e7059a8e220eb6ff4305866bfc70 Mon Sep 17 00:00:00 2001 From: Yuriy Movchan Date: Mon, 6 Jan 2025 08:47:18 +0300 Subject: [PATCH 2/7] =?UTF-8?q?feat(jans-lock):=20lock=20should=20collect?= =?UTF-8?q?=20MAU=20and=20MAC=20based=20on=20log=20entries=E2=80=A6=20(#10?= =?UTF-8?q?328)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(jans-lock): lock should collect MAU and MAC based on log entries requests Signed-off-by: Yuriy Movchan * feat(jans-lock): add sse/config endpoints protection Signed-off-by: Yuriy Movchan * feat(jans-lock): add configurable error response types support Signed-off-by: Yuriy Movchan * feat(jans-lock): add table for lock stat entries Signed-off-by: Yuriy Movchan * feat(jans-lock): increase clnData size Signed-off-by: Yuriy Movchan * feat(jans-lock): fix errorResponseFactory dependecy Signed-off-by: Yuriy Movchan * feat(jans-lock): enable stats by default Signed-off-by: Yuriy Movchan * feat(jans-lock): update protection api to allow use annotations defined in interfaces Signed-off-by: Yuriy Movchan * feat(jans-lock): define lock SSE scope in installer to pre-create it Signed-off-by: Yuriy Movchan * feat(jans-linux-setup): create jans-lock scopes Signed-off-by: Mustafa Baser * feat(jans-lock): add lock scopes Signed-off-by: Yuriy Movchan * feat(jans-lock): use same scopes namespace Signed-off-by: Yuriy Movchan * feat(jans-bom): remove artifact duplicates Signed-off-by: Yuriy Movchan --------- Signed-off-by: Yuriy Movchan Signed-off-by: Mustafa Baser Co-authored-by: Mustafa Baser Co-authored-by: Yuriy M. <95305560+yuremm@users.noreply.github.com> --- jans-auth-server/pom.xml | 1 + jans-auth-server/server/pom.xml | 1 - jans-bom/pom.xml | 25 +- .../jans_setup/schema/jans_schema.json | 33 ++ .../jans_setup/setup_app/installers/base.py | 52 +++ .../setup_app/installers/jans_casa.py | 35 +- .../static/rdbm/sql_data_types.json | 5 + .../templates/jans-lock/dynamic-conf.json | 1 + .../templates/jans-lock/errors.json | 33 +- .../templates/jans-lock/scopes.json | 10 + .../templates/jans-lock/static-conf.json | 3 +- .../lock/model/config/AppConfiguration.java | 67 +++- .../model/config/BaseDnConfiguration.java | 9 + .../jans/lock/model/config/ErrorMessages.java | 21 ++ .../lock/server/service/AppInitializer.java | 13 +- jans-lock/lock-server/service/pom.xml | 13 +- .../main/java/io/jans/lock/model/Stat.java | 61 ++++ .../java/io/jans/lock/model/StatEntry.java | 76 ++++ .../model/error/CommonErrorResponseType.java | 38 ++ .../model/error/ErrorResponseFactory.java | 149 ++++++++ .../model/error/StatErrorResponseType.java | 70 ++++ .../jans/lock/service/audit/AuditService.java | 9 + .../service/config/ConfigurationFactory.java | 12 + .../io/jans/lock/service/event/StatEvent.java | 7 + .../openid/OpenIdProtectionService.java | 17 + .../service/stat/StatResponseService.java | 179 ++++++++++ .../jans/lock/service/stat/StatService.java | 334 ++++++++++++++++++ .../io/jans/lock/service/stat/StatTimer.java | 97 +++++ .../service/util/ResteasyInitializer.java | 25 +- .../ws/rs/ConfigurationRestWebService.java | 4 +- .../ws/rs/audit/AuditRestWebService.java | 12 +- .../ws/rs/audit/AuditRestWebServiceImpl.java | 108 ++++-- .../ws/rs/config/ConfigRestWebService.java | 5 + .../service/ws/rs/sse/SseRestWebService.java | 2 + .../ws/rs/sse/SseRestWebServiceImpl.java | 2 +- .../service/ws/rs/stat/FlatStatResponse.java | 37 ++ .../jans/lock/service/ws/rs/stat/Months.java | 99 ++++++ .../lock/service/ws/rs/stat/StatResponse.java | 33 ++ .../service/ws/rs/stat/StatResponseItem.java | 64 ++++ .../ws/rs/stat/StatRestWebService.java | 41 +++ .../ws/rs/stat/StatRestWebServiceImpl.java | 183 ++++++++++ .../java/io/jans/lock/util/Constants.java | 20 ++ .../java/io/jans/lock/util/ServerUtil.java | 16 + 43 files changed, 1910 insertions(+), 112 deletions(-) create mode 100644 jans-linux-setup/jans_setup/templates/jans-lock/scopes.json create mode 100644 jans-lock/lock-server/service/src/main/java/io/jans/lock/model/Stat.java create mode 100644 jans-lock/lock-server/service/src/main/java/io/jans/lock/model/StatEntry.java create mode 100644 jans-lock/lock-server/service/src/main/java/io/jans/lock/model/error/CommonErrorResponseType.java create mode 100644 jans-lock/lock-server/service/src/main/java/io/jans/lock/model/error/ErrorResponseFactory.java create mode 100644 jans-lock/lock-server/service/src/main/java/io/jans/lock/model/error/StatErrorResponseType.java create mode 100644 jans-lock/lock-server/service/src/main/java/io/jans/lock/service/event/StatEvent.java create mode 100644 jans-lock/lock-server/service/src/main/java/io/jans/lock/service/stat/StatResponseService.java create mode 100644 jans-lock/lock-server/service/src/main/java/io/jans/lock/service/stat/StatService.java create mode 100644 jans-lock/lock-server/service/src/main/java/io/jans/lock/service/stat/StatTimer.java create mode 100644 jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/FlatStatResponse.java create mode 100644 jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/Months.java create mode 100644 jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatResponse.java create mode 100644 jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatResponseItem.java create mode 100644 jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatRestWebService.java create mode 100644 jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatRestWebServiceImpl.java create mode 100644 jans-lock/lock-server/service/src/main/java/io/jans/lock/util/Constants.java diff --git a/jans-auth-server/pom.xml b/jans-auth-server/pom.xml index 80483d7a52c..05999ad6882 100644 --- a/jans-auth-server/pom.xml +++ b/jans-auth-server/pom.xml @@ -134,6 +134,7 @@ 1.6.0 + org.jboss.weld diff --git a/jans-auth-server/server/pom.xml b/jans-auth-server/server/pom.xml index 8f4fcb0d6e7..6c08b7e9434 100644 --- a/jans-auth-server/server/pom.xml +++ b/jans-auth-server/server/pom.xml @@ -273,7 +273,6 @@ io.prometheus simpleclient_common - 0.9.0 net.agkn diff --git a/jans-bom/pom.xml b/jans-bom/pom.xml index 2bd364bc104..2015b451200 100644 --- a/jans-bom/pom.xml +++ b/jans-bom/pom.xml @@ -499,16 +499,21 @@ commons-text 1.12.0 - - commons-beanutils - commons-beanutils - 1.9.4 - commons-collections commons-collections 3.2.2 + + io.prometheus + simpleclient_common + 0.9.0 + + + net.agkn + hll + 1.6.0 + @@ -605,7 +610,7 @@ jackson-dataformat-cbor ${jackson.version} - org.quartz-scheduler @@ -832,7 +837,7 @@ velocity-engine-core 2.3 - + joda-time @@ -897,8 +902,8 @@ ${mockito.version} test - - + + net.openhft compiler diff --git a/jans-linux-setup/jans_setup/schema/jans_schema.json b/jans-linux-setup/jans_setup/schema/jans_schema.json index a9d5c3eeba3..865de347097 100644 --- a/jans-linux-setup/jans_setup/schema/jans_schema.json +++ b/jans-linux-setup/jans_setup/schema/jans_schema.json @@ -2454,6 +2454,17 @@ "syntax": "1.3.6.1.4.1.1466.115.121.1.15", "x_origin": "Jans created attribute" }, + { + "desc": "Jans client data", + "equality": "caseIgnoreMatch", + "names": [ + "clntDat" + ], + "oid": "jansAttr", + "substr": "caseIgnoreSubstringsMatch", + "syntax": "1.3.6.1.4.1.1466.115.121.1.15", + "x_origin": "Jans created attribute" + }, { "desc": "OX PKCE code challenge", "equality": "caseIgnoreMatch", @@ -4083,6 +4094,7 @@ "requestedResource" ], "oid": "jansAttr", + "rdbm_json_column": true, "substr": "caseIgnoreSubstringsMatch", "syntax": "1.3.6.1.4.1.1466.115.121.1.15", "x_origin": "Jans created attribute" @@ -5209,6 +5221,27 @@ ], "x_origin": "Jans created objectclass" }, + { + "kind": "STRUCTURAL", + "may": [ + "jansId", + "dat", + "clntDat", + "jansData", + "attr" + ], + "must": [ + "objectclass" + ], + "names": [ + "jansLockStatEntry" + ], + "oid": "jansObjClass", + "sup": [ + "top" + ], + "x_origin": "Jans Lock created objectclass" + }, { "kind": "STRUCTURAL", "may": [ diff --git a/jans-linux-setup/jans_setup/setup_app/installers/base.py b/jans-linux-setup/jans_setup/setup_app/installers/base.py index 7d32a96e211..b848834f7be 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/base.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/base.py @@ -1,10 +1,13 @@ import os import uuid import inspect +import json from setup_app import paths from setup_app.utils import base from setup_app.config import Config +from setup_app.pylib.ldif4.ldif import LDIFWriter + from setup_app.utils.db_utils import dbUtils from setup_app.utils.progress import jansProgress from setup_app.utils.printVersion import get_war_info @@ -12,11 +15,19 @@ class BaseInstaller: needdb = True dbUtils = dbUtils + service_scopes_created = False def register_progess(self): + if not hasattr(self, 'output_folder'): + self.output_folder = os.path.join(Config.output_dir, self.service_name) + + if not hasattr(self, 'templates_dir'): + self.templates_dir = os.path.join(Config.templateFolder, self.service_name) + jansProgress.register(self) def start_installation(self): + if not hasattr(self, 'pbar_text'): pbar_text = "Installing " + self.service_name.title() else: @@ -44,6 +55,9 @@ def start_installation(self): self.render_unit_file() self.render_import_templates() + if not self.service_scopes_created: + self.create_scopes() + self.update_backend() self.service_post_setup() @@ -244,3 +258,41 @@ def service_post_setup(self): def service_post_install_tasks(self): pass + + def create_scopes(self): + scopes_json_fn = os.path.join(self.templates_dir, 'scopes.json') + + if not os.path.exists(scopes_json_fn): + return + + self.logIt(f"Creating {self.service_name} scopes from {scopes_json_fn}") + scopes = base.readJsonFile(scopes_json_fn) + scopes_ldif_fn = os.path.join(self.output_folder, 'scopes.ldif') + self.createDirs(self.output_folder) + + scopes_list = [] + + with open(scopes_ldif_fn, 'wb') as scope_ldif_fd: + ldif_scopes_writer = LDIFWriter(scope_ldif_fd, cols=1000) + for scope in scopes: + scope_dn = 'inum={},ou=scopes,o=jans'.format(scope['inum']) + scopes_list.append(scope_dn) + ldif_dict = { + 'objectClass': ['top', 'jansScope'], + 'description': [scope['description']], + 'displayName': [scope['displayName']], + 'inum': [scope['inum']], + 'jansDefScope': [str(scope['jansDefScope'])], + 'jansId': [scope['jansId']], + 'jansScopeTyp': [scope['jansScopeTyp']], + 'jansAttrs': [json.dumps({ + "spontaneousClientId":None, + "spontaneousClientScopes":[], + "showInConfigurationEndpoint": False + })], + } + ldif_scopes_writer.unparse(scope_dn, ldif_dict) + + self.dbUtils.import_ldif([scopes_ldif_fn]) + self.service_scopes_created = True + return scopes_list diff --git a/jans-linux-setup/jans_setup/setup_app/installers/jans_casa.py b/jans-linux-setup/jans_setup/setup_app/installers/jans_casa.py index 7c47effb813..3d00ac032ae 100644 --- a/jans-linux-setup/jans_setup/setup_app/installers/jans_casa.py +++ b/jans-linux-setup/jans_setup/setup_app/installers/jans_casa.py @@ -78,7 +78,8 @@ def add_plugins(self): def generate_configuration(self): - self.casa_scopes = self.create_scopes() + if not hasattr(self, 'casa_scopes'): + self.casa_scopes = self.create_scopes() self.check_clients([('casa_client_id', self.client_id_prefix)]) @@ -117,38 +118,6 @@ def create_folders(self): self.createDirs(os.path.join(self.jetty_service_dir, cdir)) - def create_scopes(self): - self.logIt("Creating Casa client scopes") - scopes = base.readJsonFile(self.scopes_fn) - casa_scopes_ldif_fn = os.path.join(self.output_folder, 'scopes.ldif') - self.createDirs(self.output_folder) - scope_ldif_fd = open(casa_scopes_ldif_fn, 'wb') - scopes_list = [] - - ldif_scopes_writer = LDIFWriter(scope_ldif_fd, cols=1000) - - for scope in scopes: - scope_dn = 'inum={},ou=scopes,o=jans'.format(scope['inum']) - scopes_list.append(scope_dn) - ldif_dict = { - 'objectClass': ['top', 'jansScope'], - 'description': [scope['description']], - 'displayName': [scope['displayName']], - 'inum': [scope['inum']], - 'jansDefScope': [str(scope['jansDefScope'])], - 'jansId': [scope['jansId']], - 'jansScopeTyp': [scope['jansScopeTyp']], - 'jansAttrs': [json.dumps({"spontaneousClientId":None, "spontaneousClientScopes":[], "showInConfigurationEndpoint": False})], - } - ldif_scopes_writer.unparse(scope_dn, ldif_dict) - - scope_ldif_fd.close() - - self.dbUtils.import_ldif([casa_scopes_ldif_fn]) - - return scopes_list - - def service_post_setup(self): self.writeFile(os.path.join(self.jetty_service_dir, '.administrable'), '', backup=False) self.chown(self.jetty_service_dir, Config.jetty_user, Config.jetty_group, recursive=True) diff --git a/jans-linux-setup/jans_setup/static/rdbm/sql_data_types.json b/jans-linux-setup/jans_setup/static/rdbm/sql_data_types.json index 88f52e7e44d..be4af219580 100644 --- a/jans-linux-setup/jans_setup/static/rdbm/sql_data_types.json +++ b/jans-linux-setup/jans_setup/static/rdbm/sql_data_types.json @@ -4,6 +4,11 @@ "type": "TEXT" } }, + "clntDat": { + "mysql": { + "type": "TEXT" + } + }, "description": { "mysql": { "size": 768, diff --git a/jans-linux-setup/jans_setup/templates/jans-lock/dynamic-conf.json b/jans-linux-setup/jans_setup/templates/jans-lock/dynamic-conf.json index 28b61e923ff..81f47b5119c 100644 --- a/jans-linux-setup/jans_setup/templates/jans-lock/dynamic-conf.json +++ b/jans-linux-setup/jans_setup/templates/jans-lock/dynamic-conf.json @@ -51,6 +51,7 @@ "metricReporterInterval": 300, "metricReporterKeepDataDays": 15, "metricReporterEnabled": true, + "statEnabled": true, "errorReasonEnabled": false, "opaConfiguration": { "baseUrl": "http://%(jans_opa_host)s:%(jans_opa_port)s/v1/", diff --git a/jans-linux-setup/jans_setup/templates/jans-lock/errors.json b/jans-linux-setup/jans_setup/templates/jans-lock/errors.json index c6cf6d78f14..442fcfcb538 100644 --- a/jans-linux-setup/jans_setup/templates/jans-lock/errors.json +++ b/jans-linux-setup/jans_setup/templates/jans-lock/errors.json @@ -1,9 +1,26 @@ { - "common": [ - { - "id": "unknown_error", - "description": "Unknown or not found error", - "uri": null - } - ] -} \ No newline at end of file + "common": [ + { + "id": "invalid_request", + "description": "The request is missing a required parameter, includes an unsupported parameter or parameter value, or is otherwise malformed", + "uri": null + }, + { + "id": "unknown_error", + "description": "Unknown or not found error", + "uri": null + } + ], + "stat":[ + { + "id":"invalid_request", + "description":"The request is missing a required parameter, includes an unsupported parameter or parameter value, repeats a parameter, includes multiple credentials, utilizes more than one mechanism for authenticating the client, or is otherwise malformed.", + "uri":null + }, + { + "id":"access_denied", + "description":"The resource owner or authorization server denied the request.", + "uri":null + } + ] + } diff --git a/jans-linux-setup/jans_setup/templates/jans-lock/scopes.json b/jans-linux-setup/jans_setup/templates/jans-lock/scopes.json new file mode 100644 index 00000000000..104baf2a1ac --- /dev/null +++ b/jans-linux-setup/jans_setup/templates/jans-lock/scopes.json @@ -0,0 +1,10 @@ +[ + { + "inum": "4000.01.1", + "jansId": "https://jans.io/oauth/lock/sse.read", + "displayName": "Lock API scope", + "description": "Permission to access SSE endpoint", + "jansDefScope": false, + "jansScopeTyp": "oauth" + } +] diff --git a/jans-linux-setup/jans_setup/templates/jans-lock/static-conf.json b/jans-linux-setup/jans_setup/templates/jans-lock/static-conf.json index da3b6aee9a7..4647113b47d 100644 --- a/jans-linux-setup/jans_setup/templates/jans-lock/static-conf.json +++ b/jans-linux-setup/jans_setup/templates/jans-lock/static-conf.json @@ -6,6 +6,7 @@ "attributes":"ou=attributes,o=jans", "tokens":"ou=tokens,o=jans", "sessions":"ou=sessions,o=jans", - "metric":"ou=statistic,o=metric" + "metric":"ou=statistic,o=metric", + "stat": "ou=lock,ou=stat,o=jans" } } diff --git a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/AppConfiguration.java b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/AppConfiguration.java index 45e59344cf3..042ee8849b1 100644 --- a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/AppConfiguration.java +++ b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/AppConfiguration.java @@ -46,6 +46,14 @@ public class AppConfiguration implements Configuration { @DocProperty(description = "OpenID issuer URL") @Schema(description = "OpenID issuer URL") private String openIdIssuer; + + @DocProperty(description = "Active stat enabled") + @Schema(description = "Active stat enabled") + private boolean statEnabled; + + @DocProperty(description = "Statistical data capture time interval") + @Schema(description = "Statistical data capture time interval") + private int statTimerIntervalInSeconds; @DocProperty(description = "List of token channel names", defaultValue = "jans_token") @Schema(description = "List of token channel names") @@ -135,6 +143,9 @@ public class AppConfiguration implements Configuration { @Schema(description = "List of Zip Uris with policies") private List policiesZipUris; + @DocProperty(description = "Boolean value specifying whether to return detailed reason of the error from AS. Default value is false", defaultValue = "false") + private Boolean errorReasonEnabled = false; + public String getBaseDN() { return baseDN; } @@ -159,7 +170,23 @@ public void setOpenIdIssuer(String openIdIssuer) { this.openIdIssuer = openIdIssuer; } - public List getTokenChannels() { + public boolean isStatEnabled() { + return statEnabled; + } + + public void setStatEnabled(boolean statEnabled) { + this.statEnabled = statEnabled; + } + + public int getStatTimerIntervalInSeconds() { + return statTimerIntervalInSeconds; + } + + public void setStatTimerIntervalInSeconds(int statTimerIntervalInSeconds) { + this.statTimerIntervalInSeconds = statTimerIntervalInSeconds; + } + + public List getTokenChannels() { return tokenChannels; } @@ -335,19 +362,31 @@ public void setPoliciesZipUris(List policiesZipUris) { this.policiesZipUris = policiesZipUris; } - @Override - public String toString() { - return "AppConfiguration [baseDN=" + baseDN + ", baseEndpoint=" + baseEndpoint + ", openIdIssuer=" - + openIdIssuer + ", tokenChannels=" + tokenChannels + ", clientId=" + clientId + ", tokenUrl=" - + tokenUrl + ", groupScopeEnabled=" + groupScopeEnabled+ ", endpointGroups=" + endpointGroups + ", endpointDetails=" + endpointDetails - + ", disableJdkLogger=" + disableJdkLogger + ", loggingLevel=" + loggingLevel + ", loggingLayout=" - + loggingLayout + ", externalLoggerConfiguration=" + externalLoggerConfiguration + ", metricChannel=" - + metricChannel + ", metricReporterInterval=" + metricReporterInterval + ", metricReporterKeepDataDays=" - + metricReporterKeepDataDays + ", metricReporterEnabled=" + metricReporterEnabled - + ", cleanServiceInterval=" + cleanServiceInterval + ", opaConfiguration=" + opaConfiguration - + ", pdpType=" + pdpType + ", policiesJsonUrisAuthorizationToken=" + policiesJsonUrisAuthorizationToken - + ", policiesJsonUris=" + policiesJsonUris + ", policiesZipUrisAuthorizationToken=" - + policiesZipUrisAuthorizationToken + ", policiesZipUris=" + policiesZipUris + "]"; + public Boolean getErrorReasonEnabled() { + if (errorReasonEnabled == null) errorReasonEnabled = false; + return errorReasonEnabled; + } + + public void setErrorReasonEnabled(Boolean errorReasonEnabled) { + this.errorReasonEnabled = errorReasonEnabled; } + @Override + public String toString() { + return "AppConfiguration [baseDN=" + baseDN + ", baseEndpoint=" + baseEndpoint + ", openIdIssuer=" + + openIdIssuer + ", statEnabled=" + statEnabled + ", statTimerIntervalInSeconds=" + + statTimerIntervalInSeconds + ", tokenChannels=" + tokenChannels + ", clientId=" + clientId + + ", clientPassword=" + clientPassword + ", tokenUrl=" + tokenUrl + ", groupScopeEnabled=" + + groupScopeEnabled + ", endpointGroups=" + endpointGroups + ", endpointDetails=" + endpointDetails + + ", disableJdkLogger=" + disableJdkLogger + ", loggingLevel=" + loggingLevel + ", loggingLayout=" + + loggingLayout + ", externalLoggerConfiguration=" + externalLoggerConfiguration + ", metricChannel=" + + metricChannel + ", metricReporterInterval=" + metricReporterInterval + ", metricReporterKeepDataDays=" + + metricReporterKeepDataDays + ", metricReporterEnabled=" + metricReporterEnabled + + ", cleanServiceInterval=" + cleanServiceInterval + ", opaConfiguration=" + opaConfiguration + + ", pdpType=" + pdpType + ", policiesJsonUrisAuthorizationToken=" + policiesJsonUrisAuthorizationToken + + ", policiesJsonUris=" + policiesJsonUris + ", policiesZipUrisAuthorizationToken=" + + policiesZipUrisAuthorizationToken + ", policiesZipUris=" + policiesZipUris + ", errorReasonEnabled=" + + errorReasonEnabled + "]"; + } + } diff --git a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/BaseDnConfiguration.java b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/BaseDnConfiguration.java index 4e911975ee3..265f7bcf74f 100644 --- a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/BaseDnConfiguration.java +++ b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/BaseDnConfiguration.java @@ -36,6 +36,7 @@ public class BaseDnConfiguration { private String tokens; private String scripts; private String metric; + private String stat; public String getConfiguration() { return configuration; @@ -93,4 +94,12 @@ public void setMetric(String metric) { this.metric = metric; } + public String getStat() { + return stat; + } + + public void setStat(String stat) { + this.stat = stat; + } + } diff --git a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/ErrorMessages.java b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/ErrorMessages.java index d0b15cb02e0..bb318aabf66 100644 --- a/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/ErrorMessages.java +++ b/jans-lock/lock-server/model/src/main/java/io/jans/lock/model/config/ErrorMessages.java @@ -22,6 +22,11 @@ import io.jans.model.error.ErrorMessage; import jakarta.enterprise.inject.Vetoed; +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlElementWrapper; +import jakarta.xml.bind.annotation.XmlRootElement; /** * Base interface for all Jans Auth configurations @@ -29,11 +34,19 @@ * @author Yuriy Movchan Date: 12/18/2023 */ @Vetoed +@XmlRootElement(name = "errors") +@XmlAccessorType(XmlAccessType.FIELD) @JsonIgnoreProperties(ignoreUnknown = true) public class ErrorMessages implements Configuration { + @XmlElementWrapper(name = "common") + @XmlElement(name = "error") private List common; + @XmlElementWrapper(name = "stat") + @XmlElement(name = "error") + private List stat; + public List getCommon() { return common; } @@ -42,4 +55,12 @@ public void setCommon(List common) { this.common = common; } + public List getStat() { + return stat; + } + + public void setStat(List stat) { + this.stat = stat; + } + } diff --git a/jans-lock/lock-server/server/src/main/java/io/jans/lock/server/service/AppInitializer.java b/jans-lock/lock-server/server/src/main/java/io/jans/lock/server/service/AppInitializer.java index 6274f2ae623..b454ca1981f 100644 --- a/jans-lock/lock-server/server/src/main/java/io/jans/lock/server/service/AppInitializer.java +++ b/jans-lock/lock-server/server/src/main/java/io/jans/lock/server/service/AppInitializer.java @@ -28,6 +28,8 @@ import io.jans.exception.ConfigurationException; import io.jans.lock.service.config.ApplicationFactory; import io.jans.lock.service.config.ConfigurationFactory; +import io.jans.lock.service.stat.StatService; +import io.jans.lock.service.stat.StatTimer; import io.jans.lock.service.status.StatusCheckerTimer; import io.jans.model.custom.script.CustomScriptType; import io.jans.orm.PersistenceEntryManager; @@ -123,6 +125,11 @@ public class AppInitializer { @Inject private DocumentStoreManager documentStoreManager; + + @Inject + private StatService statService; + + @Inject StatTimer statTimer; @PostConstruct public void createApplicationComponents() { @@ -154,6 +161,9 @@ public void applicationInitialized(@Observes @Initialized(ApplicationScoped.clas // Initialize script manager List supportedCustomScriptTypes = Lists.newArrayList(CustomScriptType.LOCK_EXTENSION); + + // Initialize stat service + statService.init(); // Start timer initSchedulerService(); @@ -162,10 +172,11 @@ public void applicationInitialized(@Observes @Initialized(ApplicationScoped.clas loggerService.initTimer(); statusCheckerTimer.initTimer(); customScriptManager.initTimer(supportedCustomScriptTypes); + statTimer.initTimer(); // Initialize Document Store Manager documentStoreManager.initTimer(Arrays.asList(DOCUMENT_STORE_MANAGER_JANS_LOCK_TYPE)); - + // Notify plugins about finish application initialization eventApplicationInitialized.select(ApplicationInitialized.Literal.APPLICATION) .fire(new ApplicationInitializedEvent()); diff --git a/jans-lock/lock-server/service/pom.xml b/jans-lock/lock-server/service/pom.xml index 02d91f09c5e..5a35cea34df 100644 --- a/jans-lock/lock-server/service/pom.xml +++ b/jans-lock/lock-server/service/pom.xml @@ -67,7 +67,6 @@ io.jans jans-auth-model - ${project.version} @@ -109,6 +108,10 @@ commons-codec commons-codec + + commons-beanutils + commons-beanutils + commons-collections commons-collections @@ -121,6 +124,14 @@ commons-io commons-io + + io.prometheus + simpleclient_common + + + net.agkn + hll + diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/Stat.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/Stat.java new file mode 100644 index 00000000000..7cfffeaf620 --- /dev/null +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/Stat.java @@ -0,0 +1,61 @@ +package io.jans.lock.model; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author Yuriy Movchan Date: 12/02/2024 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class Stat implements Serializable { + + private static final long serialVersionUID = -1659698750177377994L; + + @JsonProperty("countOpByType") + private Map> operationsByType; + + @JsonProperty("lastUpdatedAt") + private long lastUpdatedAt; + + @JsonProperty("month") + private String month; + + public Map> getOperationsByType() { + if (operationsByType == null) operationsByType = new HashMap<>(); + return operationsByType; + } + + public void setOperationsByType(Map> operationsByType) { + this.operationsByType = operationsByType; + } + + public long getLastUpdatedAt() { + return lastUpdatedAt; + } + + public void setLastUpdatedAt(long lastUpdatedAt) { + this.lastUpdatedAt = lastUpdatedAt; + } + + public String getMonth() { + return month; + } + + public void setMonth(String month) { + this.month = month; + } + + @Override + public String toString() { + return "Stat{" + + "operationsByType=" + operationsByType + + ", lastUpdatedAt=" + lastUpdatedAt + + ", month='" + month + '\'' + + '}'; + } +} + diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/StatEntry.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/StatEntry.java new file mode 100644 index 00000000000..40922bddd6f --- /dev/null +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/StatEntry.java @@ -0,0 +1,76 @@ +package io.jans.lock.model; + +import io.jans.orm.annotation.AttributeName; +import io.jans.orm.annotation.DataEntry; +import io.jans.orm.annotation.JsonObject; +import io.jans.orm.annotation.ObjectClass; +import io.jans.orm.model.base.BaseEntry; + +/** + * @author Yuriy Movchan Date: 12/02/2024 + */ +@DataEntry +@ObjectClass(value = "jansLockStatEntry") +public class StatEntry extends BaseEntry { + + private static final long serialVersionUID = 7349181838267756343L; + + @AttributeName(name = "jansId") + private String id; + + @AttributeName(name = "jansData") + private String month; + + @AttributeName(name = "dat") + private String userHllData; + + @AttributeName(name = "clntDat") + private String clientHllData; + + @AttributeName(name = "attr") + @JsonObject + private Stat stat; + + public String getMonth() { + return month; + } + + public void setMonth(String month) { + this.month = month; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getUserHllData() { + return userHllData; + } + + public void setUserHllData(String userHllData) { + this.userHllData = userHllData; + } + + public String getClientHllData() { + return clientHllData; + } + + public void setClientHllData(String clientHllData) { + this.clientHllData = clientHllData; + } + + public Stat getStat() { + if (stat == null) { + stat = new Stat(); + } + return stat; + } + + public void setStat(Stat stat) { + this.stat = stat; + } +} diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/error/CommonErrorResponseType.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/error/CommonErrorResponseType.java new file mode 100644 index 00000000000..fe8d5547478 --- /dev/null +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/error/CommonErrorResponseType.java @@ -0,0 +1,38 @@ +package io.jans.lock.model.error; + +import io.jans.as.model.error.IErrorType; + +/** + * @author Yuriy Movchan Date: 23/02/2024 + */ +public enum CommonErrorResponseType implements IErrorType { + + /** + * The request is missing a required parameter, includes an + * invalid parameter value, includes a parameter more than + * once, or is otherwise malformed. + */ + INVALID_REQUEST("invalid_request"), + + /** + * Unknown or not found error. + */ + UNKNOWN_ERROR("unknown_error"), + ; + + private final String paramName; + + CommonErrorResponseType(String paramName) { + this.paramName = paramName; + } + + @Override + public String getParameter() { + return paramName; + } + + @Override + public String toString() { + return paramName; + } +} diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/error/ErrorResponseFactory.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/error/ErrorResponseFactory.java new file mode 100644 index 00000000000..25d0d4d55e9 --- /dev/null +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/error/ErrorResponseFactory.java @@ -0,0 +1,149 @@ +package io.jans.lock.model.error; + +import io.jans.as.model.config.Constants; +import io.jans.as.model.configuration.Configuration; +import io.jans.as.model.error.DefaultErrorResponse; +import io.jans.as.model.error.IErrorType; +import io.jans.lock.model.config.AppConfiguration; +import io.jans.lock.model.config.ErrorMessages; +import io.jans.model.error.ErrorMessage; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.logging.log4j.ThreadContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; + +import static jakarta.ws.rs.core.Response.Status.*; + +/** + * @author Yuriy Movchan Date: 23/02/2024 + */ +public class ErrorResponseFactory implements Configuration { + + private static final Logger log = LoggerFactory.getLogger(ErrorResponseFactory.class); + + private ErrorMessages messages; + + private AppConfiguration appConfiguration; + + public ErrorResponseFactory() { + } + + public ErrorResponseFactory(ErrorMessages messages, AppConfiguration appConfiguration) { + this.messages = messages; + this.appConfiguration = appConfiguration; + } + + public WebApplicationException createWebApplicationException(Response.Status status, IErrorType type, String reason) { + return createWebApplicationException(status, type, reason, null); + } + + private WebApplicationException createWebApplicationException(Response.Status status, IErrorType type, String reason, Throwable e) { + WebApplicationException error = new WebApplicationException(Response + .status(status) + .entity(errorAsJson(type, reason)) + .type(MediaType.APPLICATION_JSON_TYPE) + .build()); + if (log.isErrorEnabled()) { + log.error("Exception Handle, status: {}, body: {}", formatStatus(error.getResponse().getStatusInfo()), error.getResponse().getEntity(), e); + } + return error; + } + + public WebApplicationException badRequestException(IErrorType type, String reason) { + return createWebApplicationException(BAD_REQUEST, type, reason); + } + + public WebApplicationException badRequestException(IErrorType type, String reason, Throwable e) { + return createWebApplicationException(BAD_REQUEST, type, reason, e); + } + + public WebApplicationException notFoundException(IErrorType type, String reason) { + return createWebApplicationException(NOT_FOUND, type, reason); + } + + public WebApplicationException forbiddenException() { + WebApplicationException error = new WebApplicationException(Response + .status(FORBIDDEN) + .entity("") + .build()); + if (log.isErrorEnabled()) { + log.error("Exception Handle, status: {}", formatStatus(error.getResponse().getStatusInfo())); + } + return error; + } + + public WebApplicationException invalidRequest(String reason) { + return createWebApplicationException(BAD_REQUEST, CommonErrorResponseType.INVALID_REQUEST, reason); + } + + public WebApplicationException invalidRequest(String reason, Throwable e) { + return createWebApplicationException(BAD_REQUEST, CommonErrorResponseType.INVALID_REQUEST, reason, e); + } + + public WebApplicationException unknownError(String reason) { + throw createWebApplicationException(INTERNAL_SERVER_ERROR, CommonErrorResponseType.UNKNOWN_ERROR, reason); + } + + private String errorAsJson(IErrorType type, String reason) { + final DefaultErrorResponse error = getErrorResponse(type); + error.setReason(BooleanUtils.isTrue(appConfiguration.getErrorReasonEnabled()) ? reason : ""); + return error.toJSonString(); + } + + private DefaultErrorResponse getErrorResponse(IErrorType type) { + final DefaultErrorResponse response = new DefaultErrorResponse(); + response.setType(type); + if (type != null && messages != null) { + List list = null; + if (type instanceof CommonErrorResponseType) { + list = messages.getCommon(); + } else if (type instanceof StatErrorResponseType) { + list = messages.getStat(); + } + if (list != null) { + final ErrorMessage m = getError(list, type); + String description = Optional.ofNullable(ThreadContext.get(Constants.CORRELATION_ID_HEADER)) + .map(id -> m.getDescription().concat(" CorrelationId: " + id)) + .orElse(m.getDescription()); + response.setErrorDescription(description); + response.setErrorUri(m.getUri()); + } + } + + return response; + } + + /** + * Looks for an error message. + * + * @param list error list + * @param type The type of the error. + * @return Error message or null if not found. + */ + private ErrorMessage getError(List list, IErrorType type) { + log.debug("Looking for the error with id: {}", type); + + if (list != null) { + Predicate equalsErrorMessageId = s -> s.getId().equals(type.getParameter()); + Optional errorMessage = list.stream().filter(equalsErrorMessageId).findFirst(); + if (errorMessage.isPresent()) { + log.debug("Found error, id: {}", type); + return errorMessage.get(); + } + } + + log.error("Error not found, id: {}", type); + return new ErrorMessage(type.getParameter(), type.getParameter(), null); + } + + private String formatStatus(Response.StatusType status) { + return String.format("%s %s", status.getStatusCode(), status); + } +} diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/error/StatErrorResponseType.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/error/StatErrorResponseType.java new file mode 100644 index 00000000000..4db9a85bd07 --- /dev/null +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/model/error/StatErrorResponseType.java @@ -0,0 +1,70 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2020, Janssen Project + */ + +package io.jans.lock.model.error; + +import io.jans.as.model.error.IErrorType; + +/** + * @author Yuriy Movchan Date: 23/02/2024 + */ +public enum StatErrorResponseType implements IErrorType { + /** + * The request is missing a required parameter, includes an unsupported + * parameter or parameter value, repeats a parameter, includes multiple + * credentials, utilizes more than one mechanism for authenticating the + * client, or is otherwise malformed. + */ + INVALID_REQUEST("invalid_request"), + + /** + * The end-user denied the authorization request. + */ + ACCESS_DENIED("access_denied"); + + private final String paramName; + + StatErrorResponseType(String paramName) { + this.paramName = paramName; + } + + /** + * Returns the corresponding {@link StatErrorResponseType} from a given string. + * + * @param param The string value to convert. + * @return The corresponding {@link StatErrorResponseType}, otherwise null. + */ + public static StatErrorResponseType fromString(String param) { + if (param != null) { + for (StatErrorResponseType err : StatErrorResponseType.values()) { + if (param.equals(err.paramName)) { + return err; + } + } + } + return null; + } + + /** + * Returns a string representation of the object. In this case the parameter name. + * + * @return The string representation of the object. + */ + @Override + public String toString() { + return paramName; + } + + /** + * Gets error parameter. + * + * @return error parameter + */ + @Override + public String getParameter() { + return paramName; + } +} diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/audit/AuditService.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/audit/AuditService.java index 9e6e8b7f3ab..dccfb3300ce 100644 --- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/audit/AuditService.java +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/audit/AuditService.java @@ -22,6 +22,15 @@ @ApplicationScoped public class AuditService { + public static final String AUDIT_TELEMETRY = "telemetry"; + public static final String AUDIT_TELEMETRY_BULK = "telemetry/bulk"; + + public static final String AUDIT_LOG = "log"; + public static final String AUDIT_LOG_BULK = "log/bulk"; + + public static final String AUDIT_HEALTH = "health"; + public static final String AUDIT_HEALTH_BULK = "health/bulk"; + @Inject private Logger log; diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/config/ConfigurationFactory.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/config/ConfigurationFactory.java index a3a1052764d..240abfcab4a 100644 --- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/config/ConfigurationFactory.java +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/config/ConfigurationFactory.java @@ -28,6 +28,7 @@ import io.jans.lock.model.config.Conf; import io.jans.lock.model.config.Configuration; import io.jans.lock.model.config.StaticConfiguration; +import io.jans.lock.model.error.ErrorResponseFactory; import io.jans.orm.PersistenceEntryManager; import io.jans.orm.exception.BasePersistenceException; import io.jans.orm.model.PersistenceConfiguration; @@ -80,6 +81,8 @@ public class ConfigurationFactory extends ApplicationConfigurationFactory { @Inject private Instance configurationInstance; + private ErrorResponseFactory errorResponseFactory; + public final static String PERSISTENCE_CONFIGUARION_RELOAD_EVENT_TYPE = "persistenceConfigurationReloadEvent"; public final static String BASE_CONFIGUARION_RELOAD_EVENT_TYPE = "baseConfigurationReloadEvent"; @@ -250,6 +253,12 @@ public StaticConfiguration getStaticConfiguration() { return staticConf; } + @Produces + @ApplicationScoped + public ErrorResponseFactory getFido2ErrorResponseFactory() { + return errorResponseFactory; + } + public BaseDnConfiguration getBaseDn() { return getStaticConfiguration().getBaseDn(); } @@ -327,6 +336,9 @@ private void initConfigurationConf(Conf conf) { if (conf.getStatics() != null) { staticConf = conf.getStatics(); } + if (conf.getErrors() != null) { + errorResponseFactory = new ErrorResponseFactory(conf.getErrors(), conf.getDynamic()); + } } private void loadBaseConfiguration() { diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/event/StatEvent.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/event/StatEvent.java new file mode 100644 index 00000000000..c05cdcd010d --- /dev/null +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/event/StatEvent.java @@ -0,0 +1,7 @@ +package io.jans.lock.service.event; + +/** + * @author Yuriy Movchan Date: 12/02/2024 + */ +public class StatEvent { +} diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/filter/openid/OpenIdProtectionService.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/filter/openid/OpenIdProtectionService.java index 7dcd87831f7..39cbc12c1f4 100644 --- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/filter/openid/OpenIdProtectionService.java +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/filter/openid/OpenIdProtectionService.java @@ -6,6 +6,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; @@ -160,6 +161,22 @@ private List getRequestedScopes(ResourceInfo resourceInfo) { scopes.addAll(getScopesFromAnnotation(resourceInfo.getResourceClass())); scopes.addAll(getScopesFromAnnotation(resourceInfo.getResourceMethod())); + Method baseMethod = resourceInfo.getResourceMethod(); + for (Class interfaces : resourceInfo.getResourceClass().getInterfaces()) { + scopes.addAll(getScopesFromAnnotation(interfaces)); + + Method method = null; + try { + method = interfaces.getDeclaredMethod(baseMethod.getName(), baseMethod.getParameterTypes()); + } catch (NoSuchMethodException | SecurityException e) { + // It's expected behavior + } + if (method != null) { + scopes.addAll(getScopesFromAnnotation(method)); + } + + } + return scopes; } diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/stat/StatResponseService.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/stat/StatResponseService.java new file mode 100644 index 00000000000..120b25516bf --- /dev/null +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/stat/StatResponseService.java @@ -0,0 +1,179 @@ +package io.jans.lock.service.stat; + +import static io.jans.as.model.util.Util.escapeLog; + +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.Lists; + +import io.jans.lock.model.StatEntry; +import io.jans.lock.service.ws.rs.stat.StatResponse; +import io.jans.lock.service.ws.rs.stat.StatResponseItem; +import io.jans.orm.PersistenceEntryManager; +import io.jans.orm.search.filter.Filter; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import net.agkn.hll.HLL; + +/** + * @author Yuriy Movchan Date: 12/02/2024 + */ +@ApplicationScoped +public class StatResponseService { + + @Inject + private Logger log; + + @Inject + private PersistenceEntryManager entryManager; + + @Inject + private StatService statService; + + private final Cache responseCache = CacheBuilder + .newBuilder() + .expireAfterWrite(1, TimeUnit.HOURS) + .build(); + + public StatResponse buildResponse(Set months) { + final String cacheKey = months.toString(); + final StatResponse cachedResponse = responseCache.getIfPresent(cacheKey); + if (cachedResponse != null) { + if (log.isTraceEnabled()) { + log.trace("Get stat response from cache for: {}", escapeLog(cacheKey)); + } + return cachedResponse; + } + + StatResponse response = new StatResponse(); + for (String month : months) { + final StatResponseItem responseItem = buildItem(month); + if (responseItem != null) { + response.getResponse().put(month, responseItem); + } + } + + responseCache.put(cacheKey, response); + return response; + } + + private StatResponseItem buildItem(String month) { + try { + final String escapedMonth = escapeLog(month); + log.trace("Trying to fetch stat for month: {}", escapedMonth); + + final List entries = entryManager.findEntries(statService.getBaseDn(), StatEntry.class, Filter.createEqualityFilter("jansData", month)); + if (entries == null || entries.isEmpty()) { + log.trace("Can't find stat entries for month: {}", escapedMonth); + return null; + } + log.trace("Fetched stat entries for month {} successfully", escapedMonth); + + checkNotMatchedEntries(month, entries); + if (entries.isEmpty()) { + log.trace("No stat entries for month: {}", escapedMonth); + return null; + } + + final StatResponseItem responseItem = new StatResponseItem(); + responseItem.setMonthlyActiveUsers(userCardinality(entries)); + responseItem.setMonthlyActiveClients(clientCardinality(entries)); + responseItem.setMonth(month); + + unionOpearationsMapIntoResponseItem(entries, responseItem); + + return responseItem; + } catch (Exception e) { + log.error(e.getMessage(), e); + return null; + } + } + + // This should not occur for newly created StatEntry (only outdated db) + private void checkNotMatchedEntries(String month, List entries) { + final List notMatched = Lists.newArrayList(); + for (StatEntry entry : entries) { + if (!Objects.equals(month, entry.getMonth())) { + log.error("Not matched entry: {}", entry.getDn()); + notMatched.add(entry); + } + } + + entries.removeAll(notMatched); + } + + + private long userCardinality(List entries) { + HLL hll = decodeUserHll(entries.get(0)); + + // Union hll + if (entries.size() > 1) { + for (int i = 1; i < entries.size(); i++) { + hll.union(decodeUserHll(entries.get(i))); + } + } + return hll.cardinality(); + } + + private long clientCardinality(List entries) { + HLL hll = decodeClientHll(entries.get(0)); + + // Union hll + if (entries.size() > 1) { + for (int i = 1; i < entries.size(); i++) { + hll.union(decodeClientHll(entries.get(i))); + } + } + return hll.cardinality(); + } + + private HLL decodeUserHll(StatEntry entry) { + try { + return HLL.fromBytes(Base64.getDecoder().decode(entry.getUserHllData())); + } catch (Exception e) { + log.error("Failed to decode user HLL data, entry dn: {}, data: {}", entry.getDn(), entry.getUserHllData()); + return statService.newUserHll(); + } + } + + + private HLL decodeClientHll(StatEntry entry) { + try { + return HLL.fromBytes(Base64.getDecoder().decode(entry.getClientHllData())); + } catch (Exception e) { + log.error("Failed to decode client HLL data, entry dn: {}, data: {}", entry.getDn(), entry.getUserHllData()); + return statService.newClientHll(); + } + } + + private static void unionOpearationsMapIntoResponseItem(List entries, StatResponseItem responseItem) { + for (StatEntry entry : entries) { + entry.getStat().getOperationsByType().entrySet().stream().filter(en -> en.getValue() != null).forEach(en -> { + final Map operationMap = responseItem.getOperationsByType().get(en.getKey()); + if (operationMap == null) { + responseItem.getOperationsByType().put(en.getKey(), en.getValue()); + return; + } + for (Map.Entry operationEntry : en.getValue().entrySet()) { + final Long counter = operationMap.get(operationEntry.getKey()); + if (counter == null) { + operationMap.put(operationEntry.getKey(), operationEntry.getValue()); + continue; + } + + operationMap.put(operationEntry.getKey(), counter + operationEntry.getValue()); + } + }); + } + } + +} diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/stat/StatService.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/stat/StatService.java new file mode 100644 index 00000000000..3756017dc21 --- /dev/null +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/stat/StatService.java @@ -0,0 +1,334 @@ +package io.jans.lock.service.stat; + +import java.text.SimpleDateFormat; +import java.util.Base64; +import java.util.Date; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.ReentrantLock; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; + +import io.jans.lock.model.Stat; +import io.jans.lock.model.StatEntry; +import io.jans.lock.model.config.AppConfiguration; +import io.jans.lock.model.config.StaticConfiguration; +import io.jans.net.InetAddressUtility; +import io.jans.orm.PersistenceEntryManager; +import io.jans.orm.exception.EntryPersistenceException; +import io.jans.orm.model.base.SimpleBranch; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import net.agkn.hll.HLL; + +/** + * @author Yuriy Movchan Date: 12/02/2024 + */ +@ApplicationScoped +public class StatService { + + private static final String RESULT_ALLOW = "allow"; + private static final String RESULT_DENY = "deny"; + + // January - 202001, December - 202012 + private static final int REGWIDTH = 5; + private static final int LOG_2_M = 15; + + @Inject + private Logger log; + + @Inject + private PersistenceEntryManager entryManager; + + @Inject + private StaticConfiguration staticConfiguration; + + @Inject + private AppConfiguration appConfiguration; + + private String nodeId; + private String monthlyDn; + private StatEntry currentEntry; + private HLL userHll, clientHll; + private ConcurrentMap> opearationCounters; + private final SimpleDateFormat periodDateFormat = new SimpleDateFormat("yyyyMM"); + + private boolean initialized = false; + private final ReentrantLock setupCurrentEntryLock = new ReentrantLock(); + + @PostConstruct + public void create() { + initialized = false; + } + + public boolean init() { + try { + if (!appConfiguration.isStatEnabled()) { + log.trace("Stat service is not enabled"); + return false; + } + log.info("Initializing Stat Service"); + + final Date now = new Date(); + initNodeId(now); + if (StringUtils.isBlank(nodeId)) { + log.error("Failed to initialize stat service. statNodeId is not set in configuration"); + return false; + } + if (StringUtils.isBlank(getBaseDn())) { + log.error("Failed to initialize stat service. 'stat' base dn is not set in configuration"); + return false; + } + + prepareMonthlyBranch(now); + log.trace("Monthly branch created: {}", monthlyDn); + + setupCurrentEntry(now); + log.info("Initialized Stat Service"); + + initialized = true; + return true; + } catch (Exception ex) { + log.error("Failed to initialize Stat Service", ex); + return false; + } + } + + public void updateStat() { + if (!initialized) { + return; + } + + log.trace("Started updateStat ..."); + + Date now = new Date(); + prepareMonthlyBranch(now); + + setupCurrentEntry(now); + + final Stat stat = currentEntry.getStat(); + stat.setOperationsByType(opearationCounters); + stat.setLastUpdatedAt(now.getTime()); + + synchronized (userHll) { + currentEntry.setUserHllData(Base64.getEncoder().encodeToString(userHll.toBytes())); + } + synchronized (clientHll) { + currentEntry.setClientHllData(Base64.getEncoder().encodeToString(clientHll.toBytes())); + } + entryManager.merge(currentEntry); + + log.trace("Finished updateStat"); + } + + private void setupCurrentEntry() { + setupCurrentEntry(new Date()); + } + + private void setupCurrentEntry(Date now) { + String dn = String.format("jansId=%s,%s", nodeId, monthlyDn); // jansId=,ou=yyyyMM,ou=lock,ou=stat,o=jans + + final String month = monthString(now); + initNodeId(now); + + if (currentEntry != null && month.equals(currentEntry.getStat().getMonth())) { + return; + } + + setupCurrentEntryLock.lock(); + try { + // After getting lock check if another thread did initialization already + if (currentEntry != null && month.equals(currentEntry.getStat().getMonth())) { + return; + } + + StatEntry entryFromPersistence = entryManager.find(StatEntry.class, dn); + if ((entryFromPersistence != null) && month.equals(entryFromPersistence.getStat().getMonth())) { + userHll = HLL.fromBytes(Base64.getDecoder().decode(entryFromPersistence.getUserHllData())); + clientHll = HLL.fromBytes(Base64.getDecoder().decode(entryFromPersistence.getClientHllData())); + opearationCounters = new ConcurrentHashMap<>(entryFromPersistence.getStat().getOperationsByType()); + currentEntry = entryFromPersistence; + log.trace("Stat entry loaded"); + + if (StringUtils.isBlank(currentEntry.getMonth()) && currentEntry.getStat() != null) { + currentEntry.setMonth(currentEntry.getStat().getMonth()); + } + return; + } + } catch (EntryPersistenceException e) { + log.trace("Stat entry is not found in persistence"); + } finally { + setupCurrentEntryLock.unlock(); + } + + log.trace("Creating stat entry ..."); + userHll = newUserHll(); + clientHll = newClientHll(); + opearationCounters = new ConcurrentHashMap<>(); + final String monthString = periodDateFormat.format(new Date()); + + currentEntry = new StatEntry(); + currentEntry.setId(nodeId); + currentEntry.setDn(dn); + currentEntry.setUserHllData(Base64.getEncoder().encodeToString(userHll.toBytes())); + currentEntry.setClientHllData(Base64.getEncoder().encodeToString(clientHll.toBytes())); + + currentEntry.getStat().setMonth(monthString); + currentEntry.setMonth(monthString); + entryManager.persist(currentEntry); + + log.trace("Created stat entry"); + } + + protected HLL newUserHll() { + return new HLL(LOG_2_M, REGWIDTH); + } + + protected HLL newClientHll() { + return new HLL(LOG_2_M, REGWIDTH); + } + + private void initNodeId(Date now) { + if (StringUtils.isNotBlank(nodeId)) { + return; + } + + try { + nodeId = InetAddressUtility.getMACAddressOrNull() + "_" + monthString(now); + if (StringUtils.isNotBlank(nodeId)) { + return; + } + + nodeId = UUID.randomUUID().toString() + "_" + monthString(now); + } catch (Exception e) { + log.error("Failed to identify nodeId.", e); + nodeId = UUID.randomUUID().toString() + "_" + monthString(now); + } + } + + public String getNodeId() { + return nodeId; + } + + public String monthString(Date now) { + return periodDateFormat.format(now); // yyyyMM + } + + private void prepareMonthlyBranch(Date now) { + final String baseDn = getBaseDn(); + final String month = monthString(now); // yyyyMM + monthlyDn = String.format("ou=%s,%s", month, baseDn); // ou=yyyyMM,ou=lock,ou=stat,o=jans + + if (!entryManager.hasBranchesSupport(baseDn)) { + return; + } + + try { + if (!entryManager.contains(monthlyDn, SimpleBranch.class)) { // Create ou=yyyyMM branch if needed + createBranch(monthlyDn, month); + } + } catch (Exception e) { + if (log.isErrorEnabled()) + log.error("Failed to prepare monthly branch: " + monthlyDn, e); + throw e; + } + } + + public String getBaseDn() { + return staticConfiguration.getBaseDn().getStat(); + } + + public void createBranch(String branchDn, String ou) { + try { + SimpleBranch branch = new SimpleBranch(); + branch.setOrganizationalUnitName(ou); + branch.setDn(branchDn); + + entryManager.persist(branch); + } catch (EntryPersistenceException ex) { + // Check if another process added this branch already + if (!entryManager.contains(branchDn, SimpleBranch.class)) { + throw ex; + } + } + } + + public void reportActiveUser(String id) { + if (!initialized) { + return; + } + + if (StringUtils.isBlank(id)) { + return; + } + + final int hashCode = id.hashCode(); + try { + setupCurrentEntry(); + synchronized (userHll) { + userHll.addRaw(hashCode); + } + } catch (Exception e) { + log.error("Failed to report active user, id: " + id + ", hash: " + hashCode, e); + } + } + + public void reportActiveClient(String id) { + if (!initialized) { + return; + } + + if (StringUtils.isBlank(id)) { + return; + } + + final int hashCode = id.hashCode(); + try { + setupCurrentEntry(); + synchronized (clientHll) { + clientHll.addRaw(hashCode); + } + } catch (Exception e) { + log.error("Failed to report active client, id: " + id + ", hash: " + hashCode, e); + } + } + + public void reportAllow(String operationGroup) { + reportOpearation(operationGroup, RESULT_ALLOW); + } + + public void reportDeny(String operationGroup) { + reportOpearation(operationGroup, RESULT_DENY); + } + + public void reportOpearation(String operationGroup, String operationType) { + if (!initialized) { + return; + } + + if (operationGroup == null || operationType == null) { + return; + } + if (opearationCounters == null) { + log.error("Stat service is not initialized"); + return; + } + + Map operationMap = opearationCounters.computeIfAbsent(operationGroup, v -> new ConcurrentHashMap<>()); + + Long counter = operationMap.get(operationType); + + if (counter == null) { + counter = 1L; + } else { + counter++; + } + + operationMap.put(operationType, counter); + } + +} diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/stat/StatTimer.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/stat/StatTimer.java new file mode 100644 index 00000000000..ba6c6165045 --- /dev/null +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/stat/StatTimer.java @@ -0,0 +1,97 @@ +package io.jans.lock.service.stat; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.slf4j.Logger; + +import io.jans.lock.model.config.AppConfiguration; +import io.jans.lock.service.event.StatEvent; +import io.jans.service.cdi.async.Asynchronous; +import io.jans.service.cdi.event.Scheduled; +import io.jans.service.timer.event.TimerEvent; +import io.jans.service.timer.schedule.TimerSchedule; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Event; +import jakarta.enterprise.event.Observes; +import jakarta.inject.Inject; + +/** + * @author Yuriy Movchan Date: 12/02/2024 + */ +@ApplicationScoped +public class StatTimer { + + private static final int TIMER_TICK_INTERVAL_IN_SECONDS = 60; // 1 min + private static final int TIMER_INTERVAL_IN_SECONDS = 15 * 60; // 15 min + + @Inject + private Logger log; + + @Inject + private Event timerEvent; + + @Inject + private AppConfiguration appConfiguration; + + @Inject + private StatService statService; + + private AtomicBoolean isActive; + private long lastFinishedTime; + + @Asynchronous + public void initTimer() { + log.info("Initializing Stat Service Timer"); + + this.isActive = new AtomicBoolean(false); + + timerEvent.fire(new TimerEvent(new TimerSchedule(TIMER_TICK_INTERVAL_IN_SECONDS, TIMER_TICK_INTERVAL_IN_SECONDS), new StatEvent(), Scheduled.Literal.INSTANCE)); + + this.lastFinishedTime = System.currentTimeMillis(); + log.info("Initialized Stat Service Timer"); + } + + @Asynchronous + public void process(@Observes @Scheduled StatEvent event) { + if (!appConfiguration.isStatEnabled()) { + return; + } + + if (this.isActive.get()) { + return; + } + + if (!this.isActive.compareAndSet(false, true)) { + return; + } + + try { + if (!allowToRun()) { + return; + } + statService.updateStat(); + this.lastFinishedTime = System.currentTimeMillis(); + } catch (Exception ex) { + log.error("Exception happened while updating stat", ex); + } finally { + this.isActive.set(false); + } + } + + private boolean allowToRun() { + int interval = appConfiguration.getStatTimerIntervalInSeconds(); + if (interval < 0) { + log.info("Stat Timer is disabled."); + log.warn("Stat Timer Interval (statTimerIntervalInSeconds in server configuration) is negative which turns OFF statistic on the server. Please set it to positive value if you wish it to run."); + return false; + } + if (interval == 0) + interval = TIMER_INTERVAL_IN_SECONDS; + + long timerInterval = interval * 1000L; + + long timeDiff = System.currentTimeMillis() - this.lastFinishedTime; + + return timeDiff >= timerInterval; + } +} diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/util/ResteasyInitializer.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/util/ResteasyInitializer.java index 4755e28efc2..45b5fdf7737 100644 --- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/util/ResteasyInitializer.java +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/util/ResteasyInitializer.java @@ -24,31 +24,32 @@ import io.jans.lock.service.ws.rs.audit.AuditRestWebServiceImpl; import io.jans.lock.service.ws.rs.config.ConfigRestWebServiceImpl; import io.jans.lock.service.ws.rs.sse.SseRestWebServiceImpl; +import io.jans.lock.service.ws.rs.stat.StatRestWebServiceImpl; import jakarta.ws.rs.ApplicationPath; import jakarta.ws.rs.core.Application; - /** * Integration with Resteasy * * @author Yuriy Movchan Date: 06/06/2024 */ @ApplicationPath("/v1") -public class ResteasyInitializer extends Application { +public class ResteasyInitializer extends Application { @Override - public Set> getClasses() { - HashSet> classes = new HashSet>(); - classes.add(ConfigurationRestWebService.class); + public Set> getClasses() { + HashSet> classes = new HashSet>(); + classes.add(ConfigurationRestWebService.class); + + classes.add(AuditRestWebServiceImpl.class); + classes.add(ConfigRestWebServiceImpl.class); + classes.add(StatRestWebServiceImpl.class); - classes.add(AuditRestWebServiceImpl.class); - classes.add(ConfigRestWebServiceImpl.class); + classes.add(SseRestWebServiceImpl.class); - classes.add(SseRestWebServiceImpl.class); - - classes.add(AuthorizationProcessingFilter.class); + classes.add(AuthorizationProcessingFilter.class); - return classes; - } + return classes; + } } diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/ConfigurationRestWebService.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/ConfigurationRestWebService.java index 4a8b4bb4ccc..aa4bbe60348 100644 --- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/ConfigurationRestWebService.java +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/ConfigurationRestWebService.java @@ -19,7 +19,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import io.jans.lock.service.config.ConfigurationService; -import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Dependent; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @@ -32,7 +32,7 @@ * * @author Yuriy Movchan Date: 12/19/2018 */ -@ApplicationScoped +@Dependent @Path("/configuration") public class ConfigurationRestWebService { diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/audit/AuditRestWebService.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/audit/AuditRestWebService.java index dac5464b53d..21cb2db3a77 100644 --- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/audit/AuditRestWebService.java +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/audit/AuditRestWebService.java @@ -38,37 +38,37 @@ public interface AuditRestWebService { @POST @Path("/health") @Produces({ MediaType.APPLICATION_JSON }) - @ProtectedApi(scopes = {"https://jans.io/lock-server/audit.write"}) + @ProtectedApi(scopes = {"https://jans.io/oauth/lock/health.write"}) Response processHealthRequest(@Context HttpServletRequest request, @Context HttpServletResponse response, @Context SecurityContext sec); @POST @Path("/health/bulk") @Produces({ MediaType.APPLICATION_JSON }) - @ProtectedApi(scopes = {"https://jans.io/lock-server/audit.write"}) + @ProtectedApi(scopes = {"https://jans.io/oauth/lock/health.write"}) Response processBulkHealthRequest(@Context HttpServletRequest request, @Context HttpServletResponse response, @Context SecurityContext sec); @POST @Path("/log") @Produces({ MediaType.APPLICATION_JSON }) - @ProtectedApi(scopes = {"https://jans.io/lock-server/audit.write"}) + @ProtectedApi(scopes = {"https://jans.io/oauth/lock/log.write"}) Response processLogRequest(@Context HttpServletRequest request, @Context HttpServletResponse response, @Context SecurityContext sec); @POST @Path("/log/bulk") @Produces({ MediaType.APPLICATION_JSON }) - @ProtectedApi(scopes = {"https://jans.io/lock-server/audit.write"}) + @ProtectedApi(scopes = {"https://jans.io/oauth/lock/log.write"}) Response processBulkLogRequest(@Context HttpServletRequest request, @Context HttpServletResponse response, @Context SecurityContext sec); @POST @Path("/telemetry") @Produces({ MediaType.APPLICATION_JSON }) - @ProtectedApi(scopes = {"https://jans.io/lock-server/audit.write"}) + @ProtectedApi(scopes = {"https://jans.io/oauth/lock/telemetry.write"}) Response processTelemetryRequest(@Context HttpServletRequest request, @Context HttpServletResponse response, @Context SecurityContext sec); @POST @Path("/telemetry/bulk") @Produces({ MediaType.APPLICATION_JSON }) - @ProtectedApi(scopes = {"https://jans.io/lock-server/audit.write"}) + @ProtectedApi(scopes = {"https://jans.io/oauth/lock/telemetry.write"}) Response processBulkTelemetryRequest(@Context HttpServletRequest request, @Context HttpServletResponse response, @Context SecurityContext sec); } \ No newline at end of file diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/audit/AuditRestWebServiceImpl.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/audit/AuditRestWebServiceImpl.java index adf121597ef..a3fdfc3e4ba 100644 --- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/audit/AuditRestWebServiceImpl.java +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/audit/AuditRestWebServiceImpl.java @@ -22,6 +22,8 @@ import com.fasterxml.jackson.databind.JsonNode; import io.jans.lock.service.audit.AuditService; +import io.jans.lock.service.stat.StatResponseService; +import io.jans.lock.service.stat.StatService; import io.jans.lock.util.ServerUtil; import jakarta.enterprise.context.Dependent; import jakarta.inject.Inject; @@ -32,6 +34,8 @@ import jakarta.ws.rs.core.Response.Status; import jakarta.ws.rs.core.SecurityContext; +import static io.jans.lock.service.audit.AuditService.*; + /** * Provides interface for audit REST web services * @@ -41,75 +45,98 @@ @Path("/audit") public class AuditRestWebServiceImpl implements AuditRestWebService { - @Inject + private static final String LOG_PRINCIPAL_ID = "principalId"; + private static final String LOG_CLIENT_ID = "clientId"; + private static final String LOG_DECISION_RESULT = "decisionResult"; + private static final String LOG_ACTION = "action"; + + private static final String LOG_DECISION_RESULT_ALLOW = "allow"; + private static final String LOG_DECISION_RESULT_DENY = "deny"; + + @Inject private Logger log; @Inject - AuditService auditService; + private AuditService auditService; + + @Inject + private StatService statService; @Override public Response processHealthRequest(HttpServletRequest request, HttpServletResponse response, SecurityContext sec) { - log.info("Processing Health request - request:{}", request); - return processAuditRequest(request, "health"); + log.info("Processing Health request - request: {}", request); + return processAuditRequest(request, AUDIT_HEALTH); } @Override public Response processBulkHealthRequest(HttpServletRequest request, HttpServletResponse response, SecurityContext sec) { - log.info("Processing Bulk Health request - request:{}", request); - return processAuditRequest(request, "health/bulk"); + log.info("Processing Bulk Health request - request: {}", request); + return processAuditRequest(request, AUDIT_HEALTH_BULK); } @Override public Response processLogRequest(HttpServletRequest request, HttpServletResponse response, SecurityContext sec) { - log.info("Processing Log request - request:{}", request); - return processAuditRequest(request, "log"); - + log.info("Processing Log request - request: {}", request); + return processAuditRequest(request, AUDIT_LOG, true, false); } @Override public Response processBulkLogRequest(HttpServletRequest request, HttpServletResponse response, SecurityContext sec) { - log.info("Processing Bulk Log request - request:{}", request); - return processAuditRequest(request, "log/bulk"); + log.info("Processing Bulk Log request - request: {}", request); + return processAuditRequest(request, AUDIT_LOG_BULK, true, true); } @Override public Response processTelemetryRequest(HttpServletRequest request, HttpServletResponse response, SecurityContext sec) { - log.info("Processing Telemetry request - request:{}", request); - return processAuditRequest(request, "telemetry"); + log.info("Processing Telemetry request - request: {}", request); + return processAuditRequest(request, AUDIT_TELEMETRY); } @Override public Response processBulkTelemetryRequest(HttpServletRequest request, HttpServletResponse response, SecurityContext sec) { - log.info("Processing Bulk Telemetry request - request:{}", request); - return processAuditRequest(request, "telemetry/bulk"); + log.info("Processing Bulk Telemetry request - request: {}", request); + return processAuditRequest(request, AUDIT_TELEMETRY_BULK); } - private Response processAuditRequest(HttpServletRequest request, String requestType) { - log.info("Processing request - request:{}, requestType:{}", request, requestType); + private Response processAuditRequest(HttpServletRequest request, String requestType) { + return processAuditRequest(request, requestType, false, false); + } + + private Response processAuditRequest(HttpServletRequest request, String requestType, boolean reportStat, boolean bulkData) { + log.info("Processing request - request: {}, requestType: {}", request, requestType); Response.ResponseBuilder builder = Response.ok(); builder.cacheControl(ServerUtil.cacheControlWithNoStoreTransformAndPrivate()); builder.header(ServerUtil.PRAGMA, ServerUtil.NO_CACHE); JsonNode json = this.auditService.getJsonNode(request); + + if (reportStat) { + if (bulkData) { + reportBulkStat(json); + } else { + reportStat(json); + } + } + Response response = this.auditService.post(requestType, json.toString(), ContentType.APPLICATION_JSON); - log.debug("response:{}", response); + log.debug("response: {}", response); if (response != null) { log.debug( - "Response for Access Token - response.getStatus():{}, response.getStatusInfo():{}, response.getEntity().getClass():{}", + "Response for Access Token - response.getStatus(): {}, response.getStatusInfo(): {}, response.getEntity().getClass(): {}", response.getStatus(), response.getStatusInfo(), response.getEntity().getClass()); String entity = response.readEntity(String.class); - log.debug(" entity:{}", entity); + log.debug(" entity: {}", entity); builder.entity(entity); if (response.getStatusInfo().equals(Status.OK)) { - log.debug(" Status.CREATED:{}, entity:{}", Status.OK, entity); + log.debug(" Status.CREATED: {}, entity: {}", Status.OK, entity); } else { - log.error("Error while saving audit data - response.getStatusInfo():{}, entity:{}", + log.error("Error while saving audit data - response.getStatusInfo(): {}, entity: {}", response.getStatusInfo(), entity); builder.status(response.getStatusInfo()); } @@ -118,4 +145,41 @@ private Response processAuditRequest(HttpServletRequest request, String requestT return builder.build(); } + private void reportStat(JsonNode json) { + boolean hasClientId = json.hasNonNull(LOG_CLIENT_ID); + if (hasClientId) { + statService.reportActiveClient(json.get(LOG_CLIENT_ID).asText()); + } + + boolean hasPrincipalId = json.hasNonNull(LOG_PRINCIPAL_ID); + if (hasPrincipalId) { + statService.reportActiveUser(json.get(LOG_PRINCIPAL_ID).asText()); + } + + boolean hasВecisionResult = json.hasNonNull(LOG_DECISION_RESULT); + if (hasВecisionResult) { + String decisionResult = json.get(LOG_DECISION_RESULT).asText(); + if (LOG_DECISION_RESULT_ALLOW.equals(decisionResult)) { + statService.reportAllow(LOG_DECISION_RESULT); + } else if (LOG_DECISION_RESULT_DENY.equals(decisionResult)) { + statService.reportDeny(LOG_DECISION_RESULT); + } + } + + boolean hasAction = json.hasNonNull(LOG_ACTION); + if (hasAction) { + statService.reportOpearation(LOG_ACTION, json.get(LOG_ACTION).asText()); + } + } + + private void reportBulkStat(JsonNode json) { + if (!json.isArray()) { + log.error("Failed to calculate stat for bulk log entry: {}", json); + } + + for (JsonNode jsonItem : json) { + reportStat(jsonItem); + } + + } } diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/config/ConfigRestWebService.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/config/ConfigRestWebService.java index c377b12fb2c..8ac8e46ffe5 100644 --- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/config/ConfigRestWebService.java +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/config/ConfigRestWebService.java @@ -16,6 +16,7 @@ package io.jans.lock.service.ws.rs.config; +import io.jans.service.security.api.ProtectedApi; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.ws.rs.GET; @@ -36,22 +37,26 @@ public interface ConfigRestWebService { @GET @Path("/config") @Produces({ MediaType.APPLICATION_JSON }) + @ProtectedApi(scopes = {"https://jans.io/lock-server/config.read"}) Response processConfigRequest(@Context HttpServletRequest request, @Context HttpServletResponse response, @Context SecurityContext sec); @GET @Path("/config/issuers") @Produces({ MediaType.APPLICATION_JSON }) + @ProtectedApi(scopes = {"https://jans.io/lock-server/issuers.read"}) Response processIssuersRequest(@Context HttpServletRequest request, @Context HttpServletResponse response, @Context SecurityContext sec); @GET @Path("/config/schema") @Produces({ MediaType.APPLICATION_JSON }) + @ProtectedApi(scopes = {"https://jans.io/lock-server/schema.read"}) Response processSchemaRequest(@Context HttpServletRequest request, @Context HttpServletResponse response, @Context SecurityContext sec); @GET @Path("/config/policy") @Produces({ MediaType.APPLICATION_JSON }) + @ProtectedApi(scopes = {"https://jans.io/lock-server/policy.read"}) Response processPolicyRequest(@Context HttpServletRequest request, @Context HttpServletResponse response, @Context SecurityContext sec); } \ No newline at end of file diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/sse/SseRestWebService.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/sse/SseRestWebService.java index 01594c92d2f..b518d19af49 100644 --- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/sse/SseRestWebService.java +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/sse/SseRestWebService.java @@ -16,6 +16,7 @@ package io.jans.lock.service.ws.rs.sse; +import io.jans.service.security.api.ProtectedApi; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; @@ -32,6 +33,7 @@ public interface SseRestWebService { @GET @Path("/sse") @Produces(MediaType.SERVER_SENT_EVENTS) + @ProtectedApi(scopes = {"https://jans.io/oauth/lock/sse.read"}) public void subscribe(@Context Sse sse, @Context SseEventSink sseEventSink); } \ No newline at end of file diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/sse/SseRestWebServiceImpl.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/sse/SseRestWebServiceImpl.java index 14dc0ca6219..c45f6eefebe 100644 --- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/sse/SseRestWebServiceImpl.java +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/sse/SseRestWebServiceImpl.java @@ -40,7 +40,7 @@ public class SseRestWebServiceImpl implements SseRestWebService { @Override public void subscribe(@Context Sse sse, @Context SseEventSink sseEventSink) { - log.info("Sibscribe broadcaster"); + log.info("Subscribe broadcaster"); if (lockSseBroadcater.getSseBroadcaster() == null) { log.info("Init broadcaster"); lockSseBroadcater.setSse(sse); diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/FlatStatResponse.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/FlatStatResponse.java new file mode 100644 index 00000000000..40892da5862 --- /dev/null +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/FlatStatResponse.java @@ -0,0 +1,37 @@ +package io.jans.lock.service.ws.rs.stat; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.jboss.resteasy.annotations.providers.jaxb.IgnoreMediaTypes; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Yuriy Movchan Date: 12/02/2024 + */ +@IgnoreMediaTypes("application/*+json") +public class FlatStatResponse { + @JsonProperty(value = "response") // month to stat item + private List response = new ArrayList<>(); + + public FlatStatResponse() { + } + + public FlatStatResponse(List response) { + this.response = response; + } + + public List getResponse() { + if (response == null) response = new ArrayList<>(); + return response; + } + + public void setResponse(List response) { + this.response = response; + } + + @Override + public String toString() { + return "FlatStatResponse [response=" + response + "]"; + } +} diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/Months.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/Months.java new file mode 100644 index 00000000000..3f1b497b376 --- /dev/null +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/Months.java @@ -0,0 +1,99 @@ +package io.jans.lock.service.ws.rs.stat; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.LinkedHashSet; +import java.util.Set; + +import static java.time.temporal.TemporalAdjusters.firstDayOfMonth; + +/** + * @author Yuriy Movchan Date: 12/02/2024 + */ +public class Months { + + private static final Logger log = LoggerFactory.getLogger(Months.class); + + public static final DateTimeFormatter YYYYMMDD = DateTimeFormatter.ofPattern("yyyyMMdd"); + public static final DateTimeFormatter YYYYMM = DateTimeFormatter.ofPattern("yyyyMM"); + + private Months() { + } + + public static boolean isValid(String months, String startMonth, String endMonth) { + boolean hasMonths = StringUtils.isNotBlank(months); + boolean hasRange = StringUtils.isNotBlank(startMonth) && StringUtils.isNotBlank(endMonth); + if (hasMonths && hasRange) { // if both are present then invalid + return false; + } + return hasMonths || hasRange; + } + + public static Set getMonths(String months, String startMonth, String endMonth) { + if (!isValid(months, startMonth, endMonth)) { + return new LinkedHashSet<>(); + } + + boolean hasMonths = StringUtils.isNotBlank(months); + if (hasMonths) { + return getMonths(months); + } + return getMonths(startMonth, endMonth); + } + + public static LocalDate parse(String month) { + // append first day of month -> "01" + return LocalDate.parse(month + "01", YYYYMMDD).with(firstDayOfMonth()); + } + + public static Set getMonths(String startMonth, String endMonth) { + Set monthList = new LinkedHashSet<>(); + if (!checkMonthFormat(startMonth) || !checkMonthFormat(endMonth)) { + return monthList; + } + + LocalDate start = parse(startMonth); + LocalDate end = parse(endMonth); + + LocalDate date = start; + + while (date.isBefore(end)) { + monthList.add(date.format(YYYYMM)); + + date = date.plusMonths(1).with(firstDayOfMonth()); + } + + if (!monthList.isEmpty()) { // add last month + monthList.add(date.format(YYYYMM)); + } + return monthList; + } + + public static boolean checkMonthFormat(String month) { + if (month.length() == 6) { + return true; + } + + log.error("Invalid month `{}`, month must be 6 chars length in format yyyyMM, e.g. 202212", month); + return false; + } + + public static Set getMonths(String months) { + Set monthList = new LinkedHashSet<>(); + if (StringUtils.isBlank(months)) { + return monthList; + } + + for (String m : months.split(" ")) { + m = m.trim(); + if (checkMonthFormat(m)) { + monthList.add(m); + } + } + return monthList; + } +} diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatResponse.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatResponse.java new file mode 100644 index 00000000000..df7a2b233f5 --- /dev/null +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatResponse.java @@ -0,0 +1,33 @@ +package io.jans.lock.service.ws.rs.stat; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.jboss.resteasy.annotations.providers.jaxb.IgnoreMediaTypes; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Yuriy Movchan Date: 12/02/2024 + */ +@IgnoreMediaTypes("application/*+json") +@JsonIgnoreProperties(ignoreUnknown = true) +public class StatResponse { + + @JsonProperty(value = "response") // month to stat item + private Map response = new HashMap<>(); + + public Map getResponse() { + if (response == null) response = new HashMap<>(); + return response; + } + + public void setResponse(Map response) { + this.response = response; + } + + @Override + public String toString() { + return "StatResponse [response=" + response + "]"; + } +} \ No newline at end of file diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatResponseItem.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatResponseItem.java new file mode 100644 index 00000000000..d734dbf7b7a --- /dev/null +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatResponseItem.java @@ -0,0 +1,64 @@ +package io.jans.lock.service.ws.rs.stat; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Yuriy Movchan Date: 12/02/2024 + */ +public class StatResponseItem { + + @JsonProperty + private String month; + + @JsonProperty(value = "monthly_active_users") + private long monthlyActiveUsers; + + @JsonProperty(value = "monthly_active_clients") + private long monthlyActiveClients; + + @JsonProperty("operations_by_type") + private Map> operationsByType; + + public long getMonthlyActiveUsers() { + return monthlyActiveUsers; + } + + public void setMonthlyActiveUsers(long monthlyActiveUsers) { + this.monthlyActiveUsers = monthlyActiveUsers; + } + + public long getMonthlyActiveClients() { + return monthlyActiveClients; + } + + public void setMonthlyActiveClients(long monthlyActiveClients) { + this.monthlyActiveClients = monthlyActiveClients; + } + + public Map> getOperationsByType() { + if (operationsByType == null) operationsByType = new HashMap<>(); + return operationsByType; + } + + public void setOperationsByType(Map> operationsByType) { + this.operationsByType = operationsByType; + } + + public String getMonth() { + return month; + } + + public void setMonth(String month) { + this.month = month; + } + + @Override + public String toString() { + return "StatResponseItem [month=" + month + ", monthlyActiveUsers=" + monthlyActiveUsers + + ", monthlyActiveClients=" + monthlyActiveClients + ", operationsByType=" + operationsByType + "]"; + } +} + diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatRestWebService.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatRestWebService.java new file mode 100644 index 00000000000..b3864974174 --- /dev/null +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatRestWebService.java @@ -0,0 +1,41 @@ +package io.jans.lock.service.ws.rs.stat; + +import io.jans.service.security.api.ProtectedApi; +import jakarta.enterprise.context.Dependent; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +/** + * Provides server with basic statistic + * + * @author Yuriy Movchan Date: 12/02/2024 + */ +@Dependent +@Path("/internal/stat") +public interface StatRestWebService { + + @GET + @ProtectedApi(scopes = {"jans_stat"}) + @Produces(MediaType.APPLICATION_JSON) + public Response statGet(@HeaderParam("Authorization") String authorization, + @QueryParam("month") String months, + @QueryParam("start-month") String startMonth, + @QueryParam("end-month") String endMonth, + @QueryParam("format") String format); + + @POST + @ProtectedApi(scopes = {"jans_stat"}) + @Produces(MediaType.APPLICATION_JSON) + public Response statPost(@HeaderParam("Authorization") String authorization, + @FormParam("month") String months, + @FormParam("start-month") String startMonth, + @FormParam("end-month") String endMonth, + @FormParam("format") String format); +} diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatRestWebServiceImpl.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatRestWebServiceImpl.java new file mode 100644 index 00000000000..b581f34968a --- /dev/null +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/service/ws/rs/stat/StatRestWebServiceImpl.java @@ -0,0 +1,183 @@ +package io.jans.lock.service.ws.rs.stat; + +import static io.jans.as.model.util.Util.escapeLog; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.slf4j.Logger; + +import io.jans.lock.model.config.AppConfiguration; +import io.jans.lock.model.error.ErrorResponseFactory; +import io.jans.lock.model.error.StatErrorResponseType; +import io.jans.lock.service.stat.StatResponseService; +import io.jans.lock.util.Constants; +import io.jans.lock.util.ServerUtil; +import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.Counter; +import io.prometheus.client.exporter.common.TextFormat; +import jakarta.enterprise.context.Dependent; +import jakarta.inject.Inject; +import jakarta.ws.rs.FormParam; +import jakarta.ws.rs.HeaderParam; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +/** + * Provides server with basic statistic + * + * @author Yuriy Movchan Date: 12/02/2024 + */ +@Dependent +@Path("/internal/stat") +public class StatRestWebServiceImpl implements StatRestWebService { + + @Inject + private Logger log; + + @Inject + private StatResponseService statResponseService; + + @Inject + private ErrorResponseFactory errorResponseFactory; + + @Inject + private AppConfiguration appConfiguration; + + @Override + public Response statGet(@HeaderParam("Authorization") String authorization, + @QueryParam("month") String months, + @QueryParam("start-month") String startMonth, + @QueryParam("end-month") String endMonth, + @QueryParam("format") String format) { + return stat(authorization, months, startMonth, endMonth, format); + } + + @Override + public Response statPost(@HeaderParam("Authorization") String authorization, + @FormParam("month") String months, + @FormParam("start-month") String startMonth, + @FormParam("end-month") String endMonth, + @FormParam("format") String format) { + return stat(authorization, months, startMonth, endMonth, format); + } + + public static String createOpenMetricsResponse(StatResponse statResponse) throws IOException { + Writer writer = new StringWriter(); + CollectorRegistry registry = new CollectorRegistry(); + + final Counter usersCounter = Counter.build().name("monthly_active_users").labelNames(Constants.MONTH) + .help("Monthly active users").register(registry); + + final Counter clientsCounter = Counter.build().name("monthly_active_clients").labelNames(Constants.MONTH) + .help("Monthly active clients").register(registry); + + Map counterMap = new HashMap(); + for (Map.Entry entry : statResponse.getResponse().entrySet()) { + final String month = entry.getKey(); + final StatResponseItem item = entry.getValue(); + + usersCounter.labels(month).inc(item.getMonthlyActiveUsers()); + + clientsCounter.labels(month).inc(item.getMonthlyActiveClients()); + + for (Map.Entry> operationTypeEntry : item.getOperationsByType().entrySet()) { + final String operationType = operationTypeEntry.getKey(); + final Map operationTypeMap = operationTypeEntry.getValue(); + + for (Map.Entry operationEntry : operationTypeMap.entrySet()) { + final String operation = operationEntry.getKey(); + + Counter operationCounter; + if (counterMap.containsKey(operationType)) { + operationCounter = counterMap.get(operationType); + } else { + operationCounter = Counter.build() + .name(operationType) + .labelNames(Constants.MONTH, "decision") + .help(operationType).register(registry); + counterMap.put(operationType, operationCounter); + } + + operationCounter.labels(month, operation).inc(getOperationCount(operationTypeMap, operation)); + } + } + } + + TextFormat.write004(writer, registry.metricFamilySamples()); + + return writer.toString(); + } + + private static long getOperationCount(Map map, String key) { + Long v = map.get(key); + return v != null ? v : 0; + } + + public Response stat(String authorization, String monthsParam, String startMonth, String endMonth, String format) { + if (log.isDebugEnabled()) { + log.debug("Attempting to request stat, month: {}, startMonth: {}, endMonth: {}, format: {}", + escapeLog(monthsParam), escapeLog(startMonth), escapeLog(endMonth), escapeLog(format)); + } + + if (!appConfiguration.isStatEnabled()) { + throw errorResponseFactory.createWebApplicationException(Response.Status.FORBIDDEN, StatErrorResponseType.ACCESS_DENIED, "Future stat is disabled on server."); + } + + final Set months = validateMonths(monthsParam, startMonth, endMonth); + + try { + if (log.isTraceEnabled()) { + log.trace("Recognized months: {}", escapeLog(months)); + } + final StatResponse statResponse = statResponseService.buildResponse(months); + + final String responseAsStr; + if ("openmetrics".equalsIgnoreCase(format)) { + responseAsStr = createOpenMetricsResponse(statResponse); + } else if ("jsonmonth".equalsIgnoreCase(format)) { + responseAsStr = ServerUtil.asJson(statResponse); + } else { + responseAsStr = ServerUtil.asJson(new FlatStatResponse(new ArrayList<>(statResponse.getResponse().values()))); + } + + if (log.isTraceEnabled()) { + log.trace("Stat: {}", responseAsStr); + } + return Response.ok().entity(responseAsStr).build(); + } catch (WebApplicationException e) { + if (log.isTraceEnabled()) { + log.trace(e.getMessage(), e); + } + throw e; + } catch (Exception e) { + log.error(e.getMessage(), e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).type(MediaType.APPLICATION_JSON_TYPE).build(); + } + } + + private Set validateMonths(String months, String startMonth, String endMonth) { + if (!Months.isValid(months, startMonth, endMonth)) { + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, StatErrorResponseType.INVALID_REQUEST, "`month` or `start-month`/`end-month` parameter(s) can't be blank and should be in format yyyyMM (e.g. 202012)"); + } + + months = ServerUtil.urlDecode(months); + + Set monthList = Months.getMonths(months, startMonth, endMonth); + + if (monthList.isEmpty()) { + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, StatErrorResponseType.INVALID_REQUEST, "Unable to identify months. Check `month` or `start-month`/`end-month` parameter(s). It can't be blank and should be in format yyyyMM (e.g. 202012). start-month must be before end-month"); + } + + return monthList; + } + +} diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/util/Constants.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/util/Constants.java new file mode 100644 index 00000000000..d4eac1f29cc --- /dev/null +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/util/Constants.java @@ -0,0 +1,20 @@ +/* + * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. + * + * Copyright (c) 2020, Janssen Project + */ +package io.jans.lock.util; + +/** + * Provides server with basic statistic + * + * @author Yuriy Movchan Date: 12/24/2024 + */ +public class Constants { + + private Constants() { + } + + + public static final String MONTH = "month"; +} diff --git a/jans-lock/lock-server/service/src/main/java/io/jans/lock/util/ServerUtil.java b/jans-lock/lock-server/service/src/main/java/io/jans/lock/util/ServerUtil.java index 5395e76df23..55ca7062b7b 100644 --- a/jans-lock/lock-server/service/src/main/java/io/jans/lock/util/ServerUtil.java +++ b/jans-lock/lock-server/service/src/main/java/io/jans/lock/util/ServerUtil.java @@ -17,7 +17,10 @@ package io.jans.lock.util; import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,6 +32,7 @@ import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.jans.util.Util; import jakarta.ws.rs.core.CacheControl; /** @@ -93,4 +97,16 @@ public static String toPrettyJson(ObjectNode jsonObject) throws JsonProcessingEx ObjectMapper mapper = new ObjectMapper(); return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonObject); } + + + public static String urlDecode(String str) { + if (StringUtils.isNotBlank(str)) { + try { + return URLDecoder.decode(str, Util.UTF8); + } catch (UnsupportedEncodingException e) { + log.trace(e.getMessage(), e); + } + } + return str; + } } From 57f970e71f819e90734fd6f0b7f9fb07b9000ec8 Mon Sep 17 00:00:00 2001 From: YuriyZ Date: Mon, 6 Jan 2025 16:42:51 +0200 Subject: [PATCH 3/7] chore(jans-auth-server): sanitized username to avoid fake logs from input (#10543) chore(jans-auth-server): sanitized username to avoid fake logs from input #10543 Signed-off-by: YuriyZ --- .../io/jans/as/server/auth/Authenticator.java | 27 ++++++++++--------- .../io/jans/as/server/util/ServerUtil.java | 16 +++++++++++ 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/auth/Authenticator.java b/jans-auth-server/server/src/main/java/io/jans/as/server/auth/Authenticator.java index bb99ae6cbed..16a0aa5052d 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/auth/Authenticator.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/auth/Authenticator.java @@ -44,6 +44,7 @@ import java.util.Map.Entry; import static io.jans.as.model.config.Constants.AUTH_STEP; +import static io.jans.as.server.util.ServerUtil.sanitizeUsernameForLog; import static org.apache.commons.lang3.BooleanUtils.isFalse; import static org.apache.commons.lang3.BooleanUtils.isTrue; @@ -213,7 +214,7 @@ public String authenticateImpl(HttpServletRequest servletRequest, boolean intera boolean service) { String result = Constants.RESULT_FAILURE; try { - logger.trace("Authenticating ... (interactive: {}, skipPassword: {}, credentials.username: {})", interactive, skipPassword, credentials.getUsername()); + logger.trace("Authenticating ... (interactive: {}, skipPassword: {}, credentials.username: {})", interactive, skipPassword, sanitizeUsernameForLog(credentials.getUsername())); if (isServiceAuthentication(service, skipPassword, servletRequest)) { boolean authenticated = clientAuthentication(credentials, interactive, skipPassword); if (authenticated) { @@ -237,11 +238,11 @@ public String authenticateImpl(HttpServletRequest servletRequest, boolean intera } if (Constants.RESULT_SUCCESS.equals(result)) { - logger.trace("Authentication successfully for '{}'", credentials.getUsername()); + logger.trace("Authentication successfully for '{}'", sanitizeUsernameForLog(credentials.getUsername())); return result; } - logger.info("Authentication failed for '{}'", credentials.getUsername()); + logger.debug("Authentication failed for '{}'", sanitizeUsernameForLog(credentials.getUsername())); return result; } @@ -259,7 +260,7 @@ public boolean clientAuthentication(Credentials credentials, boolean interactive boolean result = externalAuthenticationService.executeExternalAuthenticate(customScriptConfiguration, null, 1); - logger.info("Authentication result for user '{}', result: '{}'", credentials.getUsername(), result); + logger.info("Authentication result for user '{}', result: '{}'", sanitizeUsernameForLog(credentials.getUsername()), result); if (result) { Client client = authenticationService.configureSessionClient(); @@ -445,10 +446,10 @@ private String userAuthenticationInteractive(HttpServletRequest servletRequest) authenticationService.quietLogin(credentials.getUsername()); // Redirect to authorization workflow - logger.debug("Sending event to trigger user redirection: '{}'", credentials.getUsername()); + logger.debug("Sending event to trigger user redirection: '{}'", sanitizeUsernameForLog(credentials.getUsername())); authenticationService.onSuccessfulLogin(eventSessionId); - logger.info(AUTHENTICATION_SUCCESS_FOR_USER, credentials.getUsername()); + logger.info(AUTHENTICATION_SUCCESS_FOR_USER, sanitizeUsernameForLog(credentials.getUsername())); return Constants.RESULT_SUCCESS; } } else { @@ -460,14 +461,14 @@ private String userAuthenticationInteractive(HttpServletRequest servletRequest) sessionIdAttributes); // Redirect to authorization workflow - logger.debug("Sending event to trigger user redirection: '{}'", credentials.getUsername()); + logger.debug("Sending event to trigger user redirection: '{}'", sanitizeUsernameForLog(credentials.getUsername())); authenticationService.onSuccessfulLogin(eventSessionId); } else { // Force session lastUsedAt update if authentication attempt is failed sessionIdService.updateSessionId(sessionId); } - logger.info(AUTHENTICATION_SUCCESS_FOR_USER, credentials.getUsername()); + logger.info(AUTHENTICATION_SUCCESS_FOR_USER, sanitizeUsernameForLog(credentials.getUsername())); return Constants.RESULT_SUCCESS; } } @@ -528,16 +529,16 @@ private boolean userAuthenticationService() { boolean result = externalAuthenticationService.executeExternalAuthenticate(customScriptConfiguration, null, 1); - logger.info("Authentication result for '{}'. auth_step: '{}', result: '{}'", credentials.getUsername(), + logger.info("Authentication result for '{}'. auth_step: '{}', result: '{}'", sanitizeUsernameForLog(credentials.getUsername()), this.authStep, result); if (result) { authenticationService.configureEventUser(); - logger.info(AUTHENTICATION_SUCCESS_FOR_USER, credentials.getUsername()); + logger.info(AUTHENTICATION_SUCCESS_FOR_USER, sanitizeUsernameForLog(credentials.getUsername())); return true; } - logger.info("Authentication failed for User: '{}'", credentials.getUsername()); + logger.info("Authentication failed for User: '{}'", sanitizeUsernameForLog(credentials.getUsername())); } } @@ -547,10 +548,10 @@ private boolean userAuthenticationService() { if (authenticated) { authenticationService.configureEventUser(); - logger.info(AUTHENTICATION_SUCCESS_FOR_USER, credentials.getUsername()); + logger.info(AUTHENTICATION_SUCCESS_FOR_USER, sanitizeUsernameForLog(credentials.getUsername())); return true; } - logger.info("Authentication failed for User: '{}'", credentials.getUsername()); + logger.info("Authentication failed for User: '{}'", sanitizeUsernameForLog(credentials.getUsername())); } return false; diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/util/ServerUtil.java b/jans-auth-server/server/src/main/java/io/jans/as/server/util/ServerUtil.java index 7e6ad125190..1bbf17244a2 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/util/ServerUtil.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/util/ServerUtil.java @@ -26,6 +26,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.ws.rs.core.CacheControl; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.text.StringEscapeUtils; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -66,6 +67,21 @@ public class ServerUtil { private ServerUtil() { } + /** + * Sanitized username before output by logger + * @param username username + * + * @return sanitized username + */ + public static String sanitizeUsernameForLog(String username) { + if (username == null) { + return "unknown_user"; + } + final int maximumUsernameLength = 50; + username = username.length() > maximumUsernameLength ? username.substring(0, maximumUsernameLength) : username; + return StringEscapeUtils.escapeJava(username).replaceAll("[\\r\\n]", "_"); + } + public static Map prepareForLogs(Map parameters) { if (parameters == null || parameters.isEmpty()) { return new HashMap<>(); From 595201850937ef901777eae2877b32938e364019 Mon Sep 17 00:00:00 2001 From: Mohammad Abudayyeh <47318409+moabu@users.noreply.github.com> Date: Tue, 7 Jan 2025 10:43:12 +0000 Subject: [PATCH 4/7] ci: address missing search indexes on clean ups (#10560) --- .github/workflows/build-docs.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 7f62905eff6..b1b771aa930 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -207,6 +207,10 @@ jobs: git add index.yaml && git update-index --refresh cd .. # END move generated chart from a previous step + + # copy search from nightly to all other versions. This is to ensure that the search index is available for all versions + for folder in v*/; do cp -r nightly/search "$folder"; done + # END copy search from nightly to all other versions echo "Replacing release number markers with actual release number" cd ${LATEST} From 02c3df77be977248529ccfc23145a37049e12633 Mon Sep 17 00:00:00 2001 From: YuriyZ Date: Tue, 7 Jan 2025 14:13:21 +0200 Subject: [PATCH 5/7] fix(jans-auth-server): challenge endpoint returns 400 if authorize throws an unexpected exception (#10553) https://github.com/JanssenProject/jans/issues/10553 Signed-off-by: YuriyZ --- .../authorize/ws/rs/AuthorizationChallengeService.java | 4 ++-- .../java/io/jans/as/server/model/common/CacheGrant.java | 6 +++++- .../external/ExternalAuthorizationChallengeService.java | 4 ++++ jans-linux-setup/jans_setup/templates/scripts.ldif | 8 ++++---- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeService.java index e6543f948e4..2cc8ece3184 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizationChallengeService.java @@ -164,8 +164,8 @@ public Response authorize(AuthzRequest authzRequest) throws IOException, TokenBi if (!ok) { log.debug("Not allowed by authorization challenge script, client_id {}.", client.getClientId()); throw new WebApplicationException(errorResponseFactory - .newErrorResponse(Response.Status.BAD_REQUEST) - .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.ACCESS_DENIED, state, "No allowed by authorization challenge script.")) + .newErrorResponse(Response.Status.UNAUTHORIZED) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.ACCESS_DENIED, state, "Not allowed by authorization challenge script.")) .build()); } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/CacheGrant.java b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/CacheGrant.java index b86ef6aa28f..c73914ab508 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/CacheGrant.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/model/common/CacheGrant.java @@ -43,6 +43,7 @@ public class CacheGrant implements Serializable { private String acrValues; private String sessionDn; private int expiresIn = 1; + private boolean isAuthorizationChallenge; // CIBA private String authReqId; @@ -73,6 +74,7 @@ public CacheGrant(AuthorizationGrant grant, AppConfiguration appConfiguration) { codeChallengeMethod = grant.getCodeChallengeMethod(); claims = grant.getClaims(); sessionDn = grant.getSessionDn(); + isAuthorizationChallenge = grant.isAuthorizationChallenge(); } public CacheGrant(CIBAGrant grant, AppConfiguration appConfiguration) { @@ -263,6 +265,7 @@ public AuthorizationCodeGrant asCodeGrant(Instance g grant.setAcrValues(acrValues); grant.setNonce(nonce); grant.setClaims(claims); + grant.setAuthorizationChallenge(isAuthorizationChallenge); return grant; } @@ -335,11 +338,12 @@ public String getDeviceCode() { @Override public String toString() { - return "MemcachedGrant{" + + return "CacheGrant{" + "authorizationCode=" + authorizationCodeString + ", user=" + user + ", client=" + client + ", authenticationTime=" + authenticationTime + + ", isAuthorizationChallenge=" + isAuthorizationChallenge + '}'; } } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/ExternalAuthorizationChallengeService.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/ExternalAuthorizationChallengeService.java index 32d93f7070c..36700006f0e 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/ExternalAuthorizationChallengeService.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/ExternalAuthorizationChallengeService.java @@ -104,6 +104,10 @@ public boolean externalAuthorize(ExecutionContext executionContext) { } catch (Exception ex) { log.error(ex.getMessage(), ex); saveScriptError(script.getCustomScript(), ex); + throw new WebApplicationException(errorResponseFactory + .newErrorResponse(Response.Status.INTERNAL_SERVER_ERROR) + .entity(errorResponseFactory.getErrorAsJson(AuthorizeErrorResponseType.ACCESS_DENIED, executionContext.getAuthzRequest().getState(), "Unable to run authorization challenge script.")) + .build()); } log.trace("Finished 'authorize' method, script name: {}, clientId: {}, result: {}", script.getName(), executionContext.getAuthzRequest().getClientId(), result); diff --git a/jans-linux-setup/jans_setup/templates/scripts.ldif b/jans-linux-setup/jans_setup/templates/scripts.ldif index c8c45f9c485..5a4defd2202 100644 --- a/jans-linux-setup/jans_setup/templates/scripts.ldif +++ b/jans-linux-setup/jans_setup/templates/scripts.ldif @@ -532,7 +532,7 @@ jansEnabled: FALSE jansLevel: 1 jansModuleProperty: {"value1":"location_type","value2":"db","description":""} jansProgLng: java -jansRevision: 11 +jansRevision: 1 jansScr::%(discovery_discovery)s jansScrTyp: discovery @@ -546,7 +546,7 @@ jansEnabled: true jansLevel: 1 jansModuleProperty: {"value1":"location_type","value2":"db","description":""} jansProgLng: java -jansRevision: 11 +jansRevision: 1 jansScr::%(authz_detail_authzdetail)s jansScrTyp: authz_detail @@ -560,7 +560,7 @@ jansEnabled: true jansLevel: 1 jansModuleProperty: {"value1":"location_type","value2":"db","description":""} jansProgLng: java -jansRevision: 11 +jansRevision: 1 jansScr::%(authorization_challenge_authorizationchallenge)s jansScrTyp: authorization_challenge @@ -574,7 +574,7 @@ jansEnabled: true jansLevel: 1 jansModuleProperty: {"value1":"location_type","value2":"db","description":""} jansProgLng: java -jansRevision: 11 +jansRevision: 1 jansScr::%(access_evaluation_accessevaluation)s jansScrTyp: access_evaluation From 4fa6699cb5a43416080e01d7f10ab37598cd310b Mon Sep 17 00:00:00 2001 From: Mohammad Abudayyeh <47318409+moabu@users.noreply.github.com> Date: Tue, 7 Jan 2025 15:52:30 +0000 Subject: [PATCH 6/7] ci: enhance the security of gh workflows (#10564) * ci: enhance security of workflows * ci: fix docs git add of search folders * chore: remove jans-tent * docs: update docs with the removal of jans tent * ci: fix pip upgrade Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: fix ignore previously installed packages Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: fix clean up dep in build of assets Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: fix clean up dep in build of assets Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: fix clean up Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: fix clean up Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: fix clean up Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: fix clean up Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: fix permission level for clean up Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: skip deleting if the service doesn't have any packages Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: load all pages Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: load all pages Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> * ci: load all pages Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> --------- Signed-off-by: moabu <47318409+moabu@users.noreply.github.com> --- .github/dependabot.yml | 4 - .github/workflows/build-docker-image.yml | 18 +- .github/workflows/build-docs.yml | 5 +- .github/workflows/build-packages.yml | 8 +- .github/workflows/build-test.yml | 67 +++-- .github/workflows/lint-flak8.yml | 17 +- .github/workflows/ops-docs.yml | 4 +- .github/workflows/ops-label-pr-issues.yml | 10 +- .github/workflows/release.yaml | 7 +- .github/workflows/sanitary-github-cache.yml | 3 +- .github/workflows/sanitary-workflow-runs.yml | 2 + .github/workflows/scan-sonar.yml | 7 +- .github/workflows/test-cedarling.yml | 4 +- .github/workflows/test-jans-pycloudlib.yml | 2 +- README.md | 1 - demos/README.md | 8 +- demos/jans-tent/.flaskenv | 2 - demos/jans-tent/.gitignore | 146 --------- demos/jans-tent/LICENSE | 201 ------------- demos/jans-tent/README.md | 144 --------- demos/jans-tent/behave.ini | 3 - demos/jans-tent/clientapp/__init__.py | 251 ---------------- demos/jans-tent/clientapp/config.py | 36 --- demos/jans-tent/clientapp/helpers/__init__.py | 0 .../clientapp/helpers/cgf_checker.py | 17 -- .../clientapp/helpers/client_handler.py | 117 -------- .../clientapp/helpers/custom_msg_factory.py | 59 ---- demos/jans-tent/clientapp/templates/home.html | 21 -- demos/jans-tent/clientapp/utils/__init__.py | 0 .../clientapp/utils/dcr_from_config.py | 41 --- demos/jans-tent/clientapp/utils/logger.py | 16 - .../docs/images/authorize_code_flow.png | Bin 53305 -> 0 bytes demos/jans-tent/main.py | 6 - demos/jans-tent/register_new_client.py | 12 - demos/jans-tent/requirements.txt | 119 -------- .../tests/behaver/features/environment.py | 31 -- .../tests/behaver/features/oidc_auth.feature | 40 --- .../features/passport_social_auth.feature | 26 -- .../tests/behaver/features/steps/allow.py | 116 -------- .../tests/unit_integration/helper.py | 189 ------------ .../test_callback_endpoint.py | 62 ---- .../unit_integration/test_cfg_checker.py | 0 .../test_client_register_endpoint.py | 145 --------- .../tests/unit_integration/test_config.py | 19 -- .../test_configuration_endpoint.py | 107 ------- .../unit_integration/test_dcr_from_config.py | 76 ----- .../test_dynamic_client_registration.py | 277 ------------------ .../unit_integration/test_flask_factory.py | 93 ------ .../test_gluu_preselected_provider.py | 46 --- .../unit_integration/test_logout_endpoint.py | 65 ---- .../test_protected_content_endpoint.py | 68 ----- .../agama/quick-start-using-agama-lab.md | 2 +- 52 files changed, 103 insertions(+), 2617 deletions(-) delete mode 100644 demos/jans-tent/.flaskenv delete mode 100644 demos/jans-tent/.gitignore delete mode 100644 demos/jans-tent/LICENSE delete mode 100644 demos/jans-tent/README.md delete mode 100644 demos/jans-tent/behave.ini delete mode 100644 demos/jans-tent/clientapp/__init__.py delete mode 100644 demos/jans-tent/clientapp/config.py delete mode 100644 demos/jans-tent/clientapp/helpers/__init__.py delete mode 100644 demos/jans-tent/clientapp/helpers/cgf_checker.py delete mode 100644 demos/jans-tent/clientapp/helpers/client_handler.py delete mode 100644 demos/jans-tent/clientapp/helpers/custom_msg_factory.py delete mode 100644 demos/jans-tent/clientapp/templates/home.html delete mode 100644 demos/jans-tent/clientapp/utils/__init__.py delete mode 100644 demos/jans-tent/clientapp/utils/dcr_from_config.py delete mode 100644 demos/jans-tent/clientapp/utils/logger.py delete mode 100644 demos/jans-tent/docs/images/authorize_code_flow.png delete mode 100644 demos/jans-tent/main.py delete mode 100644 demos/jans-tent/register_new_client.py delete mode 100644 demos/jans-tent/requirements.txt delete mode 100644 demos/jans-tent/tests/behaver/features/environment.py delete mode 100644 demos/jans-tent/tests/behaver/features/oidc_auth.feature delete mode 100644 demos/jans-tent/tests/behaver/features/passport_social_auth.feature delete mode 100644 demos/jans-tent/tests/behaver/features/steps/allow.py delete mode 100644 demos/jans-tent/tests/unit_integration/helper.py delete mode 100644 demos/jans-tent/tests/unit_integration/test_callback_endpoint.py delete mode 100644 demos/jans-tent/tests/unit_integration/test_cfg_checker.py delete mode 100644 demos/jans-tent/tests/unit_integration/test_client_register_endpoint.py delete mode 100644 demos/jans-tent/tests/unit_integration/test_config.py delete mode 100644 demos/jans-tent/tests/unit_integration/test_configuration_endpoint.py delete mode 100644 demos/jans-tent/tests/unit_integration/test_dcr_from_config.py delete mode 100644 demos/jans-tent/tests/unit_integration/test_dynamic_client_registration.py delete mode 100644 demos/jans-tent/tests/unit_integration/test_flask_factory.py delete mode 100644 demos/jans-tent/tests/unit_integration/test_gluu_preselected_provider.py delete mode 100644 demos/jans-tent/tests/unit_integration/test_logout_endpoint.py delete mode 100644 demos/jans-tent/tests/unit_integration/test_protected_content_endpoint.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f33fe6fac22..97049410915 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -26,10 +26,6 @@ updates: schedule: interval: daily - - package-ecosystem: pip - directory: /demos/jans-tent - schedule: - interval: daily - package-ecosystem: docker directory: /docker-jans-all-in-one diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml index d398da52eca..6a0dfef7cb1 100644 --- a/.github/workflows/build-docker-image.yml +++ b/.github/workflows/build-docker-image.yml @@ -57,7 +57,7 @@ jobs: egress-policy: audit - name: Install Cosign - uses: sigstore/cosign-installer@v3.5.0 + uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0 - name: Checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 @@ -91,9 +91,9 @@ jobs: if: steps.build_docker_image.outputs.build || github.event_name == 'tags' run: | sudo apt-get update - sudo python3 -m pip install --upgrade pip - sudo pip3 install setuptools --upgrade - sudo pip3 install -r ./automation/requirements.txt + sudo python3 -m pip install --upgrade pip || echo "Failed to upgrade pip" + sudo pip3 install --ignore-installed setuptools --upgrade + sudo pip3 install --ignore-installed -r ./automation/requirements.txt sudo apt-get update #- uses: actions/delete-package-versions@v5 @@ -165,19 +165,19 @@ jobs: fi # UPDATE BUILD DATES INSIDE THE DOCKERFILE BEFORE BUILDING THE DEV IMAGES TRIGGERED BY JENKINS - - name: Setup Python 3.7 + - name: Setup Python 3.10 if: github.event_name == 'workflow_dispatch' && ${{ matrix.docker-images }} != 'loadtesting-jmeter' uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 with: - python-version: 3.7 + python-version: "3.10" - name: Install Python dependencies if: github.event_name == 'workflow_dispatch' && ${{ matrix.docker-images }} != 'loadtesting-jmeter' run: | sudo apt-get update - sudo python3 -m pip install --upgrade pip - sudo pip3 install setuptools --upgrade - sudo pip3 install -r ./automation/requirements.txt + sudo python3 -m pip install --upgrade pip || echo "Failed to upgrade pip" + sudo pip3 install --ignore-installed setuptools --upgrade + sudo pip3 install --ignore-installed -r ./automation/requirements.txt sudo apt-get update sudo apt-get install jq diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index b1b771aa930..28e9ce016b9 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -209,7 +209,10 @@ jobs: # END move generated chart from a previous step # copy search from nightly to all other versions. This is to ensure that the search index is available for all versions - for folder in v*/; do cp -r nightly/search "$folder"; done + for folder in v*/; do + cp -r nightly/search "$folder" + git add $folder/search && git update-index --refresh + done # END copy search from nightly to all other versions echo "Replacing release number markers with actual release number" diff --git a/.github/workflows/build-packages.yml b/.github/workflows/build-packages.yml index 6d5687ec882..258caa392d2 100644 --- a/.github/workflows/build-packages.yml +++ b/.github/workflows/build-packages.yml @@ -5,6 +5,8 @@ on: tags: - 'v**' - 'nightly' +permissions: + contents: read jobs: publish_binary_packages: if: github.repository == 'JanssenProject/jans' @@ -196,7 +198,7 @@ jobs: run: | sudo apt-get update sudo apt-get install -y python3 build-essential ca-certificates dbus systemd iproute2 gpg python3-pip python3-dev libpq-dev gcc - python3 -m pip install --upgrade pip + python3 -m pip install --upgrade pip || echo "Failed to upgrade pip" pip3 install shiv wheel setuptools echo "Building jans-linux-setup package" sudo chown -R runner:docker /home/runner/work/jans/jans @@ -356,8 +358,8 @@ jobs: git_user_signingkey: true git_commit_gpgsign: true - - uses: actions/setup-python@v5 - - uses: PyO3/maturin-action@v1 + - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 + - uses: PyO3/maturin-action@ea5bac0f1ccd0ab11c805e2b804bfcb65dac2eab # v1.45.0 with: working-directory: ${{ github.workspace }}/jans-cedarling/bindings/cedarling_python command: build diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 0142d4bc5b4..845e8702a16 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -40,35 +40,50 @@ on: concurrency: group: run-once cancel-in-progress: false +permissions: + contents: read jobs: cleanup: - if: github.event_name == 'push' && github.event.ref == 'refs/heads/main' runs-on: ubuntu-20.04 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + permissions: + contents: read + packages: write steps: - name: Harden Runner uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 with: egress-policy: audit - name: Get version ID for 0.0.0-nightly - if: github.event_name == 'push' && github.ref == 'refs/heads/main' id: get_version_id run: | - services=$(gh api -H "Accept: application/vnd.github+json" \ - /orgs/JanssenProject/packages?package_type=maven \ - | jq -r '.[].name') - for service in "${services}"; do - version_id=$(gh api -H "Accept: application/vnd.github+json" \ - /orgs/JanssenProject/packages/maven/io.jans.${service}/versions \ - | jq -r '.[] | select(.name == "0.0.0-nightly") | .id') - echo "version_id=$version_id" >> $GITHUB_ENV - gh api --method DELETE \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - /orgs/JanssenProject/packages/maven/io.jans."${service}"/versions/"${version_id}" + page=1 + services="" + while true; do + response=$(gh api -H "Accept: application/vnd.github+json" \ + /orgs/JanssenProject/packages?package_type=maven\&per_page=100\&page=$page) + names=$(echo "$response" | jq -r '.[].name') + if [ -z "$names" ]; then + break + fi + services="$services $names" + page=$((page + 1)) done - + + services=$(echo "$services" | tr '\n' ' ' | sed 's/ *$//') + echo "Services: $services" + for service in $services; do + echo "Checking $service" + version_id=$(gh api -H "Accept: application/vnd.github+json" \ + /orgs/JanssenProject/packages/maven/"${service}"/versions \ + | jq -r '.[] | select(.name == "0.0.0-nightly") | .id') + echo "version_id=$version_id" >> $GITHUB_ENV + gh api --method DELETE \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /orgs/JanssenProject/packages/maven/"${service}"/versions/"${version_id}" || echo "Failed to delete $service" + done prep-matrix: needs: cleanup @@ -126,18 +141,18 @@ jobs: with: egress-policy: audit - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.inputs.branch }} - name: Set up Java 17 - uses: actions/setup-java@v4 + uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 with: java-version: '17' distribution: 'adopt' - name: Set up Maven - uses: actions/setup-java@v4 + uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 with: java-version: '17' distribution: 'adopt' @@ -159,7 +174,7 @@ jobs: - name: Archive results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 with: name: build-results path: ${{ matrix.service }}/target @@ -170,7 +185,9 @@ jobs: run-tests: if: github.event_name == 'push' || github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && github.event.inputs.project == 'jans-bom, jans-orm, jans-core, jans-lock/lock-server, agama, jans-auth-server, jans-link, jans-fido2, jans-scim, jans-keycloak-link, jans-config-api, jans-keycloak-integration, jans-casa') - permissions: write-all + permissions: + contents: read + packages: write needs: cleanup runs-on: ubuntu-20.04 env: @@ -198,18 +215,18 @@ jobs: with: egress-policy: audit - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.inputs.branch }} - name: Set up Java 17 - uses: actions/setup-java@v4 + uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 with: java-version: '17' distribution: 'adopt' - name: Set up Maven - uses: actions/setup-java@v4 + uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 with: java-version: '17' distribution: 'adopt' @@ -276,13 +293,13 @@ jobs: ls /tmp/reports/ - name: Upload Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 with: name: ${{ matrix.persistence }}-test-results path: /tmp/reports - name: Publish Test Report ${{ matrix.persistence }} - uses: starburstdata/action-testng-report@v1 + uses: starburstdata/action-testng-report@f245422953fb97ec5075d07782a1b596124b7cc4 # v1.0.5 with: report_paths: /tmp/reports/${{ matrix.persistence }}*.xml github_token: ${{ github.token }} diff --git a/.github/workflows/lint-flak8.yml b/.github/workflows/lint-flak8.yml index 63dadc76b64..e8eff713621 100644 --- a/.github/workflows/lint-flak8.yml +++ b/.github/workflows/lint-flak8.yml @@ -4,14 +4,16 @@ on: branches: - main paths: - #TODO: add all python projects paths below "jans-pycloudlib", "jans-cli-tui", "jans-linux-setup" - - 'demos/jans-tent/**' + - 'jans-pycloudlib/**' + - 'jans-cli-tui/**' + - 'jans-linux-setup/**' pull_request: branches: - main paths: - #TODO: add all python projects paths below "jans-pycloudlib", "jans-cli-tui", "jans-linux-setup" - - 'demos/jans-tent/**' + - 'jans-pycloudlib/**' + - 'jans-cli-tui/**' + - 'jans-linux-setup/**' permissions: contents: read @@ -23,8 +25,11 @@ jobs: #max-parallel: 1 fail-fast: false matrix: - #TODO: add all python projects paths below "jans-pycloudlib", "jans-cli-tui", "jans-linux-setup" - python-projects: ["demos/jans-tent"] + python-projects: [ + "jans-pycloudlib", + "jans-cli-tui", + "jans-linux-setup" + ] steps: - name: Harden Runner uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 diff --git a/.github/workflows/ops-docs.yml b/.github/workflows/ops-docs.yml index 07c61013b3f..34311ad3e04 100644 --- a/.github/workflows/ops-docs.yml +++ b/.github/workflows/ops-docs.yml @@ -71,10 +71,10 @@ jobs: with: fetch-depth: 0 - - name: Set up Python 3.7 + - name: Set up Python 3.10 uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 with: - python-version: 3.7 + python-version: "3.10" - name: Auto-merge inhouse doc prs run: | diff --git a/.github/workflows/ops-label-pr-issues.yml b/.github/workflows/ops-label-pr-issues.yml index 73528021229..bf0b1cd5f83 100644 --- a/.github/workflows/ops-label-pr-issues.yml +++ b/.github/workflows/ops-label-pr-issues.yml @@ -31,17 +31,17 @@ jobs: - name: check out code uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - name: Setup Python 3.7 + - name: Set up Python 3.10 uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 with: - python-version: 3.7 + python-version: "3.10" - name: Install dependencies run: | sudo apt-get update - sudo python3 -m pip install --upgrade pip - sudo pip3 install setuptools --upgrade - sudo pip3 install -r ./automation/requirements.txt + sudo python3 -m pip install --upgrade pip || echo "Failed to upgrade pip" + sudo pip3 install --ignore-installed setuptools --upgrade + sudo pip3 install --ignore-installed -r ./automation/requirements.txt sudo apt-get update sudo apt-get install jq curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ca0b9fcd917..d7df2a159cb 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -10,12 +10,17 @@ jobs: strategy: fail-fast: false steps: + - name: Harden Runner + uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 + with: + egress-policy: audit + - name: Checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: fetch-depth: 0 - - uses: googleapis/release-please-action@v4 + - uses: googleapis/release-please-action@7987652d64b4581673a76e33ad5e98e3dd56832f # v4.1.3 id: release-please with: release-type: simple diff --git a/.github/workflows/sanitary-github-cache.yml b/.github/workflows/sanitary-github-cache.yml index b2bfb70f57d..e1dd3fa9676 100644 --- a/.github/workflows/sanitary-github-cache.yml +++ b/.github/workflows/sanitary-github-cache.yml @@ -4,7 +4,8 @@ on: types: - closed workflow_dispatch: - +permissions: + contents: read jobs: cleanup: runs-on: ubuntu-latest diff --git a/.github/workflows/sanitary-workflow-runs.yml b/.github/workflows/sanitary-workflow-runs.yml index fd3137becc7..c8115cc62a8 100644 --- a/.github/workflows/sanitary-workflow-runs.yml +++ b/.github/workflows/sanitary-workflow-runs.yml @@ -3,6 +3,8 @@ on: schedule: - cron: '0 0 */2 * *' workflow_dispatch: +permissions: + contents: read jobs: del_runs: runs-on: ubuntu-latest diff --git a/.github/workflows/scan-sonar.yml b/.github/workflows/scan-sonar.yml index 66284080304..69cac4cfc36 100644 --- a/.github/workflows/scan-sonar.yml +++ b/.github/workflows/scan-sonar.yml @@ -55,7 +55,8 @@ on: - '!**.txt' workflow_dispatch: - +permissions: + contents: read jobs: sonar-scan: name: sonar scan @@ -82,7 +83,9 @@ jobs: jans-linux-setup jans-cli-tui jans-pycloudlib - + permissions: + contents: read + pull-requests: read steps: - name: Harden Runner uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 diff --git a/.github/workflows/test-cedarling.yml b/.github/workflows/test-cedarling.yml index 6647eba00c0..896caa6c449 100644 --- a/.github/workflows/test-cedarling.yml +++ b/.github/workflows/test-cedarling.yml @@ -19,7 +19,7 @@ jobs: egress-policy: audit - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@1ff72ee08e3cb84d84adba594e0a297990fc1ed3 # stable - name: Run Tests run: | cd ./jans-cedarling @@ -45,7 +45,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python3 -m pip install --upgrade pip + python3 -m pip install --upgrade pip || echo "Failed to upgrade pip" python3 -m pip install tox - name: Test with pytest run: | diff --git a/.github/workflows/test-jans-pycloudlib.yml b/.github/workflows/test-jans-pycloudlib.yml index 3603b64f320..b673adb249f 100644 --- a/.github/workflows/test-jans-pycloudlib.yml +++ b/.github/workflows/test-jans-pycloudlib.yml @@ -41,7 +41,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python3 -m pip install --upgrade pip + python3 -m pip install --upgrade pip || echo "Failed to upgrade pip" python3 -m pip install tox - name: Test with pytest run: | diff --git a/README.md b/README.md index 44b01972fad..5aca229dbed 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,6 @@ commercial distribution of Janssen Project Components called | **[Jans Lock](jans-lock)** | An enterprise authorization solution featuring the Cedarling, a stateless PDP and the Lock Server which centralizes audit logs and configuration. | ![Incubating](https://img.shields.io/badge/Incubating-%23f79307) | | **[Jans Tarp](demos/jans-tarp)** | An OpenID Connect RP test website that runs as a browser plugin in Chrome or Firefox. | ![Incubating](https://img.shields.io/badge/Incubating-%23f79307) | | **[Jans Chip](demos/jans-chip)** | Sample iOS and Android mobile applications that implement the full OAuth and FIDO security stack for app integrity, client constrained access tokens, and user presence. | ![Demo](https://img.shields.io/badge/Demo-%23368af7) | -| **[Jans Tent](demos/jans-tent)** | A test Relying Party ("RP") built using Python and Flask. Enables you to send different requests by quickly modifying just one configuration file. | ![Demo](https://img.shields.io/badge/Demo-%23368af7) | ## Installation diff --git a/demos/README.md b/demos/README.md index d16501ccf25..66b93674826 100644 --- a/demos/README.md +++ b/demos/README.md @@ -4,6 +4,10 @@ This folder holds different demos for different applications with janssen author ## [Benchmarking](benchmarking) Holds a docker load test image packaging for Janssen. This image can load test users to a janssen environment and can execute jmeter tests. -## [Jans-tent](jans-tent) -Reliable OpenID client to be used in auth testing. +## [Janssen Chip](jans-chip) +- A first party android mobile application that leverages dynamic client registration (DCR), DPoP access tokens. +- Passkey authentication + +## [Janssen Tarp](jans-tarp) +A Relying Party tool in form of a Browser Extension for convenient testing of authentication flows on a browser. diff --git a/demos/jans-tent/.flaskenv b/demos/jans-tent/.flaskenv deleted file mode 100644 index bc1b2cf6e71..00000000000 --- a/demos/jans-tent/.flaskenv +++ /dev/null @@ -1,2 +0,0 @@ -#.flaskenv -FLASK_APP=clientapp diff --git a/demos/jans-tent/.gitignore b/demos/jans-tent/.gitignore deleted file mode 100644 index 6b3dc1fcd19..00000000000 --- a/demos/jans-tent/.gitignore +++ /dev/null @@ -1,146 +0,0 @@ -#jans-tent-specific -client_info.json -*.log.* - -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -.vscode/ -.scannerwork - diff --git a/demos/jans-tent/LICENSE b/demos/jans-tent/LICENSE deleted file mode 100644 index 6912a5f93c9..00000000000 --- a/demos/jans-tent/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2023 Christian Eland - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file diff --git a/demos/jans-tent/README.md b/demos/jans-tent/README.md deleted file mode 100644 index 3f6c6c1ff87..00000000000 --- a/demos/jans-tent/README.md +++ /dev/null @@ -1,144 +0,0 @@ -# Jans Tent - -To test an OpenID Provider ("OP"), you need a test Relying Party ("RP"). Jans -Tent is easy to configure RP which enables you to send different requests by -quickly modifying one file (`config.py`). It's a Python Flask application, -so it's easy to hack for other testing requirements. - -By default, it uses `localhost` as the `redirect_uri`, so if you run it on your -laptop, all you need to do is specify the OP hostname to run it. Tent uses -dynamic client registration to obtain client credentials. But you can also use -an existing client_id if you like. - -## Installation - -**Important**: Ensure you have `Python >= 3.11` - -**Mac Users**: We recommend using [pyenv - simple python version management](https://github.com/pyenv/pyenv) instead of Os x native python. - -1. Navigate to the project root folder `jans/demos/jans-tent` -2. Create virtual environment -```bash -python3 -m venv venv -```` -3. Activate the virtual virtual environment -```bash -source venv/bin/activate -``` -4. Install dependencies -```bash -pip install -r requirements.txt -``` - -## Setup - -### 1. Edit configuration file `clientapp/config.py` according to your needs: - * Set `ISSUER`, replace `op_hostname` (required) - * Set any other desired configuration - -### 2. Generate test RP server self signed certs - -Generate `key.pem` and `cert.pem` at `jans-tent` project root folder (`jans/demos/jans-tent`). i.e: -```bash -openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 365 -nodes -``` - -### 3. Import your OP TLS certificate - -(remember to be inside your virtual environment) - -Supply the hostname of the ISSUER after the `=` - -```bash -export OP_HOSTNAME= -``` - -```bash -echo | openssl s_client -servername $OP_HOSTNAME -connect $OP_HOSTNAME:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > op_web_cert.cer -``` - -```bash -export CERT_PATH=$(python3 -m certifi) -``` - -```bash -export SSL_CERT_FILE=${CERT_PATH} -``` - -```bash -export REQUESTS_CA_BUNDLE=${CERT_PATH} && mv op_web_cert.cer $CERT_PATH -``` - -## Using the server - -### Start the server - -Please notice that your client will be automatically registered once the server -starts. If your client was already registered, when you start the server again, -it won't register. Remember to be inside your virtual environment! - -```bash -python main.py -``` - -### Login! - -Navigate your browser to `https://localhost:9090` and click the link to start. - -## Manual client configuration - -In case your OP doesn't support dynamic registration, manually configure your -client by creating a file caled `client_info.json` in the `jans-tent` folder -with the following claims: - -```json -{ - "op_metadata_url": "https://op_hostname/.well-known/openid-configuration", - "client_id": "e4f2c3a9-0797-4c6c-9268-35c5546fb3e9", - "client_secret": "a3e71cf1-b9b4-44c5-a9e6-4c7b5c660a5d" -} -``` - -## Updating Tent to use a different OP - -If you want to test a different OP, do the following: - -1. Remove `op_web_cert` from the tent folder, and follow the procedure above -to download and install a new OP TLS certificate -2. Remove `client_info.json` from the tent folder -3. Update the value of `ISSUER` in `./clientapp/config.py` -4. Run `./register_new_client.py` - -## Other Tent endpoints - -### Auto-register endpoint - -Sending a `POST` request to Jans Tent `/register` endpoint containing a `JSON` -with the OP/AS url and client url, like this: - -```json -{ - "op_url": "https://OP_HOSTNAME", - "client_url": "https://localhost:9090", - "additional_params": { - "scope": "openid mail profile" - } -} -``` -Please notice that `additional_params` is not required by endpoint. - -The response will return the registered client id and client secret - -### Auto-config endpoint - -Sending a `POST` request to the Tent `/configuration` endpoint, containing the -client id, client secret, and metadata endpoint will fetch data from OP metadata -url and override the `config.py` settings during runtime. - -```json -{ - "client_id": "e4f2c3a9-0797-4c6c-9268-35c5546fb3e9", - "client_secret": "5c9e4775-0f1d-4a56-87c9-a629e1f88b9b", - "op_metadata_url": "https://OP_HOSTNAME/.well-known/openid-configuration" -} -``` diff --git a/demos/jans-tent/behave.ini b/demos/jans-tent/behave.ini deleted file mode 100644 index cbb1bc67a71..00000000000 --- a/demos/jans-tent/behave.ini +++ /dev/null @@ -1,3 +0,0 @@ -[behave] -stderr_capture=False -stdout_capture=False diff --git a/demos/jans-tent/clientapp/__init__.py b/demos/jans-tent/clientapp/__init__.py deleted file mode 100644 index a7429e815a5..00000000000 --- a/demos/jans-tent/clientapp/__init__.py +++ /dev/null @@ -1,251 +0,0 @@ -''' -Project: Test Auth Client -Author: Christian Hawk - - -Licensed under the Apache License, Version 2.0 (the 'License'); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an 'AS IS' BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -''' -import base64 -import urllib -import json -import os -from urllib.parse import urlparse -from authlib.integrations.flask_client import OAuth -from flask import (Flask, jsonify, redirect, render_template, request, session, - url_for) -from . import config as cfg -from .helpers.client_handler import ClientHandler -from .helpers.cgf_checker import register_client_if_no_client_info -from .utils.logger import setup_logger - -setup_logger() - -oauth = OAuth() - - -def add_config_from_json(): - with open('client_info.json', 'r') as openfile: - client_info = json.load(openfile) - cfg.SERVER_META_URL = client_info['op_metadata_url'] - cfg.CLIENT_ID = client_info['client_id'] - cfg.CLIENT_SECRET = client_info['client_secret'] - cfg.END_SESSION_ENDPOINT = client_info['end_session_endpoint'] # separate later - - -def get_preselected_provider(): - provider_id_string = cfg.PRE_SELECTED_PROVIDER_ID - provider_object = '{ "provider" : "%s" }' % provider_id_string - provider_object_bytes = provider_object.encode() - base64url_bytes = base64.urlsafe_b64encode(provider_object_bytes) - base64url_value = base64url_bytes.decode() - # if base64url_value.endswith('='): - # base64url_value_unpad = base64url_value.replace('=', '') - # return base64url_value_unpad - return base64url_value - - -def get_provider_host(): - provider_host_string = cfg.PROVIDER_HOST_STRING - provider_object = '{ "providerHost" : "%s" }' % provider_host_string - provider_object_bytes = provider_object.encode() - base64url_bytes = base64.urlsafe_b64encode(provider_object_bytes) - base64url_value = base64url_bytes.decode() - # if base64url_value.endswith('='): - # base64url_value_unpad = base64url_value.replace('=', '') - # return base64url_value_unpad - return base64url_value - - -def ssl_verify(ssl_verify=cfg.SSL_VERIFY): - if ssl_verify is False: - os.environ['CURL_CA_BUNDLE'] = "" - - -class BaseClientErrors(Exception): - status_code = 500 - - -def create_app(): - register_client_if_no_client_info() - add_config_from_json() - ssl_verify() - - app = Flask(__name__) - - app.secret_key = b'fasfafpj3rasdaasfglaksdgags331s' - app.config['OP_CLIENT_ID'] = cfg.CLIENT_ID - app.config['OP_CLIENT_SECRET'] = cfg.CLIENT_SECRET - oauth.init_app(app) - oauth.register( - 'op', - server_metadata_url=cfg.SERVER_META_URL, - client_kwargs={ - 'scope': cfg.SCOPE - }, - token_endpoint_auth_method=cfg.SERVER_TOKEN_AUTH_METHOD - ) - - @app.route('/') - def index(): - user = session.get('user') - id_token = session.get('id_token') - return render_template("home.html", user=user, id_token=id_token) - - @app.route('/logout') - def logout(): - app.logger.info('Called /logout') - if 'id_token' in session.keys(): - app.logger.info('Cleaning session credentials') - token_hint = session.get('id_token') - session.pop('id_token') - session.pop('user') - parsed_redirect_uri = urllib.parse.urlparse(cfg.REDIRECT_URIS[0]) - post_logout_redirect_uri = '%s://%s' % (parsed_redirect_uri.scheme, parsed_redirect_uri.netloc) - return redirect( - '%s?post_logout_redirect_uri=%s&token_hint=%s' % ( - cfg.END_SESSION_ENDPOINT, post_logout_redirect_uri, token_hint - ) - ) - - app.logger.info('Not authorized to logout, redirecting to index') - return redirect(url_for('index')) - - @app.route('/register', methods=['POST']) - def register(): - app.logger.info('/register called') - content = request.json - app.logger.debug('data = %s' % content) - status = 0 - data = '' - if content is None: - status = 400 - # message = 'No json data posted' - elif 'op_url' and 'redirect_uris' not in content: - status = 400 - # message = 'Not needed keys found in json' - else: - app.logger.info('Trying to register client %s on %s' % - (content['redirect_uris'], content['op_url'])) - op_url = content['op_url'] - redirect_uris = content['redirect_uris'] - - op_parsed_url = urlparse(op_url) - client_parsed_redirect_uri = urlparse(redirect_uris[0]) - - if op_parsed_url.scheme != 'https' or client_parsed_redirect_uri.scheme != 'https': - status = 400 - - elif ((( - op_parsed_url.path != '' or op_parsed_url.query != '') or client_parsed_redirect_uri.path == '') or client_parsed_redirect_uri.query != ''): - status = 400 - - else: - additional_metadata = {} - if 'additional_params' in content.keys(): - additional_metadata = content['additional_params'] - client_handler = ClientHandler( - content['op_url'], content['redirect_uris'], additional_metadata - ) - data = client_handler.get_client_dict() - status = 200 - return jsonify(data), status - - @app.route('/protected-content', methods=['GET']) - def protected_content(): - app.logger.debug('/protected-content - cookies = %s' % request.cookies) - app.logger.debug('/protected-content - session = %s' % session) - if 'user' in session: - return session['user'] - - return redirect(url_for('login')) - - @app.route('/login') - def login(): - app.logger.info('/login requested') - redirect_uri = cfg.REDIRECT_URIS[0] - app.logger.debug('/login redirect_uri = %s' % redirect_uri) - # response = oauth.op.authorize_redirect() - query_args = { - 'redirect_uri': redirect_uri, - } - - if cfg.ACR_VALUES is not None: - query_args['acr_values'] = cfg.ACR_VALUES - - # used for inbound-saml, uncomment and set config.py to use it - # if cfg.PRE_SELECTED_PROVIDER is True: - # query_args[ - # 'preselectedExternalProvider'] = get_preselected_provider() - - # used for gluu-passport, , uncomment and set config.py to use it - # if cfg.PROVIDER_HOST_STRING is not None: - # query_args["providerHost"] = get_provider_host() - - if cfg.ADDITIONAL_PARAMS is not None: - query_args |= cfg.ADDITIONAL_PARAMS - - response = oauth.op.authorize_redirect(**query_args) - - app.logger.debug('/login authorize_redirect(redirect_uri) url = %s' % - (response.location)) - - return response - - @app.route('/oidc_callback') - @app.route('/callback') - def callback(): - try: - if not request.args['code']: - return {}, 400 - - app.logger.info('/callback - received %s - %s' % - (request.method, request.query_string)) - token = oauth.op.authorize_access_token() - app.logger.debug('/callback - token = %s' % token) - user = oauth.op.userinfo() - app.logger.debug('/callback - user = %s' % user) - session['user'] = user - session['id_token'] = token['userinfo'] - app.logger.debug('/callback - cookies = %s' % request.cookies) - app.logger.debug('/callback - session = %s' % session) - - return redirect('/') - - except Exception as error: - app.logger.error(str(error)) - return {'error': str(error)}, 400 - - @app.route("/configuration", methods=["POST"]) - def configuration(): - # Receives client configuration via API - app.logger.info('/configuration called') - content = request.json - app.logger.debug("content = %s" % content) - if content is not None: - if 'provider_id' in content: - cfg.PRE_SELECTED_PROVIDER_ID = content['provider_id'] - cfg.PRE_SELECTED_PROVIDER = True - app.logger.debug('/configuration: provider_id = %s' % - content['provider_id']) - - return jsonify({"provider_id": content['provider_id']}), 200 - - if "client_id" in content and "client_secret" in content: - # Setup client_id and client_secret - oauth.op.client_id = content['client_id'] - oauth.op.client_secret = content['client_secret'] - return {}, 200 - else: - return {}, 400 - - return app diff --git a/demos/jans-tent/clientapp/config.py b/demos/jans-tent/clientapp/config.py deleted file mode 100644 index 04bcb8df3b9..00000000000 --- a/demos/jans-tent/clientapp/config.py +++ /dev/null @@ -1,36 +0,0 @@ -# REQUIRED -# Replace op_hostname -ISSUER = 'https://op_hostname' - -# Tent redirect uri -REDIRECT_URIS = [ - 'https://localhost:9090/oidc_callback' -] - -# OPTIONAL: Use at your own risk - -# Token authentication method can be -# client_secret_basic -# client_secret_post -# none -SERVER_TOKEN_AUTH_METHOD = 'client_secret_post' - -# ACR VALUES -# Examples: -# ACR_VALUES = "agama" -# ACR_VALUES = 'simple_password_auth' -ACR_VALUES = None - -# ADDITIONAL PARAMS TO CALL AUTHORIZE ENDPOINT, WITHOUT BASE64 ENCODING. USE DICT {'param': 'value'} -# ADDITIONAL_PARAMS = {'paramOne': 'valueOne', 'paramTwo': 'valueTwo'} -ADDITIONAL_PARAMS = None - -# SYSTEM SETTINGS -# use with caution, unsecure requests, for development environments -SSL_VERIFY = False - -# SCOPES -# Only scope "openid" is required for a pairwise identifier from the OP. -# OP can provision additional optional scopes as needed. -# SCOPE = 'openid email profile' -SCOPE = 'openid' diff --git a/demos/jans-tent/clientapp/helpers/__init__.py b/demos/jans-tent/clientapp/helpers/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/demos/jans-tent/clientapp/helpers/cgf_checker.py b/demos/jans-tent/clientapp/helpers/cgf_checker.py deleted file mode 100644 index e5ade597adf..00000000000 --- a/demos/jans-tent/clientapp/helpers/cgf_checker.py +++ /dev/null @@ -1,17 +0,0 @@ -from os.path import exists -import logging -from clientapp.utils.dcr_from_config import register - -logger = logging.getLogger(__name__) - - -def configuration_exists() -> bool: - return exists('client_info.json') - - -def register_client_if_no_client_info() -> None: - if configuration_exists() : - logger.info('Found configuration file client_info.json, skipping auto-register') - else: - logger.info('Client configuration not found, trying to auto-register through DCR') - register() diff --git a/demos/jans-tent/clientapp/helpers/client_handler.py b/demos/jans-tent/clientapp/helpers/client_handler.py deleted file mode 100644 index 7e5f8f12e2a..00000000000 --- a/demos/jans-tent/clientapp/helpers/client_handler.py +++ /dev/null @@ -1,117 +0,0 @@ -''' -Project: Test Auth Client -Author: Christian Hawk - - -Licensed under the Apache License, Version 2.0 (the 'License'); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an 'AS IS' BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -''' -import logging -import json -from httplib2 import RelativeURIError -from typing import Optional, Dict, Any - -from oic.oauth2 import ASConfigurationResponse -from oic.oic import Client -from oic.utils.authn.client import CLIENT_AUTHN_METHOD -from .custom_msg_factory import CustomMessageFactory - - -logger = logging.getLogger(__name__) - - -class ClientHandler: - __redirect_uris = None - __client_id = None - __client_secret = None - __metadata_url = None - __op_url = None - __additional_metadata = None - __end_session_endpoint = None - op_data = None - - def __init__(self, op_url: str, redirect_uris: list[str], additional_metadata: dict): - """[initializes] - - :param op_url: [url from oidc provider starting with https] - :type op_url: str - :param redirect_uris: [url from client starting with https] - :type redirect_uris: list - :param additional_metadata: additional client metadata - :type additional_metadata: dict - """ - self.__additional_metadata = additional_metadata - self.clientAdapter = Client(client_authn_method=CLIENT_AUTHN_METHOD, message_factory=CustomMessageFactory) - self.__op_url = op_url - self.__redirect_uris = redirect_uris - self.__metadata_url = '%s/.well-known/openid-configuration' % op_url - self.op_data = self.discover(op_url) - self.reg_info = self.register_client(op_data=self.op_data, redirect_uris=redirect_uris) - self.__end_session_endpoint = self.op_data['end_session_endpoint'] - self.__client_id = self.reg_info['client_id'] - self.__client_secret = self.reg_info['client_secret'] - - def get_client_dict(self) -> dict: - r = { - 'op_metadata_url': self.__metadata_url, - 'client_id': self.__client_id, - 'client_secret': self.__client_secret, - 'end_session_endpoint': self.__end_session_endpoint - } - - return r - - def register_client(self, op_data: ASConfigurationResponse = op_data, redirect_uris: Optional[list[str]] = __redirect_uris) -> dict: - """[register client and returns client information] - - :param op_data: [description] - :type op_data: dict - :param redirect_uris: [description] - :type redirect_uris: list[str] - :return: [client information including client-id and secret] - :rtype: dict - """ - logger.debug('called ClientHandler´s register_client method') - registration_args = {'redirect_uris': redirect_uris, - 'response_types': ['code'], - 'grant_types': ['authorization_code'], - 'application_type': 'web', - 'client_name': 'Jans Tent', - 'token_endpoint_auth_method': 'client_secret_post', - **self.__additional_metadata - } - logger.info('calling register with registration_args: %s', json.dumps(registration_args, indent=2)) - reg_info = self.clientAdapter.register(op_data['registration_endpoint'], **registration_args) - logger.info('register_client - reg_info = %s', json.dumps(reg_info.to_dict(), indent=2)) - return reg_info - - def discover(self, op_url: Optional[str] = __op_url) -> ASConfigurationResponse: - """Discover op information on .well-known/open-id-configuration - :param op_url: [description], defaults to __op_url - :type op_url: str, optional - :return: [data retrieved from OP url] - :rtype: ASConfigurationResponse - """ - logger.debug('called discover') - try: - op_data = self.clientAdapter.provider_config(op_url) - return op_data - - except json.JSONDecodeError as err: - logger.error('Error trying to decode JSON: %s' % err) - - except RelativeURIError as err: - logger.error(err) - - except Exception as e: - logging.error('An unexpected ocurred: %s' % e) - diff --git a/demos/jans-tent/clientapp/helpers/custom_msg_factory.py b/demos/jans-tent/clientapp/helpers/custom_msg_factory.py deleted file mode 100644 index 11f0ae09a60..00000000000 --- a/demos/jans-tent/clientapp/helpers/custom_msg_factory.py +++ /dev/null @@ -1,59 +0,0 @@ -""" -Custom message factory required by pyoic to add scope param -Overrides RegistrationRequest, RegistrationResponse -and use them to create CustomMessageFactory -""" - -from oic.oic.message import OIDCMessageFactory, RegistrationRequest, RegistrationResponse, MessageTuple, OPTIONAL_LOGICAL -from oic.oauth2.message import OPTIONAL_LIST_OF_STRINGS, REQUIRED_LIST_OF_STRINGS, SINGLE_OPTIONAL_STRING, SINGLE_OPTIONAL_INT - - -class MyRegistrationRequest(RegistrationRequest): - c_param = { - "redirect_uris": REQUIRED_LIST_OF_STRINGS, - "response_types": OPTIONAL_LIST_OF_STRINGS, - "grant_types": OPTIONAL_LIST_OF_STRINGS, - "application_type": SINGLE_OPTIONAL_STRING, - "contacts": OPTIONAL_LIST_OF_STRINGS, - "client_name": SINGLE_OPTIONAL_STRING, - "logo_uri": SINGLE_OPTIONAL_STRING, - "client_uri": SINGLE_OPTIONAL_STRING, - "policy_uri": SINGLE_OPTIONAL_STRING, - "tos_uri": SINGLE_OPTIONAL_STRING, - "jwks": SINGLE_OPTIONAL_STRING, - "jwks_uri": SINGLE_OPTIONAL_STRING, - "sector_identifier_uri": SINGLE_OPTIONAL_STRING, - "subject_type": SINGLE_OPTIONAL_STRING, - "id_token_signed_response_alg": SINGLE_OPTIONAL_STRING, - "id_token_encrypted_response_alg": SINGLE_OPTIONAL_STRING, - "id_token_encrypted_response_enc": SINGLE_OPTIONAL_STRING, - "userinfo_signed_response_alg": SINGLE_OPTIONAL_STRING, - "userinfo_encrypted_response_alg": SINGLE_OPTIONAL_STRING, - "userinfo_encrypted_response_enc": SINGLE_OPTIONAL_STRING, - "request_object_signing_alg": SINGLE_OPTIONAL_STRING, - "request_object_encryption_alg": SINGLE_OPTIONAL_STRING, - "request_object_encryption_enc": SINGLE_OPTIONAL_STRING, - "token_endpoint_auth_method": SINGLE_OPTIONAL_STRING, - "token_endpoint_auth_signing_alg": SINGLE_OPTIONAL_STRING, - "default_max_age": SINGLE_OPTIONAL_INT, - "require_auth_time": OPTIONAL_LOGICAL, - "default_acr_values": OPTIONAL_LIST_OF_STRINGS, - "initiate_login_uri": SINGLE_OPTIONAL_STRING, - "request_uris": OPTIONAL_LIST_OF_STRINGS, - "post_logout_redirect_uris": OPTIONAL_LIST_OF_STRINGS, - "frontchannel_logout_uri": SINGLE_OPTIONAL_STRING, - "frontchannel_logout_session_required": OPTIONAL_LOGICAL, - "backchannel_logout_uri": SINGLE_OPTIONAL_STRING, - "backchannel_logout_session_required": OPTIONAL_LOGICAL, - "scope": OPTIONAL_LIST_OF_STRINGS, # added - } - c_default = {"application_type": "web", "response_types": ["code"]} - c_allowed_values = { - "application_type": ["native", "web"], - "subject_type": ["public", "pairwise"], - } - - -class CustomMessageFactory(OIDCMessageFactory): - registration_endpoint = MessageTuple(MyRegistrationRequest, RegistrationResponse) - diff --git a/demos/jans-tent/clientapp/templates/home.html b/demos/jans-tent/clientapp/templates/home.html deleted file mode 100644 index 021c6fcfaac..00000000000 --- a/demos/jans-tent/clientapp/templates/home.html +++ /dev/null @@ -1,21 +0,0 @@ - - Index Test - -

Welcome to the test of your life

-

- {% if user %} -

Userinfo JSON payload

-
-        {{ user|tojson }}
-        
-

-

id_token JSON payload

-
-        {{ id_token|tojson }}
-        
- logout - {% else %} -

Click here to start!

- {% endif %} - - \ No newline at end of file diff --git a/demos/jans-tent/clientapp/utils/__init__.py b/demos/jans-tent/clientapp/utils/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/demos/jans-tent/clientapp/utils/dcr_from_config.py b/demos/jans-tent/clientapp/utils/dcr_from_config.py deleted file mode 100644 index 7ab19246abf..00000000000 --- a/demos/jans-tent/clientapp/utils/dcr_from_config.py +++ /dev/null @@ -1,41 +0,0 @@ -import logging -import urllib.parse - -from clientapp import config as cfg -from clientapp.helpers.client_handler import ClientHandler -import json -from urllib import parse - -OP_URL = cfg.ISSUER -REDIRECT_URIS = cfg.REDIRECT_URIS -SCOPE = cfg.SCOPE -parsed_redirect_uri = urllib.parse.urlparse(cfg.REDIRECT_URIS[0]) -POST_LOGOUT_REDIRECT_URI = '%s://%s' % (parsed_redirect_uri.scheme, parsed_redirect_uri.netloc) - - -def setup_logging() -> None: - logging.getLogger('oic') - logging.getLogger('urllib3') - logging.basicConfig( - level=logging.DEBUG, - handlers=[logging.StreamHandler(), logging.FileHandler('register_new_client.log')], - format='[%(asctime)s] %(levelname)s %(name)s in %(module)s : %(message)s') - - -def register() -> None: - """ - Register client with information from config and write info to client_info.json - :return: None - """ - logger = logging.getLogger(__name__) - scope_as_list = SCOPE.split(" ") - additional_params = { - 'scope': scope_as_list, - 'post_logout_redirect_uris': [POST_LOGOUT_REDIRECT_URI] - } - client_handler = ClientHandler(OP_URL, REDIRECT_URIS, additional_params) - json_client_info = json.dumps(client_handler.get_client_dict(), indent=4) - with open('client_info.json', 'w') as outfile: - logger.info('Writing registered client information to client_info.json') - outfile.write(json_client_info) - diff --git a/demos/jans-tent/clientapp/utils/logger.py b/demos/jans-tent/clientapp/utils/logger.py deleted file mode 100644 index acbcc2ca7bf..00000000000 --- a/demos/jans-tent/clientapp/utils/logger.py +++ /dev/null @@ -1,16 +0,0 @@ -import logging -from logging.handlers import TimedRotatingFileHandler - - -def setup_logger() -> None: - formatter = logging.Formatter("[%(asctime)s] %(levelname)s %(name)s in %(module)s : %(message)s") - log_file = "test-client.log" - file_handler = TimedRotatingFileHandler(log_file, when='midnight') - console_handler = logging.StreamHandler() - console_handler.setFormatter(formatter) - file_handler.setFormatter(formatter) - logging.getLogger("oic") - logging.getLogger("oauth") - logging.getLogger("flask-oidc") - logging.getLogger("urllib3") - logging.basicConfig(level=logging.DEBUG, handlers=[file_handler, console_handler]) diff --git a/demos/jans-tent/docs/images/authorize_code_flow.png b/demos/jans-tent/docs/images/authorize_code_flow.png deleted file mode 100644 index a0c262649890154165f225d1e515e0b521ff9f2e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53305 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 V$=j@=`}m(jJUx~#Dq1Mn{SW6T6ej=x diff --git a/demos/jans-tent/main.py b/demos/jans-tent/main.py deleted file mode 100644 index ebd89f31fd6..00000000000 --- a/demos/jans-tent/main.py +++ /dev/null @@ -1,6 +0,0 @@ -from clientapp import create_app - -if __name__ == '__main__': - app = create_app() - app.debug = True - app.run(host='0.0.0.0', ssl_context=('cert.pem', 'key.pem'), port=9090, use_reloader=False) diff --git a/demos/jans-tent/register_new_client.py b/demos/jans-tent/register_new_client.py deleted file mode 100644 index 89061c42275..00000000000 --- a/demos/jans-tent/register_new_client.py +++ /dev/null @@ -1,12 +0,0 @@ -# executes a new client auto-register from config.py -import logging -from clientapp.utils.dcr_from_config import register - -# add independent logging for CLI script -logging.getLogger('oic') -logging.getLogger('urllib3') -logging.basicConfig( - level=logging.DEBUG, - handlers=[logging.StreamHandler(), logging.FileHandler('register_new_client.log')], - format='[%(asctime)s] %(levelname)s %(name)s in %(module)s : %(message)s') -register() diff --git a/demos/jans-tent/requirements.txt b/demos/jans-tent/requirements.txt deleted file mode 100644 index f6438fdbaae..00000000000 --- a/demos/jans-tent/requirements.txt +++ /dev/null @@ -1,119 +0,0 @@ -appnope==0.1.3 -astroid==2.12.5 -asttokens==2.0.8 -async-generator==1.10 -attrs==22.1.0 -Authlib==1.2.0 -autopep8==1.7.0 -backcall==0.2.0 -bandit==1.7.4 -behave==1.2.6 -certifi==2022.12.7 -cffi==1.15.1 -chardet==5.0.0 -charset-normalizer==2.1.1 -click==8.1.3 -coverage==6.4.4 -cryptography==42.0.0 -decorator==5.1.1 -defusedxml==0.7.1 -dill==0.3.5.1 -dodgy==0.2.1 -EasyProcess==1.1 -executing==1.0.0 -flake8==5.0.4 -Flask==2.2.2 -flask-oidc==1.4.0 -future==0.18.3 -gitdb==4.0.9 -GitPython==3.1.37 -h11==0.13.0 -httplib2==0.21.0 -idna==3.3 -importlib-metadata==4.12.0 -iniconfig==1.1.1 -install==1.3.5 -ipdb==0.13.9 -ipython==8.10.0 -ipython-genutils==0.2.0 -isort==5.10.1 -itsdangerous==2.0.0 -jedi==0.18.1 -Jinja2==3.1.2 -lazy-object-proxy==1.7.1 -Mako==1.2.4 -MarkupSafe==2.1.1 -matplotlib-inline==0.1.6 -mccabe==0.7.0 -more-itertools==8.14.0 -mypy==0.971 -mypy-extensions==0.4.3 -oauth2client==4.1.3 -oic==1.5.0 -outcome==1.2.0 -packaging==21.3 -parse==1.19.0 -parse-type==0.6.0 -parso==0.8.3 -pbr==5.10.0 -pep8==1.7.1 -pep8-naming==0.13.2 -pexpect==4.8.0 -pickleshare==0.7.5 -platformdirs==2.5.2 -pluggy==1.0.0 -poetry-semver==0.1.0 -prompt-toolkit==3.0.31 -prospector==0.12.2 -ptyprocess==0.7.0 -pure-eval==0.2.2 -py==1.11.0 -pyasn1==0.4.8 -pyasn1-modules==0.2.8 -pycodestyle==2.9.1 -pycparser==2.21 -pycryptodomex==3.17 -pydocstyle==6.1.1 -pyflakes==2.5.0 -Pygments==2.13.0 -pyjwkest==1.4.2 -pylama==8.4.1 -pylint==2.15.0 -pylint-celery==0.3 -pylint-common==0.2.5 -pylint-django==2.5.3 -pylint-flask==0.6 -pylint-plugin-utils==0.7 -pyOpenSSL==22.0.0 -pyparsing==3.0.9 -PySocks==1.7.1 -pytest==7.1.3 -python-dotenv==0.21.0 -PyVirtualDisplay==3.0 -PyYAML==6.0 -requests==2.28.1 -requirements-detector==1.0.3 -rsa==4.9 -selenium==4.4.3 -setoptconf==0.3.0 -six==1.16.0 -smmap==5.0.0 -sniffio==1.3.0 -snowballstemmer==2.2.0 -sortedcontainers==2.4.0 -stack-data==0.5.0 -stevedore==4.0.0 -toml==0.10.2 -tomli==2.0.1 -tomlkit==0.11.4 -traitlets==5.3.0 -trio==0.21.0 -trio-websocket==0.9.2 -typed-ast==1.5.4 -typing_extensions==4.3.0 -urllib3==1.26.12 -wcwidth==0.2.5 -Werkzeug==2.2.2 -wrapt==1.14.1 -wsproto==1.2.0 -zipp==3.8.1 diff --git a/demos/jans-tent/tests/behaver/features/environment.py b/demos/jans-tent/tests/behaver/features/environment.py deleted file mode 100644 index 037be43286f..00000000000 --- a/demos/jans-tent/tests/behaver/features/environment.py +++ /dev/null @@ -1,31 +0,0 @@ -from selenium import webdriver -import os -from pyvirtualdisplay import Display - -display = Display(visible=0, size=(1024, 768)) - - -def before_all(context): - os.environ['CURL_CA_BUNDLE'] = "" - display.start() - - -def before_scenario(context, scenario): - options = webdriver.FirefoxOptions() - options.headless = True - context.web = webdriver.Firefox() - - # context.web = webdriver.Firefox() - - -def after_scenario(context, scenario): - context.web.delete_all_cookies() - context.web.close() - - -def after_step(context, step): - print() - - -def after_all(context): - pass diff --git a/demos/jans-tent/tests/behaver/features/oidc_auth.feature b/demos/jans-tent/tests/behaver/features/oidc_auth.feature deleted file mode 100644 index b95f5df3e61..00000000000 --- a/demos/jans-tent/tests/behaver/features/oidc_auth.feature +++ /dev/null @@ -1,40 +0,0 @@ -Feature: Allow authenticated users to access protected pages - - @authenticated - Scenario: User is authenticated - Given username is "johndoe" - And user is authenticated - And protected content link is https://chris.testingenv.org/protected-content - When user clicks the protected content link - Then user access the protected content link - - Scenario: User does not exist - Given user does not exist - And protected content link is https://chris.testingenv.org/protected-content - When user clicks the protected content link - Then user goes to external login page - - Scenario: User is not authenticated - Given username is "johndoe" - And protected content link is https://chris.testingenv.org/protected-content - When user clicks the protected content link - Then user goes to external login page - - # Scenario: Normal user try to access admin content - # Given username is "johndoe" - # And user role is "user" - # And protected content link is https://chris.testingenv.org/admin/admin-protected-content - # When user clicks the protected content link - # Then user gets a 403 error - - # Scenario: Admin can access admin contents - # Given username is "johndoe" - # And user role is "admin" - # And protected content link is https://chris.testingenv.org/admin/admin-protected-content - # When user clicks the protected content link - # Then user access the protected content link - - - - - diff --git a/demos/jans-tent/tests/behaver/features/passport_social_auth.feature b/demos/jans-tent/tests/behaver/features/passport_social_auth.feature deleted file mode 100644 index 685576147f2..00000000000 --- a/demos/jans-tent/tests/behaver/features/passport_social_auth.feature +++ /dev/null @@ -1,26 +0,0 @@ -Feature: use passport social github to login - """ - As an user, - I want to use passport-social flow to authenticate - So I can access protected-content - """ - - Background: - Given auth method is passport-social - And user is visiting "/" - - Scenario: User is authenticated - Given username is "johndoe" - And protected content link is https://localhost:5000/content/protected-user-content - When user clicks the protected content link - Then user access the protected content link - - Scenario: User is not authenticated - Given user is not authenticated - When user clicks the protected content link - Then user goes to external login page - - - - - \ No newline at end of file diff --git a/demos/jans-tent/tests/behaver/features/steps/allow.py b/demos/jans-tent/tests/behaver/features/steps/allow.py deleted file mode 100644 index 97a6ddfdb4a..00000000000 --- a/demos/jans-tent/tests/behaver/features/steps/allow.py +++ /dev/null @@ -1,116 +0,0 @@ -from behave import when, then, given -import requests -import time -from selenium.webdriver.common.by import By - -base_url = "https://chris.testingenv.org" - - -def cookiesTransformer(sel_session_id, sel_other_cookies): - ''' This transform cookies from selenium to requests ''' - s = requests.Session() - s.cookies.set('session_id', sel_session_id) - i = 0 - while i < len(sel_other_cookies): - s.cookies.set(sel_other_cookies[i]['name'], - sel_other_cookies[i]['value'], - path=sel_other_cookies[i]['path'], - domain=sel_other_cookies[i]['domain'], - secure=sel_other_cookies[i]['secure'], - rest={'httpOnly': sel_other_cookies[i]['httpOnly']}) - i = i + 1 - - return s - - -@given(u'username is "{username}"') -def define_username(context, username): - context.username = username - context.password = "test123" - - -@given(u'user is authenticated') -def user_authenticates(context): - context.web.get("https://chris.testingenv.org/login") - time.sleep(3) - context.web.set_window_size(625, 638) - context.web.find_element(By.ID, "username").click() - context.web.find_element(By.ID, "username").send_keys("johndoo") - time.sleep(3) - context.web.find_element(By.ID, "password").send_keys("test123") - context.web.find_element(By.ID, "loginButton").click() - time.sleep(3) - - -@given(u'protected content link is {protected_content}') -def define_protected_content_link(context, protected_content): - context.protected_content = protected_content - - -@when(u'user clicks the protected content link') -def user_clicks_protected_content_link(context): - - context.web.get(base_url) - time.sleep(2) - context.web.find_element_by_xpath( - '//a[@href="' + "https://chris.testingenv.org/protected-content" + - '"]').click() - context.has_clicked = True - context.response = requests.get(context.protected_content) - - -@then(u'user access the protected content link') -def user_access_protected_content_link(context): - # WE FETCH THE COOKIES FROM SELENIUM AND PASS THEM TO REQUESTS TO VALIDATE - #sel_cookies = context.web.get_cookies() - #sel_cookie = sel_cookies[0] - # set cookie in requests - - # get session id from selenium - #sel_session_id = context.web.session_id - ''' - sess = requests.Session() - - sess.cookies.set('session_id',sel_session_id) - sess.cookies.set( - sel_cookie['name'], - sel_cookie['value'], - path = sel_cookie['path'], - domain = sel_cookie['domain'], - secure = sel_cookie['secure'], - rest= {'httpOnly' : sel_cookie['httpOnly']} - ) - - new_sess = cookiesTransformer(sel_session_id,sel_cookies) - ''' - new_sess = cookiesTransformer(context.web.session_id, - context.web.get_cookies()) - res = new_sess.get(context.protected_content, verify=False) - - assert res.url == context.protected_content - - -@given(u'user does not exist') -def user_does_not_exist(context): - pass - - -@then(u'user goes to external login page') -def user_directed_to_external_login_page(context): - #context.web.get("https://chris.testingenv.org/login") - - time.sleep(1) - external_login_url = 'https://chris.gluutwo.org/oxauth/login.htm' - #import ipdb; ipdb.set_trace() - assert (context.web.current_url == external_login_url) - #new_sess = cookiesTransformer(context.web.session_id,context.web.get_cookies()) - - -@given(u'user role is "{role}"') -def define_user_role(context, role): - context.role = role - - -@then(u'user gets a 403 error') -def step_impl(context): - raise NotImplementedError(u'STEP: Then user gets a 403 error') diff --git a/demos/jans-tent/tests/unit_integration/helper.py b/demos/jans-tent/tests/unit_integration/helper.py deleted file mode 100644 index b282f4b64ab..00000000000 --- a/demos/jans-tent/tests/unit_integration/helper.py +++ /dev/null @@ -1,189 +0,0 @@ - -from unittest import TestCase -from unittest.mock import MagicMock -import clientapp -from clientapp import create_app -from clientapp.helpers.client_handler import ClientHandler -from flask import Flask -from typing import List -import helper -import os -import builtins - - -class FlaskBaseTestCase(TestCase): - def setUp(self): - self.stashed_add_config_from_json = clientapp.add_config_from_json - clientapp.cfg.CLIENT_ID = 'any-client-id-stub' - clientapp.cfg.CLIENT_SECRET = 'any-client-secret-stub' - clientapp.cfg.SERVER_META_URL = 'https://ophostname.com/server/meta/url' - clientapp.cfg.END_SESSION_ENDPOINT = 'https://ophostname.com/end_session_endpoint' - clientapp.add_config_from_json = MagicMock(name='add_config_from_json') - clientapp.add_config_from_json.return_value(None) - self.stashed_discover = ClientHandler.discover - self.stashed_register_client = ClientHandler.register_client - self.stashed_open = builtins.open - builtins.open = MagicMock(name='open') - ClientHandler.discover = MagicMock(name='discover') - ClientHandler.discover.return_value = helper.OP_DATA_DICT_RESPONSE - ClientHandler.register_client = MagicMock(name='register_client') - ClientHandler.register_client.return_value = helper.REGISTER_CLIENT_RESPONSE - self.app = create_app() - self.app.testing = True - self.app_context = self.app.test_request_context( - base_url="https://chris.testingenv.org") - self.app_context.push() - self.client = self.app.test_client() - - #self.oauth = OAuth(self.app) - os.environ['AUTHLIB_INSECURE_TRANSPORT'] = "1" - - def tearDown(self) -> None: - ClientHandler.discover = self.stashed_discover - ClientHandler.register_client = self.stashed_register_client - builtins.open = self.stashed_open - clientapp.add_config_from_json = self.stashed_add_config_from_json - - -# Helper functions -def app_endpoints(app: Flask) -> List[str]: - """ Return all enpoints in app """ - endpoints = [] - for item in app.url_map.iter_rules(): - endpoint = item.endpoint.replace("_", "-") - endpoints.append(endpoint) - return endpoints - - -# Mocks -OP_DATA_DICT_RESPONSE = { - 'request_parameter_supported': True, - 'token_revocation_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/revoke', - 'introspection_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/introspection', - 'claims_parameter_supported': False, - 'issuer': 'https://t1.techno24x7.com', - 'userinfo_encryption_enc_values_supported': ['RSA1_5', 'RSA-OAEP', 'A128KW', 'A256KW'], - 'id_token_encryption_enc_values_supported': ['A128CBC+HS256', 'A256CBC+HS512', 'A128GCM', 'A256GCM'], - 'authorization_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/authorize', - 'service_documentation': 'http://gluu.org/docs', - 'id_generation_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/id', - 'claims_supported': ['street_address', 'country', 'zoneinfo', 'birthdate', 'role', 'gender', 'formatted', - 'user_name', 'phone_mobile_number', 'preferred_username', 'locale', 'inum', 'updated_at', - 'nickname', 'email', 'website', 'email_verified', 'profile', 'locality', - 'phone_number_verified', 'given_name', 'middle_name', 'picture', 'name', 'phone_number', - 'postal_code', 'region', 'family_name'], - 'scope_to_claims_mapping': [{ - 'profile': ['name', 'family_name', 'given_name', 'middle_name', 'nickname', 'preferred_username', 'profile', - 'picture', 'website', 'gender', 'birthdate', 'zoneinfo', 'locale', 'updated_at'] - }, { - 'openid': [] - }, { - 'https://t1.techno24x7.com/oxauth/restv1/uma/scopes/scim_access': [] - }, { - 'permission': ['role'] - }, { - 'super_gluu_ro_session': [] - }, { - 'https://t1.techno24x7.com/oxauth/restv1/uma/scopes/passport_access': [] - }, { - 'phone': ['phone_number_verified', 'phone_number'] - }, { - 'revoke_session': [] - }, { - 'address': ['formatted', 'postal_code', 'street_address', 'locality', 'country', 'region'] - }, { - 'clientinfo': ['name', 'inum'] - }, { - 'mobile_phone': ['phone_mobile_number'] - }, { - 'email': ['email_verified', 'email'] - }, { - 'user_name': ['user_name'] - }, { - 'oxtrust-api-write': [] - }, { - 'oxd': [] - }, { - 'uma_protection': [] - }, { - 'oxtrust-api-read': [] - }], - 'op_policy_uri': 'http://ox.gluu.org/doku.php?id=oxauth:policy', - 'token_endpoint_auth_methods_supported': ['client_secret_basic', 'client_secret_post', 'client_secret_jwt', - 'private_key_jwt', 'tls_client_auth', 'self_signed_tls_client_auth'], - 'tls_client_certificate_bound_access_tokens': True, - 'response_modes_supported': ['query', 'form_post', 'fragment'], - 'backchannel_logout_session_supported': True, - 'token_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/token', - 'response_types_supported': ['code id_token', 'code', 'id_token', 'token', 'code token', 'code id_token token', - 'id_token token'], - 'request_uri_parameter_supported': True, - 'backchannel_user_code_parameter_supported': False, - 'grant_types_supported': ['implicit', 'refresh_token', 'client_credentials', 'authorization_code', 'password', - 'urn:ietf:params:oauth:grant-type:uma-ticket'], - 'ui_locales_supported': ['en', 'bg', 'de', 'es', 'fr', 'it', 'ru', 'tr'], - 'userinfo_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/userinfo', - 'op_tos_uri': 'http://ox.gluu.org/doku.php?id=oxauth:tos', - 'auth_level_mapping': { - '-1': ['simple_password_auth'], - '60': ['passport_saml'], - '40': ['passport_social'] - }, - 'require_request_uri_registration': False, - 'id_token_encryption_alg_values_supported': ['RSA1_5', 'RSA-OAEP', 'A128KW', 'A256KW'], - 'frontchannel_logout_session_supported': True, - 'claims_locales_supported': ['en'], - 'clientinfo_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/clientinfo', - 'request_object_signing_alg_values_supported': ['none', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', - 'ES256', 'ES384', 'ES512'], - 'request_object_encryption_alg_values_supported': ['RSA1_5', 'RSA-OAEP', 'A128KW', 'A256KW'], - 'session_revocation_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/revoke_session', - 'check_session_iframe': 'https://t1.techno24x7.com/oxauth/opiframe.htm', - 'scopes_supported': ['address', 'openid', 'clientinfo', 'user_name', 'profile', - 'https://t1.techno24x7.com/oxauth/restv1/uma/scopes/scim_access', 'uma_protection', - 'permission', 'revoke_session', 'oxtrust-api-write', 'oxtrust-api-read', 'phone', - 'mobile_phone', 'oxd', 'super_gluu_ro_session', 'email', - 'https://t1.techno24x7.com/oxauth/restv1/uma/scopes/passport_access'], - 'backchannel_logout_supported': True, - 'acr_values_supported': ['simple_password_auth', 'passport_saml', 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password', - 'urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocol', - 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport', 'passport_social'], - 'request_object_encryption_enc_values_supported': ['A128CBC+HS256', 'A256CBC+HS512', 'A128GCM', 'A256GCM'], - 'display_values_supported': ['page', 'popup'], - 'userinfo_signing_alg_values_supported': ['HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', - 'ES512'], - 'claim_types_supported': ['normal'], - 'userinfo_encryption_alg_values_supported': ['RSA1_5', 'RSA-OAEP', 'A128KW', 'A256KW'], - 'end_session_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/end_session', - 'revocation_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/revoke', - 'backchannel_authentication_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/bc-authorize', - 'token_endpoint_auth_signing_alg_values_supported': ['HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', 'ES256', - 'ES384', 'ES512'], - 'frontchannel_logout_supported': True, - 'jwks_uri': 'https://t1.techno24x7.com/oxauth/restv1/jwks', - 'subject_types_supported': ['public', 'pairwise'], - 'id_token_signing_alg_values_supported': ['none', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', 'ES256', - 'ES384', 'ES512'], - 'registration_endpoint': 'https://t1.techno24x7.com/oxauth/restv1/register', - 'id_token_token_binding_cnf_values_supported': ['tbh'] -} - -REGISTER_CLIENT_RESPONSE = {'allow_spontaneous_scopes': False, 'application_type': 'web', 'rpt_as_jwt': False, - 'registration_client_uri': 'https://t1.techno24x7.com/jans-auth/restv1/register?client_id' - '=079f3682-3d60-4bca-8ff7-bbc7dbc18cd7', - 'run_introspection_script_before_jwt_creation': False, - 'registration_access_token': '89c51fd6-34ec-497e-a4ae-85e21b7e725b', - 'client_id': '079f3682-3d60-4bca-8ff7-bbc7dbc18cd7', - 'token_endpoint_auth_method': 'client_secret_post', - 'scope': 'online_access device_sso openid permission uma_protection offline_access', - 'client_secret': '8f53c454-f6ee-4181-8581-9f1ee120b878', 'client_id_issued_at': 1680038429, - 'backchannel_logout_session_required': False, 'client_name': 'Jans Tent', - 'par_lifetime': 600, 'spontaneous_scopes': [], 'id_token_signed_response_alg': 'RS256', - 'access_token_as_jwt': False, 'grant_types': ['authorization_code'], - 'subject_type': 'pairwise', 'additional_token_endpoint_auth_methods': [], - 'keep_client_authorization_after_expiration': False, 'require_par': False, - 'redirect_uris': ['https://localhost:9090/oidc_callback'], 'additional_audience': [], - 'frontchannel_logout_session_required': False, 'client_secret_expires_at': 0, - 'access_token_signing_alg': 'RS256', 'response_types': ['code']} - - diff --git a/demos/jans-tent/tests/unit_integration/test_callback_endpoint.py b/demos/jans-tent/tests/unit_integration/test_callback_endpoint.py deleted file mode 100644 index 91ebb43edca..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_callback_endpoint.py +++ /dev/null @@ -1,62 +0,0 @@ -import clientapp -from flask import Flask, url_for -from typing import List -from helper import FlaskBaseTestCase - - -def app_endpoints(app: Flask) -> List[str]: - """ Return all enpoints in app """ - - endpoints = [] - for item in app.url_map.iter_rules(): - endpoint = item.endpoint.replace("_", "-") - endpoints.append(endpoint) - return endpoints - - -# class FlaskBaseTestCase(TestCase): -# def setUp(self): -# self.app = clientapp.create_app() -# self.app.testing = True -# self.app_context = self.app.test_request_context( -# base_url="https://chris.testingenv.org") -# self.app_context.push() -# self.client = self.app.test_client() -# #self.oauth = OAuth(self.app) -# os.environ['AUTHLIB_INSECURE_TRANSPORT'] = "1" - - -class TestCallbackEndpoint(FlaskBaseTestCase): - def test_oidc_callback_endpoint_exist(self): - endpoints = [] - for item in clientapp.create_app().url_map.iter_rules(): - endpoint = item.rule - endpoints.append(endpoint) - - self.assertTrue('/oidc_callback' in endpoints, - "enpoint /oidc_callback knão existe no app") - - def test_callback_endpoint_should_exist(self): - - self.assertTrue('callback' in app_endpoints(clientapp.create_app()), - 'endpoint /callback does not exist in app') - - def test_endpoint_args_without_code_should_return_400(self): - resp = self.client.get(url_for('callback')) - - self.assertEqual(resp.status_code, 400) - - -''' - def test_endpoint_should_return_status_code_302(self): - # if there is - - self.assertEqual( - self.client.get(url_for('callback')).status_code, - 302, - 'Callback endpoint is not returning 302 status_code' - ) - - - #def test_endpoint_should_return_ -''' diff --git a/demos/jans-tent/tests/unit_integration/test_cfg_checker.py b/demos/jans-tent/tests/unit_integration/test_cfg_checker.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/demos/jans-tent/tests/unit_integration/test_client_register_endpoint.py b/demos/jans-tent/tests/unit_integration/test_client_register_endpoint.py deleted file mode 100644 index 83aef096738..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_client_register_endpoint.py +++ /dev/null @@ -1,145 +0,0 @@ -from helper import FlaskBaseTestCase -import clientapp -import helper -from flask import url_for -from clientapp.helpers.client_handler import ClientHandler -from unittest.mock import MagicMock, patch - - -class TestRegisterEndpoint(FlaskBaseTestCase): - - def test_if_app_has_register_endpoint(self): - self.assertIn( - 'register', - helper.app_endpoints(clientapp.create_app()) - ) - - def test_if_endpoint_accepts_post(self): - methods = None - for rule in self.app.url_map.iter_rules('register'): - methods = rule.methods - self.assertIn( - 'POST', - methods - ) - - # def test_init_should_call_discover_once(self): - # ClientHandler.discover = MagicMock(name='discover') - # ClientHandler.discover.return_value = helper.OP_DATA_DICT_RESPONSE - # ClientHandler.discover.assert_called_once() - - def test_endpoint_should_return_valid_req(self): - self.assertIn( - self.client.post(url_for('register')).status_code, - range(100, 511), - '/register returned invalid requisition' - ) - - @patch('clientapp.helpers.client_handler.ClientHandler.__init__', MagicMock(return_value=None)) - def test_endpoint_should_init_client_handler(self): - self.client.post(url_for('register'), json={ - 'op_url': 'https://test.com', - 'redirect_uris': ['https://clienttoberegistered.com/oidc_callback'] - }) - ClientHandler.__init__.assert_called_once() - - @patch('clientapp.helpers.client_handler.ClientHandler.__init__', MagicMock(return_value=None)) - def test_endpoint_should_accept_2_params(self): - first_value = 'https://op' - second_value = ['https://client.com.br/oidc_callback'] - - self.client.post(url_for('register'), json={ - 'op_url': first_value, - 'redirect_uris': second_value - }) - ClientHandler.__init__.assert_called_once_with(first_value, second_value, {}) - - @patch('clientapp.helpers.client_handler.ClientHandler.__init__', MagicMock(return_value=None)) - def test_endpoint_should_accept_3_params(self): - first_value = 'https://op' - second_value = ['https://client.com.br/oidc_callback'] - third_value = {'scope': 'openid email profile'} - - self.client.post(url_for('register'), json={ - 'op_url': first_value, - 'redirect_uris': second_value, - 'additional_params': third_value - }) - - ClientHandler.__init__.assert_called_once_with(first_value, second_value, third_value) - - def test_endpoint_should_return_error_code_400_if_no_data_sent(self): - self.assertEqual( - self.client.post(url_for('register')).status_code, - 400, - 'status_code for empty request is NOT 400' - ) - - def test_should_return_400_error_if_no_needed_keys_provided(self): - self.assertEqual( - self.client.post(url_for('register'), json={ - 'other_key': 'othervalue', - 'another_key': 'another_value' - }).status_code, - 400, - 'not returning 400 code if no needed keys provided' - ) - - def test_should_return_400_if_values_are_not_valid_urls(self): - self.assertEqual( - self.client.post(url_for('register'), json={ - 'op_url': 'not_valid_url', - 'redirect_uris': ['https://clienttoberegistered.com/oidc_callback'] - }).status_code, - 400, - 'not returning status 400 if values are not valid urls' - ) - - @patch('clientapp.helpers.client_handler.ClientHandler.get_client_dict', MagicMock(return_value=None)) - def test_valid_post_should_should_call_get_client_dict_once(self): - op_url = 'https://op.com.br' - self.client.post(url_for('register'), json={ - 'op_url': op_url, - 'redirect_uris': ['https://clienttoberegistered.com/oidc_callback'] - }) - ClientHandler.get_client_dict.assert_called_once() - - def test_should_should_return_200_if_registered(self): - op_url = 'https://op.com.br' - test_client_id = '1234-5678-9ten11' - test_client_secret = 'mysuperprotectedsecret' - with patch.object(ClientHandler, 'get_client_dict', return_value={ - 'op_metadata_url': '%s/.well-known/open-id-configuration' % op_url, - 'client_id': test_client_id, - 'client_secret': test_client_secret - }) as get_client_dict: - response = self.client.post(url_for('register'), json={ - 'op_url': op_url, - 'redirect_uris': ['https://clienttoberegistered.com/oidc_callback'] - }) - self.assertEqual(response.status_code, 200) - get_client_dict.reset() - - def test_should_return_expected_keys(self): - op_url = 'https://op.com.br' - redirect_uris = ['https://client.com.br/oidc_calback'] - test_client_id = '1234-5678-9ten11' - test_client_secret = 'mysuperprotectedsecret' - additional_params = {'param1': 'value1'} - - expected_keys = {'op_metadata_url', 'client_id', 'client_secret'} - - with patch.object(ClientHandler, 'get_client_dict', return_value={ - 'op_metadata_url': '%s/.well-known/open-id-configuration' % op_url, - 'client_id': test_client_id, - 'client_secret': test_client_secret - }) as get_client_dict: - response = self.client.post(url_for('register'), json={ - 'op_url': op_url, - 'redirect_uris': redirect_uris, - 'additional_params': additional_params - }) - print(response) - assert expected_keys <= response.json.keys(), response.json - - get_client_dict.reset() diff --git a/demos/jans-tent/tests/unit_integration/test_config.py b/demos/jans-tent/tests/unit_integration/test_config.py deleted file mode 100644 index 419e3bd56a9..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_config.py +++ /dev/null @@ -1,19 +0,0 @@ -import clientapp.config as cfg -from unittest import TestCase - - -class TestConfig(TestCase): - def test_has_attribute_SSL_VERIFY(self): - self.assertTrue(hasattr(cfg, 'SSL_VERIFY'), - 'SSL_VERIFY attribute is missing in config.') - - def test_SSL_VERIFY_has_boolean_value(self): - self.assertTrue('__bool__' in cfg.SSL_VERIFY.__dir__(), - 'SSL_VERIFY is not boolean.') - - def test_has_attribute_SCOPE(self): - self.assertTrue(hasattr(cfg, 'SCOPE'), - 'SCOPE attribute is missing in config.') - - def test_SCOPE_default_should_be_openid(self): - self.assertTrue(cfg.SCOPE == 'openid') diff --git a/demos/jans-tent/tests/unit_integration/test_configuration_endpoint.py b/demos/jans-tent/tests/unit_integration/test_configuration_endpoint.py deleted file mode 100644 index 1890b1d4cee..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_configuration_endpoint.py +++ /dev/null @@ -1,107 +0,0 @@ -import clientapp -from flask import Flask, url_for -from typing import List -import json -from helper import FlaskBaseTestCase - - -def app_endpoints(app: Flask) -> List[str]: - """ Return all enpoints in app """ - - endpoints = [] - for item in app.url_map.iter_rules(): - endpoint = item.endpoint.replace("_", "-") - endpoints.append(endpoint) - return endpoints - - -def valid_client_configuration(): - return { - "client_id": "my-client-id", - "client_secret": "my-client-secret", - "op_metadata_url": "https://op.com/.well-known/openidconfiguration" - } - - -class TestConfigurationEndpoint(FlaskBaseTestCase): - def test_create_app_has_configuration(self): - self.assertTrue( - 'configuration' in app_endpoints(clientapp.create_app()), - 'endpoint /configuration does not exist in app') - - def test_configuration_endpoint_should_return_valid_req(self): - self.assertIn( - self.client.post(url_for('configuration')).status_code, - range(100, 511), '/configuration returned invalid requisition') - - def test_endpoint_should_return_200_if_valid_json(self): - headers = {'Content-type': 'application/json'} - data = {'provider_id': 'whatever'} - json_data = json.dumps(data) - response = self.client.post(url_for('configuration'), - data=json_data, - headers=headers) - self.assertEqual(response.status_code, 200) - - def test_endpoint_should_return_posted_data_if_valid_json(self): - headers = {'Content-type': 'application/json'} - data = {'provider_id': 'whatever'} - json_data = json.dumps(data) - response = self.client.post(url_for('configuration'), - data=json_data, - headers=headers) - - self.assertEqual(json_data, json.dumps(response.json)) - - def test_endpoint_should_setup_cfg_with_provider_id(self): - headers = {'Content-type': 'application/json'} - data = {'provider_id': 'whatever'} - json_data = json.dumps(data) - self.client.post(url_for('configuration'), - data=json_data, - headers=headers) - - self.assertEqual(clientapp.cfg.PRE_SELECTED_PROVIDER_ID, 'whatever') - - def test_endpoint_should_setup_cfg_with_pre_selected_provider_true(self): - clientapp.cfg.PRE_SELECTED_PROVIDER = False - headers = {'Content-type': 'application/json'} - data = {'provider_id': 'whatever'} - json_data = json.dumps(data) - self.client.post(url_for('configuration'), - data=json_data, - headers=headers) - - self.assertTrue(clientapp.cfg.PRE_SELECTED_PROVIDER, ) - - def test_endpoint_should_return_200_if_valid_client_config(self): - headers = {'Content-type': 'application/json'} - json_data = json.dumps(valid_client_configuration()) - response = self.client.post( - url_for('configuration'), data=json_data, headers=headers) - self.assertEqual(response.status_code, 200, - 'endpoint is NOT returning 200 for valid client configuration') - - def test_endpoint_should_register_new_oauth_client_id(self): - headers = {'Content-type': 'application/json'} - client_id = "my-client-id" - client_secret = "my-client-secret" - op_metadata_url = "https://op.com/.well-known/openidconfiguration" - json_data = json.dumps({ - "client_id": client_id, - "client_secret": client_secret, - "op_metadata_url": op_metadata_url - }) - self.client.post( - url_for('configuration'), data=json_data, headers=headers) - self.assertTrue(clientapp.oauth.op.client_id == client_id, - 'endpoint is NOT changing op.client_id') - - def test_endpoint_should_register_new_oauth_client_secret(self): - headers = {'Content-type': 'application/json'} - json_data = json.dumps(valid_client_configuration()) - client_secret = valid_client_configuration()['client_secret'] - self.client.post( - url_for('configuration'), data=json_data, headers=headers) - self.assertTrue(clientapp.oauth.op.client_secret == client_secret, - '%s is is not %s' % (clientapp.oauth.op.client_secret, client_secret)) diff --git a/demos/jans-tent/tests/unit_integration/test_dcr_from_config.py b/demos/jans-tent/tests/unit_integration/test_dcr_from_config.py deleted file mode 100644 index e02f909ea28..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_dcr_from_config.py +++ /dev/null @@ -1,76 +0,0 @@ -from clientapp.utils import dcr_from_config -from clientapp import config as cfg -from unittest.mock import MagicMock, patch, mock_open -from unittest import TestCase -from clientapp.helpers.client_handler import ClientHandler -import helper -import json -import builtins - -class TestDrcFromConfig(TestCase): - - def setUp(self) -> None: - # stashing to restore on teardown - self.stashed_discover = ClientHandler.discover - self.stashed_register_client = ClientHandler.register_client - self.stashed_open = builtins.open - ClientHandler.discover = MagicMock(name='discover') - ClientHandler.discover.return_value = helper.OP_DATA_DICT_RESPONSE - ClientHandler.register_client = MagicMock(name='register_client') - ClientHandler.register_client.return_value = helper.REGISTER_CLIENT_RESPONSE - builtins.open = MagicMock(name='open') - - def tearDown(self) -> None: - ClientHandler.discover = self.stashed_discover - ClientHandler.register_client = self.stashed_register_client - builtins.open = self.stashed_open - - def test_if_setup_logging_exists(self): - assert hasattr(dcr_from_config, 'setup_logging') - - def test_if_static_variables_exists(self): - assert hasattr(dcr_from_config, 'OP_URL') - assert hasattr(dcr_from_config, 'REDIRECT_URIS') - - def test_if_static_variables_from_config(self): - assert dcr_from_config.OP_URL == cfg.ISSUER - assert dcr_from_config.REDIRECT_URIS == cfg.REDIRECT_URIS - - def test_register_should_be_calable(self): - assert callable(dcr_from_config.register), 'not callable' - - @patch('clientapp.helpers.client_handler.ClientHandler.__init__', MagicMock(return_value=None)) - def test_register_should_call_ClientHandler(self): - dcr_from_config.register() - ClientHandler.__init__.assert_called_once() - - @patch('clientapp.helpers.client_handler.ClientHandler.__init__', MagicMock(return_value=None)) - def test_register_should_call_ClientHandler_with_params(self): - dcr_from_config.register() - ClientHandler.__init__.assert_called_once_with( - cfg.ISSUER, cfg.REDIRECT_URIS, { - 'scope': cfg.SCOPE.split(" "), - "post_logout_redirect_uris": ['https://localhost:9090'] - } - ) - - def test_register_should_call_open(self): - with patch('builtins.open', mock_open()) as open_mock: - dcr_from_config.register() - - open_mock.assert_called_once() - - def test_register_should_call_open_with_correct_params(self): - with patch('builtins.open', mock_open()) as open_mock: - dcr_from_config.register() - open_mock.assert_called_once_with('client_info.json', 'w') - - def test_register_should_call_write_with_client_info(self): - client = ClientHandler(cfg.ISSUER, cfg.REDIRECT_URIS, {}) - expected_json_client_info = json.dumps(client.get_client_dict(), indent=4) - with patch('builtins.open', mock_open()) as open_mock: - dcr_from_config.register() - open_mock_handler = open_mock() - open_mock_handler.write.assert_called_once_with(expected_json_client_info) - - diff --git a/demos/jans-tent/tests/unit_integration/test_dynamic_client_registration.py b/demos/jans-tent/tests/unit_integration/test_dynamic_client_registration.py deleted file mode 100644 index 0f51b7cb596..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_dynamic_client_registration.py +++ /dev/null @@ -1,277 +0,0 @@ -from unittest import TestCase -from unittest.mock import MagicMock -import inspect - -import clientapp.helpers.client_handler as client_handler -from typing import Optional -import helper -from oic.oauth2 import ASConfigurationResponse - - -ClientHandler = client_handler.ClientHandler - -# helper -def get_class_instance(op_url='https://t1.techno24x7.com', - client_url='https://mock.test.com', - additional_metadata={}): - client_handler_obj = ClientHandler(op_url, client_url, additional_metadata) - return client_handler_obj - - -class TestDynamicClientRegistration(TestCase): - - def setUp(self) -> None: - self.register_client_stash = ClientHandler.register_client - self.discover_stash = ClientHandler.discover - - @staticmethod - def mock_methods(): - ClientHandler.register_client = MagicMock(name='register_client') - ClientHandler.register_client.return_value = helper.REGISTER_CLIENT_RESPONSE - ClientHandler.discover = MagicMock(name='discover') - ClientHandler.discover.return_value = helper.OP_DATA_DICT_RESPONSE - - def restore_stashed_mocks(self): - ClientHandler.discover = self.discover_stash - ClientHandler.register_client = self.register_client_stash - - def test_if_json_exists(self): - self.assertTrue(hasattr(client_handler, 'json'), - 'json does not exists in client_handler') - - def test_if_json_is_from_json_package(self): - self.assertTrue(client_handler.json.__package__ == 'json', - 'json is not from json') - - # testing ClientHandler class - def test_if_ClientHandler_is_class(self): - self.assertTrue(inspect.isclass(ClientHandler)) - - def test_if_register_client_exists(self): - self.assertTrue(hasattr(ClientHandler, 'register_client'), - 'register_client does not exists in ClientHandler') - - def test_if_register_client_is_callable(self): - self.assertTrue(callable(ClientHandler.register_client), - 'register_client is not callable') - - def test_if_register_client_receives_params(self): - expected_args = ['self', 'op_data', 'redirect_uris'] - self.assertTrue( - inspect.getfullargspec( - ClientHandler.register_client).args == expected_args, - 'register_client does not receive expected args') - - def test_if_register_client_params_are_expected_type(self): - insp = inspect.getfullargspec(ClientHandler.register_client) - self.assertTrue( - insp.annotations['op_data'] == ASConfigurationResponse - and insp.annotations['redirect_uris'] == Optional[list[str]], - 'register_client is not receiving the right params') - - def test_if_class_has_initial_expected_attrs(self): - initial_expected_attrs = [ - '_ClientHandler__client_id', - '_ClientHandler__client_secret', - '_ClientHandler__redirect_uris', - '_ClientHandler__metadata_url', - '_ClientHandler__additional_metadata', - 'discover', # method - 'register_client' # method - ] - - self.assertTrue( - all(attr in ClientHandler.__dict__.keys() - for attr in initial_expected_attrs), - 'ClientHandler does not have initial attrs') - - def test_if_discover_exists(self): - self.assertTrue(hasattr(ClientHandler, 'discover'), - 'discover does not exists in ClientHandler') - - def test_if_discover_is_callable(self): - self.assertTrue(callable(ClientHandler.discover), - 'discover is not callable') - - def test_if_discover_receives_params(self): - expected_args = ['self', 'op_url'] - self.assertTrue( - inspect.getfullargspec( - ClientHandler.discover).args == expected_args, - 'discover does not receive expected args') - - def test_if_discover_params_are_expected_type(self): - insp = inspect.getfullargspec(ClientHandler.discover) - self.assertTrue( - insp.annotations['op_url'] == Optional[str], - 'discover is not receiving the right params') - - def test_discover_should_return_valid_dict(self): - """[Checks if returns main keys] - """ - - main_keys = { - 'issuer', 'authorization_endpoint', 'token_endpoint', - 'userinfo_endpoint', 'clientinfo_endpoint', - 'session_revocation_endpoint', 'end_session_endpoint', - 'revocation_endpoint', 'registration_endpoint' - } - - self.mock_methods() - op_data = ClientHandler.discover(ClientHandler, - 'https://t1.techno24x7.com') - self.assertTrue(main_keys <= set(op_data), - 'discovery return data does not have main keys') - self.restore_stashed_mocks() - - def test_if_get_client_dict_exists(self): - self.assertTrue(hasattr(ClientHandler, 'get_client_dict'), - 'get_client_dict does not exists in ClientHandler') - - def test_if_get_client_dict_is_callable(self): - self.assertTrue(callable(ClientHandler.get_client_dict), - 'get_client_dict is not callable') - - def test_if_get_client_dict_receives_params(self): - expected_args = ['self'] - self.assertTrue( - inspect.getfullargspec( - ClientHandler.get_client_dict).args == expected_args, - 'get_client_dict does not receive expected args') - - def test_client_id_should_return_something(self): - self.assertIsNotNone( - ClientHandler.get_client_dict(ClientHandler), - 'get_client_dict returning NoneType. It has to return something!') - - def test_get_client_dict_should_return_a_dict(self): - self.assertIsInstance(ClientHandler.get_client_dict(ClientHandler), - dict, 'get_client_dict is not returning a dict') - - def test_class_init_should_set_op_url(self): - self.mock_methods() - - op_url = 'https://t1.techno24x7.com' - - client_handler_obj = get_class_instance(op_url) - - self.restore_stashed_mocks() - - self.assertEqual(client_handler_obj.__dict__['_ClientHandler__op_url'], - op_url) - - def test_class_init_should_set_redirect_uris(self): - self.mock_methods() - - op_url = 'https://t1.techno24x7.com' - redirect_uris = 'https://mock.test.com/oidc_callback' - client_handler_obj = ClientHandler(op_url, redirect_uris, {}) - - self.restore_stashed_mocks() - - self.assertEqual( - client_handler_obj.__dict__['_ClientHandler__redirect_uris'], - redirect_uris) - - def test_class_init_should_set_metadata_url(self): - self.mock_methods() - - op_url = 'https://t1.techno24x7.com' - - client_handler_obj = get_class_instance(op_url) - - self.restore_stashed_mocks() - - expected_metadata_url = op_url + '/.well-known/openid-configuration' - - self.assertEqual( - client_handler_obj.__dict__['_ClientHandler__metadata_url'], - expected_metadata_url) - - def test_class_init_should_set_additional_params(self): - self.mock_methods() - expected_metadata = {'metakey1': 'meta value 1'} - client_handler_obj = get_class_instance(additional_metadata=expected_metadata) - self.restore_stashed_mocks() - - self.assertEqual( - client_handler_obj.__dict__['_ClientHandler__additional_metadata'], - expected_metadata - ) - - def test_class_init_should_have_docstring(self): - self.assertTrue(ClientHandler.__init__.__doc__, - 'ClientHandler.__init__ has doc') - - def test_if_get_client_dict_return_expected_keys(self): - expected_keys = [ - 'op_metadata_url', - 'client_id', - 'client_secret', - ] - - self.mock_methods() - - client_handler_obj = get_class_instance() - client_dict = client_handler_obj.get_client_dict() - - self.restore_stashed_mocks() - - self.assertTrue( - all(key in client_dict.keys() for key in expected_keys), - 'there is no %s IN %s: get_client_dict is NOT returning expected keys' - % (str(expected_keys), str(client_dict.keys()))) - - def test_get_client_dict_values_cannot_be_none(self): - self.mock_methods() - - op_url = 'https://t1.techno24x7.com' - client_handler_obj = get_class_instance(op_url) - client_dict = client_handler_obj.get_client_dict() - - self.restore_stashed_mocks() - - for key in client_dict.keys(): - self.assertIsNotNone(client_dict[key], - 'get_client_dict[%s] cannot be None!' % key) - - def test_get_client_dict_should_return_url_metadata_value(self): - self.mock_methods() - - client_handler_obj = get_class_instance() - - self.restore_stashed_mocks() - - self.assertEqual( - client_handler_obj.get_client_dict()['op_metadata_url'], - client_handler_obj._ClientHandler__metadata_url) - - def test_get_client_dict_should_return_client_id_value(self): - self.mock_methods() - - client_handler_obj = get_class_instance() - - self.restore_stashed_mocks() - - self.assertEqual( - client_handler_obj.get_client_dict()['client_id'], - client_handler_obj._ClientHandler__client_id - ) - - def test_init_should_call_discover_once(self): - self.mock_methods() - - get_class_instance() - - ClientHandler.discover.assert_called_once() - - self.restore_stashed_mocks() - - def test_init_should_call_register_client_once(self): - self.mock_methods() - - get_class_instance() - ClientHandler.register_client.assert_called_once() - - self.restore_stashed_mocks() - diff --git a/demos/jans-tent/tests/unit_integration/test_flask_factory.py b/demos/jans-tent/tests/unit_integration/test_flask_factory.py deleted file mode 100644 index 0813c3b1ad1..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_flask_factory.py +++ /dev/null @@ -1,93 +0,0 @@ -from unittest import TestCase -from unittest.mock import MagicMock -import clientapp -from flask import Flask -import os -import builtins -from clientapp.helpers.client_handler import ClientHandler -import helper - - -class TestFlaskApp(TestCase): - - def setUp(self) -> None: - self.stashed_add_config_from_json = clientapp.add_config_from_json - clientapp.cfg.CLIENT_ID = 'any-client-id-stub' - clientapp.cfg.CLIENT_SECRET = 'any-client-secret-stub' - clientapp.cfg.SERVER_META_URL = 'https://ophostname.com/server/meta/url' - clientapp.add_config_from_json = MagicMock(name='add_config_from_json') - clientapp.add_config_from_json.return_value(None) - self.stashed_discover = ClientHandler.discover - self.stashed_register_client = ClientHandler.register_client - self.stashed_open = builtins.open - builtins.open = MagicMock(name='open') - ClientHandler.discover = MagicMock(name='discover') - ClientHandler.discover.return_value = helper.OP_DATA_DICT_RESPONSE - ClientHandler.register_client = MagicMock(name='register_client') - ClientHandler.register_client.return_value = helper.REGISTER_CLIENT_RESPONSE - - def tearDown(self) -> None: - ClientHandler.discover = self.stashed_discover - ClientHandler.register_client = self.stashed_register_client - builtins.open = self.stashed_open - clientapp.add_config_from_json = self.stashed_add_config_from_json - - def test_create_app_should_exist(self): - self.assertEqual(hasattr(clientapp, 'create_app'), True, - 'app factory does not exists') - - def test_create_app_should_be_invokable(self): - self.assertEqual(callable(clientapp.create_app), True, - 'cannot invoke create_app from clientapp') - - def test_create_app_should_return_a_flask_app(self): - - self.assertIsInstance(clientapp.create_app(), Flask, - 'create_app is not returning a Flask instance') - - def test_if_app_has_secret_key(self): - self.assertTrue(hasattr(clientapp.create_app(), 'secret_key'), ) - - def test_if_secret_key_not_none(self): - self.assertIsNotNone(clientapp.create_app().secret_key, - 'app secret key is unexpectedly None') - - def test_if_oauth_is_app_extension(self): - self.assertTrue('authlib.integrations.flask_client' in - clientapp.create_app().extensions) - - def test_if_settings_py_exists(self): - self.assertTrue(os.path.exists('clientapp/config.py'), - 'File clientapp/config.py does not exist') - - def test_if_op_client_id_exists_in_app_configuration(self): - self.assertTrue('OP_CLIENT_ID' in clientapp.create_app().config, - 'No OP_CLIENT_ID in app.config') - - def test_if_clientapp_has_cfg(self): - self.assertTrue(hasattr(clientapp, 'cfg')) - - def test_if_cfg_is_module_from_configpy(self): - self.assertTrue( - os.path.relpath(clientapp.cfg.__file__) == 'clientapp/config.py') - - ... - - def test_if_OP_CLIENT_ID_is_equal_cfg_CLIENT_ID(self): - self.assertEqual(clientapp.create_app().config['OP_CLIENT_ID'], - clientapp.cfg.CLIENT_ID) - - def test_if_OP_CLIENT_SECRET_exists_in_app_configuration(self): - self.assertTrue('OP_CLIENT_SECRET' in clientapp.create_app().config, - 'No OP_CLIENT_SECRET in app.config') - - def test_if_OP_CLIENT_SECRET_is_equal_cfg_CLIENT_ID(self): - self.assertEqual(clientapp.create_app().config['OP_CLIENT_SECRET'], - clientapp.cfg.CLIENT_SECRET) - - def test_if_has_attr_ssl_verify(self): - self.assertTrue(hasattr(clientapp, 'ssl_verify'), - 'There is no ssl_verify in clientapp') - - def test_should_have_method_to_set_CA_CURL_CERT(self): - self.assertTrue(clientapp.ssl_verify.__call__) diff --git a/demos/jans-tent/tests/unit_integration/test_gluu_preselected_provider.py b/demos/jans-tent/tests/unit_integration/test_gluu_preselected_provider.py deleted file mode 100644 index 4f7fc9dc13f..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_gluu_preselected_provider.py +++ /dev/null @@ -1,46 +0,0 @@ -import clientapp -from helper import FlaskBaseTestCase - - -class TestPreselectedProvider(FlaskBaseTestCase): - - # """ - # We should be able to send Preselected passport provider to gluu OIDC as a authorization param - # like this: preselectedExternalProvider= - # Where is the Base64-encoded representation of a small JSON - # content that looking like this: - # { "provider" : } - # """ - def setUp(self): - clientapp.cfg.PRE_SELECTED_PROVIDER = True - FlaskBaseTestCase.setUp(FlaskBaseTestCase) - - def test_config_should_have_preselected_provider_option(self): - self.assertTrue(hasattr(clientapp.cfg, 'PRE_SELECTED_PROVIDER'), - 'cfg doesnt have PRE_SELECTED_PROVIDER attribute') - - def test_config_pre_selected_provider_should_be_boolean(self): - self.assertTrue( - type(clientapp.cfg.PRE_SELECTED_PROVIDER) == bool, - 'cfg.PRE_SELECTED_PROVIDER is not bool') - - def test_preselected_provider_id_should_exist_in_cfg(self): - self.assertTrue(hasattr(clientapp.cfg, 'PRE_SELECTED_PROVIDER_ID')) - - def test_clientapp_should_have_get_preselected_provider(self): - self.assertTrue( - hasattr(clientapp, 'get_preselected_provider'), - 'client app does not have get_preselected_provider attr') - - def test_get_preselected_provider_should_be_callable(self): - self.assertTrue(callable(clientapp.get_preselected_provider), - 'get_preselected_provider is not callable') - - def test_get_selected_provider_should_return_base64(self): - - clientapp.cfg.PRE_SELECTED_PROVIDER_ID = 'saml-emaillink' - expected_response = "eyAicHJvdmlkZXIiIDogInNhbWwtZW1haWxsaW5rIiB9" - self.assertEqual(clientapp.get_preselected_provider(), - expected_response) - - diff --git a/demos/jans-tent/tests/unit_integration/test_logout_endpoint.py b/demos/jans-tent/tests/unit_integration/test_logout_endpoint.py deleted file mode 100644 index c0bb85bfc87..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_logout_endpoint.py +++ /dev/null @@ -1,65 +0,0 @@ -import clientapp -from helper import FlaskBaseTestCase, app_endpoints -from flask import url_for, session -from urllib import parse -from clientapp import config as cfg - -class TestLogoutEndpoint(FlaskBaseTestCase): - def authenticated_session_mock(self): - with self.client.session_transaction() as session: - session['id_token'] = 'id_token_stub' - - def test_endpoint_exists(self): - self.assertIn( - 'logout', - app_endpoints(clientapp.create_app()) - ) - - def test_endpoint_should_require_authentication(self): - ... - def test_logout_endpoint_should_redirect_to_home_if_unauthenticated(self): - # print(self.client.get(url_for('logout')).response) - response = self.client.get(url_for('logout')) - assert(response.status_code == 302) - assert(response.location == url_for('index')) - - - def test_logout_endpoint_should_clear_session(self): - with self.client.session_transaction() as sess: - sess['id_token'] = 'id_token_stub' - sess['user'] = 'userinfo stub' - - with self.client: - self.client.get(url_for('logout')) - assert 'id_token' not in session - assert 'user' not in session - - def test_endpoint_should_redirect_to_end_session_endpoint(self): - with self.client.session_transaction() as session: - session['id_token'] = 'id_token_stub' - session['user'] = 'userinfo stub' - - response = self.client.get(url_for('logout')) - - parsed_location = parse.urlparse(response.location) - assert parsed_location.scheme == 'https' - assert parsed_location.netloc == 'ophostname.com' - assert parsed_location.path == '/end_session_endpoint' - - - - def test_endpoint_should_redirect_to_end_session_endpoint_with_params(self): - token_stub = 'id_token_stub' - with self.client.session_transaction() as session: - session['id_token'] = token_stub - session['user'] = 'userinfo stub' - - parsed_redirect_uri = parse.urlparse(cfg.REDIRECT_URIS[0]) - post_logout_uri = '%s://%s' % (parsed_redirect_uri.scheme, parsed_redirect_uri.netloc) - - expected_query = 'post_logout_redirect_uri=%s&token_hint=%s' % (post_logout_uri, token_stub) - response = self.client.get(url_for('logout')) - - parsed_location = parse.urlparse(response.location) - assert parsed_location.query == expected_query - diff --git a/demos/jans-tent/tests/unit_integration/test_protected_content_endpoint.py b/demos/jans-tent/tests/unit_integration/test_protected_content_endpoint.py deleted file mode 100644 index 68f7012c88e..00000000000 --- a/demos/jans-tent/tests/unit_integration/test_protected_content_endpoint.py +++ /dev/null @@ -1,68 +0,0 @@ -from clientapp import create_app, session -from flask import Flask, url_for -from typing import List -from werkzeug import local -from helper import FlaskBaseTestCase - - -def app_endpoint(app: Flask) -> List[str]: - """ Return all enpoints in app """ - - endpoints = [] - for item in app.url_map.iter_rules(): - endpoint = item.endpoint.replace("_", "-") - endpoints.append(endpoint) - return endpoints - - -class TestProtectedContentEndpoint(FlaskBaseTestCase): - def test_app_should_contain_protected_content_route(self): - - endpoints = app_endpoint(create_app()) - self.assertIn('protected-content', endpoints, - 'protected-content route not found in app endpoints') - - def test_app_protected_content_route_should_return_valid_requisition(self): - - self.client.get(url_for('protected_content')) - - self.assertIn( - self.client.get(url_for('protected_content')).status_code, - range(100, 511), - 'protected content route returned invalid requisition') - - def test_should_return_if_session_exists_in_clientapp(self): - import clientapp - self.assertTrue(hasattr(clientapp, 'session'), - "session is not an attribute of clientapp") - del clientapp - - def test_should_check_if_session_is_LocalProxy_instance(self): - self.assertIsInstance(session, local.LocalProxy) - - def test_protected_content_return_status_200_ir_session_profile_exists( - self): - - with self.client.session_transaction() as sess: - sess['user'] = 'foo' - - self.assertEqual( - self.client.get(url_for('protected_content')).status_code, 200) - - def test_should_return_302_if_no_session_profile(self): - self.assertEqual( - self.client.get(url_for('protected_content')).status_code, 302) - - def test_protected_content_should_redirect_to_login_if_session_profile_doesnt_exist( - self): - - response = self.client.get(url_for('protected_content')) - self.assertTrue(response.location.endswith(url_for('login')), - 'Protected page is not redirecting to login page') - - ''' TODO - def test_should_return_if_user_logged_in_exists(self): - self.assertTrue( - hasattr(app,'user_logged_in') - ) - ''' diff --git a/docs/janssen-server/developer/agama/quick-start-using-agama-lab.md b/docs/janssen-server/developer/agama/quick-start-using-agama-lab.md index 57d0a9597f6..df56dce9e18 100644 --- a/docs/janssen-server/developer/agama/quick-start-using-agama-lab.md +++ b/docs/janssen-server/developer/agama/quick-start-using-agama-lab.md @@ -387,7 +387,7 @@ Server deployment ## Test -1. [Setup](https://github.com/JanssenProject/jans/tree/main/demos/jans-tent) Janssen Tent +1. [Setup](https://github.com/JanssenProject/jans/tree/v1.2.0/demos/jans-tent) Janssen Tent 2. Change the configuration as given below in `config.py` ``` From 85b95ec91f17ca26964558bb085a5bbc9aad13d0 Mon Sep 17 00:00:00 2001 From: Mohammad Abudayyeh <47318409+moabu@users.noreply.github.com> Date: Wed, 8 Jan 2025 18:16:48 +0000 Subject: [PATCH 7/7] fix: build acct linking agama (#10575) --- .../main/webapp/scripts/font-awesome-5.12.1.all.min.js | 5 +++++ jans-casa/plugins/acct-linking/agama/pom.xml | 8 +++++++- jans-casa/plugins/email_2fa/agama/pom.xml | 8 ++++++++ jans-casa/plugins/email_2fa/pom.xml | 8 ++++++++ 4 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 jans-casa/app/src/main/webapp/scripts/font-awesome-5.12.1.all.min.js diff --git a/jans-casa/app/src/main/webapp/scripts/font-awesome-5.12.1.all.min.js b/jans-casa/app/src/main/webapp/scripts/font-awesome-5.12.1.all.min.js new file mode 100644 index 00000000000..91bcb117205 --- /dev/null +++ b/jans-casa/app/src/main/webapp/scripts/font-awesome-5.12.1.all.min.js @@ -0,0 +1,5 @@ +/*! + * Font Awesome Free 5.12.1 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +!function(){"use strict";var c={},l={};try{"undefined"!=typeof window&&(c=window),"undefined"!=typeof document&&(l=document)}catch(c){}var h=(c.navigator||{}).userAgent,z=void 0===h?"":h,v=c,a=l,m=(v.document,!!a.documentElement&&!!a.head&&"function"==typeof a.addEventListener&&a.createElement,~z.indexOf("MSIE")||z.indexOf("Trident/"),"___FONT_AWESOME___"),s=function(){try{return!0}catch(c){return!1}}();var e=v||{};e[m]||(e[m]={}),e[m].styles||(e[m].styles={}),e[m].hooks||(e[m].hooks={}),e[m].shims||(e[m].shims=[]);var t=e[m];function M(c,z){var l=(2>>0;h--;)l[h]=c[h];return l}function gc(c){return c.classList?bc(c.classList):(c.getAttribute("class")||"").split(" ").filter(function(c){return c})}function Ac(c,l){var h,z=l.split("-"),v=z[0],a=z.slice(1).join("-");return v!==c||""===a||(h=a,~T.indexOf(h))?null:a}function Sc(c){return"".concat(c).replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")}function yc(h){return Object.keys(h||{}).reduce(function(c,l){return c+"".concat(l,": ").concat(h[l],";")},"")}function wc(c){return c.size!==Lc.size||c.x!==Lc.x||c.y!==Lc.y||c.rotate!==Lc.rotate||c.flipX||c.flipY}function kc(c){var l=c.transform,h=c.containerWidth,z=c.iconWidth,v={transform:"translate(".concat(h/2," 256)")},a="translate(".concat(32*l.x,", ").concat(32*l.y,") "),m="scale(".concat(l.size/16*(l.flipX?-1:1),", ").concat(l.size/16*(l.flipY?-1:1),") "),s="rotate(".concat(l.rotate," 0 0)");return{outer:v,inner:{transform:"".concat(a," ").concat(m," ").concat(s)},path:{transform:"translate(".concat(z/2*-1," -256)")}}}var xc={x:0,y:0,width:"100%",height:"100%"};function Zc(c){var l=!(1").concat(m.map(Jc).join(""),"")}var $c=function(){};function cl(c){return"string"==typeof(c.getAttribute?c.getAttribute(B):null)}var ll={replace:function(c){var l=c[0],h=c[1].map(function(c){return Jc(c)}).join("\n");if(l.parentNode&&l.outerHTML)l.outerHTML=h+(K.keepOriginalSource&&"svg"!==l.tagName.toLowerCase()?"\x3c!-- ".concat(l.outerHTML," --\x3e"):"");else if(l.parentNode){var z=document.createElement("span");l.parentNode.replaceChild(z,l),z.outerHTML=h}},nest:function(c){var l=c[0],h=c[1];if(~gc(l).indexOf(K.replacementClass))return ll.replace(c);var z=new RegExp("".concat(K.familyPrefix,"-.*"));delete h[0].attributes.style,delete h[0].attributes.id;var v=h[0].attributes.class.split(" ").reduce(function(c,l){return l===K.replacementClass||l.match(z)?c.toSvg.push(l):c.toNode.push(l),c},{toNode:[],toSvg:[]});h[0].attributes.class=v.toSvg.join(" ");var a=h.map(function(c){return Jc(c)}).join("\n");l.setAttribute("class",v.toNode.join(" ")),l.setAttribute(B,""),l.innerHTML=a}};function hl(c){c()}function zl(h,c){var z="function"==typeof c?c:$c;if(0===h.length)z();else{var l=hl;K.mutateApproach===y&&(l=o.requestAnimationFrame||hl),l(function(){var c=!0===K.autoReplaceSvg?ll.replace:ll[K.autoReplaceSvg]||ll.replace,l=_c.begin("mutate");h.map(c),l(),z()})}}var vl=!1;function al(){vl=!1}var ml=null;function sl(c){if(t&&K.observeMutations){var v=c.treeCallback,a=c.nodeCallback,m=c.pseudoElementsCallback,l=c.observeMutationsRoot,h=void 0===l?V:l;ml=new t(function(c){vl||bc(c).forEach(function(c){if("childList"===c.type&&0io.jans.casa.plugins acct-linking-agama 0.0.0-nightly - + + + github + GitHub Packages + https://maven.pkg.github.com/JanssenProject/jans + + diff --git a/jans-casa/plugins/email_2fa/agama/pom.xml b/jans-casa/plugins/email_2fa/agama/pom.xml index 8c7d6e4e5a7..6694fe7d193 100644 --- a/jans-casa/plugins/email_2fa/agama/pom.xml +++ b/jans-casa/plugins/email_2fa/agama/pom.xml @@ -6,6 +6,14 @@ io.jans.casa.plugins email_2fa-agama 0.0.0-nightly + + + + github + GitHub Packages + https://maven.pkg.github.com/JanssenProject/jans + + diff --git a/jans-casa/plugins/email_2fa/pom.xml b/jans-casa/plugins/email_2fa/pom.xml index ed8dd41867f..d9117c3f930 100644 --- a/jans-casa/plugins/email_2fa/pom.xml +++ b/jans-casa/plugins/email_2fa/pom.xml @@ -8,6 +8,14 @@ 0.0.0-nightly jar + + + github + GitHub Packages + https://maven.pkg.github.com/JanssenProject/jans + + + 11 11