From 97a85950d2b98aa19d6fccc3c822e45b899da700 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Tue, 10 Dec 2019 09:08:13 +0100 Subject: [PATCH 001/122] Programs to research PassTickets Signed-off-by: Petr Plavjanik --- passticket/test-programs/PtEval.java | 22 ++++++++ passticket/test-programs/PtGen.java | 28 ++++++++++ passticket/test-programs/README.md | 40 +++++++++++++++ passticket/test-programs/build.sh | 9 ++++ passticket/test-programs/pt_passwd.c | 77 ++++++++++++++++++++++++++++ passticket/test-programs/racf.jcl | 25 +++++++++ 6 files changed, 201 insertions(+) create mode 100644 passticket/test-programs/PtEval.java create mode 100644 passticket/test-programs/PtGen.java create mode 100644 passticket/test-programs/README.md create mode 100755 passticket/test-programs/build.sh create mode 100644 passticket/test-programs/pt_passwd.c create mode 100644 passticket/test-programs/racf.jcl diff --git a/passticket/test-programs/PtEval.java b/passticket/test-programs/PtEval.java new file mode 100644 index 0000000000..84dd19d180 --- /dev/null +++ b/passticket/test-programs/PtEval.java @@ -0,0 +1,22 @@ +import com.ibm.eserver.zos.racf.IRRPassTicket; +import com.ibm.eserver.zos.racf.IRRPassTicketEvaluationException; + +public class PtEval { + public static void main(String args[]) { + IRRPassTicket passTicketService = new IRRPassTicket(); + String userid = args[0]; + String applid = args[1]; + String passTicket = args[2]; + + try { + System.out.println("userid=" + userid + " applid=" + applid + " passTicket=" + passTicket); + passTicketService.evaluate(userid, applid, passTicket); + System.out.println("PassTicket is valid (1st evaluation)"); + + passTicketService.evaluate(userid, applid, passTicket); + System.out.println("PassTicket is valid (2nd evaluation)"); + } catch (IRRPassTicketEvaluationException e) { + System.out.println("PassTicket evaluation failed: " + e); + } + } +} diff --git a/passticket/test-programs/PtGen.java b/passticket/test-programs/PtGen.java new file mode 100644 index 0000000000..15ce240cbc --- /dev/null +++ b/passticket/test-programs/PtGen.java @@ -0,0 +1,28 @@ +import java.io.FileNotFoundException; +import java.io.PrintWriter; + +import com.ibm.eserver.zos.racf.IRRPassTicket; +import com.ibm.eserver.zos.racf.IRRPassTicketGenerationException; + +public class PtGen { + public static void main(String args[]) { + IRRPassTicket passTicketService; + String userid = args[0]; + String applid = args[1]; + + try { + passTicketService = new IRRPassTicket(); + System.out.println("userid=" + userid + " applid=" + applid); + String passTicket = passTicketService.generate(userid, applid); + System.out.println("New PassTicket: " + passTicket); + String passTicket2 = passTicketService.generate(userid, applid); + System.out.println("New PassTicket 2: " + passTicket2); + + try (PrintWriter out = new PrintWriter(".passticket")) { + out.print(passTicket); + } + } catch (IRRPassTicketGenerationException | FileNotFoundException e) { + System.out.println("Generation failed: " + e); + } + } +} diff --git a/passticket/test-programs/README.md b/passticket/test-programs/README.md new file mode 100644 index 0000000000..4832bd6a5c --- /dev/null +++ b/passticket/test-programs/README.md @@ -0,0 +1,40 @@ +# PassTicket Test Programs + +Files in this directory can be used to test PassTicket setup on different systems. + +Source code can be built using `build.sh`. You need to have zosmf and ssh profiles setup in Zowe CLI and change the workspace directory in `build.sh`. + +The `pt_passwd` executable needs to be program-controlled. The user ID used to run the build needs to have `BPX.FILEATTR.APF CLASS(FACILITY) ACCESS(READ)`. + +You need to setup security to generate PassTickets using: + +- `racf.jcl` + +These jobs are using following user IDs: + +- `SDKBLD1` - to generate and evaluate PassTickets +- `SDKTST1` - to use the PassTicket in C (`__passwd` and `__passw_applid`) + +Then you can run it under user `SDKBLD1` that can both generate and evaluate PassTickets: + +```sh +java -Xquickstart -cp /usr/include/java_classes/IRRRacf.jar:. PtGen SDKTST1 TSTAPPL +``` + +```sh +java -Xquickstart -cp /usr/include/java_classes/IRRRacf.jar:. PtEval SDKTST1 TSTAPPL $(cat .passticket) +``` + +You can use the PassTicket under user ID `SDKTST1` which has access to `TSTAPPL` APPLID: + +```sh +pt_passwd SDKTST1 TSTAPPL $(cat .passticket) +``` + +Findings: + +- Without reply protection, you can generate PassTickets as much as you need, it will return the same PassTicket, if you run it at the similar time. Both of them are valid and can be used. +- To generate PassTicket via Java class `IRRPassTicket`, the user ID that runs it needs to have `IRRPTAUTH.TSTAPPL.* CLASS(PTKTDATA) ACCESS(UPDATE)` +- To evaluation via Java class `IRRPassTicket`, the user ID that runs it needs to have `IRRPTAUTH.TSTAPPL.* CLASS(PTKTDATA) ACCESS(READ)` +- `IRRPassTicket` does not check that the user ID for which the ticket is issued can use it +- `IRRPassTicket` requires that PassTicket is set up properly using `RDEFINE PTKTDATA ...` diff --git a/passticket/test-programs/build.sh b/passticket/test-programs/build.sh new file mode 100755 index 0000000000..2d76a7486e --- /dev/null +++ b/passticket/test-programs/build.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +WORKSPACE_DIR=/zaas1/sdkbld1/pt2 +zowe uss issue ssh "mkdir -p $WORKSPACE_DIR" +zowe zos-files upload file-to-uss "PtEval.java" "$WORKSPACE_DIR/PtEval.java" +zowe zos-files upload file-to-uss "PtGen.java" "$WORKSPACE_DIR/PtGen.java" +zowe zos-files upload file-to-uss "pt_passwd.c" "$WORKSPACE_DIR/pt_passwd.c" +zowe uss issue ssh "xlc -o pt_passwd pt_passwd.c; extattr +p pt_passwd" --cwd $WORKSPACE_DIR +zowe uss issue ssh "javac -cp /usr/include/java_classes/IRRRacf.jar PtGen.java PtEval.java" --cwd $WORKSPACE_DIR +zowe uss issue ssh "ls -E" --cwd $WORKSPACE_DIR diff --git a/passticket/test-programs/pt_passwd.c b/passticket/test-programs/pt_passwd.c new file mode 100644 index 0000000000..8b17e319c2 --- /dev/null +++ b/passticket/test-programs/pt_passwd.c @@ -0,0 +1,77 @@ +#define _OPEN_SYS 1 +#include +#include +#include +#include +#include +#include + +#define PSATOLD 0x21C +#define TCBSTCB 0x138 +#define STCBOTCB 0x0D8 +#define OTCBTHLI 0x0BC + +#define THLIAPPLIDLEN 0x052 +#define THLIAPPLID 0x070 + +int set_applid(const char* applid) { + void *__ptr32 psa = 0; + void *__ptr32 tcb = *(void *__ptr32 *)(psa + PSATOLD); + printf("tcb=%p\n", tcb); + void *__ptr32 stcb = *(void *__ptr32 *)(tcb + TCBSTCB); + printf("stcb=%p\n", stcb); + void *__ptr32 otcb = *(void *__ptr32 *)(stcb + STCBOTCB); + printf("otcb=%p\n", otcb); + void *__ptr32 thli = *(void *__ptr32 *)(otcb + OTCBTHLI); + printf("thli=%p\n", thli); + + if (memcmp("THLI", thli, 4) != 0) + { + int rc = -2; + printf("Could not set APPLID: BPXYTHLI control block not found\b"); + return -1; + } + + char *origApplid = (char *)malloc(9); + + const int applidLength = strlen(applid); + + printf("APPLID length: %d\n", applidLength); + printf("APPLID value: %s\n", applid); + + char *__ptr32 thliApplidLen = (char *__ptr32)(thli + THLIAPPLIDLEN); + *thliApplidLen = applidLength; + + void *__ptr32 thliApplid = (void *__ptr32)(thli + THLIAPPLID); + memset(thliApplid, ' ', 8); + origApplid[8] = 0; + memcpy(origApplid, thliApplid, 8); + memcpy(thliApplid, applid, applidLength); + + printf("Orig APPLID value: %s\n", origApplid); + + /* A call to pthread_security_np causes that the value set above is correctly propagated */ + pthread_security_np(0, 0, 0, NULL, NULL, 0); + errno = 0; + return 0; +} + +int main(int argc, char *argv[]) { + for (int i = 0; i < argc; i++) + printf("argv[%d] == \"%s\"\n", i, argv[i]); + + const char *userid = argv[1]; + const char *applid = argv[2]; + const char *passticket = argv[3]; + + int rc; + + set_applid(applid); + rc = __passwd(userid, passticket, 0); + printf("__passwd rc=%d, errno=%d strerror=%s\n", rc, errno, strerror(errno)); + + rc = __passwd_applid(userid, passticket, 0, applid); + printf("__passwd_applid rc=%d, errno=%d strerror=%s\n", rc, errno, strerror(errno)); + + return 0; +} diff --git a/passticket/test-programs/racf.jcl b/passticket/test-programs/racf.jcl new file mode 100644 index 0000000000..e8a442f3a8 --- /dev/null +++ b/passticket/test-programs/racf.jcl @@ -0,0 +1,25 @@ +//TESTPTKT JOB MSGCLASS=X,MSGLEVEL=(1,1) +//STEP1 EXEC PGM=IKJEFT01 +//SYSTSPRT DD SYSOUT=* +//SYSTSIN DD * + /* Define APPLID TSTAPPL that can be used by SDKTST1 user */ + RDEFINE APPL TSTAPPL UACC(NONE) + PERMIT TSTAPPL CL(APPL) ACCESS(READ) ID(SDKTST1) + PERMIT TSTAPPL CL(APPL) ACCESS(NONE) ID(SDKBLD1) + SETROPTS RACLIST(APPL) REFRESH + + /* Activate PassTickets in RACF, if not activated */ + SETROPTS CLASSACT(PTKTDATA) + SETROPTS RACLIST(PTKTDATA) + SETROPTS GENERIC(PTKTDATA) + + /* Define PassTicket for APPLID TSTAPPL without replay protection */ + RDEFINE PTKTDATA TSTAPPL SSIGNON(KEYMASKED(66f4f9331e095436)) + + APPLDATA('NO REPLAY PROTECTION') UACC(NONE) + SETROPTS RACLIST(PTKTDATA) REFRESH + + /* Allow user SDKBLD1 to generate PassTickets for TSTAPPL */ + RDEFINE PTKTDATA IRRPTAUTH.TSTAPPL.* UACC(NONE) + PERMIT IRRPTAUTH.TSTAPPL.* CL(PTKTDATA) ID(SDKBLD1) ACCESS(UPDATE) + SETROPTS RACLIST(PTKTDATA) REFRESH + From 361acd56eeee1c95ebdea8d3aed24187fdc48f80 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Wed, 11 Dec 2019 10:27:46 +0100 Subject: [PATCH 002/122] Download test artificats Signed-off-by: Petr Plavjanik --- passticket/test-programs/build.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/passticket/test-programs/build.sh b/passticket/test-programs/build.sh index 2d76a7486e..d8629526fa 100755 --- a/passticket/test-programs/build.sh +++ b/passticket/test-programs/build.sh @@ -7,3 +7,6 @@ zowe zos-files upload file-to-uss "pt_passwd.c" "$WORKSPACE_DIR/pt_passwd.c" zowe uss issue ssh "xlc -o pt_passwd pt_passwd.c; extattr +p pt_passwd" --cwd $WORKSPACE_DIR zowe uss issue ssh "javac -cp /usr/include/java_classes/IRRRacf.jar PtGen.java PtEval.java" --cwd $WORKSPACE_DIR zowe uss issue ssh "ls -E" --cwd $WORKSPACE_DIR +zowe zos-files download uss-file "$WORKSPACE_DIR/pt_passwd" -b -f "build/pt_passwd" +zowe zos-files download uss-file "$WORKSPACE_DIR/PtGen.class" -b -f "build/PtGen.class" +zowe zos-files download uss-file "$WORKSPACE_DIR/PtEval.class" -b -f "build/PtEval.class" From 81edbb8c5509f3b239015a163e8884a5fcaa6991 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Wed, 11 Dec 2019 10:28:11 +0100 Subject: [PATCH 003/122] TopSecret job Signed-off-by: Petr Plavjanik --- passticket/test-programs/tss.jcl | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 passticket/test-programs/tss.jcl diff --git a/passticket/test-programs/tss.jcl b/passticket/test-programs/tss.jcl new file mode 100644 index 0000000000..25d864a4f6 --- /dev/null +++ b/passticket/test-programs/tss.jcl @@ -0,0 +1,9 @@ +//TESTPTKT JOB MSGCLASS=X,MSGLEVEL=(1,1) +//STEP1 EXEC PGM=IKJEFT01 +//SYSTSPRT DD SYSOUT=* +//SYSTSIN DD * + /* Define PassTicket for APPLID TSTAPPL without replay protection */ + TSS ADDTO(NDT) PSTKAPPL(TSTAPPL) SESSKEY(66f4f9331e095436) SIGNMULTI + /* Allow user SDKBLD1 to generate PassTickets for TSTAPPL */ + TSS PERMIT(SDKBLD1) PTKTDATA(IRRPTAUTH.TSTAPPL.) ACCESS(READ,UPDATE) + TSS REFRESH From a7fe02da6daf3c4f334894772be85d803aa1302d Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Wed, 11 Dec 2019 10:30:14 +0100 Subject: [PATCH 004/122] Draft documentation Signed-off-by: Petr Plavjanik --- passticket/docs/api-mediation-passtickets.md | 126 +++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 passticket/docs/api-mediation-passtickets.md diff --git a/passticket/docs/api-mediation-passtickets.md b/passticket/docs/api-mediation-passtickets.md new file mode 100644 index 0000000000..56d4026ef3 --- /dev/null +++ b/passticket/docs/api-mediation-passtickets.md @@ -0,0 +1,126 @@ +# Enabling PassTicket creation for API Services that Accept PassTickets + +**Note**: This is a draft documentation that needs to be migrated to after the functionality is completed. + +- [Enabling PassTicket creation for API Services that Accept PassTickets](#enabling-passticket-creation-for-api-services-that-accept-passtickets) + - [How to Enable PassTicket Support](#how-to-enable-passticket-support) + - [Enable the Zowe started task user ID to generate PassTickets for the API service](#enable-the-zowe-started-task-user-id-to-generate-passtickets-for-the-api-service) + - [ACF2](#acf2) + - [RACF](#racf) + - [TopSecret](#topsecret) + - [API Services that Register Dynamically into API ML](#api-services-that-register-dynamically-into-api-ml) + - [API Services that Register Dynamically into API ML but not Provide Metadata](#api-services-that-register-dynamically-into-api-ml-but-not-provide-metadata) + - [API Services that are Defined using Static YAML Definition](#api-services-that-are-defined-using-static-yaml-definition) + - [What Developers Need to with API Services that Register Dynamically into API ML](#what-developers-need-to-with-api-services-that-register-dynamically-into-api-ml) + +The API Mediation Layer provides transparent authentication using PassTickets for API service that accept them. + +It means that API clients can use the Zowe JWT obtained from [authentication endpoint](https://docs.zowe.org/stable/extend/extend-apiml/api-mediation-security.html#authentication-for-api-ml-services) of the API gateway to access such API service even if the API service +does not support the JWT token. + +If the API client provide a valid Zowe JWT token, the API gateway generates a valid PassTicket and uses that to access the API service. +The user ID and password are provided in the Authorization header of the HTTP requests using the +[Basic authentication scheme](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#Basic_authentication_scheme). + +## How to Enable PassTicket Support + +You need to do following for each service that requires PassTickets: + +- Ensure that PassTickets are enabled for every user who might require access to API service + - Follow documentation of your API service that explains how to active support for PassTickets + - Remember the value of the APPLID of the API service +- Enable the Zowe started task user ID to generate PassTickets for the API service +- Enable the PassTicket support in the API gateway for your API service + +### Enable the Zowe started task user ID to generate PassTickets for the API service + +Following variables are used in the commands: + +- `` is the APPLID value that is used by the API service for PassTicket support (e.g. `OMVSAPPL`) + +- ``is Zowe started task user ID permission + +#### ACF2 + +Grant the Zowe started task user ID permission to generate PassTickets for users of that API service. For example: + +```txt +ACF +SET RESOURCE(PTK) +RECKEY IRRPTAUTH ADD(.- UID() SERVICE(UPDATE,READ) ALLOW) +F ACF2,REBUILD(PTK),CLASS(P) +END +``` + +#### RACF + +To enable PassTicket creation for API service users, define the profile `IRRPTAUTH..*` in the `PTKTDATA` class and set the universal access authority to NONE and grant the Zowe started task user ID permission to generate PassTickets for users of that API service. For example: + +```txt +RDEFINE PTKTDATA IRRPTAUTH..* UACC(NONE) +PERMIT IRRPTAUTH..* CL(PTKTDATA) ID() ACCESS(UPDATE) +SETROPTS RACLIST(PTKTDATA) REFRESH +``` + +#### TopSecret + +Grant the Zowe started task user ID permission to generate PassTickets for users of that API service. For example: + +```txt +TSS PERMIT() PTKTDATA(IRRPTAUTH..) ACCESS(READ,UPDATE) +TSS REFRESH +``` + +### API Services that Register Dynamically into API ML + +The API services that support Zowe API Mediation Layer and use dynamic registration into Discovery Service provide metadata +that enable PassTicket support. + +As a user of the API you are not require to do anything in this case. All required information is provided by the + +### API Services that Register Dynamically into API ML but not Provide Metadata + +Some services that can use PassTickets do not provide the metadata yet. For such service you can provide them +extenally in the same files as for the static YAML definitons. + +Add following section to a YAML file with static definition. + +```yaml +additionalServiceMetadata: + : + authenticationScheme: httpBasicPassTicket + applid: +``` + +`` is the service ID of the service where you want to add metadata. + +### API Services that are Defined using Static YAML Definition + +Add the following metadata to the same level as the `serviceId`, for example: + +```yaml + - serviceId: ... + authenticationScheme: httpBasicPassTicket + applid: TSTAPPL +``` + +The fields are explained below. + +## What Developers Need to with API Services that Register Dynamically into API ML + +As a developer of this application, you need to provide additional metadata. +This metadata tell API gateway that it needs to use PassTickets and how to generate them. + +```yaml +authenticationScheme: httpBasicPassTicket +applid: +``` + +`httpBasicPassTicket` is the value that means that HTTP Basic authentication schmeme is used with PassTickets. + +`` if the APPLID value that is used by the API service for PassTicket support (e.g. `OMVSAPPL`). + +The other values of `authenticationScheme` that supported: + +- `bypass` (default) - API gateway does not modify authentication headers for the API service. +- `zoweJwt` - The Zowe JWT token is expected. API gateway does not modify but can process it. From 877810043ea698b9e42cdee42f73fd9f45b824af Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Wed, 11 Dec 2019 15:29:43 +0100 Subject: [PATCH 005/122] Updated PassTicket documentation Signed-off-by: Petr Plavjanik --- passticket/docs/api-mediation-passtickets.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/passticket/docs/api-mediation-passtickets.md b/passticket/docs/api-mediation-passtickets.md index 56d4026ef3..34c37a27aa 100644 --- a/passticket/docs/api-mediation-passtickets.md +++ b/passticket/docs/api-mediation-passtickets.md @@ -88,8 +88,9 @@ Add following section to a YAML file with static definition. ```yaml additionalServiceMetadata: : - authenticationScheme: httpBasicPassTicket - applid: + authentication: + scheme: httpBasicPassTicket + applid: ``` `` is the service ID of the service where you want to add metadata. @@ -100,8 +101,9 @@ Add the following metadata to the same level as the `serviceId`, for example: ```yaml - serviceId: ... - authenticationScheme: httpBasicPassTicket - applid: TSTAPPL + authentication: + scheme: httpBasicPassTicket + applid: TSTAPPL ``` The fields are explained below. @@ -112,15 +114,16 @@ As a developer of this application, you need to provide additional metadata. This metadata tell API gateway that it needs to use PassTickets and how to generate them. ```yaml -authenticationScheme: httpBasicPassTicket -applid: +authentication: + scheme: httpBasicPassTicket + applid: ``` -`httpBasicPassTicket` is the value that means that HTTP Basic authentication schmeme is used with PassTickets. +`httpBasicPassTicket` is the value that means that HTTP Basic authentication scheme is used with PassTickets. `` if the APPLID value that is used by the API service for PassTicket support (e.g. `OMVSAPPL`). -The other values of `authenticationScheme` that supported: +The other values of `authentication.scheme` that are supported: - `bypass` (default) - API gateway does not modify authentication headers for the API service. - `zoweJwt` - The Zowe JWT token is expected. API gateway does not modify but can process it. From 526e6dafe54af490d224465f65b90351fe3189fe Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Wed, 11 Dec 2019 15:31:43 +0100 Subject: [PATCH 006/122] US634092 - Support new authentication metadata in the static API definitions Signed-off-by: Petr Plavjanik --- .../constants/EurekaMetadataDefinition.java | 3 ++ .../discovery/staticdef/Authentication.java | 20 +++++++++++ .../staticdef/AuthenticationScheme.java | 36 +++++++++++++++++++ .../ca/mfaas/discovery/staticdef/Service.java | 1 + .../staticdef/ServiceDefinitionProcessor.java | 10 ++++++ .../ServiceDefinitionProcessorTest.java | 19 ++++++++++ 6 files changed, 89 insertions(+) create mode 100644 discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/Authentication.java create mode 100644 discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/AuthenticationScheme.java diff --git a/common-service-core/src/main/java/com/ca/mfaas/constants/EurekaMetadataDefinition.java b/common-service-core/src/main/java/com/ca/mfaas/constants/EurekaMetadataDefinition.java index 162e77a84c..b608f8b2fc 100644 --- a/common-service-core/src/main/java/com/ca/mfaas/constants/EurekaMetadataDefinition.java +++ b/common-service-core/src/main/java/com/ca/mfaas/constants/EurekaMetadataDefinition.java @@ -38,6 +38,9 @@ private EurekaMetadataDefinition() { public static final String API_INFO_SWAGGER_URL = "swaggerUrl"; public static final String API_INFO_DOCUMENTATION_URL = "documentationUrl"; + public static final String AUTHENTICATION_SCHEME = "apiml.authentication.scheme"; + public static final String AUTHENTICATION_APPLID = "apiml.authentication.applid"; + //v1 public static final String ROUTES_V1 = "routed-services"; public static final String ROUTES_GATEWAY_URL_V1 = "gateway-url"; diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/Authentication.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/Authentication.java new file mode 100644 index 0000000000..1f031207c3 --- /dev/null +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/Authentication.java @@ -0,0 +1,20 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.discovery.staticdef; + +import lombok.Data; + +/** + * Information about expected authentication scheme and APPLID for PassTickets generation. + */ + @Data class Authentication { + private AuthenticationScheme scheme; + private String applid; +} diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/AuthenticationScheme.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/AuthenticationScheme.java new file mode 100644 index 0000000000..a85c15a243 --- /dev/null +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/AuthenticationScheme.java @@ -0,0 +1,36 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package com.ca.mfaas.discovery.staticdef; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Getter; + +@Getter +public enum AuthenticationScheme { + @JsonProperty("bypass") + BYPASS("bypass"), + @JsonProperty("zoweJwt") + ZOWE_JWT("zoweJwt"), + @JsonProperty("httpBasicPassTicket") + HTTP_BASIC_PASSTICKET("httpBasicPassTicket"); + + private final String scheme; + + AuthenticationScheme(String scheme) { + this.scheme = scheme; + } + + @Override + public String toString() { + return scheme; + } +} diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/Service.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/Service.java index ce767a80c8..e609f13fca 100644 --- a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/Service.java +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/Service.java @@ -33,4 +33,5 @@ @JsonAlias({"routedServices"}) private List routes; private List apiInfo; + private Authentication authentication; } diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/ServiceDefinitionProcessor.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/ServiceDefinitionProcessor.java index 604bb439ef..fec1ea2527 100644 --- a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/ServiceDefinitionProcessor.java +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/ServiceDefinitionProcessor.java @@ -300,6 +300,16 @@ private Map createMetadata(Service service, URL url, CatalogUiTi } } + if (service.getAuthentication() != null) { + AuthenticationScheme scheme = service.getAuthentication().getScheme(); + if (service.getAuthentication().getScheme() != null) { + mt.put(AUTHENTICATION_SCHEME, service.getAuthentication().getScheme().toString()); + } + if (service.getAuthentication().getApplid() != null) { + mt.put(AUTHENTICATION_APPLID, service.getAuthentication().getApplid()); + } + } + return mt; } } diff --git a/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/ServiceDefinitionProcessorTest.java b/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/ServiceDefinitionProcessorTest.java index acacb4357d..f953ea00ff 100644 --- a/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/ServiceDefinitionProcessorTest.java +++ b/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/ServiceDefinitionProcessorTest.java @@ -495,5 +495,24 @@ public void testFindServicesWithSecondEmptyDirectory() { assertThat(instances.size(), is(1)); } + + @Test + public void testProcessServicesDataWithAuthenticationMetadata() { + ServiceDefinitionProcessor serviceDefinitionProcessor = new ServiceDefinitionProcessor(); + String routedServiceYaml = "services:\n" + + " - serviceId: casamplerestapiservice\n" + + " instanceBaseUrls:\n" + + " - https://localhost:10019/casamplerestapiservice/\n" + + " authentication:\n" + + " scheme: httpBasicPassTicket\n" + + " applid: TSTAPPL\n"; + ServiceDefinitionProcessor.ProcessServicesDataResult result = serviceDefinitionProcessor + .processServicesData(Collections.singletonMap("test", routedServiceYaml)); + assertTrue(result.getErrors().isEmpty()); + List instances = result.getInstances(); + assertEquals(1, instances.size()); + assertEquals("httpBasicPassTicket", instances.get(0).getMetadata().get(AUTHENTICATION_SCHEME)); + assertEquals("TSTAPPL", instances.get(0).getMetadata().get(AUTHENTICATION_APPLID)); + } } From 9d42d819884d2e46cfb01705bd5bedddd14b0795 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Wed, 11 Dec 2019 16:07:21 +0100 Subject: [PATCH 007/122] Test for invalid authentication scheme values Signed-off-by: Petr Plavjanik --- .../staticdef/AuthenticationScheme.java | 7 ++- .../ServiceDefinitionProcessorTest.java | 46 ++++++++++++++++--- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/AuthenticationScheme.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/AuthenticationScheme.java index a85c15a243..e7381a1334 100644 --- a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/AuthenticationScheme.java +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/AuthenticationScheme.java @@ -18,10 +18,15 @@ public enum AuthenticationScheme { @JsonProperty("bypass") BYPASS("bypass"), + @JsonProperty("zoweJwt") ZOWE_JWT("zoweJwt"), + @JsonProperty("httpBasicPassTicket") - HTTP_BASIC_PASSTICKET("httpBasicPassTicket"); + HTTP_BASIC_PASSTICKET("httpBasicPassTicket"), + + @JsonProperty("zosmf") + ZOSMF("zosmf"); private final String scheme; diff --git a/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/ServiceDefinitionProcessorTest.java b/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/ServiceDefinitionProcessorTest.java index f953ea00ff..0278c724e4 100644 --- a/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/ServiceDefinitionProcessorTest.java +++ b/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/ServiceDefinitionProcessorTest.java @@ -9,19 +9,37 @@ */ package com.ca.mfaas.discovery.staticdef; -import com.netflix.appinfo.InstanceInfo; -import org.junit.Test; +import static com.ca.mfaas.constants.EurekaMetadataDefinition.API_INFO; +import static com.ca.mfaas.constants.EurekaMetadataDefinition.API_INFO_SWAGGER_URL; +import static com.ca.mfaas.constants.EurekaMetadataDefinition.API_INFO_VERSION; +import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_APPLID; +import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_SCHEME; +import static com.ca.mfaas.constants.EurekaMetadataDefinition.CATALOG_ID; +import static com.ca.mfaas.constants.EurekaMetadataDefinition.CATALOG_TITLE; +import static com.ca.mfaas.constants.EurekaMetadataDefinition.CATALOG_VERSION; +import static com.ca.mfaas.constants.EurekaMetadataDefinition.ROUTES; +import static com.ca.mfaas.constants.EurekaMetadataDefinition.ROUTES_GATEWAY_URL; +import static com.ca.mfaas.constants.EurekaMetadataDefinition.ROUTES_SERVICE_URL; +import static com.ca.mfaas.constants.EurekaMetadataDefinition.SERVICE_DESCRIPTION; +import static com.ca.mfaas.constants.EurekaMetadataDefinition.SERVICE_TITLE; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; import java.net.URISyntaxException; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import static com.ca.mfaas.constants.EurekaMetadataDefinition.*; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.*; +import com.netflix.appinfo.InstanceInfo; + +import org.junit.Test; public class ServiceDefinitionProcessorTest { @@ -508,11 +526,27 @@ public void testProcessServicesDataWithAuthenticationMetadata() { " applid: TSTAPPL\n"; ServiceDefinitionProcessor.ProcessServicesDataResult result = serviceDefinitionProcessor .processServicesData(Collections.singletonMap("test", routedServiceYaml)); - assertTrue(result.getErrors().isEmpty()); + assertEquals(new ArrayList<>(), result.getErrors()); List instances = result.getInstances(); assertEquals(1, instances.size()); assertEquals("httpBasicPassTicket", instances.get(0).getMetadata().get(AUTHENTICATION_SCHEME)); assertEquals("TSTAPPL", instances.get(0).getMetadata().get(AUTHENTICATION_APPLID)); } + + @Test + public void testProcessServicesDataWithInvalidAuthenticationScheme() { + ServiceDefinitionProcessor serviceDefinitionProcessor = new ServiceDefinitionProcessor(); + String routedServiceYaml = "services:\n" + + " - serviceId: casamplerestapiservice\n" + + " instanceBaseUrls:\n" + + " - https://localhost:10019/casamplerestapiservice/\n" + + " authentication:\n" + + " scheme: bad\n"; + ServiceDefinitionProcessor.ProcessServicesDataResult result = serviceDefinitionProcessor + .processServicesData(Collections.singletonMap("test", routedServiceYaml)); + assertEquals(1, result.getErrors().size()); + List instances = result.getInstances(); + assertEquals(0, instances.size()); + } } From c711de256308a3739a9056478d5d0b28bd3b62ec Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Fri, 13 Dec 2019 13:28:33 +0100 Subject: [PATCH 008/122] Sample static service with PassTickets Signed-off-by: Petr Plavjanik --- config/local/api-defs/staticclient.yml | 12 +++++++++++- passticket/docs/api-mediation-passtickets.md | 6 +++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/config/local/api-defs/staticclient.yml b/config/local/api-defs/staticclient.yml index 273f7da6f4..1033f1876e 100644 --- a/config/local/api-defs/staticclient.yml +++ b/config/local/api-defs/staticclient.yml @@ -21,13 +21,16 @@ services: serviceRelativeUrl: / - gatewayUrl: ws/v1 serviceRelativeUrl: /ws - # List of APIs provided by the service (currenly only one is supported): + # List of APIs provided by the service (currently only one is supported): apiInfo: - apiId: org.zowe.discoverableclient gatewayUrl: api/v1 swaggerUrl: https://localhost:10012/discoverableclient/api-doc - serviceId: staticclient2 # unique lowercase ID of the service + authentication: + scheme: httpBasicPassTicket # This service expects credentials in HTTP basic scheme with a PassTicket + applid: TSTAPPL # APPLID to generate PassTickets for this service catalogUiTileId: static # ID of the API Catalog UI tile (visual grouping of the services) title: Staticaly Defined Service 2 # Title of the service in the API catalog description: Sample to demonstrate how to add an API service without Swagger documentation to API Catalog using a static YAML definition # Description of the service in the API catalog @@ -49,6 +52,13 @@ services: gatewayUrl: api/v1 version: 1.0.0 +# Proposal - Additional metadata that will be added to existing dynamically registered services: +additionalServiceMetadata: + - serviceId: staticclient # The staticclient service metadata will be extended + authentication: + scheme: httpBasicPassTicket # This service expects credentials in HTTP basic scheme with a PassTicket + applid: TSTAPPL # APPLID to generate PassTickets for this service + # List of tiles that can be used by services defined in the YAML file: catalogUiTiles: static: diff --git a/passticket/docs/api-mediation-passtickets.md b/passticket/docs/api-mediation-passtickets.md index 34c37a27aa..61d28e9305 100644 --- a/passticket/docs/api-mediation-passtickets.md +++ b/passticket/docs/api-mediation-passtickets.md @@ -9,7 +9,7 @@ - [RACF](#racf) - [TopSecret](#topsecret) - [API Services that Register Dynamically into API ML](#api-services-that-register-dynamically-into-api-ml) - - [API Services that Register Dynamically into API ML but not Provide Metadata](#api-services-that-register-dynamically-into-api-ml-but-not-provide-metadata) + - [API Services that Register Dynamically into API ML but do not Provide Metadata](#api-services-that-register-dynamically-into-api-ml-but-do-not-provide-metadata) - [API Services that are Defined using Static YAML Definition](#api-services-that-are-defined-using-static-yaml-definition) - [What Developers Need to with API Services that Register Dynamically into API ML](#what-developers-need-to-with-api-services-that-register-dynamically-into-api-ml) @@ -76,9 +76,9 @@ TSS REFRESH The API services that support Zowe API Mediation Layer and use dynamic registration into Discovery Service provide metadata that enable PassTicket support. -As a user of the API you are not require to do anything in this case. All required information is provided by the +As a user of the API you are not require to do anything in this case. All required information is provided by the API service automatically. -### API Services that Register Dynamically into API ML but not Provide Metadata +### API Services that Register Dynamically into API ML but do not Provide Metadata Some services that can use PassTickets do not provide the metadata yet. For such service you can provide them extenally in the same files as for the static YAML definitons. From 4ea25933a3718a24d5d0cc29ea4ed6e84a28ead5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Tue, 17 Dec 2019 10:50:39 +0100 Subject: [PATCH 009/122] Dynamic proxy and using IRRPassTicket service, static additional data and update on service register, refactor of creating static services --- .../apiml/security/service/IRRPassTicket.java | 37 ++ .../security/service/PassTicketService.java | 61 ++++ .../service/PassTicketServiceTest.java | 71 ++++ .../com/ca/mfaas/message/log/ApimlLogger.java | 5 +- .../mfaas/util/ClassOrDefaultProxyUtils.java | 188 +++++++++++ .../java/com/ca/mfaas/util/ObjectUtil.java | 35 ++ .../util/ClassOrDefaultProxyUtilsTest.java | 134 ++++++++ .../com/ca/mfaas/util/ObjectUtilTest.java | 31 +- config/local/api-defs/staticclient.yml | 1 + .../EurekaInstanceRegisteredListener.java | 22 +- .../metadata/MetadataDefaultsService.java | 58 ++++ .../mfaas/discovery/staticdef/Definition.java | 4 +- .../staticdef/ServiceDefinitionProcessor.java | 315 ++++++++++-------- .../discovery/staticdef/ServiceOverride.java | 41 +++ .../staticdef/ServiceOverrideData.java | 24 ++ .../staticdef/StaticApiRestController.java | 5 +- .../staticdef/StaticRegistrationResult.java | 23 ++ .../StaticServicesRegistrationService.java | 29 +- .../EurekaInstanceRegisteredListenerTest.java | 60 ++++ .../metadata/MetadataDefaultsServiceTest.java | 118 +++++++ .../ServiceDefinitionProcessorTest.java | 251 +++++++++----- .../StaticApiRestControllerTest.java | 7 +- ...StaticServicesRegistrationServiceTest.java | 71 ++-- .../test/resources/api-defs/staticclient.yml | 33 ++ 24 files changed, 1357 insertions(+), 267 deletions(-) create mode 100644 apiml-security-common/src/main/java/com/ca/apiml/security/service/IRRPassTicket.java create mode 100644 apiml-security-common/src/main/java/com/ca/apiml/security/service/PassTicketService.java create mode 100644 apiml-security-common/src/test/java/com/ca/apiml/security/service/PassTicketServiceTest.java create mode 100644 common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java create mode 100644 common-service-core/src/test/java/com/ca/mfaas/util/ClassOrDefaultProxyUtilsTest.java create mode 100644 discovery-service/src/main/java/com/ca/mfaas/discovery/metadata/MetadataDefaultsService.java create mode 100644 discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/ServiceOverride.java create mode 100644 discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/ServiceOverrideData.java create mode 100644 discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/StaticRegistrationResult.java create mode 100644 discovery-service/src/test/java/com/ca/mfaas/discovery/EurekaInstanceRegisteredListenerTest.java create mode 100644 discovery-service/src/test/java/com/ca/mfaas/discovery/metadata/MetadataDefaultsServiceTest.java diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/service/IRRPassTicket.java b/apiml-security-common/src/main/java/com/ca/apiml/security/service/IRRPassTicket.java new file mode 100644 index 0000000000..6787439c58 --- /dev/null +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/service/IRRPassTicket.java @@ -0,0 +1,37 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.apiml.security.service; + +/** + * Interface covering com.ibm.eserver.zos.racf.IRRPassTicket class + */ +public interface IRRPassTicket { + + /** + * @param userId User for whom a PassTicket operation is performed. + * @param applId The Application name for this PassTicket operation. The same Application must be specified when calling the evaluate method. + * @param passTicket If successful, a PassTicket is returned. + * @throws IRRPassTicketGenerationException (only on mainframe) if an error occurs during the generation of PassTicket. The IRRPassTicketGeneration exception will contain the RACF return codes which identify the problem. + * @throws java.lang.IllegalStateException when an internal occurs. + * @throws java.lang.IllegalArgumentException when a NULL or invalid length parameter is passed. + */ + public void evaluate(String userId, String applId, String passTicket); + + /** + * @param userId User for whom a PassTicket operation is performed. + * @param applId The Application name for this PassTicket operation. The same Application must be specified when calling the generate method. + * @return The PassTicket to evaluate for this userid and application + * @throws IRRPassTicketEvaluationException - (only on mainframe) When an error occurs during the evaluation of PassTicket. This can mean that the PassTicket is invalid, expired or has been used previously. This can also indicate that the user does not have the proper RACF authority to perform a PassTicket evaluation, or may indicate an internal error. The IRRPassTicketEvaluation exception will contain the RACF return codes which identify the problem. + * @throws java.lang.IllegalStateException - when an internal error occurs + * @throws java.lang.IllegalArgumentException - when a NULL or invalid length argument is passed P1C- AMR: Removed RETURN Parameter that existed here. This caused a Javadoc error in Java8 compilation. There is no return parameter here. + */ + public String generate(String userId, String applId); + +} diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/service/PassTicketService.java b/apiml-security-common/src/main/java/com/ca/apiml/security/service/PassTicketService.java new file mode 100644 index 0000000000..e595fc687f --- /dev/null +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/service/PassTicketService.java @@ -0,0 +1,61 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.apiml.security.service; + +import com.ca.mfaas.util.ClassOrDefaultProxyUtils; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; + +/** + * This method allow to get a passticket from RAC. + */ +@Service +public class PassTicketService { + + private IRRPassTicket irrPassTicket; + + @PostConstruct + public void init() { + this.irrPassTicket = ClassOrDefaultProxyUtils.createProxy( + IRRPassTicket.class, + "com.ibm.eserver.zos.racf.IRRPassTicket", + () -> new IRRPassTicket() { + @Override + public void evaluate(String userId, String applId, String passTicket) { + if (userId == null) throw new IllegalArgumentException("Parameter userId is empty"); + if (applId == null) throw new IllegalArgumentException("Parameter applId is empty"); + if (passTicket == null) throw new IllegalArgumentException("Parameter passTicket is empty"); + + throw new IllegalStateException("This implementation only for testing purpose"); + } + + @Override + public String generate(String userId, String applId) { + return null; + } + } + ); + } + + public void evaluate(String userId, String applId, String passTicket) { + irrPassTicket.evaluate(userId, applId, passTicket); + } + + public String generate(String userId, String applId) { + return irrPassTicket.generate(userId, applId); + } + + public boolean isUsingRacImplementation() { + ClassOrDefaultProxyUtils.ClassOrDefaultProxyState stateInterface = (ClassOrDefaultProxyUtils.ClassOrDefaultProxyState) irrPassTicket; + return stateInterface.isUsingBaseImplementation(); + } + +} diff --git a/apiml-security-common/src/test/java/com/ca/apiml/security/service/PassTicketServiceTest.java b/apiml-security-common/src/test/java/com/ca/apiml/security/service/PassTicketServiceTest.java new file mode 100644 index 0000000000..6af3ad8304 --- /dev/null +++ b/apiml-security-common/src/test/java/com/ca/apiml/security/service/PassTicketServiceTest.java @@ -0,0 +1,71 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.apiml.security.service; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@RunWith(SpringRunner.class) +@ContextConfiguration +public class PassTicketServiceTest { + + @Autowired + private PassTicketService passTicketService; + + private static String evaluated; + + @Test + @Order(1) + public void testInit() { + assertNotNull(passTicketService); + assertNotNull(ReflectionTestUtils.getField(passTicketService, "irrPassTicket")); + ReflectionTestUtils.setField(passTicketService, "irrPassTicket", new IRRPassTicket() { + @Override + public void evaluate(String userId, String applId, String passTicket) { + evaluated = userId + "-" + applId + "-" + passTicket; + } + + @Override + public String generate(String userId, String applId) { + return userId + "-" + applId; + } + }); + } + + @Test + @Order(2) + public void testCalledMethod() { + evaluated = null; + passTicketService.evaluate("userId", "applId", "passticket"); + assertEquals("userId-applId-passticket", evaluated); + passTicketService.evaluate("1", "2", "3"); + assertEquals("1-2-3", evaluated); + + assertEquals("userId-applId", passTicketService.generate("userId", "applId")); + assertEquals("1-2", passTicketService.generate("1", "2")); + } + + @Configuration + @ComponentScan(basePackageClasses = {PassTicketService.class}) + public static class SpringConfig { + + } + +} diff --git a/common-service-core/src/main/java/com/ca/mfaas/message/log/ApimlLogger.java b/common-service-core/src/main/java/com/ca/mfaas/message/log/ApimlLogger.java index bbc0cee1bd..f7f14fea9d 100644 --- a/common-service-core/src/main/java/com/ca/mfaas/message/log/ApimlLogger.java +++ b/common-service-core/src/main/java/com/ca/mfaas/message/log/ApimlLogger.java @@ -65,14 +65,17 @@ public static ApimlLogger empty() { * @param key of the message * @param parameters for message */ - public void log(String key, Object... parameters) { + public Message log(String key, Object... parameters) { ObjectUtil.requireNotNull(key, "key can't be null"); ObjectUtil.requireNotNull(parameters, "parameters can't be null"); if (messageService != null) { Message message = messageService.createMessage(key, parameters); log(message); + return message; } + + return null; } /** diff --git a/common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java b/common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java new file mode 100644 index 0000000000..11353788fd --- /dev/null +++ b/common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java @@ -0,0 +1,188 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.util; + +import lombok.AllArgsConstructor; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +/** + * Purpose of this library of methods is to support multiple implementation and serve them as under one interface. + * + * It could be helpful if implementation is available only on specific environment (ie. libraries in IBM JVM, which + * cannot be distributed with project.). It is necessary to generate new interface, which cover both implementation + * (dummy for local purpose and original, represented as fully class name). This util will create implementation by + * class name or create a local (dummy) implementation. Outside this library it will be used and programmer don't + * need test if class exists or not. + * + * Created proxy offer also interface @link #MethodInvocationHandler to check state of created proxy. This is the + * reason exclude method names getImplementationClass and isUsingBaseImplementation from proxied object. Any object + * cannot have methods with the names without attributes. In case of conflict methods for checking state of proxy + * has higher priority. + * + * How to use that: + * 1. create interface A for proxied object (a subset of methods is enough) + * 2. create dummy implementation B (must implement the interface) + * 3. create proxy object + * + * A i=ClassOrDefaultProxyUtils.createProxy(A.class, "", () -> new B()); + * + * 4. you can test if implementation is dummy or not + * + * if (!((ClassOrDefaultProxyUtils.ClassOrDefaultProxyState) i).isUsingBaseImplementation()) { + * log.error("The searched class was not found, use " + + * ((ClassOrDefaultProxyUtils.ClassOrDefaultProxyState) i).isUsingBaseImplementation() + + * "instanceof"); + * } + * + */ +@Slf4j +public final class ClassOrDefaultProxyUtils { + + /** + * Create a proxy, which implement interfaceClass and ClassOrDefaultProxyState. This proxy will call object created + * for defaultImplementation class. If this object is not available it will call defaultImplementation instance of. + * Both implementationClassName and defaultImplementation have to have default constructor to be created. + * + * @param interfaceClass Interface of created proxy + * @param implementationClassName Full name of prefer implementation + * @param defaultImplementation Supplier to fetch implementation to use, if the prefer one is missing + * @param Common interface for prefer and default implementation + * @return Proxy object implementing interfaceClass and ClassOrDefaultProxyState + */ + public static T createProxy(Class interfaceClass, String implementationClassName, Supplier defaultImplementation) { + ObjectUtil.requireNotNull(interfaceClass, "interfaceClass can't be null"); + ObjectUtil.requireNotEmpty(implementationClassName, "implementationClassName can't be empty"); + ObjectUtil.requireNotNull(defaultImplementation, "defaultImplementation can't be null"); + + try { + final Class implementationClazz = Class.forName(implementationClassName); + final Object implementation = implementationClazz.getDeclaredConstructor().newInstance(); + return makeProxy(interfaceClass, implementation, true); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { + log.warn("Implementation {} is not available, it will continue with default one {} : " + e.getLocalizedMessage(), implementationClassName, defaultImplementation); + } + + return makeProxy(interfaceClass, defaultImplementation.get(), false); + } + + private static T makeProxy(Class interfaceClass, Object implementation, boolean usingBaseImplementation) { + return (T) Proxy.newProxyInstance( + ClassOrDefaultProxyUtils.class.getClassLoader(), + new Class[] {interfaceClass, ClassOrDefaultProxyUtils.ClassOrDefaultProxyState.class}, + new MethodInvocationHandler(implementation, usingBaseImplementation)); + } + + /** + * Interface to check state of created proxy object + */ + public interface ClassOrDefaultProxyState { + + /** + * + * @return class which is now proxied. It could be one of implementationClassName or defaultImplementation + */ + public Class getImplementationClass(); + + /** + * + * @return true if proxy use the original class, false if is using default (dummy) class + */ + public boolean isUsingBaseImplementation(); + + } + + private static class MethodInvocationHandler implements InvocationHandler, ClassOrDefaultProxyState { + + private final Map mapping = new HashMap<>(); + + private final boolean usingBaseImplementation; + private final Object implementation; + + public MethodInvocationHandler(Object implementation, boolean usingBaseImplementation) { + this.usingBaseImplementation = usingBaseImplementation; + this.implementation = implementation; + + this.initMapping(); + } + + private void addMapping(Object target, Method caller, Method callee) { + final String key = ObjectUtil.getMethodIdentifier(caller); + final EndPoint endPoint = new EndPoint(target, callee); + mapping.put(key, endPoint); + } + + private void initMapping() { + // first map methods of target + Class clazz = implementation.getClass(); + while (true) { + for (final Method method : clazz.getDeclaredMethods()) { + addMapping(implementation, method, method); + } + + // the highest superclass (Object) was scanned, end the loop + if (clazz == Object.class) break; + + // check also superclass + clazz = clazz.getSuperclass(); + } + + // second check the state interface. It has higher priority, could rewrite previous mapping + for (final Method method : ClassOrDefaultProxyState.class.getDeclaredMethods()) { + addMapping(this, method, method); + } + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + final String methodKey = ObjectUtil.getMethodIdentifier(method); + final EndPoint endPoint = mapping.get(methodKey); + + if (endPoint == null) { + throw new NoSuchMethodException(String.format("Cannot found method %s", endPoint)); + } + + return endPoint.invoke(args); + } + + @Override + public Class getImplementationClass() { + return implementation.getClass(); + } + + @Override + public boolean isUsingBaseImplementation() { + return usingBaseImplementation; + } + + @Value + @AllArgsConstructor + private static final class EndPoint { + + private final Object target; + private final Method method; + + public Object invoke(Object[] args) throws InvocationTargetException, IllegalAccessException { + return method.invoke(target, args); + } + + } + + } + +} diff --git a/common-service-core/src/main/java/com/ca/mfaas/util/ObjectUtil.java b/common-service-core/src/main/java/com/ca/mfaas/util/ObjectUtil.java index 831da7843c..0d05ec7e66 100644 --- a/common-service-core/src/main/java/com/ca/mfaas/util/ObjectUtil.java +++ b/common-service-core/src/main/java/com/ca/mfaas/util/ObjectUtil.java @@ -11,7 +11,9 @@ import lombok.experimental.UtilityClass; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import java.lang.reflect.Method; import java.util.*; @Slf4j @@ -31,6 +33,19 @@ public static void requireNotNull(Object param, String message) { } } + /** + * Check whether the specified object reference is not empty (null or empty string) and + * throws a {@link IllegalArgumentException} if it is. + * + * @param param the object reference to check for empty + * @param message detail message to be used in the event + */ + public static void requireNotEmpty(String param, String message) { + if (StringUtils.isEmpty(param)) { + throw new IllegalArgumentException(message); + } + } + /** * * @return the class object, from which this function was called @@ -102,4 +117,24 @@ private static Map mergeMapsDeep(Map map1, Map map2) { } return map1; } + + /** + * Construct string describes name of method like () + * @param method Instance of method to make description + * @return String describing method with name and arguments types + */ + public static String getMethodIdentifier(Method method) { + final StringBuilder sb = new StringBuilder(); + sb.append(method.getName()); + sb.append('('); + + int i = 0; + for (final Class clazz : method.getParameterTypes()) { + if (i++ > 0) sb.append(','); + sb.append(clazz); + } + sb.append(')'); + return sb.toString(); + } + } diff --git a/common-service-core/src/test/java/com/ca/mfaas/util/ClassOrDefaultProxyUtilsTest.java b/common-service-core/src/test/java/com/ca/mfaas/util/ClassOrDefaultProxyUtilsTest.java new file mode 100644 index 0000000000..dc303f09aa --- /dev/null +++ b/common-service-core/src/test/java/com/ca/mfaas/util/ClassOrDefaultProxyUtilsTest.java @@ -0,0 +1,134 @@ +package com.ca.mfaas.util; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import static org.junit.Assert.*; + +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +@RunWith(JUnit4.class) +public class ClassOrDefaultProxyUtilsTest { + + private static String voidResponse; + + @Test + public void testBasics() { + TestInterface1 ti; + + // create foreign class (without interface implementation) by string + ti = ClassOrDefaultProxyUtils.createProxy(TestInterface1.class, TestImplementation1A.class.getName(), TestImplementation1B::new); + assertEquals("response_1a_1", ti.method1()); + assertEquals("response_1a_2_X_3", ti.method2("X", 3)); + assertTrue(((ClassOrDefaultProxyUtils.ClassOrDefaultProxyState) ti).isUsingBaseImplementation()); + assertEquals(TestImplementation1A.class, ((ClassOrDefaultProxyUtils.ClassOrDefaultProxyState) ti).getImplementationClass()); + + voidResponse = null; + ti.method3(); + assertEquals("response_1a_3", voidResponse); + + // foreign class is not known, use internal one + ti = ClassOrDefaultProxyUtils.createProxy(TestInterface1.class, "uknown", TestImplementation1B::new); + assertEquals("response_1b_1", ti.method1()); + assertEquals("response_1b_2_X_3", ti.method2("X", 3)); + assertFalse(((ClassOrDefaultProxyUtils.ClassOrDefaultProxyState) ti).isUsingBaseImplementation()); + assertEquals(TestImplementation1B.class, ((ClassOrDefaultProxyUtils.ClassOrDefaultProxyState) ti).getImplementationClass()); + + voidResponse = null; + ti.method3(); + assertEquals("response_1b_3", voidResponse); + } + + @Test + public void testOverride() { + TestInterface2 ti; + + // create foreign class (without interface implementation) by string + ti = ClassOrDefaultProxyUtils.createProxy(TestInterface2.class, TestImplementation2.class.getName(), TestImplementation2::new); + + // method1 is not overrided + assertEquals("response_2_1", ti.method1()); + + // getImplementationClass is override with base implementation + assertEquals(TestImplementation2.class, ((ClassOrDefaultProxyUtils.ClassOrDefaultProxyState) ti).getImplementationClass()); + } + + public interface TestInterface1Super { + + public String method1(); + + } + + public interface TestInterface1 extends TestInterface1Super { + + public String method2(String x, int y); + + public void method3(); + + } + + public interface TestInterface2 { + + public String method1(); + + public Class getImplementationClass(); + + } + + public static class TestImplementation1A { + + public String method1() { + return "response_1a_1"; + } + + public String method2(String x, int y) { + return "response_1a_2_" + x + "_" + y; + } + + public void method3() { + voidResponse = "response_1a_3"; + } + + } + + public static class TestImplementation1B implements TestInterface1 { + + @Override + public String method1() { + return "response_1b_1"; + } + + @Override + public String method2(String x, int y) { + return "response_1b_2_" + x + "_" + y; + } + + @Override + public void method3() { + voidResponse = "response_1b_3"; + } + + } + + public static class TestImplementation2 implements TestInterface2 { + + @Override + public String method1() { + return "response_2_1"; + } + + @Override + public Class getImplementationClass() { + return Object.class; + } + + } + +} diff --git a/common-service-core/src/test/java/com/ca/mfaas/util/ObjectUtilTest.java b/common-service-core/src/test/java/com/ca/mfaas/util/ObjectUtilTest.java index b331cfe452..e06bc9e066 100644 --- a/common-service-core/src/test/java/com/ca/mfaas/util/ObjectUtilTest.java +++ b/common-service-core/src/test/java/com/ca/mfaas/util/ObjectUtilTest.java @@ -19,7 +19,6 @@ import java.util.Map; import static org.junit.Assert.*; -import static org.junit.Assert.assertNotNull; public class ObjectUtilTest { @@ -34,6 +33,25 @@ public void testRequireNotNull() { ObjectUtil.requireNotNull(null, "Parameter can't be null"); } + @Test + public void testRequireNotEmpty() { + try { + ObjectUtil.requireNotEmpty(null, "Parameter can't be empty"); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Parameter can't be empty", e.getMessage()); + } + + try { + ObjectUtil.requireNotEmpty(new String(), "Parameter can't be empty"); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Parameter can't be empty", e.getMessage()); + } + + ObjectUtil.requireNotEmpty(" ", "Parameter can't be empty"); + } + @Test public void testGetThisClass() { Class aClass = ObjectUtil.getThisClass(); @@ -183,7 +201,6 @@ private Map getMap1() { return map1; } - @Test public void testMapMerge_PART_serviceid_ciphers() { Map defaultConfigPropertiesMap = getMap1(); @@ -199,4 +216,14 @@ public void testMapMerge_PART_serviceid_ciphers() { assertEquals("../keystore/localhost/localhost.keystore.p12", ((Map)map3.get("ssl")).get("trustStore")); assertEquals("password2", ((Map)map3.get("ssl")).get("trustStorePassword")); } + + private void testMethod1(String a, int b, Object c) { + } + + @Test + public void testGetMethodIdentifier() throws NoSuchMethodException { + assertEquals("testGetMethodIdentifier()", ObjectUtil.getMethodIdentifier(ObjectUtilTest.class.getDeclaredMethod("testGetMethodIdentifier"))); + assertEquals("testMethod1(class java.lang.String,int,class java.lang.Object)", ObjectUtil.getMethodIdentifier(ObjectUtilTest.class.getDeclaredMethod("testMethod1", String.class, int.class, Object.class))); + } + } diff --git a/config/local/api-defs/staticclient.yml b/config/local/api-defs/staticclient.yml index 1033f1876e..ba622a9aaa 100644 --- a/config/local/api-defs/staticclient.yml +++ b/config/local/api-defs/staticclient.yml @@ -55,6 +55,7 @@ services: # Proposal - Additional metadata that will be added to existing dynamically registered services: additionalServiceMetadata: - serviceId: staticclient # The staticclient service metadata will be extended + mode: UPDATE # How to update UPDATE=only missing, FORCE_UPDATE=update all set values authentication: scheme: httpBasicPassTicket # This service expects credentials in HTTP basic scheme with a PassTicket applid: TSTAPPL # APPLID to generate PassTickets for this service diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/EurekaInstanceRegisteredListener.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/EurekaInstanceRegisteredListener.java index 59e2b16cf8..f575046e9c 100644 --- a/discovery-service/src/main/java/com/ca/mfaas/discovery/EurekaInstanceRegisteredListener.java +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/EurekaInstanceRegisteredListener.java @@ -9,25 +9,45 @@ */ package com.ca.mfaas.discovery; +import com.ca.mfaas.discovery.metadata.MetadataDefaultsService; import com.ca.mfaas.discovery.metadata.MetadataTranslationService; +import com.netflix.appinfo.InstanceInfo; import lombok.RequiredArgsConstructor; import org.springframework.cloud.netflix.eureka.server.event.EurekaInstanceRegisteredEvent; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; +import java.util.Map; + /** * Called by Eureka when the service instance is registered */ @Component @RequiredArgsConstructor public class EurekaInstanceRegisteredListener { + private final MetadataTranslationService metadataTranslationService; + private final MetadataDefaultsService metadataDefaultsService; + + protected String getServiceId(String instanceId) { + final int startIndex = instanceId.indexOf(':'); + if (startIndex < 0) return null; + + final int endIndex = instanceId.indexOf(':', startIndex + 1); + if (endIndex < 0) return null; + + return instanceId.substring(startIndex + 1, endIndex); + } /** * Translates service instance Eureka metadata from older versions to the current version */ @EventListener public void listen(EurekaInstanceRegisteredEvent event) { - metadataTranslationService.translateMetadata(event.getInstanceInfo().getMetadata()); + final InstanceInfo instanceInfo = event.getInstanceInfo(); + final Map metadata = instanceInfo.getMetadata(); + + metadataTranslationService.translateMetadata(metadata); + metadataDefaultsService.updateMetadata(getServiceId(instanceInfo.getId()), metadata); } } diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/metadata/MetadataDefaultsService.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/metadata/MetadataDefaultsService.java new file mode 100644 index 0000000000..a3c5032dd0 --- /dev/null +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/metadata/MetadataDefaultsService.java @@ -0,0 +1,58 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.discovery.metadata; + +import com.ca.mfaas.discovery.staticdef.ServiceOverrideData; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Map; + +/** + * This bean will correct metadata of service. It is helpful, if service does not support all new values. In this case + * it is possible to define values in local static definition and metadata will be updated on registration. + * + * It could be also use to redefine those meta data. + */ +@Service +public class MetadataDefaultsService { + + /** + * collect default values for + */ + private Map additionalServiceMetadata = Collections.emptyMap(); + + public void updateMetadata(String serviceId, Map metadata) { + final ServiceOverrideData sod = additionalServiceMetadata.get(serviceId); + + if (sod != null) { + update(sod, metadata); + } + } + + private void update(ServiceOverrideData sod, Map metadata) { + switch (sod.getMode()) { + case FORCE_UPDATE: + metadata.putAll(sod.getMetadata()); + break; + case UPDATE: default: + for (final Map.Entry entry : sod.getMetadata().entrySet()) { + if (metadata.containsKey(entry.getKey())) continue; + metadata.put(entry.getKey(), entry.getValue()); + } + break; + } + } + + public void setAdditionalServiceMetadata(Map additionalServiceMetadata) { + this.additionalServiceMetadata = Collections.unmodifiableMap(additionalServiceMetadata); + } + +} diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/Definition.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/Definition.java index d84bbdd7dc..32fb1f8ae5 100644 --- a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/Definition.java +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/Definition.java @@ -18,7 +18,9 @@ * A wrapper for static definition file contents. * It used by Jackson object mapper. */ -@Data class Definition { +@Data +public class Definition { private List services; private Map catalogUiTiles; + private List additionalServiceMetadata; } diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/ServiceDefinitionProcessor.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/ServiceDefinitionProcessor.java index fec1ea2527..be71760cf7 100644 --- a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/ServiceDefinitionProcessor.java +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/ServiceDefinitionProcessor.java @@ -13,16 +13,17 @@ import com.ca.mfaas.eurekaservice.client.util.EurekaMetadataParser; import com.ca.mfaas.exception.MetadataValidationException; import com.ca.mfaas.exception.ServiceDefinitionException; -import com.ca.mfaas.util.UrlUtils; +import com.ca.mfaas.message.core.Message; import com.ca.mfaas.message.log.ApimlLogger; import com.ca.mfaas.product.logging.annotations.InjectApimlLogger; +import com.ca.mfaas.util.UrlUtils; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.netflix.appinfo.DataCenterInfo; import com.netflix.appinfo.InstanceInfo; import com.netflix.appinfo.LeaseInfo; -import lombok.Data; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; import org.springframework.stereotype.Component; import java.io.File; @@ -34,6 +35,7 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.util.*; +import java.util.stream.Collectors; import static com.ca.mfaas.constants.EurekaMetadataDefinition.*; @@ -53,10 +55,29 @@ public class ServiceDefinitionProcessor { private static final String DEFAULT_TILE_VERSION = "1.0.0"; - @Data - static class ProcessServicesDataResult { - private final List errors; - private final List instances; + private static final YAMLFactory YAML_FACTORY = new YAMLFactory(); + + protected List getFiles(StaticRegistrationResult context, String staticApiDefinitionsDirectories) { + if (StringUtils.isEmpty(staticApiDefinitionsDirectories)) { + log.info("No static definition directory defined"); + return Collections.emptyList(); + } + + final String[] directories = staticApiDefinitionsDirectories.split(";"); + return Arrays.stream(directories) + .filter(s -> !s.isEmpty()) + .map(File::new) + .filter(directory -> { + final boolean isDir = directory.isDirectory(); + if (isDir) { + log.debug("Found directory {}", directory.getPath()); + } else { + final Message msg = apimlLog.log("apiml.discovery.staticDefinitionsDirectoryNotValid", directory.getPath()); + context.getErrors().add(msg); + } + return isDir; + }) + .collect(Collectors.toList()); } /** @@ -65,126 +86,129 @@ static class ProcessServicesDataResult { * @param staticApiDefinitionsDirectories directories containing static definitions * @return list of instances */ - public List findServices(String staticApiDefinitionsDirectories) { - List instances = new ArrayList<>(); - - if (staticApiDefinitionsDirectories != null && !staticApiDefinitionsDirectories.isEmpty()) { - String[] directories = staticApiDefinitionsDirectories.split(";"); - Arrays.stream(directories) - .filter(s -> !s.isEmpty()) - .map(File::new) - .forEach(directory -> { - if (directory.isDirectory()) { - log.debug("Found directory {}", directory.getPath()); - instances.addAll(findServicesInDirectory(directory)); - } else { - apimlLog.log("apiml.discovery.staticDefinitionsDirectoryNotValid", directory.getPath()); - } - }); - } else { - log.info("No static definition directory defined"); - } - - return instances; - } - - private List findServicesInDirectory(File directory) { - log.info("Scanning directory with static services definition: " + directory); + public StaticRegistrationResult findStaticServicesData(String staticApiDefinitionsDirectories) { + final StaticRegistrationResult context = new StaticRegistrationResult(); + + final List directories = getFiles(context, staticApiDefinitionsDirectories); + for (final File directory : directories) { + log.info("Scanning directory with static services definition: " + directory); + final File[] files = directory.listFiles((dir, name) -> name.endsWith(".yml")); + + if (files == null) { + final Message msg = apimlLog.log("apiml.discovery.errorReadingStaticDefinitionFolder", directory.getAbsolutePath()); + context.getErrors().add(msg); + continue; + } - File[] ymlFiles = directory.listFiles((dir, name) -> name.endsWith(".yml")); - Map ymlSources = new HashMap<>(); + if (files.length == 0) { + log.info("No static service definition found in directory: {}", directory.getAbsolutePath()); + } - if (ymlFiles == null) { - apimlLog.log("apiml.discovery.errorReadingStaticDefinitionFolder", directory.getAbsolutePath()); - ymlFiles = new File[0]; - } else if (ymlFiles.length == 0) { - log.info("No static service definition found in directory: {}", directory.getAbsolutePath()); - } + for (final File file : files) { + final Definition definition = loadDefinition(context, file); + if (definition == null) continue; - for (File file : ymlFiles) { - log.info("Static API definition file: {}", file.getAbsolutePath()); - try { - ymlSources.put(file.getAbsolutePath(), new String(Files.readAllBytes(Paths.get(file.getAbsolutePath())))); - } catch (IOException e) { - apimlLog.log("apiml.discovery.errorParsingStaticDefinitionFile", file.getAbsolutePath()); + process(context, file.getAbsolutePath(), definition); } } - ProcessServicesDataResult result = processServicesData(ymlSources); - apimlLog.log("apiml.discovery.errorParsingStaticDefinitionData", result.getErrors()); - return result.getInstances(); + + return context; } - ProcessServicesDataResult processServicesData(Map ymlSources) { - List errors = new ArrayList<>(); - List instances = new ArrayList<>(); - ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); - for (Map.Entry ymlSource : ymlSources.entrySet()) { - processServiceDefinition(ymlSource.getKey(), ymlSource.getValue(), mapper, errors, instances); + protected Definition loadDefinition(StaticRegistrationResult context, File file) { + final String fileName = file.getAbsolutePath(); + log.info("Static API definition file: {}", fileName); + + final String content; + try { + content = new String(Files.readAllBytes(Paths.get(fileName))); + } catch (IOException e) { + final Message msg = apimlLog.log("apiml.discovery.errorParsingStaticDefinitionFile", fileName); + context.getErrors().add(msg); + return null; } - return new ProcessServicesDataResult(errors, instances); + + return loadDefinition(context, fileName, content); } - private void processServiceDefinition(String ymlFileName, String ymlData, - ObjectMapper mapper, - List errors, - List instances) { - List services = new ArrayList<>(); - Map tiles = new HashMap<>(); + protected Definition loadDefinition(StaticRegistrationResult context, String ymlFileName, String ymlData) { + final ObjectMapper mapper = new ObjectMapper(YAML_FACTORY); + try { - Definition def = mapper.readValue(ymlData, Definition.class); - services.addAll(def.getServices()); - if (def.getCatalogUiTiles() != null) { - tiles.putAll(def.getCatalogUiTiles()); - } + return mapper.readValue(ymlData, Definition.class); } catch (IOException e) { - errors.add(String.format("Error processing file %s - %s", ymlFileName, e.getMessage())); + context.getErrors().add(String.format("Error processing file %s - %s", ymlFileName, e.getMessage())); } - ProcessServicesDataResult result = createInstances(ymlFileName, services, tiles); - errors.addAll(result.getErrors()); - instances.addAll(result.getInstances()); + + return null; } - private ProcessServicesDataResult createInstances(String ymlFileName, - List services, - Map tiles) { - List instances = new ArrayList<>(); - List errors = new ArrayList<>(); - - for (Service service : services) { - try { - String serviceId = service.getServiceId(); - if (serviceId == null) { - throw new ServiceDefinitionException(String.format("ServiceId is not defined in the file '%s'. The instance will not be created.", ymlFileName)); - } + protected void process(StaticRegistrationResult context, String ymlFileName, Definition definition) { + if (definition == null) return; - if (service.getInstanceBaseUrls() == null) { - throw new ServiceDefinitionException(String.format("The instanceBaseUrls parameter of %s is not defined. The instance will not be created.", service.getServiceId())); + // process static services + Optional.ofNullable(definition.getServices()).ifPresent( + x -> { + final Map tiles = Optional.ofNullable(definition.getCatalogUiTiles()).orElse(Collections.emptyMap()); + x.forEach(y -> createInstances(context, ymlFileName, y, tiles)); + } + ); + + // process additional info (to override metadata of services) + if (definition.getAdditionalServiceMetadata() != null) { + for (final ServiceOverride so : definition.getAdditionalServiceMetadata()) { + final Map metadata = createMetadata(so, null, null); + final ServiceOverride.Mode mode = Optional.ofNullable(so.getMode()).orElse(ServiceOverride.Mode.UPDATE); + final ServiceOverrideData sod = new ServiceOverrideData(mode, metadata); + if (context.getAdditionalServiceMetadata().put(so.getServiceId(), sod) != null) { + context.getErrors().add(String.format("Additional service metadata of %s in processing file %s were replaced for duplicities", so.getServiceId(), ymlFileName)); } + } + } + } - CatalogUiTile tile = null; - if (service.getCatalogUiTileId() != null) { - tile = tiles.get(service.getCatalogUiTileId()); - if (tile == null) { - errors.add(String.format("Error processing file %s - The API Catalog UI tile ID %s is invalid. The service %s will not have API Catalog UI tile", ymlFileName, service.getCatalogUiTileId(), serviceId)); - } else { - tile.setId(service.getCatalogUiTileId()); - } - } + private CatalogUiTile getTile(StaticRegistrationResult context, String ymlFileName, Map tiles, Service service) { + if (service.getCatalogUiTileId() != null) { + final CatalogUiTile tile = tiles.get(service.getCatalogUiTileId()); + if (tile == null) { + context.getErrors().add(String.format("Error processing file %s - The API Catalog UI tile ID %s is invalid. The service %s will not have API Catalog UI tile", ymlFileName, service.getCatalogUiTileId(), service.getServiceId())); + } else { + tile.setId(service.getCatalogUiTileId()); + } + return tile; + } - for (String instanceBaseUrl : service.getInstanceBaseUrls()) { - buildInstanceInfo(instances, errors, service, tile, instanceBaseUrl); - } - } catch (ServiceDefinitionException e) { - errors.add(e.getMessage()); + return null; + } + + private List createInstances(StaticRegistrationResult context, String ymlFileName, Service service, Map tiles) { + try { + if (service.getServiceId() == null) { + throw new ServiceDefinitionException(String.format("ServiceId is not defined in the file '%s'. The instance will not be created.", ymlFileName)); + } + + if (service.getInstanceBaseUrls() == null) { + throw new ServiceDefinitionException(String.format("The instanceBaseUrls parameter of %s is not defined. The instance will not be created.", service.getServiceId())); } + + final CatalogUiTile tile = getTile(context, ymlFileName, tiles, service); + final List output = new ArrayList<>(service.getInstanceBaseUrls().size()); + for (final String instanceBaseUrl : service.getInstanceBaseUrls()) { + final InstanceInfo instanceInfo = buildInstanceInfo(context, service, tile, instanceBaseUrl); + if (instanceBaseUrl != null) output.add(instanceInfo); + } + + return output; + } catch (ServiceDefinitionException e) { + context.getErrors().add(e.getMessage()); } - return new ProcessServicesDataResult(errors, instances); + return Collections.emptyList(); } - private void buildInstanceInfo(List instances, - List errors, Service service, - CatalogUiTile tile, String instanceBaseUrl) throws ServiceDefinitionException { + private InstanceInfo buildInstanceInfo(StaticRegistrationResult context, + Service service, + CatalogUiTile tile, String instanceBaseUrl) throws ServiceDefinitionException { if (instanceBaseUrl == null || instanceBaseUrl.isEmpty()) { throw new ServiceDefinitionException(String.format("One of the instanceBaseUrl of %s is not defined. The instance will not be created.", service.getServiceId())); } @@ -193,9 +217,9 @@ private void buildInstanceInfo(List instances, try { URL url = new URL(instanceBaseUrl); if (url.getHost().isEmpty()) { - errors.add(String.format("The URL %s does not contain a hostname. The instance of %s will not be created", instanceBaseUrl, service.getServiceId())); + context.getErrors().add(String.format("The URL %s does not contain a hostname. The instance of %s will not be created", instanceBaseUrl, service.getServiceId())); } else if (url.getPort() == -1) { - errors.add(String.format("The URL %s does not contain a port number. The instance of %s will not be created", instanceBaseUrl, service.getServiceId())); + context.getErrors().add(String.format("The URL %s does not contain a port number. The instance of %s will not be created", instanceBaseUrl, service.getServiceId())); } else { InstanceInfo.Builder builder = InstanceInfo.Builder.newBuilder(); @@ -207,8 +231,13 @@ private void buildInstanceInfo(List instances, setPort(builder, service, instanceBaseUrl, url); log.info("Adding static instance {} for service ID {} mapped to URL {}", instanceId, serviceId, url); - instances.add(builder.build()); + + final InstanceInfo instance = builder.build(); + context.getInstances().add(instance); + return instance; } + + return null; } catch (MalformedURLException e) { throw new ServiceDefinitionException(String.format("The URL %s is malformed. The instance of %s will not be created: %s", instanceBaseUrl, serviceId, e.getMessage())); @@ -269,47 +298,65 @@ private void setPort(InstanceInfo.Builder builder, } } - private Map createMetadata(Service service, URL url, CatalogUiTile tile) { - Map mt = new HashMap<>(); + private void setMetadataRoutes(Map metadata, List routes, URL url) { + if (routes == null) return; - mt.put(VERSION, CURRENT_VERSION); - mt.put(SERVICE_TITLE, service.getTitle()); - mt.put(SERVICE_DESCRIPTION, service.getDescription()); + for (final Route rs : routes) { + String gatewayUrl = UrlUtils.trimSlashes(rs.getGatewayUrl()); + String key = gatewayUrl.replace("/", "-"); + metadata.put(String.format("%s.%s.%s", ROUTES, key, ROUTES_GATEWAY_URL), gatewayUrl); - if (service.getRoutes() != null) { - for (Route rs : service.getRoutes()) { - String gatewayUrl = UrlUtils.trimSlashes(rs.getGatewayUrl()); - String key = gatewayUrl.replace("/", "-"); + if (url != null) { String serviceUrl = url.getPath() + (rs.getServiceRelativeUrl() == null ? "" : rs.getServiceRelativeUrl()); - mt.put(String.format("%s.%s.%s", ROUTES, key, ROUTES_GATEWAY_URL), gatewayUrl); - mt.put(String.format("%s.%s.%s", ROUTES, key, ROUTES_SERVICE_URL), serviceUrl); + metadata.put(String.format("%s.%s.%s", ROUTES, key, ROUTES_SERVICE_URL), serviceUrl); } } + } - if (tile != null) { - mt.put(CATALOG_ID, tile.getId()); - mt.put(CATALOG_VERSION, DEFAULT_TILE_VERSION); - mt.put(CATALOG_TITLE, tile.getTitle()); - mt.put(CATALOG_DESCRIPTION, tile.getDescription()); + private void setMetadataTile(Map metadata, CatalogUiTile tile) { + if (tile == null) return; + + metadata.put(CATALOG_ID, tile.getId()); + metadata.put(CATALOG_VERSION, DEFAULT_TILE_VERSION); + metadata.put(CATALOG_TITLE, tile.getTitle()); + metadata.put(CATALOG_DESCRIPTION, tile.getDescription()); + } + + private void setMetadataAppInfo(Map metadata, List appInfoList, String serviceId) { + if (appInfoList == null) return; + + for (ApiInfo apiInfo : appInfoList) { + metadata.putAll(EurekaMetadataParser.generateMetadata(serviceId, apiInfo)); } + } - if (service.getApiInfo() != null) { - for (ApiInfo apiInfo : service.getApiInfo()) { - mt.putAll(EurekaMetadataParser.generateMetadata(service.getServiceId(), apiInfo)); - } + private void setMetadataAuthentication(Map metadata, Authentication authentication) { + if (authentication == null) return; + + final AuthenticationScheme scheme = authentication.getScheme(); + if (scheme != null) { + metadata.put(AUTHENTICATION_SCHEME, scheme.toString()); } - if (service.getAuthentication() != null) { - AuthenticationScheme scheme = service.getAuthentication().getScheme(); - if (service.getAuthentication().getScheme() != null) { - mt.put(AUTHENTICATION_SCHEME, service.getAuthentication().getScheme().toString()); - } - if (service.getAuthentication().getApplid() != null) { - mt.put(AUTHENTICATION_APPLID, service.getAuthentication().getApplid()); - } + final String applid = authentication.getApplid(); + if (applid != null) { + metadata.put(AUTHENTICATION_APPLID, applid); } + } + + private Map createMetadata(Service service, URL url, CatalogUiTile tile) { + final Map metadata = new HashMap<>(); + + metadata.put(VERSION, CURRENT_VERSION); + metadata.put(SERVICE_TITLE, service.getTitle()); + metadata.put(SERVICE_DESCRIPTION, service.getDescription()); + + setMetadataRoutes(metadata, service.getRoutes(), url); + setMetadataTile(metadata, tile); + setMetadataAppInfo(metadata, service.getApiInfo(), service.getServiceId()); + setMetadataAuthentication(metadata, service.getAuthentication()); - return mt; + return metadata; } } diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/ServiceOverride.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/ServiceOverride.java new file mode 100644 index 0000000000..c81d76b712 --- /dev/null +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/ServiceOverride.java @@ -0,0 +1,41 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.discovery.staticdef; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ServiceOverride extends Service { + + private Mode mode; + + public enum Mode { + + /** + * default mode + * + * This mode means, update all filled values in original, if original value is missing. If this value is + * filled do nothing + */ + UPDATE, + + /** + * This mode rewrite all filled values. It different original values are filled or not. + */ + FORCE_UPDATE + + } + +} diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/ServiceOverrideData.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/ServiceOverrideData.java new file mode 100644 index 0000000000..cb752b6d81 --- /dev/null +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/ServiceOverrideData.java @@ -0,0 +1,24 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.discovery.staticdef; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.Map; + +@Data +@AllArgsConstructor +public class ServiceOverrideData { + + private ServiceOverride.Mode mode; + private Map metadata; + +} diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/StaticApiRestController.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/StaticApiRestController.java index a5abb390e2..fd8eb3f6f5 100644 --- a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/StaticApiRestController.java +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/StaticApiRestController.java @@ -34,8 +34,7 @@ public List list() { } @PostMapping - public List reload() { - registrationService.reloadServices(); - return registrationService.getStaticInstances(); + public StaticRegistrationResult reload() { + return registrationService.reloadServices(); } } diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/StaticRegistrationResult.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/StaticRegistrationResult.java new file mode 100644 index 0000000000..a643c3f34c --- /dev/null +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/StaticRegistrationResult.java @@ -0,0 +1,23 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.discovery.staticdef; + +import com.netflix.appinfo.InstanceInfo; +import lombok.Data; + +import java.util.*; + +@Data +public class StaticRegistrationResult { + private final List errors = new LinkedList<>(); + private final List instances = new LinkedList<>(); + private final Map additionalServiceMetadata = new HashMap<>(); + private final List registredServices = new LinkedList<>(); +} diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/StaticServicesRegistrationService.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/StaticServicesRegistrationService.java index 6cdc0b1db1..c612247e00 100644 --- a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/StaticServicesRegistrationService.java +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/StaticServicesRegistrationService.java @@ -10,6 +10,7 @@ package com.ca.mfaas.discovery.staticdef; import com.ca.mfaas.discovery.EurekaRegistryAvailableListener; +import com.ca.mfaas.discovery.metadata.MetadataDefaultsService; import com.netflix.appinfo.InstanceInfo; import com.netflix.appinfo.LeaseInfo; import com.netflix.eureka.EurekaServerContext; @@ -35,14 +36,16 @@ public class StaticServicesRegistrationService { private String staticApiDefinitionsDirectories; private final ServiceDefinitionProcessor serviceDefinitionProcessor; + private final MetadataDefaultsService metadataDefaultsService; private final List staticInstances = new CopyOnWriteArrayList<>(); private final Timer renewalTimer = new Timer(); @Autowired - public StaticServicesRegistrationService(ServiceDefinitionProcessor serviceDefinitionProcessor) { + public StaticServicesRegistrationService(ServiceDefinitionProcessor serviceDefinitionProcessor, MetadataDefaultsService metadataDefaultsService) { this.serviceDefinitionProcessor = serviceDefinitionProcessor; + this.metadataDefaultsService = metadataDefaultsService; } /** @@ -81,38 +84,42 @@ synchronized void renewInstances() { * Reloads all statically defined APIs in locations specified by configuration * by reading the definitions again. */ - public synchronized Set reloadServices() { + public synchronized StaticRegistrationResult reloadServices() { List oldStaticInstances = new ArrayList<>(staticInstances); staticInstances.clear(); - Set registeredIds = registerServices(staticApiDefinitionsDirectories); + StaticRegistrationResult result = registerServices(staticApiDefinitionsDirectories); PeerAwareInstanceRegistry registry = getRegistry(); for (InstanceInfo info: oldStaticInstances) { - if (!registeredIds.contains(info.getInstanceId())) { + if (!result.getRegistredServices().contains(info.getInstanceId())) { log.info("Instance {} is not defined in the new static API definitions. It will be removed", info.getInstanceId()); registry.cancel(info.getAppName(), info.getId(), false); } } - return registeredIds; + return result; } /** * Registers all statically defined APIs in a directory. */ - Set registerServices(String staticApiDefinitionsDirectories) { + StaticRegistrationResult registerServices(String staticApiDefinitionsDirectories) { PeerAwareInstanceRegistry registry = getRegistry(); - List instances = serviceDefinitionProcessor.findServices(staticApiDefinitionsDirectories); - Set registeredIds = new LinkedHashSet<>(); + StaticRegistrationResult result = serviceDefinitionProcessor.findStaticServicesData(staticApiDefinitionsDirectories); - for (InstanceInfo instanceInfo : instances) { - registeredIds.add(instanceInfo.getInstanceId()); + // at first register service additional data, becase static could be also updated + final Map additionalServiceMetadata = result.getAdditionalServiceMetadata(); + metadataDefaultsService.setAdditionalServiceMetadata(additionalServiceMetadata); + + // register static services + for (InstanceInfo instanceInfo : result.getInstances()) { + result.getRegistredServices().add(instanceInfo.getInstanceId()); staticInstances.add(instanceInfo); registry.register(instanceInfo, false); } - return registeredIds; + return result; } private PeerAwareInstanceRegistry getRegistry() { diff --git a/discovery-service/src/test/java/com/ca/mfaas/discovery/EurekaInstanceRegisteredListenerTest.java b/discovery-service/src/test/java/com/ca/mfaas/discovery/EurekaInstanceRegisteredListenerTest.java new file mode 100644 index 0000000000..82f1f93dfa --- /dev/null +++ b/discovery-service/src/test/java/com/ca/mfaas/discovery/EurekaInstanceRegisteredListenerTest.java @@ -0,0 +1,60 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.discovery; + +import com.ca.mfaas.discovery.metadata.MetadataDefaultsService; +import com.ca.mfaas.discovery.metadata.MetadataTranslationService; +import com.netflix.appinfo.InstanceInfo; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.cloud.netflix.eureka.server.event.EurekaInstanceRegisteredEvent; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +public class EurekaInstanceRegisteredListenerTest { + + @Test + public void getServiceId() { + + MetadataTranslationService metadataTranslationService = Mockito.mock(MetadataTranslationService.class); + MetadataDefaultsService metadataDefaultsService = Mockito.mock(MetadataDefaultsService.class); + + EurekaInstanceRegisteredListener eirl = new EurekaInstanceRegisteredListener(metadataTranslationService, metadataDefaultsService); + + assertEquals("abc", eirl.getServiceId("123:abc:def:::::xyz")); + assertEquals("abc", eirl.getServiceId("123:abc:def")); + assertEquals("", eirl.getServiceId("123::def")); + assertEquals("", eirl.getServiceId("::")); + assertNull(eirl.getServiceId(":")); + assertNull(eirl.getServiceId("")); + assertNull(eirl.getServiceId("abc")); + + doAnswer( + x -> { + assertEquals("serviceName", x.getArgument(0)); + return null; + } + ).when(metadataDefaultsService).updateMetadata(anyString(), any()); + + InstanceInfo instanceInfo = mock(InstanceInfo.class); + when(instanceInfo.getId()).thenReturn("1:serviceName:2"); + EurekaInstanceRegisteredEvent event = mock(EurekaInstanceRegisteredEvent.class); + when(event.getInstanceInfo()).thenReturn(instanceInfo); + + eirl.listen(event); + + verify(metadataDefaultsService, times(1)).updateMetadata(anyString(), any()); + } + +} diff --git a/discovery-service/src/test/java/com/ca/mfaas/discovery/metadata/MetadataDefaultsServiceTest.java b/discovery-service/src/test/java/com/ca/mfaas/discovery/metadata/MetadataDefaultsServiceTest.java new file mode 100644 index 0000000000..22f6626e6c --- /dev/null +++ b/discovery-service/src/test/java/com/ca/mfaas/discovery/metadata/MetadataDefaultsServiceTest.java @@ -0,0 +1,118 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.discovery.metadata; + +import com.ca.mfaas.discovery.EurekaInstanceRegisteredListener; +import com.ca.mfaas.discovery.staticdef.ServiceDefinitionProcessor; +import com.ca.mfaas.discovery.staticdef.StaticRegistrationResult; +import com.ca.mfaas.discovery.staticdef.StaticServicesRegistrationService; +import com.netflix.appinfo.InstanceInfo; +import com.netflix.eureka.EurekaServerContext; +import com.netflix.eureka.EurekaServerContextHolder; +import com.netflix.eureka.registry.PeerAwareInstanceRegistry; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.cloud.netflix.eureka.server.event.EurekaInstanceRegisteredEvent; + +import java.io.File; +import java.net.URISyntaxException; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_APPLID; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class MetadataDefaultsServiceTest { + + @InjectMocks + private StaticServicesRegistrationService staticServicesRegistrationService; + + @Spy + @InjectMocks + private EurekaInstanceRegisteredListener eurekaInstanceRegisteredListener; + + @Spy + private MetadataTranslationService metadataTranslationService; + + @Spy + private MetadataDefaultsService metadataDefaultsService; + + @Spy + private ServiceDefinitionProcessorMock serviceDefinitionProcessor; + + private PeerAwareInstanceRegistry mockRegistry; + + @Before + public void setUp() { + mockRegistry = mock(PeerAwareInstanceRegistry.class); + doAnswer(x -> { + EurekaInstanceRegisteredEvent event = mock(EurekaInstanceRegisteredEvent.class); + when(event.getInstanceInfo()).thenReturn(x.getArgument(0)); + eurekaInstanceRegisteredListener.listen(event); + return mockRegistry; + }).when(mockRegistry).register(any(), anyBoolean()); + EurekaServerContext mockEurekaServerContext = mock(EurekaServerContext.class); + when(mockEurekaServerContext.getRegistry()).thenReturn(mockRegistry); + EurekaServerContextHolder.initialize(mockEurekaServerContext); + } + + @Test + public void testUpdating() { + serviceDefinitionProcessor.setLocation("api-defs"); + + staticServicesRegistrationService.reloadServices(); + Map map = staticServicesRegistrationService.getStaticInstances().stream() + .collect(Collectors.toMap(InstanceInfo::getId, Function.identity())); + + assertEquals( + "TSTAPPL4", + map.get("STATIC-localhost:toAddAuth:10012").getMetadata().get(AUTHENTICATION_APPLID) + ); + + assertEquals( + "TSTAPPL5", + map.get("STATIC-localhost:toReplaceAuth:10012").getMetadata().get(AUTHENTICATION_APPLID) + ); + + assertEquals( + "TSTAPPL3", + map.get("STATIC-localhost:nowFixedAuth:10012").getMetadata().get(AUTHENTICATION_APPLID) + ); + } + + class ServiceDefinitionProcessorMock extends ServiceDefinitionProcessor { + + private String location; + + public void setLocation(String location) { + this.location = location; + } + + protected List getFiles(StaticRegistrationResult context, String staticApiDefinitionsDirectories) { + try { + return Collections.singletonList(Paths.get(ClassLoader.getSystemResource(location).toURI()).toFile()); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + } + +} diff --git a/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/ServiceDefinitionProcessorTest.java b/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/ServiceDefinitionProcessorTest.java index 0278c724e4..0fe1017a8d 100644 --- a/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/ServiceDefinitionProcessorTest.java +++ b/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/ServiceDefinitionProcessorTest.java @@ -9,40 +9,38 @@ */ package com.ca.mfaas.discovery.staticdef; -import static com.ca.mfaas.constants.EurekaMetadataDefinition.API_INFO; -import static com.ca.mfaas.constants.EurekaMetadataDefinition.API_INFO_SWAGGER_URL; -import static com.ca.mfaas.constants.EurekaMetadataDefinition.API_INFO_VERSION; -import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_APPLID; -import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_SCHEME; -import static com.ca.mfaas.constants.EurekaMetadataDefinition.CATALOG_ID; -import static com.ca.mfaas.constants.EurekaMetadataDefinition.CATALOG_TITLE; -import static com.ca.mfaas.constants.EurekaMetadataDefinition.CATALOG_VERSION; -import static com.ca.mfaas.constants.EurekaMetadataDefinition.ROUTES; -import static com.ca.mfaas.constants.EurekaMetadataDefinition.ROUTES_GATEWAY_URL; -import static com.ca.mfaas.constants.EurekaMetadataDefinition.ROUTES_SERVICE_URL; -import static com.ca.mfaas.constants.EurekaMetadataDefinition.SERVICE_DESCRIPTION; -import static com.ca.mfaas.constants.EurekaMetadataDefinition.SERVICE_TITLE; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; +import com.netflix.appinfo.InstanceInfo; +import org.junit.Test; import java.net.URISyntaxException; import java.nio.file.Paths; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import com.netflix.appinfo.InstanceInfo; - -import org.junit.Test; +import static com.ca.mfaas.constants.EurekaMetadataDefinition.*; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.*; public class ServiceDefinitionProcessorTest { + private StaticRegistrationResult processServicesData(ServiceDefinitionProcessor serviceDefinitionProcessor, String ymlFile, String data) { + StaticRegistrationResult context = new StaticRegistrationResult(); + Definition definition = serviceDefinitionProcessor.loadDefinition(context, ymlFile, data); + serviceDefinitionProcessor.process(context, ymlFile, definition); + return context; + } + + private StaticRegistrationResult processServicesData(ServiceDefinitionProcessor serviceDefinitionProcessor, Map ymlSources) { + StaticRegistrationResult context = new StaticRegistrationResult(); + for (final Map.Entry entry : ymlSources.entrySet()) { + Definition definition = serviceDefinitionProcessor.loadDefinition(context, entry.getKey(), entry.getValue()); + serviceDefinitionProcessor.process(context, entry.getKey(), definition); + } + return context; + } + @Test public void testProcessServicesDataWithTwoRoutes() { ServiceDefinitionProcessor serviceDefinitionProcessor = new ServiceDefinitionProcessor(); @@ -58,8 +56,7 @@ public void testProcessServicesDataWithTwoRoutes() { " serviceRelativeUrl: api/v1\n" + " - gatewayUrl: api/v2\n" + " serviceRelativeUrl: api/v2\n"; - ServiceDefinitionProcessor.ProcessServicesDataResult result = serviceDefinitionProcessor - .processServicesData(Collections.singletonMap("test", routedServiceYaml)); + StaticRegistrationResult result = processServicesData(serviceDefinitionProcessor, "test", routedServiceYaml); List instances = result.getInstances(); assertEquals(1, instances.size()); assertEquals(10019, instances.get(0).getSecurePort()); @@ -90,8 +87,7 @@ public void testProcessServicesDataWithEmptyHomepage() { " routes:\n" + " - gatewayUrl: api/v1\n" + " serviceRelativeUrl:\n"; - ServiceDefinitionProcessor.ProcessServicesDataResult result = serviceDefinitionProcessor - .processServicesData(Collections.singletonMap("test", routedServiceYamlEmptyRelativeUrls)); + StaticRegistrationResult result = processServicesData(serviceDefinitionProcessor, "test", routedServiceYamlEmptyRelativeUrls); List instances = result.getInstances(); assertEquals(1, instances.size()); assertEquals(10019, instances.get(0).getSecurePort()); @@ -112,21 +108,23 @@ public void testProcessServicesDataWithEmptyHomepage() { public void testProcessServicesDataNoServicesNode() { ServiceDefinitionProcessor serviceDefinitionProcessor = new ServiceDefinitionProcessor(); - ServiceDefinitionProcessor.ProcessServicesDataResult result = serviceDefinitionProcessor - .processServicesData(Collections.singletonMap("test", "something: value")); + StaticRegistrationResult result = processServicesData(serviceDefinitionProcessor, "test", "something: value"); assertEquals(0, result.getInstances().size()); assertEquals(1, result.getErrors().size()); - assertTrue(result.getErrors().get(0).contains("Error processing file test - Unrecognized field \"something\"")); + assertTrue(result.getErrors().get(0) instanceof String); + + final String errorMsg = (String) result.getErrors().get(0); + assertTrue(errorMsg.contains("Error processing file test - Unrecognized field \"something\"")); } @Test public void testFileInsteadOfDirectoryForDefinitions() throws URISyntaxException { ServiceDefinitionProcessor serviceDefinitionProcessor = new ServiceDefinitionProcessor(); - List instances = serviceDefinitionProcessor.findServices( + StaticRegistrationResult result = serviceDefinitionProcessor.findStaticServicesData( Paths.get(ClassLoader.getSystemResource("api-defs/staticclient.yml").toURI()).toAbsolutePath().toString()); - assertThat(instances.size(), is(0)); + assertThat(result.getInstances().size(), is(0)); } @Test @@ -136,12 +134,14 @@ public void testProcessServicesDataWithWrongUrlNoScheme() { " - serviceId: casamplerestapiservice\n" + " instanceBaseUrls:\n" + " - localhost:10019/casamplerestapiservice/\n"; - ServiceDefinitionProcessor.ProcessServicesDataResult result = serviceDefinitionProcessor - .processServicesData(Collections.singletonMap("test", routedServiceYaml)); + StaticRegistrationResult result = processServicesData(serviceDefinitionProcessor, "test", routedServiceYaml); List instances = result.getInstances(); assertEquals(0, instances.size()); assertEquals(1, result.getErrors().size()); - assertTrue(result.getErrors().get(0).contains("The URL localhost:10019/casamplerestapiservice/ is malformed")); + assertTrue(result.getErrors().get(0) instanceof String); + + final String errorMsg = (String) result.getErrors().get(0); + assertTrue(errorMsg.contains("The URL localhost:10019/casamplerestapiservice/ is malformed")); } @Test @@ -151,12 +151,14 @@ public void testProcessServicesDataWithWrongUrlUnsupportedScheme() { " - serviceId: casamplerestapiservice\n" + " instanceBaseUrls:\n" + " - ftp://localhost:10019/casamplerestapiservice/\n"; - ServiceDefinitionProcessor.ProcessServicesDataResult result = serviceDefinitionProcessor - .processServicesData(Collections.singletonMap("test", routedServiceYaml)); + StaticRegistrationResult result = processServicesData(serviceDefinitionProcessor, "test", routedServiceYaml); List instances = result.getInstances(); assertEquals(0, instances.size()); assertEquals(1, result.getErrors().size()); - assertTrue(result.getErrors().get(0).contains("The URL ftp://localhost:10019/casamplerestapiservice/ is malformed")); + assertTrue(result.getErrors().get(0) instanceof String); + + final String errorMsg = (String) result.getErrors().get(0); + assertTrue(errorMsg.contains("The URL ftp://localhost:10019/casamplerestapiservice/ is malformed")); } @Test @@ -166,12 +168,14 @@ public void testProcessServicesDataWithWrongUrlMissingHostname() { " - serviceId: casamplerestapiservice\n" + " instanceBaseUrls:\n" + " - https:///casamplerestapiservice/\n"; - ServiceDefinitionProcessor.ProcessServicesDataResult result = serviceDefinitionProcessor - .processServicesData(Collections.singletonMap("test", routedServiceYaml)); + StaticRegistrationResult result = processServicesData(serviceDefinitionProcessor, "test", routedServiceYaml); List instances = result.getInstances(); assertEquals(0, instances.size()); assertEquals(1, result.getErrors().size()); - assertTrue(result.getErrors().get(0).contains("The URL https:///casamplerestapiservice/ does not contain a hostname. The instance of casamplerestapiservice will not be created")); + assertTrue(result.getErrors().get(0) instanceof String); + + final String errorMsg = (String) result.getErrors().get(0); + assertTrue(errorMsg.contains("The URL https:///casamplerestapiservice/ does not contain a hostname. The instance of casamplerestapiservice will not be created")); } @Test @@ -189,8 +193,7 @@ public void testProcessServicesDataWithWrongDocumentationUrl() { " version: 2.0.0\n" + " documentationUrl: httpBlah://localhost:10021/hellospring/api-doc"; - ServiceDefinitionProcessor.ProcessServicesDataResult result = serviceDefinitionProcessor - .processServicesData(Collections.singletonMap("test", routedServiceYaml)); + StaticRegistrationResult result = processServicesData(serviceDefinitionProcessor, "test", routedServiceYaml); List instances = result.getInstances(); assertEquals(0, instances.size()); @@ -207,12 +210,14 @@ public void testProcessServicesDataWithWrongUrlMissingPort() { " - serviceId: casamplerestapiservice\n" + " instanceBaseUrls:\n" + " - https://host/casamplerestapiservice/\n"; - ServiceDefinitionProcessor.ProcessServicesDataResult result = serviceDefinitionProcessor - .processServicesData(Collections.singletonMap("test", routedServiceYaml)); + StaticRegistrationResult result = processServicesData(serviceDefinitionProcessor, "test", routedServiceYaml); List instances = result.getInstances(); assertEquals(0, instances.size()); assertEquals(1, result.getErrors().size()); - assertTrue(result.getErrors().get(0).contains("The URL https://host/casamplerestapiservice/ does not contain a port number. The instance of casamplerestapiservice will not be created")); + assertTrue(result.getErrors().get(0) instanceof String); + + final String errorMsg = (String) result.getErrors().get(0); + assertTrue(errorMsg.contains("The URL https://host/casamplerestapiservice/ does not contain a port number. The instance of casamplerestapiservice will not be created")); } @Test @@ -231,8 +236,7 @@ public void testServiceWithCatalogMetadata() { " title: Tile Title\n" + " description: Tile Description\n"; - ServiceDefinitionProcessor.ProcessServicesDataResult result = serviceDefinitionProcessor - .processServicesData(Collections.singletonMap("test", yaml)); + StaticRegistrationResult result = processServicesData(serviceDefinitionProcessor, "test", yaml); List instances = result.getInstances(); assertEquals(1, instances.size()); assertEquals(7, result.getInstances().get(0).getMetadata().size()); @@ -252,11 +256,13 @@ public void testCreateInstancesWithUndefinedInstanceBaseUrls() { " tileid:\n" + " title: Tile Title\n" + " description: Tile Description\n"; - ServiceDefinitionProcessor.ProcessServicesDataResult result = serviceDefinitionProcessor - .processServicesData(Collections.singletonMap("test", yaml)); + StaticRegistrationResult result = processServicesData(serviceDefinitionProcessor, "test", yaml); List instances = result.getInstances(); assertThat(instances.size(), is(0)); - assertTrue(result.getErrors().get(0).contains("The instanceBaseUrls parameter of casamplerestapiservice is not defined. The instance will not be created.")); + assertTrue(result.getErrors().get(0) instanceof String); + + final String errorMsg = (String) result.getErrors().get(0); + assertTrue(errorMsg.contains("The instanceBaseUrls parameter of casamplerestapiservice is not defined. The instance will not be created.")); } @Test @@ -274,11 +280,13 @@ public void testCreateInstancesWithUndefinedInstanceBaseUrl() { " tileid:\n" + " title: Tile Title\n" + " description: Tile Description\n"; - ServiceDefinitionProcessor.ProcessServicesDataResult result = serviceDefinitionProcessor - .processServicesData(Collections.singletonMap("test", yaml)); + StaticRegistrationResult result = processServicesData(serviceDefinitionProcessor, "test", yaml); List instances = result.getInstances(); assertThat(instances.size(), is(0)); - assertTrue(result.getErrors().get(0).contains("One of the instanceBaseUrl of casamplerestapiservice is not defined. The instance will not be created.")); + assertTrue(result.getErrors().get(0) instanceof String); + + final String errorMsg = (String) result.getErrors().get(0); + assertTrue(errorMsg.contains("One of the instanceBaseUrl of casamplerestapiservice is not defined. The instance will not be created.")); } @Test @@ -295,11 +303,13 @@ public void testCreateInstancesWithUndefinedServiceId() { " tileid:\n" + " title: Tile Title\n" + " description: Tile Description\n"; - ServiceDefinitionProcessor.ProcessServicesDataResult result = serviceDefinitionProcessor - .processServicesData(Collections.singletonMap("test", yaml)); + StaticRegistrationResult result = processServicesData(serviceDefinitionProcessor, "test", yaml); List instances = result.getInstances(); assertThat(instances.size(), is(0)); - assertTrue(result.getErrors().get(0).contains("ServiceId is not defined in the file 'test'. The instance will not be created.")); + assertTrue(result.getErrors().get(0) instanceof String); + + final String errorMsg = (String) result.getErrors().get(0); + assertTrue(errorMsg.contains("ServiceId is not defined in the file 'test'. The instance will not be created.")); } @Test @@ -329,11 +339,13 @@ public void testCreateInstancesWithMultipleStaticDefinitions() { " title: Tile Title\n" + " description: Tile Description\n"; - ServiceDefinitionProcessor.ProcessServicesDataResult result = serviceDefinitionProcessor - .processServicesData(Collections.singletonMap("test", yaml)); + StaticRegistrationResult result = processServicesData(serviceDefinitionProcessor, "test", yaml); List instances = result.getInstances(); assertThat(instances.size(), is(2)); - assertTrue(result.getErrors().get(0).contains("The instanceBaseUrls parameter of casamplerestapiservice2 is not defined. The instance will not be created.")); + assertTrue(result.getErrors().get(0) instanceof String); + + final String errorMsg = (String) result.getErrors().get(0); + assertTrue(errorMsg.contains("The instanceBaseUrls parameter of casamplerestapiservice2 is not defined. The instance will not be created.")); } @@ -381,10 +393,13 @@ public void testCreateInstancesWithMultipleYmls() { ymlSources.put("yaml", yaml); ymlSources.put("yaml1", yaml2); ymlSources.put("yaml2", yaml3); - ServiceDefinitionProcessor.ProcessServicesDataResult result = serviceDefinitionProcessor.processServicesData(ymlSources); + StaticRegistrationResult result = processServicesData(serviceDefinitionProcessor, ymlSources); List instances = result.getInstances(); assertThat(instances.size(), is(2)); - assertTrue(result.getErrors().get(0).contains("The instanceBaseUrls parameter of casamplerestapiservice2 is not defined. The instance will not be created.")); + assertTrue(result.getErrors().get(0) instanceof String); + + final String errorMsg = (String) result.getErrors().get(0); + assertTrue(errorMsg.contains("The instanceBaseUrls parameter of casamplerestapiservice2 is not defined. The instance will not be created.")); } @@ -404,8 +419,7 @@ public void testEnableUnsecurePortIfHttp() { " - gatewayUrl: api/v2\n" + " serviceRelativeUrl: api/v2\n"; - ServiceDefinitionProcessor.ProcessServicesDataResult result = serviceDefinitionProcessor - .processServicesData(Collections.singletonMap("test", routedServiceYaml)); + StaticRegistrationResult result = processServicesData(serviceDefinitionProcessor, "test", routedServiceYaml); List instances = result.getInstances(); assertThat(instances.size(), is(1)); assertFalse(instances.get(0).isPortEnabled(InstanceInfo.PortType.SECURE)); @@ -439,8 +453,7 @@ public void shouldGenerateMetadataIfApiInfoIsNotNUll() { " title: Static API Services\n" + " description: Services which demonstrate how to make an API service discoverable in the APIML ecosystem using YAML definitions\n"; - ServiceDefinitionProcessor.ProcessServicesDataResult result = serviceDefinitionProcessor - .processServicesData(Collections.singletonMap("test", routedServiceYaml)); + StaticRegistrationResult result = processServicesData(serviceDefinitionProcessor,"test", routedServiceYaml); List instances = result.getInstances(); assertEquals(1, instances.size()); assertEquals(10019, instances.get(0).getSecurePort()); @@ -482,7 +495,7 @@ public void shouldGiveErrorIfTileIdIsInvalid() { " title: Static API Services\n" + " description: Services which demonstrate how to make an API service discoverable in the APIML ecosystem using YAML definitions\n"; - ServiceDefinitionProcessor.ProcessServicesDataResult result = serviceDefinitionProcessor.processServicesData(Collections.singletonMap("test", routedServiceYaml)); + StaticRegistrationResult result = processServicesData(serviceDefinitionProcessor, "test", routedServiceYaml); assertEquals("Error processing file test - The API Catalog UI tile ID adajand is invalid. The service casamplerestapiservice will not have API Catalog UI tile", result.getErrors().get(0)); } @@ -491,27 +504,27 @@ public void testFindServicesWithTwoDirectories() { ServiceDefinitionProcessor serviceDefinitionProcessor = new ServiceDefinitionProcessor(); String pathOne = ClassLoader.getSystemResource("api-defs/").getPath(); String pathTwo = ClassLoader.getSystemResource("ext-config/").getPath(); - List instances = serviceDefinitionProcessor.findServices(pathOne + ";" + pathTwo); + StaticRegistrationResult result = serviceDefinitionProcessor.findStaticServicesData(pathOne + ";" + pathTwo); - assertThat(instances.size(), is(2)); + assertThat(result.getInstances().size(), is(5)); } @Test public void testFindServicesWithOneDirectory() { ServiceDefinitionProcessor serviceDefinitionProcessor = new ServiceDefinitionProcessor(); String pathOne = ClassLoader.getSystemResource("api-defs/").getPath(); - List instances = serviceDefinitionProcessor.findServices(pathOne); + StaticRegistrationResult result = serviceDefinitionProcessor.findStaticServicesData(pathOne); - assertThat(instances.size(), is(1)); + assertThat(result.getInstances().size(), is(4)); } @Test public void testFindServicesWithSecondEmptyDirectory() { ServiceDefinitionProcessor serviceDefinitionProcessor = new ServiceDefinitionProcessor(); String pathOne = ClassLoader.getSystemResource("api-defs/").getPath(); - List instances = serviceDefinitionProcessor.findServices(pathOne + ";"); + StaticRegistrationResult result = serviceDefinitionProcessor.findStaticServicesData(pathOne + ";"); - assertThat(instances.size(), is(1)); + assertThat(result.getInstances().size(), is(4)); } @Test @@ -524,8 +537,7 @@ public void testProcessServicesDataWithAuthenticationMetadata() { " authentication:\n" + " scheme: httpBasicPassTicket\n" + " applid: TSTAPPL\n"; - ServiceDefinitionProcessor.ProcessServicesDataResult result = serviceDefinitionProcessor - .processServicesData(Collections.singletonMap("test", routedServiceYaml)); + StaticRegistrationResult result = processServicesData(serviceDefinitionProcessor, "test", routedServiceYaml); assertEquals(new ArrayList<>(), result.getErrors()); List instances = result.getInstances(); assertEquals(1, instances.size()); @@ -542,11 +554,96 @@ public void testProcessServicesDataWithInvalidAuthenticationScheme() { " - https://localhost:10019/casamplerestapiservice/\n" + " authentication:\n" + " scheme: bad\n"; - ServiceDefinitionProcessor.ProcessServicesDataResult result = serviceDefinitionProcessor - .processServicesData(Collections.singletonMap("test", routedServiceYaml)); + StaticRegistrationResult result = processServicesData(serviceDefinitionProcessor, "test", routedServiceYaml); assertEquals(1, result.getErrors().size()); List instances = result.getInstances(); assertEquals(0, instances.size()); } + + @Test + public void testAdditionalServiceMetadata() { + ServiceDefinitionProcessor serviceDefinitionProcessor = new ServiceDefinitionProcessor(); + String routedServiceYaml = + "additionalServiceMetadata:\n" + + " - serviceId: testService\n" + + " mode: FORCE_UPDATE\n" + + " title: testServiceTitle\n" + + " description: testServiceDescription\n" + + " authentication:\n" + + " scheme: httpBasicPassTicket\n" + + " applid: TSTAPPL\n" + + " healthCheckRelativeUrl: actuator/health\n" + + " routes:\n" + + " - gatewayUrl: api/v1\n" + + " - gatewayUrl: api/v2\n" + + " apiInfo:\n" + + " - apiId: apiId1\n" + + " gatewayUrl: api/v1\n" + + " swaggerUrl: https://localhost:10012/discoverableclient/api-doc\n" + + " - apiId: apiId2\n" + + " gatewayUrl: api/v2\n" + + " swaggerUrl: https://localhost:10012/discoverableclient2/api-doc\n"; + StaticRegistrationResult result = processServicesData(serviceDefinitionProcessor, "test", routedServiceYaml); + Map asm = result.getAdditionalServiceMetadata(); + assertEquals(1, asm.size()); + assertTrue(asm.containsKey("testService")); + + ServiceOverrideData sod = asm.get("testService"); + assertEquals(ServiceOverride.Mode.FORCE_UPDATE, sod.getMode()); + + Map md = sod.getMetadata(); + + assertEquals(11, md.size()); + + assertEquals("2.0.0", md.get(VERSION)); + assertEquals("testServiceTitle", md.get(SERVICE_TITLE)); + assertEquals("testServiceDescription", md.get(SERVICE_DESCRIPTION)); + + // routes + assertEquals("api/v1", md.get("apiml.routes.api-v1.gatewayUrl")); + assertEquals("api/v2", md.get("apiml.routes.api-v2.gatewayUrl")); + + // api info + assertEquals("api/v1", md.get("apiml.apiInfo.api-v1.gatewayUrl")); + assertEquals("https://localhost:10012/discoverableclient/api-doc", md.get("apiml.apiInfo.api-v1.swaggerUrl")); + assertEquals("api/v2", md.get("apiml.apiInfo.api-v2.gatewayUrl")); + assertEquals("https://localhost:10012/discoverableclient2/api-doc", md.get("apiml.apiInfo.api-v2.swaggerUrl")); + + assertEquals("httpBasicPassTicket", md.get(AUTHENTICATION_SCHEME)); + assertEquals("TSTAPPL", md.get(AUTHENTICATION_APPLID)); + } + + @Test + public void testAdditionalServiceMetadataMulti() { + ServiceDefinitionProcessor serviceDefinitionProcessor = new ServiceDefinitionProcessor(); + String routedServiceYaml = + "additionalServiceMetadata:\n" + + " - serviceId: service1\n" + + " mode: FORCE_UPDATE\n" + + " title: title1\n" + + " - serviceId: service1\n" + + " title: title2\n" + + " - serviceId: service3\n" + + " mode: FORCE_UPDATE\n" + + " title: title3\n"; + + StaticRegistrationResult result = processServicesData(serviceDefinitionProcessor, "test", routedServiceYaml); + Map asm = result.getAdditionalServiceMetadata(); + + assertEquals(2, asm.size()); + + assertTrue(asm.containsKey("service1")); + assertEquals(ServiceOverride.Mode.UPDATE, asm.get("service1").getMode()); + + assertTrue(asm.containsKey("service3")); + assertEquals(ServiceOverride.Mode.FORCE_UPDATE, asm.get("service3").getMode()); + + assertEquals("title2", asm.get("service1").getMetadata().get(SERVICE_TITLE)); + + assertTrue(result.getErrors().get(0) instanceof String); + final String errMsg = (String) result.getErrors().get(0); + assertTrue(errMsg.contains("were replaced for duplicities")); + } + } diff --git a/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/StaticApiRestControllerTest.java b/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/StaticApiRestControllerTest.java index a4755fd677..13fca49903 100644 --- a/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/StaticApiRestControllerTest.java +++ b/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/StaticApiRestControllerTest.java @@ -65,19 +65,20 @@ public void reloadDefinitions() throws Exception { String serviceName = "service"; String basicToken = "Basic " + Base64.getEncoder().encodeToString(CREDENTIALS.getBytes()); + StaticRegistrationResult result = new StaticRegistrationResult(); List instancesInfo = Arrays.asList( InstanceInfo.Builder.newBuilder() .setAppName(serviceName) .build() ); + result.getInstances().addAll(instancesInfo); - when(registrationService.getStaticInstances()).thenReturn(instancesInfo); + when(registrationService.reloadServices()).thenReturn(result); this.mockMvc.perform(post("/discovery/api/v1/staticApi").header("Authorization", basicToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$[*].app", hasItem(serviceName.toUpperCase()))); + .andExpect(jsonPath("$.instances[*].app", hasItem(serviceName.toUpperCase()))); verify(registrationService, times(1)).reloadServices(); - verify(registrationService, times(1)).getStaticInstances(); } } diff --git a/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/StaticServicesRegistrationServiceTest.java b/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/StaticServicesRegistrationServiceTest.java index 61d1947c49..c5c3417419 100644 --- a/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/StaticServicesRegistrationServiceTest.java +++ b/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/StaticServicesRegistrationServiceTest.java @@ -9,6 +9,7 @@ */ package com.ca.mfaas.discovery.staticdef; +import com.ca.mfaas.discovery.metadata.MetadataDefaultsService; import com.netflix.appinfo.InstanceInfo; import com.netflix.eureka.EurekaServerContext; import com.netflix.eureka.EurekaServerContextHolder; @@ -23,19 +24,14 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.util.Arrays; -import java.util.Collections; import java.util.List; -import java.util.Set; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; public class StaticServicesRegistrationServiceTest { @@ -52,15 +48,21 @@ public void setUp() { @Rule public TemporaryFolder folder = new TemporaryFolder(); + private StaticRegistrationResult createResult(InstanceInfo...instances) { + StaticRegistrationResult out = new StaticRegistrationResult(); + out.getInstances().addAll(Arrays.asList(instances)); + return out; + } + @Test public void testFindServicesInDirectoryNoFiles() { EurekaServerContext mockEurekaServerContext = mock(EurekaServerContext.class); EurekaServerContextHolder.initialize(mockEurekaServerContext); ServiceDefinitionProcessor serviceDefinitionProcessor = new ServiceDefinitionProcessor(); - StaticServicesRegistrationService registrationService = new StaticServicesRegistrationService(serviceDefinitionProcessor); - Set instances = registrationService.registerServices(folder.getRoot().getAbsolutePath()); - assertEquals(0, instances.size()); + StaticServicesRegistrationService registrationService = new StaticServicesRegistrationService(serviceDefinitionProcessor, new MetadataDefaultsService()); + StaticRegistrationResult result = registrationService.registerServices(folder.getRoot().getAbsolutePath()); + assertEquals(0, result.getInstances().size()); } @Test @@ -72,21 +74,21 @@ public void testFindServicesInDirectoryOneFile() throws IOException, URISyntaxEx ServiceDefinitionProcessor serviceDefinitionProcessor = new ServiceDefinitionProcessor(); - StaticServicesRegistrationService registrationService = new StaticServicesRegistrationService(serviceDefinitionProcessor); - Set instances = registrationService.registerServices(folder.getRoot().getAbsolutePath()); + StaticServicesRegistrationService registrationService = new StaticServicesRegistrationService(serviceDefinitionProcessor, new MetadataDefaultsService()); + StaticRegistrationResult result = registrationService.registerServices(folder.getRoot().getAbsolutePath()); - assertEquals(1, instances.size()); + assertEquals(4, result.getInstances().size()); } @Test public void testGetStaticInstances() { ServiceDefinitionProcessor serviceDefinitionProcessor = mock(ServiceDefinitionProcessor.class); - StaticServicesRegistrationService registrationService = new StaticServicesRegistrationService(serviceDefinitionProcessor); + StaticServicesRegistrationService registrationService = new StaticServicesRegistrationService(serviceDefinitionProcessor, new MetadataDefaultsService()); List instances = registrationService.getStaticInstances(); assertEquals(0, instances.size()); - verify(serviceDefinitionProcessor, times(0)).findServices(any(String.class)); + verify(serviceDefinitionProcessor, times(0)).findStaticServicesData(any(String.class)); } @Test @@ -94,16 +96,16 @@ public void testGetStaticInstancesAfterRegister() { String directory = "directory"; String service = "service"; ServiceDefinitionProcessor serviceDefinitionProcessor = mock(ServiceDefinitionProcessor.class); - when(serviceDefinitionProcessor.findServices(directory)).thenReturn(Arrays.asList( + when(serviceDefinitionProcessor.findStaticServicesData(directory)).thenReturn(createResult( InstanceInfo.Builder.newBuilder().setAppName(service).build())); - StaticServicesRegistrationService registrationService = new StaticServicesRegistrationService(serviceDefinitionProcessor); + StaticServicesRegistrationService registrationService = new StaticServicesRegistrationService(serviceDefinitionProcessor, new MetadataDefaultsService()); registrationService.registerServices(directory); List instances = registrationService.getStaticInstances(); assertEquals(1, instances.size()); assertEquals(service.toUpperCase(), instances.get(0).getAppName()); - verify(serviceDefinitionProcessor, times(1)).findServices(directory); + verify(serviceDefinitionProcessor, times(1)).findStaticServicesData(directory); } @Test @@ -111,16 +113,17 @@ public void testReloadServicesWithUnregisteringService() { String service = "service"; ServiceDefinitionProcessor serviceDefinitionProcessor = mock(ServiceDefinitionProcessor.class); InstanceInfo instance = InstanceInfo.Builder.newBuilder().setInstanceId(service).setAppName(service).build(); - when(serviceDefinitionProcessor.findServices(null)) - .thenReturn(Arrays.asList(instance)) - .thenReturn(Collections.emptyList()); - StaticServicesRegistrationService registrationService = new StaticServicesRegistrationService(serviceDefinitionProcessor); + when(serviceDefinitionProcessor.findStaticServicesData(null)) + .thenReturn(createResult(instance)) + .thenReturn(createResult()); + + StaticServicesRegistrationService registrationService = new StaticServicesRegistrationService(serviceDefinitionProcessor, new MetadataDefaultsService()); registrationService.reloadServices(); - Set services = registrationService.reloadServices(); + StaticRegistrationResult result = registrationService.reloadServices(); - assertThat(services.contains(service), is(false)); - verify(serviceDefinitionProcessor, times(2)).findServices(null); + assertThat(result.getRegistredServices().contains(service), is(false)); + verify(serviceDefinitionProcessor, times(2)).findStaticServicesData(null); verify(mockRegistry, times(1)).cancel(instance.getAppName(), instance.getId(), false); } @@ -131,17 +134,17 @@ public void testReloadServicesWithAddingNewService() { ServiceDefinitionProcessor serviceDefinitionProcessor = mock(ServiceDefinitionProcessor.class); InstanceInfo instanceA = InstanceInfo.Builder.newBuilder().setInstanceId(serviceA).setAppName(serviceA).build(); InstanceInfo instanceB = InstanceInfo.Builder.newBuilder().setInstanceId(serviceB).setAppName(serviceB).build(); - when(serviceDefinitionProcessor.findServices(null)) - .thenReturn(Arrays.asList(instanceA)) - .thenReturn(Arrays.asList(instanceA, instanceB)); + when(serviceDefinitionProcessor.findStaticServicesData(null)) + .thenReturn(createResult(instanceA)) + .thenReturn(createResult(instanceA, instanceB)); - StaticServicesRegistrationService registrationService = new StaticServicesRegistrationService(serviceDefinitionProcessor); + StaticServicesRegistrationService registrationService = new StaticServicesRegistrationService(serviceDefinitionProcessor, new MetadataDefaultsService()); registrationService.reloadServices(); - Set services = registrationService.reloadServices(); + StaticRegistrationResult result = registrationService.reloadServices(); - assertThat(services.contains(serviceA), is(true)); - assertThat(services.contains(serviceB), is(true)); - verify(serviceDefinitionProcessor, times(2)).findServices(null); + assertThat(result.getRegistredServices().contains(serviceA), is(true)); + assertThat(result.getRegistredServices().contains(serviceB), is(true)); + verify(serviceDefinitionProcessor, times(2)).findStaticServicesData(null); verify(mockRegistry, times(0)).cancel(any(String.class), any(String.class), eq(false)); } @@ -151,9 +154,9 @@ public void testRenewInstances() { String service = "service"; InstanceInfo instance = InstanceInfo.Builder.newBuilder().setInstanceId(service).setAppName(service).build(); ServiceDefinitionProcessor serviceDefinitionProcessor = mock(ServiceDefinitionProcessor.class); - when(serviceDefinitionProcessor.findServices(directory)).thenReturn(Arrays.asList(instance)); + when(serviceDefinitionProcessor.findStaticServicesData(directory)).thenReturn(createResult(instance)); - StaticServicesRegistrationService registrationService = new StaticServicesRegistrationService(serviceDefinitionProcessor); + StaticServicesRegistrationService registrationService = new StaticServicesRegistrationService(serviceDefinitionProcessor, new MetadataDefaultsService()); registrationService.registerServices(directory); registrationService.renewInstances(); diff --git a/discovery-service/src/test/resources/api-defs/staticclient.yml b/discovery-service/src/test/resources/api-defs/staticclient.yml index 8330c07349..e17d50dd7f 100644 --- a/discovery-service/src/test/resources/api-defs/staticclient.yml +++ b/discovery-service/src/test/resources/api-defs/staticclient.yml @@ -23,6 +23,39 @@ services: serviceRelativeUrl: /ws - gatewayUrl: api-doc serviceRelativeUrl: /api-doc + - serviceId: toAddAuth + title: Service to add auth info + instanceBaseUrls: + - https://localhost:10012/discoverableclient + - serviceId: toReplaceAuth + title: Service to replace auth info + instanceBaseUrls: + - https://localhost:10012/discoverableclient + authentication: + scheme: httpBasicPassTicket + applid: TSTAPPL2 + - serviceId: nowFixedAuth + title: Service with temporary auth, althought auth is defined now + instanceBaseUrls: + - https://localhost:10012/discoverableclient + authentication: + scheme: httpBasicPassTicket + applid: TSTAPPL3 + +additionalServiceMetadata: + - serviceId: toAddAuth + authentication: + scheme: httpBasicPassTicket + applid: TSTAPPL4 + - serviceId: toReplaceAuth + mode: FORCE_UPDATE + authentication: + scheme: httpBasicPassTicket + applid: TSTAPPL5 + - serviceId: nowFixedAuth + authentication: + scheme: httpBasicPassTicket + applid: TSTAPPL6 # List of tiles that can be used by services defined in YAML files: catalogUiTiles: From 195a1e9e95d482fc714969591fa99981d2cff276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Thu, 2 Jan 2020 13:45:48 +0100 Subject: [PATCH 010/122] Concept of authentication commands (ByPass, Zosmf and Passticket implemented, contains also ZoweJwt draft), support logout method, --- .../security/common/auth}/Authentication.java | 11 +- .../common/auth}/AuthenticationScheme.java | 25 +- .../config/AuthConfigurationProperties.java | 8 + .../{ => common}/service/IRRPassTicket.java | 2 +- .../service/PassTicketService.java | 4 +- .../security/common/token/QueryResponse.java | 10 +- .../common/auth/AuthenticationSchemeTest.java | 28 ++ .../service/PassTicketServiceTest.java | 6 +- .../common/token/QueryResponseTest.java | 37 +++ .../com/ca/mfaas/cache/EntryExpiration.java | 32 ++ .../mfaas/util/ClassOrDefaultProxyUtils.java | 84 +++-- config/local/api-defs/staticclient.yml | 25 ++ config/local/gateway-service.yml | 5 + .../ca/mfaas/discovery/staticdef/Service.java | 1 + .../staticdef/ServiceDefinitionProcessor.java | 4 +- gateway-service/build.gradle | 2 + .../ca/mfaas/gateway/GatewayApplication.java | 3 +- .../ca/mfaas/gateway/config/CacheConfig.java | 40 +++ .../gateway/controllers/AuthController.java | 35 ++ .../pre/ServiceAuthenticationFilter.java | 67 ++++ .../gateway/ribbon/GatewayRibbonConfig.java | 8 +- .../GatewayRibbonLoadBalancingHttpClient.java | 107 ++++++- .../gateway/routing/ApimlRoutingConfig.java | 6 + .../config/SecurityConfiguration.java | 14 + .../service/AuthenticationService.java | 94 +++++- .../service/ServiceAuthenticationService.java | 159 +++++++++ .../schema/AbstractAuthenticationScheme.java | 46 +++ .../service/schema/AuthenticationCommand.java | 50 +++ .../schema/AuthenticationSchemeFactory.java | 97 ++++++ .../security/service/schema/ByPassScheme.java | 37 +++ .../schema/HttpBasicPassTicketScheme.java | 82 +++++ .../security/service/schema/ZosmfScheme.java | 84 +++++ .../service/schema/ZoweJwtScheme.java | 30 ++ .../src/main/resources/ehcache.xml | 12 + .../controllers/AuthControllerTest.java | 54 ++++ .../pre/ServiceAuthenticationFilterTest.java | 70 ++++ .../query/SuccessfulQueryHandlerTest.java | 14 +- .../service/AuthenticationServiceTest.java | 154 ++++++++- .../ServiceAuthenticationServiceTest.java | 303 ++++++++++++++++++ .../schema/AuthenticationCommandTest.java | 25 ++ .../AuthenticationSchemeFactoryTest.java | 199 ++++++++++++ .../service/schema/ByPassSchemeTest.java | 27 ++ .../schema/HttpBasicPassTicketSchemeTest.java | 88 +++++ .../service/schema/ZosmfSchemeTest.java | 99 ++++++ gradle/versions.gradle | 3 + 45 files changed, 2225 insertions(+), 66 deletions(-) rename {discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef => apiml-security-common/src/main/java/com/ca/apiml/security/common/auth}/Authentication.java (72%) rename {discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef => apiml-security-common/src/main/java/com/ca/apiml/security/common/auth}/AuthenticationScheme.java (53%) rename apiml-security-common/src/main/java/com/ca/apiml/security/{ => common}/service/IRRPassTicket.java (98%) rename apiml-security-common/src/main/java/com/ca/apiml/security/{ => common}/service/PassTicketService.java (95%) create mode 100644 apiml-security-common/src/test/java/com/ca/apiml/security/common/auth/AuthenticationSchemeTest.java rename apiml-security-common/src/test/java/com/ca/apiml/security/{ => common}/service/PassTicketServiceTest.java (92%) create mode 100644 apiml-security-common/src/test/java/com/ca/apiml/security/common/token/QueryResponseTest.java create mode 100644 common-service-core/src/main/java/com/ca/mfaas/cache/EntryExpiration.java create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/config/CacheConfig.java create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/AuthController.java create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilter.java create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationService.java create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AbstractAuthenticationScheme.java create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationCommand.java create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactory.java create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ByPassScheme.java create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketScheme.java create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ZosmfScheme.java create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ZoweJwtScheme.java create mode 100644 gateway-service/src/main/resources/ehcache.xml create mode 100644 gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/AuthControllerTest.java create mode 100644 gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java create mode 100644 gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceTest.java create mode 100644 gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationCommandTest.java create mode 100644 gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java create mode 100644 gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ByPassSchemeTest.java create mode 100644 gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java create mode 100644 gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZosmfSchemeTest.java diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/Authentication.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/auth/Authentication.java similarity index 72% rename from discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/Authentication.java rename to apiml-security-common/src/main/java/com/ca/apiml/security/common/auth/Authentication.java index 1f031207c3..330d77a932 100644 --- a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/Authentication.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/auth/Authentication.java @@ -7,14 +7,21 @@ * * Copyright Contributors to the Zowe Project. */ -package com.ca.mfaas.discovery.staticdef; +package com.ca.apiml.security.common.auth; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; /** * Information about expected authentication scheme and APPLID for PassTickets generation. */ - @Data class Authentication { + @Data + @NoArgsConstructor + @AllArgsConstructor + public class Authentication { + private AuthenticationScheme scheme; private String applid; + } diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/AuthenticationScheme.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/auth/AuthenticationScheme.java similarity index 53% rename from discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/AuthenticationScheme.java rename to apiml-security-common/src/main/java/com/ca/apiml/security/common/auth/AuthenticationScheme.java index e7381a1334..d03363858c 100644 --- a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/AuthenticationScheme.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/auth/AuthenticationScheme.java @@ -8,12 +8,16 @@ * Copyright Contributors to the Zowe Project. */ -package com.ca.mfaas.discovery.staticdef; +package com.ca.apiml.security.common.auth; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + @Getter public enum AuthenticationScheme { @JsonProperty("bypass") @@ -30,6 +34,8 @@ public enum AuthenticationScheme { private final String scheme; + private static Map schemeToEnum; + AuthenticationScheme(String scheme) { this.scheme = scheme; } @@ -38,4 +44,21 @@ public enum AuthenticationScheme { public String toString() { return scheme; } + + public static AuthenticationScheme fromScheme(String scheme) { + if (schemeToEnum != null) return schemeToEnum.get(scheme); + + synchronized (AuthenticationScheme.class) { + if (schemeToEnum != null) return schemeToEnum.get(scheme); + + final Map map = new HashMap<>(); + for (final AuthenticationScheme as : values()) { + map.put(as.scheme, as); + } + schemeToEnum = Collections.unmodifiableMap(map); + + return schemeToEnum.get(scheme); + } + } + } diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/config/AuthConfigurationProperties.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/config/AuthConfigurationProperties.java index 264c1bcbb4..1b1c4a81c0 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/config/AuthConfigurationProperties.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/config/AuthConfigurationProperties.java @@ -41,6 +41,8 @@ public class AuthConfigurationProperties { private String zosmfServiceId; private String provider = "zosmf"; + private AuthConfigurationProperties.PassTicket passTicket; + private String jwtKeyAlias; //Token properties @@ -62,9 +64,15 @@ public static class CookieProperties { private Integer cookieMaxAge = -1; } + @Data + public static class PassTicket { + private Integer timeout = 360; + } + public AuthConfigurationProperties() { this.cookieProperties = new AuthConfigurationProperties.CookieProperties(); this.tokenProperties = new AuthConfigurationProperties.TokenProperties(); + this.passTicket = new AuthConfigurationProperties.PassTicket(); } /** diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/service/IRRPassTicket.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicket.java similarity index 98% rename from apiml-security-common/src/main/java/com/ca/apiml/security/service/IRRPassTicket.java rename to apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicket.java index 6787439c58..395d929c6b 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/service/IRRPassTicket.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicket.java @@ -7,7 +7,7 @@ * * Copyright Contributors to the Zowe Project. */ -package com.ca.apiml.security.service; +package com.ca.apiml.security.common.service; /** * Interface covering com.ibm.eserver.zos.racf.IRRPassTicket class diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/service/PassTicketService.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java similarity index 95% rename from apiml-security-common/src/main/java/com/ca/apiml/security/service/PassTicketService.java rename to apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java index e595fc687f..dec3f1dd3e 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/service/PassTicketService.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java @@ -7,7 +7,7 @@ * * Copyright Contributors to the Zowe Project. */ -package com.ca.apiml.security.service; +package com.ca.apiml.security.common.service; import com.ca.mfaas.util.ClassOrDefaultProxyUtils; import org.springframework.stereotype.Service; @@ -15,7 +15,7 @@ import javax.annotation.PostConstruct; /** - * This method allow to get a passticket from RAC. + * This method allow to get a passTicket from RAC. */ @Service public class PassTicketService { diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/token/QueryResponse.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/token/QueryResponse.java index b8166a8758..3bce8aaaf0 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/token/QueryResponse.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/token/QueryResponse.java @@ -9,6 +9,7 @@ */ package com.ca.apiml.security.common.token; +import com.ca.mfaas.cache.EntryExpiration; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -21,9 +22,16 @@ @Data @AllArgsConstructor @NoArgsConstructor -public class QueryResponse { +public class QueryResponse implements EntryExpiration { + private String domain; private String userId; private Date creation; private Date expiration; + + @Override + public boolean isExpired() { + return expiration.before(new Date()); + } + } diff --git a/apiml-security-common/src/test/java/com/ca/apiml/security/common/auth/AuthenticationSchemeTest.java b/apiml-security-common/src/test/java/com/ca/apiml/security/common/auth/AuthenticationSchemeTest.java new file mode 100644 index 0000000000..8c7ed544e8 --- /dev/null +++ b/apiml-security-common/src/test/java/com/ca/apiml/security/common/auth/AuthenticationSchemeTest.java @@ -0,0 +1,28 @@ +package com.ca.apiml.security.common.auth;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static org.junit.Assert.*; +@RunWith(JUnit4.class) +public class AuthenticationSchemeTest { + + @Test + public void testFromScheme() { + for (AuthenticationScheme as : AuthenticationScheme.values()) { + AuthenticationScheme as2 = AuthenticationScheme.fromScheme(as.getScheme()); + assertSame(as, as2); + } + assertNull(AuthenticationScheme.fromScheme("absolutly nonsence")); + } + +} diff --git a/apiml-security-common/src/test/java/com/ca/apiml/security/service/PassTicketServiceTest.java b/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java similarity index 92% rename from apiml-security-common/src/test/java/com/ca/apiml/security/service/PassTicketServiceTest.java rename to apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java index 6af3ad8304..78814a1408 100644 --- a/apiml-security-common/src/test/java/com/ca/apiml/security/service/PassTicketServiceTest.java +++ b/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java @@ -7,7 +7,7 @@ * * Copyright Contributors to the Zowe Project. */ -package com.ca.apiml.security.service; +package com.ca.apiml.security.common.service; import org.junit.Test; import org.junit.runner.RunWith; @@ -53,8 +53,8 @@ public String generate(String userId, String applId) { @Order(2) public void testCalledMethod() { evaluated = null; - passTicketService.evaluate("userId", "applId", "passticket"); - assertEquals("userId-applId-passticket", evaluated); + passTicketService.evaluate("userId", "applId", "passTicket"); + assertEquals("userId-applId-passTicket", evaluated); passTicketService.evaluate("1", "2", "3"); assertEquals("1-2-3", evaluated); diff --git a/apiml-security-common/src/test/java/com/ca/apiml/security/common/token/QueryResponseTest.java b/apiml-security-common/src/test/java/com/ca/apiml/security/common/token/QueryResponseTest.java new file mode 100644 index 0000000000..f6d4fa4b08 --- /dev/null +++ b/apiml-security-common/src/test/java/com/ca/apiml/security/common/token/QueryResponseTest.java @@ -0,0 +1,37 @@ +package com.ca.apiml.security.common.token;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.Calendar; +import java.util.Date; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@RunWith(JUnit4.class) +public class QueryResponseTest { + + @Test + public void testIsExpired() { + final Calendar c = Calendar.getInstance(); + final Date now = c.getTime(); + c.add(Calendar.MINUTE, -1); + final Date before = c.getTime(); + c.add(Calendar.MINUTE, 2); + final Date after = c.getTime(); + + assertTrue(new QueryResponse("domain", "user", now, before).isExpired()); + assertFalse(new QueryResponse("domain", "user", now, after).isExpired()); + } + +} diff --git a/common-service-core/src/main/java/com/ca/mfaas/cache/EntryExpiration.java b/common-service-core/src/main/java/com/ca/mfaas/cache/EntryExpiration.java new file mode 100644 index 0000000000..6b0e99458e --- /dev/null +++ b/common-service-core/src/main/java/com/ca/mfaas/cache/EntryExpiration.java @@ -0,0 +1,32 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.cache; + +/** + * This interface offer method isExpired. It allow to check state of DTO, because although it is in cache + * it can be expired. + * + * Example: + * + * @CacheEvict(value = "<cacheName>", condition = "#result != null && #result.isExpired()") + * @Cacheable("<cacheName>") + * public Result someBussinessMethod(...) + * + */ +public interface EntryExpiration { + + /** + * Method could be use in cache annotation to determinate if record is still valid or expirated. + * + * @return true if record is expired, otherwise false + */ + public boolean isExpired(); + +} diff --git a/common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java b/common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java index 11353788fd..6fc18ade2e 100644 --- a/common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java +++ b/common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java @@ -18,6 +18,8 @@ import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; import java.util.Map; import java.util.function.Supplier; @@ -85,7 +87,7 @@ private static T makeProxy(Class interfaceClass, Object implementation, b return (T) Proxy.newProxyInstance( ClassOrDefaultProxyUtils.class.getClassLoader(), new Class[] {interfaceClass, ClassOrDefaultProxyUtils.ClassOrDefaultProxyState.class}, - new MethodInvocationHandler(implementation, usingBaseImplementation)); + new MethodInvocationHandler(implementation, interfaceClass, usingBaseImplementation)); } /** @@ -109,52 +111,92 @@ public interface ClassOrDefaultProxyState { private static class MethodInvocationHandler implements InvocationHandler, ClassOrDefaultProxyState { - private final Map mapping = new HashMap<>(); + private final Map mapping = new HashMap<>(); private final boolean usingBaseImplementation; private final Object implementation; + private final Class interfaceClass; - public MethodInvocationHandler(Object implementation, boolean usingBaseImplementation) { + public MethodInvocationHandler(Object implementation, Class interfaceClass, boolean usingBaseImplementation) { this.usingBaseImplementation = usingBaseImplementation; this.implementation = implementation; + this.interfaceClass = interfaceClass; this.initMapping(); } - private void addMapping(Object target, Method caller, Method callee) { - final String key = ObjectUtil.getMethodIdentifier(caller); + private EndPoint addMapping(Object target, Method caller, Method callee) { final EndPoint endPoint = new EndPoint(target, callee); - mapping.put(key, endPoint); + mapping.put(caller, endPoint); + return endPoint; } - private void initMapping() { - // first map methods of target - Class clazz = implementation.getClass(); - while (true) { - for (final Method method : clazz.getDeclaredMethods()) { - addMapping(implementation, method, method); + private Method findMethod(Class clazz, Method method) { + try { + return clazz.getDeclaredMethod(method.getName(), method.getParameterTypes()); + } catch (NoSuchMethodException nsme) { + if (clazz == Object.class) { + throw new IllegalArgumentException("Cannot construct proxy", nsme); } + return findMethod(clazz.getSuperclass(), method); + } + } - // the highest superclass (Object) was scanned, end the loop - if (clazz == Object.class) break; + private void fetchAllInterfaces(Class interfaceClass, List> list) { + list.add(interfaceClass); - // check also superclass - clazz = clazz.getSuperclass(); + for (final Class superInterface : interfaceClass.getInterfaces()) { + fetchAllInterfaces(superInterface, list); } + } + + private List> fetchAllInterfaces(Class interfaceClass) { + final List> output = new LinkedList<>(); + fetchAllInterfaces(interfaceClass, output); + return output; + } - // second check the state interface. It has higher priority, could rewrite previous mapping + private void initMapping() { + final Class implementationClass = implementation.getClass(); + final Map byName = new HashMap<>(); + + // first check the state interface. It has higher priority, could rewrite previous mapping for (final Method method : ClassOrDefaultProxyState.class.getDeclaredMethods()) { - addMapping(this, method, method); + final EndPoint endPoint = addMapping(this, method, method); + byName.put(ObjectUtil.getMethodIdentifier(method), endPoint); + } + + // second map methods of target + for (Class partInterfaceClass : fetchAllInterfaces(interfaceClass)) { + for (final Method caller : partInterfaceClass.getDeclaredMethods()) { + // ignore methods of frameworks created during execution + if (caller.isSynthetic()) continue; + + // try to find by name - avoid to multiple implementation for same method name, ie. getImplementationClass (proxy vs. implementation) + final EndPoint oldEndPoint = byName.get(ObjectUtil.getMethodIdentifier(caller)); + if (oldEndPoint != null) { + // use same mapping like previous matching + mapping.put(caller, oldEndPoint); + } else { + // find it in implementation and make a mapping + try { + final Method callee = findMethod(implementationClass, caller); + final EndPoint newEndPoint = addMapping(implementation, caller, callee); + byName.put(ObjectUtil.getMethodIdentifier(caller), newEndPoint); + } catch (Exception e) { + throw new IllegalArgumentException("Method " + ObjectUtil.getMethodIdentifier(caller) + " was not found on " + partInterfaceClass); + } + } + } } } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - final String methodKey = ObjectUtil.getMethodIdentifier(method); - final EndPoint endPoint = mapping.get(methodKey); + final EndPoint endPoint = mapping.get(method); if (endPoint == null) { - throw new NoSuchMethodException(String.format("Cannot found method %s", endPoint)); + throw new NoSuchMethodException(String.format("Cannot found method %s", method)); } return endPoint.invoke(args); diff --git a/config/local/api-defs/staticclient.yml b/config/local/api-defs/staticclient.yml index ba622a9aaa..bd0991c1ab 100644 --- a/config/local/api-defs/staticclient.yml +++ b/config/local/api-defs/staticclient.yml @@ -52,6 +52,31 @@ services: gatewayUrl: api/v1 version: 1.0.0 + - serviceId: staticclient2 # unique lowercase ID of the service + authentication: + scheme: httpBasicPassTicket # This service expects credentials in HTTP basic scheme with a PassTicket + applid: TSTAPPL2 # APPLID to generate PassTickets for this service + catalogUiTileId: static # ID of the API Catalog UI tile (visual grouping of the services) + title: Staticaly Defined Service 2 # Title of the service in the API catalog + description: Sample to demonstrate how to add an API service without Swagger documentation to API Catalog using a static YAML definition # Description of the service in the API catalog + instanceBaseUrls: # list of base URLs for each instance + - https://localhost:20012/discoverableclient # scheme://hostname:port/contextPath + homePageRelativeUrl: # Normally used for informational purposes for other services to use it as a landing page + statusPageRelativeUrl: /application/info # Appended to the instanceBaseUrl + healthCheckRelativeUrl: /application/health # Appended to the instanceBaseUrl + routes: + - gatewayUrl: api/v1 # [api/ui/ws]/v{majorVersion} + serviceRelativeUrl: /api/v1 # relativePath that is added to baseUrl of an instance + - gatewayUrl: ui/v1 + serviceRelativeUrl: / + - gatewayUrl: ws/v1 + serviceRelativeUrl: /ws + # List of APIs provided by the service (currenly only one is supported): + apiInfo: + - apiId: org.zowe.discoverableclient + gatewayUrl: api/v1 + version: 1.0.0 + # Proposal - Additional metadata that will be added to existing dynamically registered services: additionalServiceMetadata: - serviceId: staticclient # The staticclient service metadata will be extended diff --git a/config/local/gateway-service.yml b/config/local/gateway-service.yml index ac26390c5e..a9350fde50 100644 --- a/config/local/gateway-service.yml +++ b/config/local/gateway-service.yml @@ -9,6 +9,8 @@ apiml: auth: provider: dummy zosmfServiceId: zosmf # Replace me with the correct z/OSMF service id + passTicket: + timeout: 360 # [s] - default timeout to expire (z/OS has 10 mins as default) ssl: verifySslCertificatesOfServices: true banner: console @@ -17,6 +19,9 @@ spring: output: ansi: enabled: always + cache: + ehcache: + config: classpath:ehcache.xml server: ssl: diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/Service.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/Service.java index e609f13fca..1e02fb7fb0 100644 --- a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/Service.java +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/Service.java @@ -9,6 +9,7 @@ */ package com.ca.mfaas.discovery.staticdef; +import com.ca.apiml.security.common.auth.Authentication; import com.ca.mfaas.config.ApiInfo; import com.fasterxml.jackson.annotation.JsonAlias; import lombok.Data; diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/ServiceDefinitionProcessor.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/ServiceDefinitionProcessor.java index be71760cf7..a437e4843e 100644 --- a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/ServiceDefinitionProcessor.java +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/ServiceDefinitionProcessor.java @@ -9,6 +9,8 @@ */ package com.ca.mfaas.discovery.staticdef; +import com.ca.apiml.security.common.auth.Authentication; +import com.ca.apiml.security.common.auth.AuthenticationScheme; import com.ca.mfaas.config.ApiInfo; import com.ca.mfaas.eurekaservice.client.util.EurekaMetadataParser; import com.ca.mfaas.exception.MetadataValidationException; @@ -195,7 +197,7 @@ private List createInstances(StaticRegistrationResult context, Str final List output = new ArrayList<>(service.getInstanceBaseUrls().size()); for (final String instanceBaseUrl : service.getInstanceBaseUrls()) { final InstanceInfo instanceInfo = buildInstanceInfo(context, service, tile, instanceBaseUrl); - if (instanceBaseUrl != null) output.add(instanceInfo); + if (instanceInfo != null) output.add(instanceInfo); } return output; diff --git a/gateway-service/build.gradle b/gateway-service/build.gradle index 9d780d4966..f956bbeef2 100644 --- a/gateway-service/build.gradle +++ b/gateway-service/build.gradle @@ -48,6 +48,8 @@ dependencies { compile libraries.spring_cloud_starter_ribbon compile libraries.jetty_websocket_client compile libraries.jjwt + compile libraries.spring_boot_starter_cache + compile libraries.eh_cache compileOnly libraries.lombok diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/GatewayApplication.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/GatewayApplication.java index 67582dc814..a99a0014b3 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/GatewayApplication.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/GatewayApplication.java @@ -37,7 +37,8 @@ "com.ca.mfaas.gateway", "com.ca.mfaas.product", "com.ca.mfaas.enable", - "com.ca.apiml.security.common"}, + "com.ca.apiml.security.common" + }, excludeFilters = { @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*RibbonConfig")}) @RibbonClients(defaultConfiguration = GatewayRibbonConfig.class) diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/config/CacheConfig.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/config/CacheConfig.java new file mode 100644 index 0000000000..4370f7996e --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/config/CacheConfig.java @@ -0,0 +1,40 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.config; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.ehcache.EhCacheCacheManager; +import org.springframework.cache.ehcache.EhCacheManagerFactoryBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; + +/** + * Spring configuration to use EhCache. This context is using from application and also from tests. + */ +@EnableCaching +@Configuration +public class CacheConfig { + + @Bean + public CacheManager cacheManager() { + return new EhCacheCacheManager(ehCacheCacheManager().getObject()); + } + + @Bean + public EhCacheManagerFactoryBean ehCacheCacheManager() { + EhCacheManagerFactoryBean cmfb = new EhCacheManagerFactoryBean(); + cmfb.setConfigLocation(new ClassPathResource("ehcache.xml")); + cmfb.setShared(true); + return cmfb; + } + +} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/AuthController.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/AuthController.java new file mode 100644 index 0000000000..4a35c3522b --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/AuthController.java @@ -0,0 +1,35 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.controllers; + +import com.ca.mfaas.gateway.security.service.AuthenticationService; +import lombok.AllArgsConstructor; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; + +/** + * Controller offer method to control security. It can contains method for user and also method for calling services + * by gateway to distribute state of authentication between nodes. + */ +@AllArgsConstructor +@RestController("/auth") +public class AuthController { + + private final AuthenticationService authenticationService; + + @DeleteMapping("/invalidate/**") + public Boolean invalidateJwtToken(HttpServletRequest request) { + final String jwtToken = request.getRequestURI().split(request.getContextPath() + "/invalidate/")[1]; + return authenticationService.invalidateJwtToken(jwtToken, false); + } + +} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilter.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilter.java new file mode 100644 index 0000000000..c56ef80a2f --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilter.java @@ -0,0 +1,67 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.filters.pre; + +import com.ca.apiml.security.common.token.TokenAuthentication; +import com.ca.mfaas.gateway.security.service.ServiceAuthenticationService; +import com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand; +import com.netflix.zuul.ZuulFilter; +import com.netflix.zuul.context.RequestContext; +import org.springframework.beans.factory.annotation.Autowired; + +import java.security.Principal; + +import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.*; + +/** + * This filter is responsible for customization request to clients from security point of view. In this filter is + * fetched AuthenticationCommand which support target security. In case it is possible decide now (all instances + * use the same authentication) it will modify immediately. Otherwise in request params will be set a command to + * load balancer. The request will be modified after specific instance will be selected. + */ +public class ServiceAuthenticationFilter extends ZuulFilter { + + @Autowired + private ServiceAuthenticationService serviceAuthenticationService; + + @Override + public String filterType() { + return PRE_TYPE; + } + + @Override + public int filterOrder() { + return PRE_DECORATION_FILTER_ORDER + 4; + } + + @Override + public boolean shouldFilter() { + return true; + } + + @Override + public Object run() { + final RequestContext context = RequestContext.getCurrentContext(); + + String jwtToken = null; + final Principal principal = RequestContext.getCurrentContext().getRequest().getUserPrincipal(); + if (principal instanceof TokenAuthentication) { + jwtToken = ((TokenAuthentication) principal).getCredentials(); + } + + final String serviceId = (String) context.get(SERVICE_ID_KEY); + + final AuthenticationCommand cmd = serviceAuthenticationService.getAuthenticationCommand(serviceId, jwtToken); + cmd.apply(null); + + return null; + } + +} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonConfig.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonConfig.java index c4a0106646..236dd96ca1 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonConfig.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonConfig.java @@ -10,6 +10,7 @@ package com.ca.mfaas.gateway.ribbon; import com.netflix.client.config.IClientConfig; +import com.netflix.discovery.EurekaClient; import org.apache.http.impl.client.CloseableHttpClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.netflix.ribbon.ServerIntrospector; @@ -25,7 +26,10 @@ public class GatewayRibbonConfig { public RibbonLoadBalancingHttpClient ribbonLoadBalancingHttpClient( CloseableHttpClient secureHttpClient, IClientConfig config, - ServerIntrospector serverIntrospector) { - return new GatewayRibbonLoadBalancingHttpClient(secureHttpClient, config, serverIntrospector); + ServerIntrospector serverIntrospector, + EurekaClient discoveryClient + ) { + return new GatewayRibbonLoadBalancingHttpClient(secureHttpClient, config, serverIntrospector, discoveryClient); } + } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java index e1fcde5edd..99b0649bae 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java @@ -9,16 +9,26 @@ */ package com.ca.mfaas.gateway.ribbon; +import com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand; import com.netflix.appinfo.InstanceInfo; import com.netflix.client.config.CommonClientConfigKey; import com.netflix.client.config.IClientConfig; +import com.netflix.discovery.EurekaClient; import com.netflix.loadbalancer.Server; +import com.netflix.loadbalancer.reactive.ExecutionContext; +import com.netflix.loadbalancer.reactive.ExecutionInfo; +import com.netflix.loadbalancer.reactive.ExecutionListener; +import com.netflix.loadbalancer.reactive.LoadBalancerCommand; import com.netflix.niws.loadbalancer.DiscoveryEnabledServer; +import com.netflix.zuul.context.RequestContext; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; import org.apache.http.HttpResponse; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.impl.client.CloseableHttpClient; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; import org.springframework.cloud.netflix.ribbon.ServerIntrospector; import org.springframework.cloud.netflix.ribbon.apache.RibbonApacheHttpRequest; import org.springframework.cloud.netflix.ribbon.apache.RibbonApacheHttpResponse; @@ -26,7 +36,10 @@ import org.springframework.web.util.UriComponentsBuilder; import java.net.URI; +import java.util.Collections; +import java.util.List; +import static com.ca.mfaas.gateway.security.service.ServiceAuthenticationService.AUTHENTICATION_COMMAND_KEY; import static org.springframework.cloud.netflix.ribbon.RibbonUtils.updateToSecureConnectionIfNeeded; @Slf4j @@ -35,14 +48,17 @@ public class GatewayRibbonLoadBalancingHttpClient extends RibbonLoadBalancingHtt private static final String HTTPS = "https"; private static final String HTTP = "http"; + private final EurekaClient discoveryClient; + /** * Ribbon load balancer * @param secureHttpClient custom http client for our certificates * @param config configuration details * @param serverIntrospector introspector */ - public GatewayRibbonLoadBalancingHttpClient(CloseableHttpClient secureHttpClient, IClientConfig config, ServerIntrospector serverIntrospector) { + public GatewayRibbonLoadBalancingHttpClient(CloseableHttpClient secureHttpClient, IClientConfig config, ServerIntrospector serverIntrospector, EurekaClient discoveryClient) { super(secureHttpClient, config, serverIntrospector); + this.discoveryClient = discoveryClient; } @Override @@ -72,11 +88,7 @@ public URI reconstructURIWithServer(Server server, URI original) { @Override public RibbonApacheHttpResponse execute(RibbonApacheHttpRequest request, IClientConfig configOverride) throws Exception { RibbonApacheHttpRequest sendRequest = null; - if (HTTPS.equals(request.getURI().getScheme())) { - configOverride.set(CommonClientConfigKey.IsSecure, true); - } else { - configOverride.set(CommonClientConfigKey.IsSecure, false); - } + configOverride.set(CommonClientConfigKey.IsSecure, HTTPS.equals(request.getURI().getScheme())); final RequestConfig.Builder builder = RequestConfig.custom(); builder.setConnectTimeout(configOverride.get( CommonClientConfigKey.ConnectTimeout, this.connectTimeout)); @@ -98,4 +110,87 @@ public RibbonApacheHttpResponse execute(RibbonApacheHttpRequest request, IClient final HttpResponse httpResponse = this.delegate.execute(httpUriRequest); return new RibbonApacheHttpResponse(httpResponse, httpUriRequest.getURI()); } + + /** + * This methods write specific InstanceInfo into cache + * @param serviceId serviceId of instance + * @param instanceId ID of instance + * @param instanceInfo instanceInfo to store + * @return cached instanceInfo object + */ + @CachePut(value = "instanceInfoByInstanceId", key = "{#serviceId, #instanceId}") + public InstanceInfo putInstanceInfo(String serviceId, String instanceId, InstanceInfo instanceInfo) { + return instanceInfo; + } + + /** + * Get the InstanceInfo by id. For searching is used serviceId. It method found another instances it will + * cache them for next using + * @param serviceId service to call + * @param instanceId selected instance of service + * @return instance with matching service and instanceId + */ + @Cacheable("instanceInfoByInstanceId") + public InstanceInfo getInstanceInfo(String serviceId, String instanceId) { + InstanceInfo output = null; + for (final InstanceInfo instanceInfo : (List) discoveryClient.getInstancesById(serviceId)) { + if (StringUtils.equals(instanceId, instanceInfo.getInstanceId())) { + // found instance, store it for output + output = instanceInfo; + } + + /* + * Getting all instance is pretty heavy, cache them, therefor it is very probably, nex using of service + * will use different instance and need to find it too. + */ + putInstanceInfo(serviceId, instanceInfo.getInstanceId(), instanceInfo); + } + return output; + } + + @Override + protected void customizeLoadBalancerCommandBuilder(RibbonApacheHttpRequest request, IClientConfig config, LoadBalancerCommand.Builder builder) { + super.customizeLoadBalancerCommandBuilder(request, config, builder); + + /* + * add into builder listener to work with request immediatelly when instance if selected + * it is helpfull for selecting {@com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand} in + * case there is multiple instances with same serviceId which have different authentication. Therefor it + * is necessary wait for selection of instance to apply right authentication command. + */ + builder.withListeners(Collections.singletonList(new ExecutionListener() { + + @Override + public void onExecutionStart(ExecutionContext context) throws AbortExecutionException { + // dont needed yet + } + + @Override + public void onStartWithServer(ExecutionContext context, ExecutionInfo info) throws AbortExecutionException { + final AuthenticationCommand cmd = (AuthenticationCommand) RequestContext.getCurrentContext().get(AUTHENTICATION_COMMAND_KEY); + if (cmd != null) { + // in context is a command, it means update of authentication is waiting for select an instance + final Server.MetaInfo metaInfo = info.getServer().getMetaInfo(); + final InstanceInfo instanceInfo = getInstanceInfo(metaInfo.getServiceIdForDiscovery(), metaInfo.getInstanceId()); + cmd.apply(instanceInfo); + } + } + + @Override + public void onExceptionWithServer(ExecutionContext context, Throwable exception, ExecutionInfo info) { + // dont needed yet + } + + @Override + public void onExecutionSuccess(ExecutionContext context, RibbonApacheHttpResponse response, ExecutionInfo info) { + // dont needed yet + } + + @Override + public void onExecutionFailed(ExecutionContext context, Throwable finalException, ExecutionInfo info) { + // dont needed yet + } + })); + builder.withExecutionContext(new ExecutionContext(request, config, config, null)); + } } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/routing/ApimlRoutingConfig.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/routing/ApimlRoutingConfig.java index d3d0b1ea75..178ea9ce30 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/routing/ApimlRoutingConfig.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/routing/ApimlRoutingConfig.java @@ -13,6 +13,7 @@ import com.ca.mfaas.gateway.filters.post.ConvertAuthTokenInUriToCookieFilter; import com.ca.mfaas.gateway.filters.post.PageRedirectionFilter; import com.ca.mfaas.gateway.filters.pre.LocationFilter; +import com.ca.mfaas.gateway.filters.pre.ServiceAuthenticationFilter; import com.ca.mfaas.gateway.filters.pre.SlashFilter; import com.ca.mfaas.gateway.filters.pre.ZosmfFilter; import com.ca.mfaas.gateway.security.service.AuthenticationService; @@ -48,6 +49,11 @@ public ZosmfFilter zosmfFilter(AuthenticationService authenticationService) { return new ZosmfFilter(authenticationService); } + @Bean + public ServiceAuthenticationFilter serviceAuthenticationFilter() { + return new ServiceAuthenticationFilter(); + } + @Bean @Autowired public PageRedirectionFilter pageRedirectionFilter(DiscoveryClient discovery, diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/config/SecurityConfiguration.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/config/SecurityConfiguration.java index c9fc2c8c7f..3251dfaec2 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/config/SecurityConfiguration.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/config/SecurityConfiguration.java @@ -27,6 +27,7 @@ import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.logout.LogoutHandler; /** * Security configuration for Gateway @@ -75,6 +76,12 @@ protected void configure(HttpSecurity http) throws Exception { .authorizeRequests() .antMatchers(HttpMethod.POST, authConfigurationProperties.getGatewayLoginEndpoint()).permitAll() + // logout endpoint + .and() + .logout() + .logoutUrl(authConfigurationProperties.getServiceLogoutEndpoint()) + .addLogoutHandler(logoutHandler()) + // endpoint protection .and() .authorizeRequests() @@ -136,4 +143,11 @@ private CookieContentFilter cookieFilter() throws Exception { authConfigurationProperties, PROTECTED_ENDPOINTS); } + + private LogoutHandler logoutHandler() { + return (request, response, authentication) -> authenticationService.getJwtTokenFromRequest(request) + .ifPresent(x -> + authenticationService.invalidateJwtToken(x, true) + ); + } } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java index 3583125455..6679d6b001 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java @@ -15,21 +15,29 @@ import com.ca.apiml.security.common.token.TokenExpireException; import com.ca.apiml.security.common.token.TokenNotValidException; import com.ca.mfaas.constants.ApimlConstants; +import com.netflix.appinfo.InstanceInfo; +import com.netflix.discovery.EurekaClient; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import javax.annotation.PostConstruct; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; -import java.util.Arrays; -import java.util.Date; -import java.util.Optional; -import java.util.UUID; +import java.util.*; /** * Service for the JWT and LTPA tokens operations @@ -37,12 +45,25 @@ @Slf4j @Service @RequiredArgsConstructor +@Scope( proxyMode = ScopedProxyMode.TARGET_CLASS ) +@EnableAspectJAutoProxy(proxyTargetClass = true) public class AuthenticationService { private static final String LTPA_CLAIM_NAME = "ltpa"; private static final String DOMAIN_CLAIM_NAME = "dom"; + private final ApplicationContext applicationContext; private final AuthConfigurationProperties authConfigurationProperties; private final JwtSecurityInitializer jwtSecurityInitializer; + private final EurekaClient discoveryClient; + private final RestTemplate restTemplate; + + // to force calling inside methods with aspects - ie. ehCache aspect + private AuthenticationService meAsProxy; + + @PostConstruct + public void afterPropertiesSet() { + meAsProxy = applicationContext.getBean(AuthenticationService.class); + } /** * Create the JWT token and set the LTPA token, the expiration time, the domain, the subject, the date of issue, the issuer and the id. @@ -70,22 +91,54 @@ public String createJwtToken(String username, String domain, String ltpaToken) { } /** - * Validate the JWT token - * - * @param token the JWT token - * @return the {@link TokenAuthentication} object containing username and valid JWT token - * @throws TokenExpireException if the token is expired - * @throws TokenNotValidException if the token is not valid + * Method will invalidate jwtToken. It could be called from two reasons: + * - on logout phase (distribute = true) + * - from another gateway instance to notify about change (distribute = false) + * @param jwtToken token to invalidated + * @param distribute distribute invalidation to another instances? + * @return state of invalidate (true - token was invalidated) */ - public TokenAuthentication validateJwtToken(TokenAuthentication token) { + @CacheEvict(value = "validationJwtToken", key = "#jwtToken") + @Cacheable(value = "invalidatedJwtTokens", key = "#jwtToken", condition = "#jwtToken != null") + public Boolean invalidateJwtToken(String jwtToken, boolean distribute) { + /* + * until ehCache is not distributed, send to other instances invalidation request + */ + if (distribute) { + final String myInstanceId = discoveryClient.getApplicationInfoManager().getInfo().getInstanceId(); + for (final InstanceInfo instanceInfo : (List) discoveryClient.getInstancesById("gateway")) { + if (StringUtils.equals(myInstanceId, instanceInfo.getInstanceId())) continue; + + final String url; + if (instanceInfo.getSecurePort() == 0) { + url = "http://" + instanceInfo.getIPAddr() + ":" + instanceInfo.getPort() + "/auth/invalidate/{}"; + } else { + url = "https://" + instanceInfo.getIPAddr() + ":" + instanceInfo.getSecurePort() + "/auth/invalidate/{}"; + } + restTemplate.delete(url, jwtToken); + } + } + + return Boolean.TRUE; + } + + @Cacheable(value = "invalidatedJwtTokens", unless = "true", key = "#jwtToken", condition = "#jwtToken != null") + public Boolean isInvalidated(String jwtToken) { + return Boolean.FALSE; + } + + @Cacheable(value = "validationJwtToken", key = "#jwtToken", condition = "#jwtToken != null") + public TokenAuthentication validateJwtToken(String jwtToken) { try { Claims claims = Jwts.parser() .setSigningKey(jwtSecurityInitializer.getJwtPublicKey()) - .parseClaimsJws(token.getCredentials()) + .parseClaimsJws(jwtToken) .getBody(); - TokenAuthentication validTokenAuthentication = new TokenAuthentication(claims.getSubject(), token.getCredentials()); - validTokenAuthentication.setAuthenticated(true); + TokenAuthentication validTokenAuthentication = new TokenAuthentication(claims.getSubject(), jwtToken); + // without a proxy cache aspect is not working, thus it is necessary get bean from application context + final boolean authenticated = !meAsProxy.isInvalidated(jwtToken); + validTokenAuthentication.setAuthenticated(authenticated); return validTokenAuthentication; } catch (ExpiredJwtException exception) { @@ -100,6 +153,18 @@ public TokenAuthentication validateJwtToken(TokenAuthentication token) { } } + /** + * Validate the JWT token + * + * @param token the JWT token + * @return the {@link TokenAuthentication} object containing username and valid JWT token + * @throws TokenExpireException if the token is expired + * @throws TokenNotValidException if the token is not valid + */ + public TokenAuthentication validateJwtToken(TokenAuthentication token) { + return validateJwtToken(Optional.ofNullable(token).map(TokenAuthentication::getCredentials).orElse(null)); + } + /** * Parse the JWT token and return a {@link QueryResponse} object containing the domain, user id, date of creation and date of expiration * @@ -203,4 +268,5 @@ private long calculateExpiration(long now, String username) { return expiration; } + } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationService.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationService.java new file mode 100644 index 0000000000..038360eb19 --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationService.java @@ -0,0 +1,159 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.security.service; + +import com.ca.apiml.security.common.auth.Authentication; +import com.ca.apiml.security.common.auth.AuthenticationScheme; +import com.ca.apiml.security.common.token.QueryResponse; +import com.ca.mfaas.gateway.security.service.schema.AbstractAuthenticationScheme; +import com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand; +import com.ca.mfaas.gateway.security.service.schema.AuthenticationSchemeFactory; +import com.netflix.appinfo.InstanceInfo; +import com.netflix.discovery.EurekaClient; +import com.netflix.zuul.context.RequestContext; +import lombok.AllArgsConstructor; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.Map; + +import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_APPLID; +import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_SCHEME; + +/** + * This bean is responsible for "translating" security to specific service. It decorate request with security data for + * specific service. Implementation of security updates are defined with beans extending + * {@link com.ca.mfaas.gateway.security.service.schema.AbstractAuthenticationScheme}. + * + * The main idea of this bean is to create command + * {@link com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand}. Command is object which update the + * request for specific scheme (defined by service). This bean is responsible for getting the right command. If it is + * possible to decide now for just one scheme type, bean return this command to update request immediatelly. Otherwise + * it returns {@link LoadBalancerAuthenticationCommand}. This command write in the ZUUL context + * UniversalAuthenticationCommand, which is used in Ribbon loadbalancer. There is a listener which work with this value. + * After load balancer will decide which instance will be used, universal command is called and update the request. + * + * All those operation are cached: + * - serviceAuthenticationByAuthentication + * - it caches command which can be deside only by serviceId (in pre filter) + * - serviceAuthenticationByAuthentication + * - it caches commands by {@link com.ca.apiml.security.common.auth.Authentication}, it could be in pre filters and + * also in loadbalancer + */ +@Service +@AllArgsConstructor +public class ServiceAuthenticationService { + + public static final String AUTHENTICATION_COMMAND_KEY = "zoweAuthenticationCommand"; + + private final LoadBalancerAuthenticationCommand loadBalancerCommand = new LoadBalancerAuthenticationCommand(); + + private final EurekaClient discoveryClient; + private final AuthenticationSchemeFactory authenticationSchemeFactory; + private final AuthenticationService authenticationService; + + protected Authentication getAuthentication(InstanceInfo instanceInfo) { + final Map metadata = instanceInfo.getMetadata(); + + final Authentication out = new Authentication(); + out.setApplid(metadata.get(AUTHENTICATION_APPLID)); + out.setScheme(AuthenticationScheme.fromScheme(metadata.get(AUTHENTICATION_SCHEME))); + return out; + } + + /** + * This method is only for testing purpose, to be set authenticationService in inner classes + * @return reference for AuthenticationService + */ + protected AuthenticationService getAuthenticationService() { + return authenticationService; + } + + @CacheEvict(value = "serviceAuthenticationByAuthentication", condition = "#result != null && #result.isExpired()") + @Cacheable("serviceAuthenticationByAuthentication") + public AuthenticationCommand getAuthenticationCommand(Authentication authentication, String jwtToken) { + final AbstractAuthenticationScheme scheme = authenticationSchemeFactory.getSchema(authentication.getScheme()); + final QueryResponse queryResponse = authenticationService.parseJwtToken(jwtToken); + return scheme.createCommand(authentication, queryResponse); + } + + @CacheEvict(value = "serviceAuthenticationByServiceId", condition = "#result != null && #result.isExpired()") + @Cacheable("serviceAuthenticationByServiceId") + public AuthenticationCommand getAuthenticationCommand(String serviceId, String jwtToken) { + final List instances = discoveryClient.getInstancesById(serviceId); + + Authentication found = null; + for (final InstanceInfo instance : instances) { + final Authentication auth = getAuthentication(instance); + + if (found == null) { + // this is the first record + found = auth; + } else if (!found.equals(auth)) { + // if next record is different, authentication cannot be determinated before load balancer + return loadBalancerCommand; + } + } + + // if no instance exist, do nothing + if (found == null) return AuthenticationCommand.EMPTY; + + return getAuthenticationCommand(found, jwtToken); + } + + public class UniversalAuthenticationCommand extends AuthenticationCommand { + + private static final long serialVersionUID = -2980076158001292742L; + + protected UniversalAuthenticationCommand() {} + + @Override + public void apply(InstanceInfo instanceInfo) { + if (instanceInfo == null) throw new NullPointerException("Argument instanceInfo is required"); + + final Authentication auth = getAuthentication(instanceInfo); + final HttpServletRequest request = RequestContext.getCurrentContext().getRequest(); + + final String jwtToken = getAuthenticationService().getJwtTokenFromRequest(request).get(); + + final AuthenticationCommand cmd = getAuthenticationCommand(auth, jwtToken); + cmd.apply(null); + } + + @Override + public boolean isExpired() { + return false; + } + } + + public class LoadBalancerAuthenticationCommand extends AuthenticationCommand { + + private static final long serialVersionUID = 3363375706967769113L; + + private final UniversalAuthenticationCommand universal = new UniversalAuthenticationCommand(); + + protected LoadBalancerAuthenticationCommand() {} + + @Override + public void apply(InstanceInfo instanceInfo) { + RequestContext.getCurrentContext().put(AUTHENTICATION_COMMAND_KEY, universal); + } + + @Override + public boolean isExpired() { + return false; + } + + } + +} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AbstractAuthenticationScheme.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AbstractAuthenticationScheme.java new file mode 100644 index 0000000000..f6c72bf01e --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AbstractAuthenticationScheme.java @@ -0,0 +1,46 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.security.service.schema; + +import com.ca.apiml.security.common.auth.Authentication; +import com.ca.apiml.security.common.auth.AuthenticationScheme; +import com.ca.apiml.security.common.token.QueryResponse; + +/** + * This is abstract class for any processor which support service's authentication. They are called from ZUUL filters + * to decorate request for target services. + * + * For each type of scheme should exist right one implementation. + */ +public interface AbstractAuthenticationScheme { + + /** + * @return Scheme which is supported by this component + */ + public AuthenticationScheme getScheme(); + + /** + * This method decorate the request for target service + * + * @param authentication DTO describing details about authentication + * @param token User's parsed (Zowe's) JWT token + */ + public AuthenticationCommand createCommand(Authentication authentication, QueryResponse token); + + /** + * Define implementation, which will be use in case no scheme is defined. + * + * @return true if this implementation is default, otherwise false + */ + public default boolean isDefault() { + return false; + } + +} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationCommand.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationCommand.java new file mode 100644 index 0000000000..33e93dea2a --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationCommand.java @@ -0,0 +1,50 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.security.service.schema; + +import com.ca.mfaas.cache.EntryExpiration; +import com.netflix.appinfo.InstanceInfo; + +import java.io.Serializable; + +/** + * This command represented a code, which distribute right access right to a service. Gateway translates requests + * to a service and by login in there generate or translate authentication to the service. + * + * Responsible for this translation is filter {@link com.ca.mfaas.gateway.filters.pre.ServiceAuthenticationFilter} + */ +public abstract class AuthenticationCommand implements EntryExpiration, Serializable { + + private static final long serialVersionUID = -4519869709905127608L; + + public static final AuthenticationCommand EMPTY = new AuthenticationCommand() { + + private static final long serialVersionUID = 5280496524123534478L; + + @Override + public void apply(InstanceInfo instanceInfo) { + // do nothing + } + + @Override + public boolean isExpired() { + return false; + } + }; + + /** + * Apply the command, if it is necessary, it is possible to use a specific instance for execution. This is + * using for loadBalancer command, where are not available all information in step of command creation. + * In all other case call apply(null). + * @param instanceInfo Specific instanceIf if it is needed + */ + public abstract void apply(InstanceInfo instanceInfo); + +} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactory.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactory.java new file mode 100644 index 0000000000..c915004b21 --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactory.java @@ -0,0 +1,97 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.security.service.schema; + +import com.ca.apiml.security.common.auth.Authentication; +import com.ca.apiml.security.common.auth.AuthenticationScheme; +import com.ca.apiml.security.common.token.QueryResponse; +import com.ca.mfaas.gateway.security.service.AuthenticationService; +import com.netflix.zuul.context.RequestContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.servlet.http.HttpServletRequest; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +/** + * This method is responsible for servering beans which can create AuthenticationCommand. This bean collect all scheme's + * beans (one for each defined scheme). + * On start bean check if all scheme's beans are valid (exactly one default bean, duplicities - by scheme type) + */ +@Service +public class AuthenticationSchemeFactory { + + private final AbstractAuthenticationScheme defaultScheme; + private final Map map; + + private AuthenticationService authenticationService; + + public AuthenticationSchemeFactory(@Autowired AuthenticationService authenticationService, @Autowired List schemes) { + this.authenticationService = authenticationService; + + map = new EnumMap<>(AuthenticationScheme.class); + + AbstractAuthenticationScheme foundDefaultScheme = null; + + // map beans to map, checking duplicity and find exactly one default bean, otherwise throw exception + for (final AbstractAuthenticationScheme aas : schemes) { + final AbstractAuthenticationScheme prev = map.put(aas.getScheme(), aas); + + if (prev != null) { + throw new IllegalArgumentException("Multiple beans for scheme " + aas.getScheme() + + " : " + prev.getScheme() + " x " + aas.getScheme()); + } + + if (aas.isDefault()) { + if (foundDefaultScheme != null) { + throw new IllegalArgumentException("Multiple scheme's beans are marked as default : " + + foundDefaultScheme.getScheme() + " x " + aas.getScheme()); + } + + foundDefaultScheme = aas; + } + } + + if (foundDefaultScheme == null) { + throw new IllegalArgumentException("No scheme marked as default"); + } + + this.defaultScheme = foundDefaultScheme; + } + + public AbstractAuthenticationScheme getSchema(AuthenticationScheme scheme) { + if (scheme == null) return defaultScheme; + + final AbstractAuthenticationScheme output = map.get(scheme); + if (output == null) { + throw new IllegalArgumentException("Unknown scheme : " + scheme); + } + return output; + } + + public AuthenticationCommand getAuthenticationCommand(Authentication authentication) { + final AbstractAuthenticationScheme scheme; + if ((authentication == null) || (authentication.getScheme() == null)) { + scheme = defaultScheme; + } else { + scheme = getSchema(authentication.getScheme()); + } + + final HttpServletRequest request = RequestContext.getCurrentContext().getRequest(); + final QueryResponse jwtQr = authenticationService.getJwtTokenFromRequest(request) + .map(x -> authenticationService.parseJwtToken(x)) + .orElse(null); + + return scheme.createCommand(authentication, jwtQr); + } + +} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ByPassScheme.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ByPassScheme.java new file mode 100644 index 0000000000..d292f6e9ab --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ByPassScheme.java @@ -0,0 +1,37 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.security.service.schema; + +import com.ca.apiml.security.common.auth.Authentication; +import com.ca.apiml.security.common.auth.AuthenticationScheme; +import com.ca.apiml.security.common.token.QueryResponse; +import org.springframework.stereotype.Component; + +/** + * Default scheme, just forward, don't set anything. + */ +@Component +public class ByPassScheme implements AbstractAuthenticationScheme { + + @Override + public AuthenticationScheme getScheme() { + return AuthenticationScheme.BYPASS; + } + + @Override + public AuthenticationCommand createCommand(Authentication authentication, QueryResponse token) { + return AuthenticationCommand.EMPTY; + } + + @Override + public boolean isDefault() { + return true; + } +} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketScheme.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketScheme.java new file mode 100644 index 0000000000..e1b4923a87 --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketScheme.java @@ -0,0 +1,82 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.security.service.schema; + +import com.ca.apiml.security.common.auth.Authentication; +import com.ca.apiml.security.common.auth.AuthenticationScheme; +import com.ca.apiml.security.common.service.PassTicketService; +import com.ca.apiml.security.common.token.QueryResponse; +import com.netflix.appinfo.InstanceInfo; +import com.netflix.zuul.context.RequestContext; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import org.apache.http.HttpHeaders; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * This bean support PassTicket. Bean is responsible for getting Passticket from RAC and generating new authentication + * header in request. + */ +@Component +@AllArgsConstructor +public class HttpBasicPassTicketScheme implements AbstractAuthenticationScheme { + + private final PassTicketService passTicketService; + + @Value("${apiml.security.auth.passTicket.timeout:600}") + private Integer timeout; + + @Override + public AuthenticationScheme getScheme() { + return AuthenticationScheme.HTTP_BASIC_PASSTICKET; + } + + @Override + public AuthenticationCommand createCommand(Authentication authentication, QueryResponse token) { + final long before = System.currentTimeMillis(); + + final String applId = authentication.getApplid(); + final String userId = token.getUserId(); + final String passTicket = passTicketService.generate(userId, applId); + final String encoded = Base64.getEncoder().encodeToString((userId + ":" + passTicket).getBytes(StandardCharsets.UTF_8)); + final String value = "Basic " + encoded; + + final long expiredAt = Math.min(before + timeout * 1000, token.getExpiration().getTime()); + + return new PassTicketCommand(value, expiredAt); + } + + @lombok.Value + @EqualsAndHashCode(callSuper = false) + public static class PassTicketCommand extends AuthenticationCommand { + + private static final long serialVersionUID = 3941300386857998443L; + + private final String authorizationValue; + private final long expireAt; + + @Override + public void apply(InstanceInfo instanceInfo) { + final RequestContext context = RequestContext.getCurrentContext(); + context.addZuulRequestHeader(HttpHeaders.AUTHORIZATION, authorizationValue); + } + + @Override + public boolean isExpired() { + return System.currentTimeMillis() > expireAt; + } + + } + +} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ZosmfScheme.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ZosmfScheme.java new file mode 100644 index 0000000000..15e6c29d14 --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ZosmfScheme.java @@ -0,0 +1,84 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.security.service.schema; + +import com.ca.apiml.security.common.auth.Authentication; +import com.ca.apiml.security.common.auth.AuthenticationScheme; +import com.ca.apiml.security.common.token.QueryResponse; +import com.ca.mfaas.gateway.security.service.AuthenticationService; +import com.netflix.appinfo.InstanceInfo; +import com.netflix.zuul.context.RequestContext; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.Optional; + +/** + * This bean provide LTPA token into request. It get LTPA from JWT token (value is set on logon) and distribute it as + * cookie. + */ +@Component +@AllArgsConstructor +public class ZosmfScheme implements AbstractAuthenticationScheme { + + private final AuthenticationService authenticationService; + + @Override + public AuthenticationScheme getScheme() { + return AuthenticationScheme.ZOSMF; + } + + @Override + public AuthenticationCommand createCommand(Authentication authentication, QueryResponse token) { + final Date expiration = token == null ? null : token.getExpiration(); + final Long expirationTime = expiration == null ? null : expiration.getTime(); + return new ZosmfCommand(expirationTime); + } + + @lombok.Value + @EqualsAndHashCode(callSuper = false) + public class ZosmfCommand extends AuthenticationCommand { + + private static final long serialVersionUID = 2284037230674275720L; + + public static final String COOKIE_HEADER = "cookie"; + + private final Long expireAt; + + @Override + public void apply(InstanceInfo instanceInfo) { + final RequestContext context = RequestContext.getCurrentContext(); + + Optional jwtToken = authenticationService.getJwtTokenFromRequest(context.getRequest()); + jwtToken.ifPresent(token -> { + String ltpaToken = authenticationService.getLtpaTokenFromJwtToken(token); + + String cookie = context.getZuulRequestHeaders().get(COOKIE_HEADER); + if (cookie != null) { + cookie += "; " + ltpaToken; + } else { + cookie = ltpaToken; + } + + context.addZuulRequestHeader(COOKIE_HEADER, cookie); + }); + } + + @Override + public boolean isExpired() { + if (expireAt == null) return false; + + return System.currentTimeMillis() > expireAt; + } + } + +} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ZoweJwtScheme.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ZoweJwtScheme.java new file mode 100644 index 0000000000..842d9c31a7 --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ZoweJwtScheme.java @@ -0,0 +1,30 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.security.service.schema; + +import com.ca.apiml.security.common.auth.Authentication; +import com.ca.apiml.security.common.auth.AuthenticationScheme; +import com.ca.apiml.security.common.token.QueryResponse; +import org.springframework.stereotype.Component; + +@Component +public class ZoweJwtScheme implements AbstractAuthenticationScheme { + + @Override + public AuthenticationScheme getScheme() { + return AuthenticationScheme.ZOWE_JWT; + } + + @Override + public AuthenticationCommand createCommand(Authentication authentication, QueryResponse token) { + return AuthenticationCommand.EMPTY; + } + +} diff --git a/gateway-service/src/main/resources/ehcache.xml b/gateway-service/src/main/resources/ehcache.xml new file mode 100644 index 0000000000..22907729fe --- /dev/null +++ b/gateway-service/src/main/resources/ehcache.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/AuthControllerTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/AuthControllerTest.java new file mode 100644 index 0000000000..bfd4092bba --- /dev/null +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/AuthControllerTest.java @@ -0,0 +1,54 @@ +package com.ca.mfaas.gateway.controllers;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import com.ca.mfaas.gateway.security.service.AuthenticationService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringJUnit4ClassRunner.class) +public class AuthControllerTest { + + private MockMvc mockMvc; + + @Mock + private AuthenticationService authenticationService; + + private AuthController authController; + + @Before + public void setUp() { + authController = new AuthController(authenticationService); + mockMvc = MockMvcBuilders.standaloneSetup(authController).build(); + } + + @Test + public void invalidateJwtToken() throws Exception { + when(authenticationService.invalidateJwtToken("a/b", false)).thenReturn(Boolean.TRUE); + this.mockMvc.perform(delete("/invalidate/a/b")).andExpect(status().isOk()).andExpect(content().string("true")); + + when(authenticationService.invalidateJwtToken("abcde", false)).thenReturn(Boolean.TRUE); + this.mockMvc.perform(delete("/invalidate/abcde")).andExpect(status().isOk()).andExpect(content().string("true")); + + this.mockMvc.perform(delete("/invalidate/xyz")).andExpect(status().isOk()).andExpect(content().string("false")); + + verify(authenticationService, times(1)).invalidateJwtToken("abcde", false); + verify(authenticationService, times(1)).invalidateJwtToken("a/b", false); + } +} diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java new file mode 100644 index 0000000000..59e664ac77 --- /dev/null +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java @@ -0,0 +1,70 @@ +package com.ca.mfaas.gateway.filters.pre;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import com.ca.apiml.security.common.token.TokenAuthentication; +import com.ca.mfaas.gateway.security.service.ServiceAuthenticationService; +import com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand; +import com.netflix.zuul.context.RequestContext; +import com.netflix.zuul.exception.ZuulException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import javax.servlet.http.HttpServletRequest; +import java.security.Principal; + +import static org.mockito.Mockito.*; +import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SERVICE_ID_KEY; + +@RunWith(MockitoJUnitRunner.class) +public class ServiceAuthenticationFilterTest { + + @Mock + private ServiceAuthenticationService serviceAuthenticationService; + + @InjectMocks + private ServiceAuthenticationFilter serviceAuthenticationFilter; + + @Mock + private AuthenticationCommand command; + + @Before + public void init() { + when(serviceAuthenticationService.getAuthenticationCommand(anyString(), any())).thenReturn(command); + } + + @Test + public void testRun() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getUserPrincipal()).thenReturn(new TokenAuthentication("user", "token")); + RequestContext requestContext = mock(RequestContext.class); + when(requestContext.getRequest()).thenReturn(request); + when(requestContext.get(SERVICE_ID_KEY)).thenReturn("service"); + RequestContext.testSetCurrentContext(requestContext); + + serviceAuthenticationFilter.run(); + verify(serviceAuthenticationService, times(1)).getAuthenticationCommand("service", "token"); + verify(command, times(1)).apply(null); + + when(request.getUserPrincipal()).thenReturn(new Principal() { + @Override + public String getName() { + return null; + } + }); + serviceAuthenticationFilter.run(); + verify(serviceAuthenticationService, times(1)).getAuthenticationCommand("service", null); + verify(command, times(2)).apply(null); + } + +} diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/query/SuccessfulQueryHandlerTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/query/SuccessfulQueryHandlerTest.java index 41a2aa1025..985e55e06d 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/query/SuccessfulQueryHandlerTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/query/SuccessfulQueryHandlerTest.java @@ -15,16 +15,19 @@ import com.ca.mfaas.gateway.security.service.JwtSecurityInitializer; import com.ca.mfaas.security.SecurityUtils; import com.fasterxml.jackson.databind.ObjectMapper; +import com.netflix.discovery.EurekaClient; import io.jsonwebtoken.SignatureAlgorithm; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.context.ApplicationContext; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.web.client.RestTemplate; import java.security.Key; import java.security.KeyPair; @@ -44,9 +47,18 @@ public class SuccessfulQueryHandlerTest { private static final String DOMAIN = "this.com"; private static final String LTPA = "ltpaToken"; + @Mock + private ApplicationContext applicationContext; + @Mock private JwtSecurityInitializer jwtSecurityInitializer; + @Mock + private RestTemplate restTemplate; + + @Mock + private EurekaClient discoveryClient; + @Before public void setup() { httpServletRequest = new MockHttpServletRequest(); @@ -61,7 +73,7 @@ public void setup() { privateKey = keyPair.getPrivate(); publicKey = keyPair.getPublic(); } - AuthenticationService authenticationService = new AuthenticationService(authConfigurationProperties, jwtSecurityInitializer); + AuthenticationService authenticationService = new AuthenticationService(applicationContext, authConfigurationProperties, jwtSecurityInitializer, discoveryClient, restTemplate); when(jwtSecurityInitializer.getSignatureAlgorithm()).thenReturn(algorithm); when(jwtSecurityInitializer.getJwtSecret()).thenReturn(privateKey); when(jwtSecurityInitializer.getJwtPublicKey()).thenReturn(publicKey); diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/AuthenticationServiceTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/AuthenticationServiceTest.java index 12abd7da47..6fedda4331 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/AuthenticationServiceTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/AuthenticationServiceTest.java @@ -14,28 +14,42 @@ import com.ca.apiml.security.common.token.TokenAuthentication; import com.ca.apiml.security.common.token.TokenExpireException; import com.ca.apiml.security.common.token.TokenNotValidException; +import com.ca.mfaas.gateway.config.CacheConfig; import com.ca.mfaas.security.SecurityUtils; +import com.netflix.appinfo.ApplicationInfoManager; +import com.netflix.appinfo.InstanceInfo; +import com.netflix.discovery.EurekaClient; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.apache.commons.lang.time.DateUtils; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.client.RestTemplate; import javax.servlet.http.Cookie; import java.security.Key; import java.security.KeyPair; import java.security.PublicKey; +import java.util.Arrays; import java.util.Date; import java.util.Optional; import static org.junit.Assert.*; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; -@RunWith(MockitoJUnitRunner.class) +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = { + CacheConfig.class, + AuthenticationServiceTest.Context.class +}) public class AuthenticationServiceTest { private static final String USER = "Me"; @@ -46,14 +60,22 @@ public class AuthenticationServiceTest { private Key privateKey; private PublicKey publicKey; + @Autowired private AuthenticationService authService; + + @Autowired private AuthConfigurationProperties authConfigurationProperties; - @Mock + @Autowired private JwtSecurityInitializer jwtSecurityInitializer; - @Before - public void setUp() { + @Autowired + private RestTemplate restTemplate; + + @Autowired + private EurekaClient discoveryClient; + + private void mockJwtSecurityInitializer() { KeyPair keyPair = SecurityUtils.generateKeyPair("RSA", 2048); if (keyPair != null) { privateKey = keyPair.getPrivate(); @@ -62,9 +84,11 @@ public void setUp() { when(jwtSecurityInitializer.getSignatureAlgorithm()).thenReturn(ALGORITHM); when(jwtSecurityInitializer.getJwtSecret()).thenReturn(privateKey); when(jwtSecurityInitializer.getJwtPublicKey()).thenReturn(publicKey); + } - authConfigurationProperties = new AuthConfigurationProperties(); - authService = new AuthenticationService(authConfigurationProperties, jwtSecurityInitializer); + @Before + public void setUp() { + mockJwtSecurityInitializer(); } @Test @@ -109,7 +133,12 @@ public void shouldThrowExceptionWhenTokenIsExpired() { @Test(expected = TokenNotValidException.class) public void shouldThrowExceptionWhenOccurUnexpectedException() { - authService.validateJwtToken(null); + authService.validateJwtToken((String) null); + } + + @Test(expected = TokenNotValidException.class) + public void shouldThrowExceptionWhenOccurUnexpectedException2() { + authService.validateJwtToken((TokenAuthentication) null); } @Test @@ -184,4 +213,109 @@ private String createExpiredJwtToken(Key secretKey) { .signWith(ALGORITHM, secretKey) .compact(); } + + private InstanceInfo createInstanceInfo(String instanceId, String ip, int port, int securePort) { + InstanceInfo out = mock(InstanceInfo.class); + when(out.getInstanceId()).thenReturn(instanceId); + when(out.getIPAddr()).thenReturn(ip); + when(out.getPort()).thenReturn(port); + when(out.getSecurePort()).thenReturn(securePort); + return out; + } + + @Test + public void invalidateToken() { + TokenAuthentication tokenAuthentication; + + reset(discoveryClient); + reset(restTemplate); + + String jwt1 = authService.createJwtToken("user1", "domain1", "ltpa1"); + assertFalse(authService.isInvalidated(jwt1)); + tokenAuthentication = authService.validateJwtToken(jwt1); + assertTrue(tokenAuthentication.isAuthenticated()); + + InstanceInfo myInstance = mock(InstanceInfo.class); + when(myInstance.getInstanceId()).thenReturn("myInstance01"); + ApplicationInfoManager applicationInfoManager = mock(ApplicationInfoManager.class); + when(applicationInfoManager.getInfo()).thenReturn(myInstance); + when(discoveryClient.getApplicationInfoManager()).thenReturn(applicationInfoManager); + + doReturn(Arrays.asList( + createInstanceInfo("instance02", "192.168.0.1", 10000, 10433), + createInstanceInfo("myInstance01", "127.0.0.0.1", 10000, 10433), + createInstanceInfo("instance03", "192.168.0.2", 10001, 0) + )).when(discoveryClient).getInstancesById("gateway"); + + authService.invalidateJwtToken(jwt1, true); + assertTrue(authService.isInvalidated(jwt1)); + tokenAuthentication = authService.validateJwtToken(jwt1); + assertFalse(tokenAuthentication.isAuthenticated()); + verify(restTemplate, times(2)).delete(anyString(), (Object[]) any()); + verify(restTemplate).delete("https://192.168.0.1:10433/auth/invalidate/{}", jwt1); + verify(restTemplate).delete("http://192.168.0.2:10001/auth/invalidate/{}", jwt1); + } + + @Test + public void invalidateTokenCache() { + reset(jwtSecurityInitializer); + mockJwtSecurityInitializer(); + + String jwtToken01 = authService.createJwtToken("user01", "domain01", "ltpa01"); + String jwtToken02 = authService.createJwtToken("user02", "domain02", "ltpa02"); + + assertFalse(authService.isInvalidated(jwtToken01)); + assertFalse(authService.isInvalidated(jwtToken02)); + + verify(jwtSecurityInitializer, never()).getJwtPublicKey(); + + assertTrue(authService.validateJwtToken(jwtToken01).isAuthenticated()); + verify(jwtSecurityInitializer, times(1)).getJwtPublicKey(); + assertTrue(authService.validateJwtToken(jwtToken01).isAuthenticated()); + verify(jwtSecurityInitializer, times(1)).getJwtPublicKey(); + + assertTrue(authService.validateJwtToken(jwtToken02).isAuthenticated()); + verify(jwtSecurityInitializer, times(2)).getJwtPublicKey(); + + authService.invalidateJwtToken(jwtToken01, false); + assertTrue(authService.validateJwtToken(jwtToken02).isAuthenticated()); + verify(jwtSecurityInitializer, times(2)).getJwtPublicKey(); + + assertFalse(authService.validateJwtToken(jwtToken01).isAuthenticated()); + verify(jwtSecurityInitializer, times(3)).getJwtPublicKey(); + } + + @Configuration + public static class Context { + + @Autowired + private ApplicationContext applicationContext; + + @Bean + public AuthConfigurationProperties getAuthConfigurationProperties() { + return new AuthConfigurationProperties(); + } + + @Bean + public JwtSecurityInitializer getJwtSecurityInitializer() { + return mock(JwtSecurityInitializer.class); + } + + @Bean + public RestTemplate getRestTemplate() { + return mock(RestTemplate.class); + } + + @Bean + public EurekaClient getDiscoveryClient() { + return mock(EurekaClient.class); + } + + @Bean + public AuthenticationService getAuthenticationService() { + return new AuthenticationService(applicationContext, getAuthConfigurationProperties(), getJwtSecurityInitializer(), getDiscoveryClient(), getRestTemplate()); + } + + } + } diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceTest.java new file mode 100644 index 0000000000..0319fb7b81 --- /dev/null +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceTest.java @@ -0,0 +1,303 @@ +package com.ca.mfaas.gateway.security.service;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import com.ca.apiml.security.common.auth.Authentication; +import com.ca.apiml.security.common.auth.AuthenticationScheme; +import com.ca.apiml.security.common.token.QueryResponse; +import com.ca.mfaas.gateway.config.CacheConfig; +import com.ca.mfaas.gateway.security.service.schema.AbstractAuthenticationScheme; +import com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand; +import com.ca.mfaas.gateway.security.service.schema.AuthenticationSchemeFactory; +import com.netflix.appinfo.InstanceInfo; +import com.netflix.discovery.EurekaClient; +import com.netflix.zuul.context.RequestContext; +import org.apache.catalina.servlet4preview.http.HttpServletRequest; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.sql.Date; +import java.time.LocalDate; +import java.util.*; + +import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_APPLID; +import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_SCHEME; +import static com.ca.mfaas.gateway.security.service.ServiceAuthenticationService.AUTHENTICATION_COMMAND_KEY; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = { + ServiceAuthenticationServiceTest.Context.class, + CacheConfig.class +}) +public class ServiceAuthenticationServiceTest { + + @Autowired + private EurekaClient discoveryClient; + + @Autowired + private AuthenticationSchemeFactory authenticationSchemeFactory; + + @Autowired + private AuthenticationService authenticationService; + + @Autowired + @InjectMocks + private ServiceAuthenticationService serviceAuthenticationService; + + @Before + public void init() { + MockitoAnnotations.initMocks(this); + RequestContext.testSetCurrentContext(null); + } + + private InstanceInfo createInstanceInfo(String instanceId, String scheme, String applid) { + InstanceInfo out = mock(InstanceInfo.class); + + Map metadata = new HashMap<>(); + metadata.put(AUTHENTICATION_SCHEME, scheme); + metadata.put(AUTHENTICATION_APPLID, applid); + + when(out.getMetadata()).thenReturn(metadata); + when(out.getId()).thenReturn(instanceId); + when(out.getInstanceId()).thenReturn(instanceId); + + return out; + } + + private InstanceInfo createInstanceInfo(String instanceId, AuthenticationScheme scheme, String applid) { + return createInstanceInfo(instanceId, scheme == null ? null : scheme.getScheme(), applid); + } + + private InstanceInfo createInstanceInfo(String id, Authentication authentication) { + return createInstanceInfo(id, authentication == null ? null : authentication.getScheme(), authentication == null ? null : authentication.getApplid()); + } + + @Test + public void testGetAuthentication() { + InstanceInfo ii; + + ii = createInstanceInfo("instance1", "bypass", "applid"); + assertEquals(new Authentication(AuthenticationScheme.BYPASS, "applid"), serviceAuthenticationService.getAuthentication(ii)); + + ii = createInstanceInfo("instance2", "zoweJwt", "applid2"); + assertEquals(new Authentication(AuthenticationScheme.ZOWE_JWT, "applid2"), serviceAuthenticationService.getAuthentication(ii)); + + ii = createInstanceInfo("instance2", "httpBasicPassTicket", null); + assertEquals(new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, null), serviceAuthenticationService.getAuthentication(ii)); + + ii = createInstanceInfo("instance2", (AuthenticationScheme) null, null); + assertEquals(new Authentication(), serviceAuthenticationService.getAuthentication(ii)); + } + + @Test + public void testGetAuthenticationCommand() { + AbstractAuthenticationScheme schemeBeanMock = mock(AbstractAuthenticationScheme.class); + // token1 - valid + QueryResponse qr1 = new QueryResponse("domain", "userId", + Date.valueOf(LocalDate.of(1900,1, 1)), + Date.valueOf(LocalDate.of(2100,1, 1)) + ); + // token2 - expired + QueryResponse qr2 = new QueryResponse("domain", "userId", + Date.valueOf(LocalDate.of(1900,1, 1)), + Date.valueOf(LocalDate.of(2000,1, 1)) + ); + AuthenticationCommand acValid = spy(new AuthenticationCommandTest(false)); + AuthenticationCommand acExpired = spy(new AuthenticationCommandTest(true)); + + when(authenticationSchemeFactory.getSchema(AuthenticationScheme.HTTP_BASIC_PASSTICKET)) + .thenReturn(schemeBeanMock); + when(authenticationService.parseJwtToken("token1")).thenReturn(qr1); + when(authenticationService.parseJwtToken("token2")).thenReturn(qr2); + when(schemeBeanMock.createCommand(new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applid"), qr1)).thenReturn(acValid); + when(schemeBeanMock.createCommand(new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applid"), qr2)).thenReturn(acExpired); + + assertSame(acValid, serviceAuthenticationService.getAuthenticationCommand(new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applid"), "token1")); + verify(schemeBeanMock, times(1)).createCommand(any(), any()); + // cache is working, it is not expired + assertSame(acValid, serviceAuthenticationService.getAuthenticationCommand(new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applid"), "token1")); + verify(schemeBeanMock, times(1)).createCommand(any(), any()); + + // new entry - expired, dont cache that + assertSame(acExpired, serviceAuthenticationService.getAuthenticationCommand(new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applid"), "token2")); + verify(schemeBeanMock, times(2)).createCommand(any(), any()); + // replace result (to know that expired record is removed and get new one) + when(schemeBeanMock.createCommand(new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applid"), qr2)).thenReturn(acValid); + assertSame(acValid, serviceAuthenticationService.getAuthenticationCommand(new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applid"), "token2")); + verify(schemeBeanMock, times(3)).createCommand(any(), any()); + } + + @Test + public void testGetAuthenticationCommandByServiceId() { + AuthenticationCommand ok = new AuthenticationCommandTest(false); + Authentication a1 = new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applid01"); + Authentication a2 = new Authentication(AuthenticationScheme.ZOWE_JWT, null); + Authentication a3 = new Authentication(AuthenticationScheme.ZOWE_JWT, "applid01"); + Authentication a4 = new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applid02"); + + InstanceInfo ii1 = createInstanceInfo("inst01", a1); + InstanceInfo ii2 = createInstanceInfo("inst01", a2); + InstanceInfo ii3 = createInstanceInfo("inst01", a3); + InstanceInfo ii4 = createInstanceInfo("inst01", a4); + + ServiceAuthenticationService sas = spy(serviceAuthenticationService); + + when(authenticationSchemeFactory.getSchema(any())).thenReturn(mock(AbstractAuthenticationScheme.class)); + + // just one instance + when(sas.getAuthenticationCommand(a1, "jwt01")).thenReturn(ok); + when(discoveryClient.getInstancesById("svr01")).thenReturn(Collections.singletonList(ii1)); + assertSame(ok, sas.getAuthenticationCommand("svr01", "jwt01")); + + // multiple same instances + when(sas.getAuthenticationCommand(a1, "jwt02")).thenReturn(ok); + when(discoveryClient.getInstancesById("svr02")).thenReturn(Arrays.asList(ii1, ii1, ii1)); + assertSame(ok, sas.getAuthenticationCommand("svr02", "jwt02")); + + // multiple different instances + reset(discoveryClient); + when(discoveryClient.getInstancesById("svr03")).thenReturn(Arrays.asList(ii1, ii2)); + assertTrue(sas.getAuthenticationCommand("svr03", "jwt03") instanceof ServiceAuthenticationService.LoadBalancerAuthenticationCommand); + + reset(discoveryClient); + when(discoveryClient.getInstancesById("svr03")).thenReturn(Arrays.asList(ii1, ii3)); + assertTrue(sas.getAuthenticationCommand("svr03", "jwt03") instanceof ServiceAuthenticationService.LoadBalancerAuthenticationCommand); + + reset(discoveryClient); + when(discoveryClient.getInstancesById("svr03")).thenReturn(Arrays.asList(ii1, ii4)); + assertTrue(sas.getAuthenticationCommand("svr03", "jwt03") instanceof ServiceAuthenticationService.LoadBalancerAuthenticationCommand); + + reset(discoveryClient); + when(discoveryClient.getInstancesById("svr03")).thenReturn(Arrays.asList(ii1, ii2, ii3, ii4)); + assertTrue(sas.getAuthenticationCommand("svr03", "jwt03") instanceof ServiceAuthenticationService.LoadBalancerAuthenticationCommand); + } + + @Test + public void testGetAuthenticationCommandByServiceIdCache() { + InstanceInfo ii1 = createInstanceInfo("i1", AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applid1"); + AuthenticationCommand ac1 = new AuthenticationCommandTest(true); + AuthenticationCommand ac2 = new AuthenticationCommandTest(false); + AbstractAuthenticationScheme aas1 = mock(AbstractAuthenticationScheme.class); + when(aas1.getScheme()).thenReturn(AuthenticationScheme.HTTP_BASIC_PASSTICKET); + + when(discoveryClient.getInstancesById("s1")).thenReturn(Collections.singletonList(ii1)); + when(authenticationSchemeFactory.getSchema(AuthenticationScheme.HTTP_BASIC_PASSTICKET)).thenReturn(aas1); + when(aas1.createCommand(eq(new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applid1")), any())) + .thenReturn(ac1); + + assertSame(ac1, serviceAuthenticationService.getAuthenticationCommand("s1", "jwt")); + verify(discoveryClient, times(1)).getInstancesById("s1"); + + Mockito.reset(aas1); + when(aas1.getScheme()).thenReturn(AuthenticationScheme.HTTP_BASIC_PASSTICKET); + when(aas1.createCommand(eq(new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applid1")), any())) + .thenReturn(ac2); + assertSame(ac2, serviceAuthenticationService.getAuthenticationCommand("s1", "jwt")); + verify(discoveryClient, times(2)).getInstancesById("s1"); + assertSame(ac2, serviceAuthenticationService.getAuthenticationCommand("s1", "jwt")); + verify(discoveryClient, times(2)).getInstancesById("s1"); + } + + @Test + public void testUniversalAuthenticationCommand() { + ServiceAuthenticationService.UniversalAuthenticationCommand uac = serviceAuthenticationService.new UniversalAuthenticationCommand(); + + try { + uac.apply(null); + fail(); + } catch (NullPointerException e) { + // this command cannot be applied without parameter (null) + } + + AuthenticationCommand ac = mock(AuthenticationCommand.class); + InstanceInfo ii = createInstanceInfo("inst0001", AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applid0001"); + RequestContext requestContext = mock(RequestContext.class); + HttpServletRequest request = mock(HttpServletRequest.class); + when(requestContext.getRequest()).thenReturn(request); + RequestContext.testSetCurrentContext(requestContext); + when(authenticationService.getJwtTokenFromRequest(request)).thenReturn(Optional.of("jwtToken01")); + AbstractAuthenticationScheme scheme = mock(AbstractAuthenticationScheme.class); + when(scheme.createCommand(eq(new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applid0001")), any())).thenReturn(ac); + when(authenticationSchemeFactory.getSchema(AuthenticationScheme.HTTP_BASIC_PASSTICKET)).thenReturn(scheme); + + uac.apply(ii); + + verify(ac, times(1)).apply(null); + } + + @Test + public void testLoadBalancerAuthenticationCommand() { + ServiceAuthenticationService.LoadBalancerAuthenticationCommand lbac = serviceAuthenticationService.new LoadBalancerAuthenticationCommand(); + + RequestContext requestContext = new RequestContext(); + RequestContext.testSetCurrentContext(requestContext); + + assertNull(requestContext.get(AUTHENTICATION_COMMAND_KEY)); + lbac.apply(null); + assertTrue(requestContext.get(AUTHENTICATION_COMMAND_KEY) instanceof ServiceAuthenticationService.UniversalAuthenticationCommand); + } + + @Configuration + public static class Context { + + @Bean + public EurekaClient getDiscoveryClient() { + return mock(EurekaClient.class); + } + + @Bean + public AuthenticationSchemeFactory getAuthenticationSchemeFactory() { + return mock(AuthenticationSchemeFactory.class); + } + + @Bean + public AuthenticationService getAuthenticationService() { + return mock(AuthenticationService.class); + } + + @Bean + public ServiceAuthenticationService getServiceAuthenticationService() { + return new ServiceAuthenticationService(getDiscoveryClient(), getAuthenticationSchemeFactory(), getAuthenticationService()); + } + + } + + public class AuthenticationCommandTest extends AuthenticationCommand { + + private static final long serialVersionUID = 8527412076986152763L; + + private boolean expired; + + public AuthenticationCommandTest(boolean expired) { + this.expired = expired; + } + + @Override + public boolean isExpired() { + return expired; + } + + @Override + public void apply(InstanceInfo instanceInfo) { + } + + } + +} diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationCommandTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationCommandTest.java new file mode 100644 index 0000000000..48c51e0f61 --- /dev/null +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationCommandTest.java @@ -0,0 +1,25 @@ +package com.ca.mfaas.gateway.security.service.schema;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static org.junit.Assert.assertFalse; +@RunWith(JUnit4.class) +public class AuthenticationCommandTest { + + @Test + public void testEmptyCommand() { + assertFalse(AuthenticationCommand.EMPTY.isExpired()); + AuthenticationCommand.EMPTY.apply(null); + } + +} diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java new file mode 100644 index 0000000000..ff74947015 --- /dev/null +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java @@ -0,0 +1,199 @@ +package com.ca.mfaas.gateway.security.service.schema;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import com.ca.apiml.security.common.auth.Authentication; +import com.ca.apiml.security.common.auth.AuthenticationScheme; +import com.ca.apiml.security.common.token.QueryResponse; +import com.ca.mfaas.gateway.security.service.AuthenticationService; +import com.netflix.zuul.context.RequestContext; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import javax.servlet.http.HttpServletRequest; +import java.util.Arrays; +import java.util.Date; +import java.util.Optional; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +@RunWith(JUnit4.class) +public class AuthenticationSchemeFactoryTest { + + private static final AuthenticationCommand COMMAND = mock(AuthenticationCommand.class); + + private AbstractAuthenticationScheme createScheme(final AuthenticationScheme scheme, final boolean isDefault) { + return new AbstractAuthenticationScheme() { + @Override + public AuthenticationScheme getScheme() { + return scheme; + } + + @Override + public boolean isDefault() { + return isDefault; + } + + @Override + public AuthenticationCommand createCommand(Authentication authentication, QueryResponse token) { + return COMMAND; + } + }; + } + + @Before + public void init() { + RequestContext.testSetCurrentContext(null); + } + + @Test + public void testInit() { + // happy day + new AuthenticationSchemeFactory( + mock(AuthenticationService.class), + Arrays.asList( + createScheme(AuthenticationScheme.BYPASS, true), + createScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET, false), + createScheme(AuthenticationScheme.ZOWE_JWT, false) + ) + ); + + // no default + try { + new AuthenticationSchemeFactory( + mock(AuthenticationService.class), + Arrays.asList( + createScheme(AuthenticationScheme.BYPASS, false), + createScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET, false), + createScheme(AuthenticationScheme.ZOWE_JWT, false) + ) + ); + fail(); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("No scheme")); + } + + // multiple default + try { + new AuthenticationSchemeFactory( + mock(AuthenticationService.class), + Arrays.asList( + createScheme(AuthenticationScheme.BYPASS, true), + createScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET, true), + createScheme(AuthenticationScheme.ZOWE_JWT, false) + ) + ); + fail(); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Multiple scheme")); + assertTrue(e.getMessage().contains("as default")); + assertTrue(e.getMessage().contains(AuthenticationScheme.BYPASS.getScheme())); + assertTrue(e.getMessage().contains(AuthenticationScheme.HTTP_BASIC_PASSTICKET.getScheme())); + } + } + + @Test + public void testGetSchema() { + AuthenticationSchemeFactory asf = new AuthenticationSchemeFactory( + mock(AuthenticationService.class), + Arrays.asList( + createScheme(AuthenticationScheme.BYPASS, true), + createScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET, false), + createScheme(AuthenticationScheme.ZOWE_JWT, false) + ) + ); + + assertEquals(AuthenticationScheme.BYPASS, asf.getSchema(AuthenticationScheme.BYPASS).getScheme()); + assertEquals(AuthenticationScheme.HTTP_BASIC_PASSTICKET, asf.getSchema(AuthenticationScheme.HTTP_BASIC_PASSTICKET).getScheme()); + assertEquals(AuthenticationScheme.ZOWE_JWT, asf.getSchema(AuthenticationScheme.ZOWE_JWT).getScheme()); + // default one + assertEquals(AuthenticationScheme.BYPASS, asf.getSchema(null).getScheme()); + } + + @Test + public void testGetAuthenticationCommand() { + final AbstractAuthenticationScheme byPass = spy(createScheme(AuthenticationScheme.BYPASS, true)); + final AbstractAuthenticationScheme passTicket = spy(createScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET, false)); + + AuthenticationService as = mock(AuthenticationService.class); + AuthenticationSchemeFactory asf = new AuthenticationSchemeFactory(as, Arrays.asList(byPass, passTicket)); + Authentication authentication = new Authentication(AuthenticationScheme.BYPASS, "applid1"); + + HttpServletRequest request = mock(HttpServletRequest.class); + RequestContext requestContext = new RequestContext(); + requestContext.setRequest(request); + + QueryResponse qr = new QueryResponse("domain", "userId", new Date(), new Date()); + when(as.getJwtTokenFromRequest(request)).thenReturn(Optional.of("jwtToken123")); + when(as.getJwtTokenFromRequest(null)).thenReturn(Optional.empty()); + when(as.parseJwtToken("jwtToken123")).thenReturn(qr); + + verify(byPass, times(0)).createCommand(authentication, null); + verify(passTicket, times(0)).createCommand(authentication, null); + asf.getAuthenticationCommand(authentication); + + verify(byPass, times(1)).createCommand(authentication, null); + verify(passTicket, times(0)).createCommand(authentication, null); + authentication.setScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET); + asf.getAuthenticationCommand(authentication); + + verify(byPass, times(1)).createCommand(authentication, null); + verify(passTicket, times(1)).createCommand(authentication, null); + authentication.setScheme(null); + asf.getAuthenticationCommand(authentication); + verify(byPass, times(2)).createCommand(authentication, null); + verify(passTicket, times(1)).createCommand(authentication, null); + + RequestContext.testSetCurrentContext(requestContext); + + verify(byPass, times(0)).createCommand(authentication, qr); + verify(passTicket, times(0)).createCommand(authentication, qr); + authentication.setScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET); + asf.getAuthenticationCommand(authentication); + verify(byPass, times(0)).createCommand(authentication, qr); + verify(passTicket, times(1)).createCommand(authentication, qr); + } + + @Test + public void testUnknownScheme() { + AuthenticationSchemeFactory asf = new AuthenticationSchemeFactory( + mock(AuthenticationService.class), + Arrays.asList( + createScheme(AuthenticationScheme.BYPASS, true), + createScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET, false), + createScheme(AuthenticationScheme.ZOWE_JWT, false) + ) + ); + + assertNotNull(asf.getSchema(AuthenticationScheme.BYPASS)); + assertNotNull(asf.getSchema(AuthenticationScheme.HTTP_BASIC_PASSTICKET)); + assertNotNull(asf.getSchema(AuthenticationScheme.ZOWE_JWT)); + try { + // missing implementation + asf.getSchema(AuthenticationScheme.ZOSMF); + fail(); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Unknown scheme")); + } + + assertSame(COMMAND, asf.getAuthenticationCommand(new Authentication(AuthenticationScheme.ZOWE_JWT, "applid"))); + assertSame(COMMAND, asf.getAuthenticationCommand(new Authentication(null, "applid"))); + try { + // missing implementation + assertSame(COMMAND, asf.getAuthenticationCommand(new Authentication(AuthenticationScheme.ZOSMF, "applid"))); + fail(); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Unknown scheme")); + } + } + +} diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ByPassSchemeTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ByPassSchemeTest.java new file mode 100644 index 0000000000..9f327c4f06 --- /dev/null +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ByPassSchemeTest.java @@ -0,0 +1,27 @@ +package com.ca.mfaas.gateway.security.service.schema;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +@RunWith(JUnit4.class) +public class ByPassSchemeTest { + + @Test + public void testScheme() { + ByPassScheme scheme = new ByPassScheme(); + assertTrue(scheme.isDefault()); + assertSame(AuthenticationCommand.EMPTY, scheme.createCommand(null, null)); + } + +} diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java new file mode 100644 index 0000000000..bd0311303e --- /dev/null +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java @@ -0,0 +1,88 @@ +package com.ca.mfaas.gateway.security.service.schema;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import com.ca.apiml.security.common.auth.Authentication; +import com.ca.apiml.security.common.auth.AuthenticationScheme; +import com.ca.apiml.security.common.service.PassTicketService; +import com.ca.apiml.security.common.token.QueryResponse; +import com.netflix.zuul.context.RequestContext; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.util.ReflectionTestUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.Calendar; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class HttpBasicPassTicketSchemeTest { + + private final int PASSTICKET_DURATION = 300; + + @Mock + private PassTicketService passTicketService; + + @InjectMocks + private HttpBasicPassTicketScheme httpBasicPassTicketScheme; + + @Before + public void init() { + ReflectionTestUtils.setField(httpBasicPassTicketScheme, "timeout", PASSTICKET_DURATION); + } + + @Test + public void testCreateCommand() { + Calendar calendar = Calendar.getInstance(); + Authentication authentication = new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applid"); + QueryResponse queryResponse = new QueryResponse("domain", "username", calendar.getTime(), calendar.getTime()); + + when(passTicketService.generate("username", "applid")).thenReturn("123456"); + + AuthenticationCommand ac = httpBasicPassTicketScheme.createCommand(authentication, queryResponse); + assertNotNull(ac); + + RequestContext requestContext = new RequestContext(); + HttpServletRequest request = new MockHttpServletRequest(); + requestContext.setRequest(request); + RequestContext.testSetCurrentContext(requestContext); + ac.apply(null); + + assertEquals("Basic dXNlcm5hbWU6MTIzNDU2", requestContext.getZuulRequestHeaders().get("authorization")); + + // JWT token expired one minute ago (command expired also if JWT token expired) + calendar.add(Calendar.MINUTE, -1); + queryResponse = new QueryResponse("domain", "username", calendar.getTime(), calendar.getTime()); + ac = httpBasicPassTicketScheme.createCommand(authentication, queryResponse); + assertTrue(ac.isExpired()); + + // JWT token will expire in one minute (command expired also if JWT token expired) + calendar.add(Calendar.MINUTE, 2); + queryResponse = new QueryResponse("domain", "username", calendar.getTime(), calendar.getTime()); + ac = httpBasicPassTicketScheme.createCommand(authentication, queryResponse); + assertFalse(ac.isExpired()); + + calendar.add(Calendar.MINUTE, 100); + queryResponse = new QueryResponse("domain", "username", calendar.getTime(), calendar.getTime()); + ac = httpBasicPassTicketScheme.createCommand(authentication, queryResponse); + + calendar = Calendar.getInstance(); + calendar.add(Calendar.SECOND, PASSTICKET_DURATION); + // checking setup of expired time, JWT expired in future (more than hour), check if set date is similar to passticket timeout (5s) + assertTrue(Math.abs(calendar.getTime().getTime() - (long) ReflectionTestUtils.getField(ac, "expireAt")) < 5); + } + +} diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZosmfSchemeTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZosmfSchemeTest.java new file mode 100644 index 0000000000..1386ab068f --- /dev/null +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZosmfSchemeTest.java @@ -0,0 +1,99 @@ +package com.ca.mfaas.gateway.security.service.schema;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import com.ca.apiml.security.common.auth.Authentication; +import com.ca.apiml.security.common.auth.AuthenticationScheme; +import com.ca.apiml.security.common.token.QueryResponse; +import com.ca.apiml.security.common.token.TokenNotValidException; +import com.ca.mfaas.gateway.security.service.AuthenticationService; +import com.netflix.zuul.context.RequestContext; +import io.jsonwebtoken.JwtException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.mock.web.MockHttpServletRequest; + +import javax.servlet.http.HttpServletRequest; +import java.util.Calendar; +import java.util.Optional; + +import static com.ca.mfaas.gateway.security.service.schema.ZosmfScheme.ZosmfCommand.COOKIE_HEADER; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class ZosmfSchemeTest { + + @Mock + private AuthenticationService authenticationService; + + @InjectMocks + private ZosmfScheme zosmfScheme; + + @Test + public void testCreateCommand() { + Calendar calendar = Calendar.getInstance(); + Authentication authentication = new Authentication(AuthenticationScheme.ZOSMF, null); + QueryResponse queryResponse = new QueryResponse("domain", "username", calendar.getTime(), calendar.getTime()); + + RequestContext requestContext = spy(new RequestContext()); + RequestContext.testSetCurrentContext(requestContext); + + HttpServletRequest request = new MockHttpServletRequest(); + requestContext.setRequest(request); + + // no token exists + when(authenticationService.getJwtTokenFromRequest(request)).thenReturn(Optional.empty()); + zosmfScheme.createCommand(authentication, queryResponse).apply(null); + verify(requestContext, never()).addZuulRequestHeader(anyString(), anyString()); + + // no cookies is set now + reset(authenticationService); + when(authenticationService.getJwtTokenFromRequest(request)).thenReturn(Optional.of("jwtToken1")); + when(authenticationService.getLtpaTokenFromJwtToken("jwtToken1")).thenReturn("ltpa1"); + requestContext.getZuulRequestHeaders().put(COOKIE_HEADER, null); + zosmfScheme.createCommand(authentication, queryResponse).apply(null); + assertEquals("ltpa1", requestContext.getZuulRequestHeaders().get(COOKIE_HEADER)); + + // a cookies is set now + reset(authenticationService); + when(authenticationService.getJwtTokenFromRequest(request)).thenReturn(Optional.of("jwtToken2")); + when(authenticationService.getLtpaTokenFromJwtToken("jwtToken2")).thenReturn("ltpa2"); + requestContext.getZuulRequestHeaders().put(COOKIE_HEADER, "cookie1=1"); + zosmfScheme.createCommand(authentication, queryResponse).apply(null); + assertEquals("cookie1=1; ltpa2", requestContext.getZuulRequestHeaders().get(COOKIE_HEADER)); + + // JWT token is not valid anymore - TokenNotValidException + try { + reset(authenticationService); + when(authenticationService.getJwtTokenFromRequest(request)).thenReturn(Optional.of("jwtToken3")); + when(authenticationService.getLtpaTokenFromJwtToken("jwtToken3")).thenThrow(new TokenNotValidException("Token is not valid")); + zosmfScheme.createCommand(authentication, queryResponse).apply(null); + fail(); + } catch (TokenNotValidException e) { + // exception is not handled + } + + // JWT token is not valid anymore - JwtException + try { + reset(authenticationService); + when(authenticationService.getJwtTokenFromRequest(request)).thenReturn(Optional.of("jwtToken3")); + when(authenticationService.getLtpaTokenFromJwtToken("jwtToken3")).thenThrow(new JwtException("Token is expired")); + zosmfScheme.createCommand(authentication, queryResponse).apply(null); + fail(); + } catch (JwtException e) { + // exception is not handled + } + } + +} diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 9dd2e99b60..a38cf0dea9 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -47,6 +47,7 @@ ext { swaggerJerseyJaxrsVersion = '1.5.10' jacksonDataformatYamlVersion = '2.9.7' jerseyVersion = '2.26' + ehCacheVersion = '2.10.6' libraries = [ lombok : "org.projectlombok:lombok:${lombokVersion}", @@ -65,6 +66,7 @@ ext { spring_boot_starter_aop : "org.springframework.boot:spring-boot-starter-aop:${springBootVersion}", spring_boot_starter_log4j2 : "org.springframework.boot:spring-boot-starter-log4j2:${springBootVersion}", spring_boot_starter_thymeleaf : "org.springframework.boot:spring-boot-starter-thymeleaf:${springBootVersion}", + spring_boot_starter_cache : "org.springframework.boot:spring-boot-starter-cache:${springBootVersion}", spring_boot_devtools : "org.springframework.boot:spring-boot-devtools:${springBootVersion}", spring_retry : "org.springframework.retry:spring-retry:${springRetryVersion}", spring_hateoas : "org.springframework.hateoas:spring-hateoas:${springHateoasVersion}", @@ -90,6 +92,7 @@ ext { swagger_jersey2_jaxrs : "io.swagger:swagger-jersey2-jaxrs:${swaggerJerseyJaxrsVersion}", http_client : "org.apache.httpcomponents:httpclient:${httpClientVersion}", http_core : "org.apache.httpcomponents:httpcore:${httpCoreVersion}", + eh_cache : "net.sf.ehcache:ehcache:${ehCacheVersion}", mockSpringRest : "com.github.skjolber:mockito-rest-spring:${mockSpringRestVersion}", reactorCore : "io.projectreactor:reactor-core:${reactorVersion}", From d7d8aae910497200799725b599528611f6a8a077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Thu, 2 Jan 2020 14:16:47 +0100 Subject: [PATCH 011/122] fix unused import (checkstyle) --- .../filters/pre/ServiceAuthenticationFilterTest.java | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java index 59e664ac77..0188e0cfd7 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java @@ -12,7 +12,6 @@ import com.ca.mfaas.gateway.security.service.ServiceAuthenticationService; import com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand; import com.netflix.zuul.context.RequestContext; -import com.netflix.zuul.exception.ZuulException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -21,7 +20,6 @@ import org.mockito.junit.MockitoJUnitRunner; import javax.servlet.http.HttpServletRequest; -import java.security.Principal; import static org.mockito.Mockito.*; import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SERVICE_ID_KEY; @@ -56,12 +54,7 @@ public void testRun() { verify(serviceAuthenticationService, times(1)).getAuthenticationCommand("service", "token"); verify(command, times(1)).apply(null); - when(request.getUserPrincipal()).thenReturn(new Principal() { - @Override - public String getName() { - return null; - } - }); + when(request.getUserPrincipal()).thenReturn(() -> null); serviceAuthenticationFilter.run(); verify(serviceAuthenticationService, times(1)).getAuthenticationCommand("service", null); verify(command, times(2)).apply(null); From 547c8e3e5f19dd23abe61ea223eacb09b066bcfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Thu, 2 Jan 2020 14:33:06 +0100 Subject: [PATCH 012/122] fix gateway startup --- gateway-service/build.gradle | 1 + .../security/service/schema/HttpBasicPassTicketScheme.java | 7 +++++-- gateway-service/src/main/resources/application.yml | 3 +++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/gateway-service/build.gradle b/gateway-service/build.gradle index f956bbeef2..47daadb672 100644 --- a/gateway-service/build.gradle +++ b/gateway-service/build.gradle @@ -40,6 +40,7 @@ dependencies { compile libraries.spring_boot_starter_web compile libraries.spring_boot_starter_websocket compile libraries.spring_boot_starter_thymeleaf + compile libraries.spring_boot_starter_cache compile libraries.spring_security_web compile libraries.spring_security_config compile libraries.tomcat_coyote diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketScheme.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketScheme.java index e1b4923a87..02037146e0 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketScheme.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketScheme.java @@ -15,9 +15,9 @@ import com.ca.apiml.security.common.token.QueryResponse; import com.netflix.appinfo.InstanceInfo; import com.netflix.zuul.context.RequestContext; -import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import org.apache.http.HttpHeaders; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -29,7 +29,6 @@ * header in request. */ @Component -@AllArgsConstructor public class HttpBasicPassTicketScheme implements AbstractAuthenticationScheme { private final PassTicketService passTicketService; @@ -37,6 +36,10 @@ public class HttpBasicPassTicketScheme implements AbstractAuthenticationScheme { @Value("${apiml.security.auth.passTicket.timeout:600}") private Integer timeout; + public HttpBasicPassTicketScheme(@Autowired PassTicketService passTicketService) { + this.passTicketService = passTicketService; + } + @Override public AuthenticationScheme getScheme() { return AuthenticationScheme.HTTP_BASIC_PASSTICKET; diff --git a/gateway-service/src/main/resources/application.yml b/gateway-service/src/main/resources/application.yml index 35a7e44b54..7ddce827a8 100644 --- a/gateway-service/src/main/resources/application.yml +++ b/gateway-service/src/main/resources/application.yml @@ -58,6 +58,9 @@ spring: output: ansi: enabled: detect + cache: + ehcache: + config: classpath:ehcache.xml main: banner-mode: ${apiml.banner:"off"} From 75015861c93c0c28ae1335de4d0eae99eb9f768b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Thu, 2 Jan 2020 16:44:05 +0100 Subject: [PATCH 013/122] fix passticket proxy (make as public class to allow call methods) --- .../common/service/PassTicketService.java | 34 +++++++++++-------- .../common/service/PassTicketServiceTest.java | 34 +++++++++++++++++-- .../mfaas/util/ClassOrDefaultProxyUtils.java | 8 +++-- 3 files changed, 57 insertions(+), 19 deletions(-) diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java index dec3f1dd3e..ee26377564 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java @@ -27,21 +27,7 @@ public void init() { this.irrPassTicket = ClassOrDefaultProxyUtils.createProxy( IRRPassTicket.class, "com.ibm.eserver.zos.racf.IRRPassTicket", - () -> new IRRPassTicket() { - @Override - public void evaluate(String userId, String applId, String passTicket) { - if (userId == null) throw new IllegalArgumentException("Parameter userId is empty"); - if (applId == null) throw new IllegalArgumentException("Parameter applId is empty"); - if (passTicket == null) throw new IllegalArgumentException("Parameter passTicket is empty"); - - throw new IllegalStateException("This implementation only for testing purpose"); - } - - @Override - public String generate(String userId, String applId) { - return null; - } - } + DefaultPassTicketImpl::new ); } @@ -58,4 +44,22 @@ public boolean isUsingRacImplementation() { return stateInterface.isUsingBaseImplementation(); } + public static class DefaultPassTicketImpl implements IRRPassTicket { + + @Override + public void evaluate(String userId, String applId, String passTicket) { + if (userId == null) throw new IllegalArgumentException("Parameter userId is empty"); + if (applId == null) throw new IllegalArgumentException("Parameter applId is empty"); + if (passTicket == null) throw new IllegalArgumentException("Parameter passTicket is empty"); + + throw new IllegalStateException("This implementation only for testing purpose"); + } + + @Override + public String generate(String userId, String applId) { + return null; + } + + } + } diff --git a/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java b/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java index 78814a1408..c6faa7330b 100644 --- a/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java +++ b/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java @@ -9,6 +9,7 @@ */ package com.ca.apiml.security.common.service; +import com.ca.mfaas.util.ClassOrDefaultProxyUtils; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -19,8 +20,7 @@ import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.util.ReflectionTestUtils; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.*; @RunWith(SpringRunner.class) @ContextConfiguration @@ -62,6 +62,36 @@ public void testCalledMethod() { assertEquals("1-2", passTicketService.generate("1", "2")); } + @Test + public void testProxy() { + IRRPassTicket irrPassTicket = ClassOrDefaultProxyUtils.createProxy( + IRRPassTicket.class, + "notExistingClass", + Impl::new + ); + + try { + irrPassTicket.evaluate("user", "applId", "passTicket"); + fail(); + } catch (Exception e) { + assertEquals("Dummy implementation of evaluate : user x applId x passTicket", e.getMessage()); + } + + assertEquals("success", irrPassTicket.generate("user", "applId")); + } + + public static class Impl implements IRRPassTicket { + @Override + public void evaluate(String userId, String applId, String passTicket) { + throw new RuntimeException("Dummy implementation of evaluate : " + userId + " x " + applId + " x " + passTicket); + } + + @Override + public String generate(String userId, String applId) { + return "success"; + } + } + @Configuration @ComponentScan(basePackageClasses = {PassTicketService.class}) public static class SpringConfig { diff --git a/common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java b/common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java index 6fc18ade2e..56f82252ca 100644 --- a/common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java +++ b/common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java @@ -199,7 +199,11 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl throw new NoSuchMethodException(String.format("Cannot found method %s", method)); } - return endPoint.invoke(args); + try { + return endPoint.invoke(args); + } catch (InvocationTargetException ite) { + throw ite.getCause(); + } } @Override @@ -214,7 +218,7 @@ public boolean isUsingBaseImplementation() { @Value @AllArgsConstructor - private static final class EndPoint { + public static final class EndPoint { private final Object target; private final Method method; From f51af44145ab9004825192bda8c861474aacbb32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Mon, 6 Jan 2020 09:51:00 +0100 Subject: [PATCH 014/122] notification of gateways about services from discovery service that they were changed to evict caches in there --- common-service-core/build.gradle | 2 + .../java/com/ca/mfaas/cache/CompositeKey.java | 73 ++++++++++ .../ca/mfaas/cache/CompositeKeyGenerator.java | 42 ++++++ .../java/com/ca/mfaas/util/EurekaUtils.java | 52 +++++++ .../cache/CompositeKeyGeneratorTest.java | 43 ++++++ .../com/ca/mfaas/cache/CompositeKeyTest.java | 66 +++++++++ .../com/ca/mfaas/util/EurekaUtilsTest.java | 52 +++++++ .../EurekaInstanceRegisteredListener.java | 18 +-- .../EurekaInstanceRenewedListener.java | 35 +++++ .../ca/mfaas/discovery/GatewayNotifier.java | 49 +++++++ .../EurekaInstanceRegisteredListenerTest.java | 35 +++-- .../EurekaInstanceRenewedListenerTest.java | 48 +++++++ .../mfaas/discovery/GatewayNotifierTest.java | 73 ++++++++++ .../metadata/MetadataDefaultsServiceTest.java | 5 + .../ca/mfaas/gateway/config/CacheConfig.java | 9 ++ .../gateway/controllers/AuthController.java | 12 +- .../controllers/CacheServiceController.java | 43 ++++++ .../pre/ServiceAuthenticationFilter.java | 4 +- .../GatewayRibbonLoadBalancingHttpClient.java | 2 +- .../service/AuthenticationService.java | 8 +- ... => ServiceAuthenticationServiceImpl.java} | 58 +++++++- .../security/service/ServiceCacheEvict.java | 30 ++++ .../schema/ServiceAuthenticationService.java | 38 +++++ .../controllers/AuthControllerTest.java | 7 +- .../CacheServiceControllerTest.java | 67 +++++++++ .../pre/ServiceAuthenticationFilterTest.java | 4 +- ...ServiceAuthenticationServiceImplTest.java} | 134 ++++++++++++------ 27 files changed, 920 insertions(+), 89 deletions(-) create mode 100644 common-service-core/src/main/java/com/ca/mfaas/cache/CompositeKey.java create mode 100644 common-service-core/src/main/java/com/ca/mfaas/cache/CompositeKeyGenerator.java create mode 100644 common-service-core/src/main/java/com/ca/mfaas/util/EurekaUtils.java create mode 100644 common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyGeneratorTest.java create mode 100644 common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyTest.java create mode 100644 common-service-core/src/test/java/com/ca/mfaas/util/EurekaUtilsTest.java create mode 100644 discovery-service/src/main/java/com/ca/mfaas/discovery/EurekaInstanceRenewedListener.java create mode 100644 discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java create mode 100644 discovery-service/src/test/java/com/ca/mfaas/discovery/EurekaInstanceRenewedListenerTest.java create mode 100644 discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/CacheServiceController.java rename gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/{ServiceAuthenticationService.java => ServiceAuthenticationServiceImpl.java} (71%) create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceCacheEvict.java create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ServiceAuthenticationService.java create mode 100644 gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/CacheServiceControllerTest.java rename gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/{ServiceAuthenticationServiceTest.java => ServiceAuthenticationServiceImplTest.java} (73%) diff --git a/common-service-core/build.gradle b/common-service-core/build.gradle index a6f2edb973..053c683e56 100644 --- a/common-service-core/build.gradle +++ b/common-service-core/build.gradle @@ -9,7 +9,9 @@ dependencies { compileOnly(libraries.javax_servlet_api) compileOnly(libraries.lombok) + compileOnly(libraries.spring_boot_starter_cache) + testCompile(libraries.spring_boot_starter_cache) testCompile(libraries.jackson_core) testCompile(libraries.jackson_databind) testCompile(libraries.javax_servlet_api) diff --git a/common-service-core/src/main/java/com/ca/mfaas/cache/CompositeKey.java b/common-service-core/src/main/java/com/ca/mfaas/cache/CompositeKey.java new file mode 100644 index 0000000000..ced315e111 --- /dev/null +++ b/common-service-core/src/main/java/com/ca/mfaas/cache/CompositeKey.java @@ -0,0 +1,73 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.cache; + +import org.apache.commons.lang.ObjectUtils; +import org.apache.commons.lang.StringUtils; + +import java.io.Serializable; +import java.util.Arrays; + +/** + * CompositeKey replace default class using in cache org.springframework.cache.interceptor.SimpleKey. Original + * implementation doesn't allow to get a part of key. It disallow to filter keys in cache etc. + * + * In additional, this implementation support make key for null values. + */ +public class CompositeKey implements Serializable { + + private static final long serialVersionUID = -5241946774009988317L; + + public static final CompositeKey EMPTY = new CompositeKey(); + + private Serializable[] values; + + private int hashCode; + + public CompositeKey(Object...values) { + if (values == null) values = new Object[0]; + this.values = Arrays.copyOf(values, values.length, Serializable[].class); + this.hashCode = Arrays.deepHashCode(this.values); + } + + public Object get(int i) { + return this.values[i]; + } + + public boolean equals(int i, Object o) { + return ObjectUtils.equals(this.values[i], o); + } + + public int size() { + return values.length; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CompositeKey that = (CompositeKey) o; + if (that.hashCode != this.hashCode) return false; + + return Arrays.deepEquals(this.values, that.values); + } + + @Override + public int hashCode() { + return this.hashCode; + } + + @Override + public String toString() { + return getClass().getSimpleName() + " [" + StringUtils.join(values, ",") + "]"; + } + +} diff --git a/common-service-core/src/main/java/com/ca/mfaas/cache/CompositeKeyGenerator.java b/common-service-core/src/main/java/com/ca/mfaas/cache/CompositeKeyGenerator.java new file mode 100644 index 0000000000..024f52db96 --- /dev/null +++ b/common-service-core/src/main/java/com/ca/mfaas/cache/CompositeKeyGenerator.java @@ -0,0 +1,42 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.cache; + +import org.springframework.cache.interceptor.KeyGenerator; + +import java.lang.reflect.Method; + +/** + * Generator for spring cache, which generate {@link CompositeKey}. It allows to + * get from key its values. It could be helpful for particularly evict of cache. + */ +public class CompositeKeyGenerator implements KeyGenerator { + + @Override + public Object generate(Object target, Method method, Object... params) { + if (params == null) return CompositeKey.EMPTY; + + switch (params.length) { + case 0: + // in case of no parameter, use the same instance of empty key + return CompositeKey.EMPTY; + case 1: + // if there is just one param and it is not array (problem with equals), use just this + Object param = params[0]; + if (param != null && !param.getClass().isArray()) { + return param; + } + return new CompositeKey(params); + default: + return new CompositeKey(params); + } + } + +} diff --git a/common-service-core/src/main/java/com/ca/mfaas/util/EurekaUtils.java b/common-service-core/src/main/java/com/ca/mfaas/util/EurekaUtils.java new file mode 100644 index 0000000000..a640736db3 --- /dev/null +++ b/common-service-core/src/main/java/com/ca/mfaas/util/EurekaUtils.java @@ -0,0 +1,52 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.util; + +import com.netflix.appinfo.InstanceInfo; + +/** + * This util offer basic operation with eureka, like: extraction serviceId from instanceId, construct URL by + * InstanceInfo etc. + */ +public final class EurekaUtils { + + private EurekaUtils() { + + } + + /** + * Extract serviceId from instanceId + * @param instanceId input, instanceId in format "host:service:random number to unique instanceId" + * @return second part, it means serviceId. If it doesn't exist return null; + */ + public static final String getServiceIdFromInstanceId(String instanceId) { + final int startIndex = instanceId.indexOf(':'); + if (startIndex < 0) return null; + + final int endIndex = instanceId.indexOf(':', startIndex + 1); + if (endIndex < 0) return null; + + return instanceId.substring(startIndex + 1, endIndex); + } + + /** + * Construct base URL for specific InstanceInfo + * @param instanceInfo Instance of service, for which we want to get an URL + * @return URL to the instance + */ + public static final String getUrl(InstanceInfo instanceInfo) { + if (instanceInfo.getSecurePort() == 0) { + return "http://" + instanceInfo.getIPAddr() + ":" + instanceInfo.getPort(); + } else { + return "https://" + instanceInfo.getIPAddr() + ":" + instanceInfo.getSecurePort(); + } + } + +} diff --git a/common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyGeneratorTest.java b/common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyGeneratorTest.java new file mode 100644 index 0000000000..4a83d8f4a0 --- /dev/null +++ b/common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyGeneratorTest.java @@ -0,0 +1,43 @@ +package com.ca.mfaas.cache;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.lang.reflect.Method; + +import static org.junit.Assert.*; + +@RunWith(JUnit4.class) +public class CompositeKeyGeneratorTest { + + @Test + public void generate() throws NoSuchMethodException { + CompositeKeyGenerator g = new CompositeKeyGenerator(); + + Object target = new Object(); + Method method = this.getClass().getDeclaredMethod("generate"); + + assertSame(CompositeKey.EMPTY, g.generate(target, method, (Object[]) null)); + assertSame(CompositeKey.EMPTY, g.generate(target, method)); + + assertEquals("a", g.generate(target, method, "a")); + + Object param = new Object[] {"a"}; + Object response = g.generate(target, method, param); + assertTrue(response instanceof CompositeKey); + assertEquals(param, ((CompositeKey) response).get(0)); + + assertEquals(new CompositeKey("a", "b"), g.generate(target, method, "a", "b")); + assertEquals(new CompositeKey(new String[] {"a"}, "b"), g.generate(target, method, new String[] {"a"}, "b")); + } + +} diff --git a/common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyTest.java b/common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyTest.java new file mode 100644 index 0000000000..8965e3cb65 --- /dev/null +++ b/common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyTest.java @@ -0,0 +1,66 @@ +package com.ca.mfaas.cache;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static org.junit.Assert.*; + +@RunWith(JUnit4.class) +public class CompositeKeyTest { + + @Test + public void testInit() { + CompositeKey ck; + + ck = new CompositeKey(); + assertEquals(0, ck.size()); + + ck = new CompositeKey((Object[]) null); + assertEquals(0, ck.size()); + + ck = new CompositeKey("a", "b", "c"); + assertEquals(new CompositeKey("a", "b", "c"), ck); + assertNotEquals(new CompositeKey("a", "b", "d"), ck); + assertEquals(3, ck.size()); + assertEquals("a", ck.get(0)); + assertEquals("b", ck.get(1)); + assertEquals("c", ck.get(2)); + try { + ck.get(3); + fail(); + } catch (IndexOutOfBoundsException e) { + // expected exception + } + try { + ck.get(-1); + fail(); + } catch (IndexOutOfBoundsException e) { + // expected exception + } + + ck = new CompositeKey(new Object[] {"a", "b"}, "c"); + assertEquals(new CompositeKey(new Object[] {"a", "b"}, "c"), ck); + assertNotEquals(new CompositeKey(new Object[] {"a", "b"}, "d"), ck); + assertNotEquals(new CompositeKey(new Object[] {"a", "d"}, "c"), ck); + assertNotEquals(new CompositeKey(new Object[] {"d", "b"}, "c"), ck); + } + + @Test + public void testToString() { + assertEquals("CompositeKey []", new CompositeKey().toString()); + assertEquals("CompositeKey []", new CompositeKey((Object[]) null).toString()); + assertEquals("CompositeKey [a]", new CompositeKey("a").toString()); + assertEquals("CompositeKey [a,b]", new CompositeKey("a", "b").toString()); + assertEquals("CompositeKey [a,1,true]", new CompositeKey("a", 1, true).toString()); + } + +} diff --git a/common-service-core/src/test/java/com/ca/mfaas/util/EurekaUtilsTest.java b/common-service-core/src/test/java/com/ca/mfaas/util/EurekaUtilsTest.java new file mode 100644 index 0000000000..6dbe8351cc --- /dev/null +++ b/common-service-core/src/test/java/com/ca/mfaas/util/EurekaUtilsTest.java @@ -0,0 +1,52 @@ +package com.ca.mfaas.util;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import com.netflix.appinfo.InstanceInfo; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(JUnit4.class) +public class EurekaUtilsTest { + + @Test + public void test() { + assertEquals("abc", EurekaUtils.getServiceIdFromInstanceId("123:abc:def:::::xyz")); + assertEquals("abc", EurekaUtils.getServiceIdFromInstanceId("123:abc:def")); + assertEquals("", EurekaUtils.getServiceIdFromInstanceId("123::def")); + assertEquals("", EurekaUtils.getServiceIdFromInstanceId("::")); + assertNull(EurekaUtils.getServiceIdFromInstanceId(":")); + assertNull(EurekaUtils.getServiceIdFromInstanceId("")); + assertNull(EurekaUtils.getServiceIdFromInstanceId("abc")); + } + + private InstanceInfo createInstanceInfo(String ip, int port, int securePort) { + InstanceInfo out = mock(InstanceInfo.class); + when(out.getIPAddr()).thenReturn(ip); + when(out.getPort()).thenReturn(port); + when(out.getSecurePort()).thenReturn(securePort); + return out; + } + + @Test + public void testGetUrl() { + InstanceInfo ii1 = createInstanceInfo("127.0.0.1", 80, 0); + InstanceInfo ii2 = createInstanceInfo("192.168.0.1", 80, 443); + + assertEquals("http://127.0.0.1:80", EurekaUtils.getUrl(ii1)); + assertEquals("https://192.168.0.1:443", EurekaUtils.getUrl(ii2)); + } + +} diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/EurekaInstanceRegisteredListener.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/EurekaInstanceRegisteredListener.java index f575046e9c..15fea64ab5 100644 --- a/discovery-service/src/main/java/com/ca/mfaas/discovery/EurekaInstanceRegisteredListener.java +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/EurekaInstanceRegisteredListener.java @@ -11,6 +11,7 @@ import com.ca.mfaas.discovery.metadata.MetadataDefaultsService; import com.ca.mfaas.discovery.metadata.MetadataTranslationService; +import com.ca.mfaas.util.EurekaUtils; import com.netflix.appinfo.InstanceInfo; import lombok.RequiredArgsConstructor; import org.springframework.cloud.netflix.eureka.server.event.EurekaInstanceRegisteredEvent; @@ -28,16 +29,7 @@ public class EurekaInstanceRegisteredListener { private final MetadataTranslationService metadataTranslationService; private final MetadataDefaultsService metadataDefaultsService; - - protected String getServiceId(String instanceId) { - final int startIndex = instanceId.indexOf(':'); - if (startIndex < 0) return null; - - final int endIndex = instanceId.indexOf(':', startIndex + 1); - if (endIndex < 0) return null; - - return instanceId.substring(startIndex + 1, endIndex); - } + private final GatewayNotifier gatewayNotifier; /** * Translates service instance Eureka metadata from older versions to the current version @@ -46,8 +38,12 @@ protected String getServiceId(String instanceId) { public void listen(EurekaInstanceRegisteredEvent event) { final InstanceInfo instanceInfo = event.getInstanceInfo(); final Map metadata = instanceInfo.getMetadata(); + final String serviceId = EurekaUtils.getServiceIdFromInstanceId(instanceInfo.getInstanceId()); metadataTranslationService.translateMetadata(metadata); - metadataDefaultsService.updateMetadata(getServiceId(instanceInfo.getId()), metadata); + metadataDefaultsService.updateMetadata(serviceId, metadata); + // ie. new instance can have different authentication (than other one), this is reason to evict caches on gateway + gatewayNotifier.serviceUpdated(serviceId); } + } diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/EurekaInstanceRenewedListener.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/EurekaInstanceRenewedListener.java new file mode 100644 index 0000000000..55be0f5756 --- /dev/null +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/EurekaInstanceRenewedListener.java @@ -0,0 +1,35 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.discovery; + +import com.ca.mfaas.util.EurekaUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.cloud.netflix.eureka.server.event.EurekaInstanceRenewedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +/** + * Listener of updated services. + */ +@Component +@RequiredArgsConstructor +public class EurekaInstanceRenewedListener { + + private final GatewayNotifier gatewayNotifier; + + @EventListener + public void listen(EurekaInstanceRenewedEvent event) { + final String instanceId = event.getInstanceInfo().getInstanceId(); + final String serviceId = EurekaUtils.getServiceIdFromInstanceId(instanceId); + // ie. update instance can have different authentication, this is reason to evict caches on gateway + gatewayNotifier.serviceUpdated(serviceId); + } + +} diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java new file mode 100644 index 0000000000..4d8d0dc54e --- /dev/null +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java @@ -0,0 +1,49 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.discovery; + +import com.ca.mfaas.util.EurekaUtils; +import com.netflix.appinfo.InstanceInfo; +import com.netflix.eureka.EurekaServerContext; +import com.netflix.eureka.EurekaServerContextHolder; +import com.netflix.eureka.registry.PeerAwareInstanceRegistry; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.util.List; + +@Component +@AllArgsConstructor +public class GatewayNotifier { + + private final RestTemplate restTemplate; + + private EurekaServerContext getServerContext() { + return EurekaServerContextHolder.getInstance().getServerContext(); + } + + private PeerAwareInstanceRegistry getRegistry() { + return getServerContext().getRegistry(); + } + + public void serviceUpdated(String serviceId) { + final PeerAwareInstanceRegistry registry = getRegistry(); + final List gatewayInstances = registry.getInstancesById("gateway"); + + for (final InstanceInfo instanceInfo : gatewayInstances) { + final StringBuilder url = new StringBuilder(); + url.append(EurekaUtils.getUrl(instanceInfo)).append("/cache/services"); + if (serviceId != null) url.append('/').append(serviceId); + restTemplate.delete(url.toString()); + } + } + +} diff --git a/discovery-service/src/test/java/com/ca/mfaas/discovery/EurekaInstanceRegisteredListenerTest.java b/discovery-service/src/test/java/com/ca/mfaas/discovery/EurekaInstanceRegisteredListenerTest.java index 82f1f93dfa..0fab46e894 100644 --- a/discovery-service/src/test/java/com/ca/mfaas/discovery/EurekaInstanceRegisteredListenerTest.java +++ b/discovery-service/src/test/java/com/ca/mfaas/discovery/EurekaInstanceRegisteredListenerTest.java @@ -17,7 +17,6 @@ import org.springframework.cloud.netflix.eureka.server.event.EurekaInstanceRegisteredEvent; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; @@ -26,19 +25,10 @@ public class EurekaInstanceRegisteredListenerTest { @Test public void getServiceId() { - MetadataTranslationService metadataTranslationService = Mockito.mock(MetadataTranslationService.class); MetadataDefaultsService metadataDefaultsService = Mockito.mock(MetadataDefaultsService.class); - EurekaInstanceRegisteredListener eirl = new EurekaInstanceRegisteredListener(metadataTranslationService, metadataDefaultsService); - - assertEquals("abc", eirl.getServiceId("123:abc:def:::::xyz")); - assertEquals("abc", eirl.getServiceId("123:abc:def")); - assertEquals("", eirl.getServiceId("123::def")); - assertEquals("", eirl.getServiceId("::")); - assertNull(eirl.getServiceId(":")); - assertNull(eirl.getServiceId("")); - assertNull(eirl.getServiceId("abc")); + EurekaInstanceRegisteredListener eirl = new EurekaInstanceRegisteredListener(metadataTranslationService, metadataDefaultsService, mock(GatewayNotifier.class)); doAnswer( x -> { @@ -48,7 +38,7 @@ public void getServiceId() { ).when(metadataDefaultsService).updateMetadata(anyString(), any()); InstanceInfo instanceInfo = mock(InstanceInfo.class); - when(instanceInfo.getId()).thenReturn("1:serviceName:2"); + when(instanceInfo.getInstanceId()).thenReturn("1:serviceName:2"); EurekaInstanceRegisteredEvent event = mock(EurekaInstanceRegisteredEvent.class); when(event.getInstanceInfo()).thenReturn(instanceInfo); @@ -57,4 +47,25 @@ public void getServiceId() { verify(metadataDefaultsService, times(1)).updateMetadata(anyString(), any()); } + private EurekaInstanceRegisteredEvent createEvent(String instanceId) { + InstanceInfo instanceInfo = mock(InstanceInfo.class); + when(instanceInfo.getInstanceId()).thenReturn(instanceId); + + EurekaInstanceRegisteredEvent out = mock(EurekaInstanceRegisteredEvent.class); + when(out.getInstanceInfo()).thenReturn(instanceInfo); + + return out; + } + + @Test + public void testListen() { + GatewayNotifier notifier = mock(GatewayNotifier.class); + EurekaInstanceRegisteredListener listener = new EurekaInstanceRegisteredListener(Mockito.mock(MetadataTranslationService.class), Mockito.mock(MetadataDefaultsService.class), notifier); + + listener.listen(createEvent("host:service:instance")); + verify(notifier, times(1)).serviceUpdated("service"); + listener.listen(createEvent("unknown format")); + verify(notifier, times(1)).serviceUpdated(null); + } + } diff --git a/discovery-service/src/test/java/com/ca/mfaas/discovery/EurekaInstanceRenewedListenerTest.java b/discovery-service/src/test/java/com/ca/mfaas/discovery/EurekaInstanceRenewedListenerTest.java new file mode 100644 index 0000000000..f9952e9103 --- /dev/null +++ b/discovery-service/src/test/java/com/ca/mfaas/discovery/EurekaInstanceRenewedListenerTest.java @@ -0,0 +1,48 @@ +package com.ca.mfaas.discovery;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import com.netflix.appinfo.InstanceInfo; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.cloud.netflix.eureka.server.event.EurekaInstanceRenewedEvent; + +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class EurekaInstanceRenewedListenerTest { + + @InjectMocks + private EurekaInstanceRenewedListener listener; + + @Mock + private GatewayNotifier notifier; + + private EurekaInstanceRenewedEvent createEvent(String instanceId) { + InstanceInfo instanceInfo = mock(InstanceInfo.class); + when(instanceInfo.getInstanceId()).thenReturn(instanceId); + + EurekaInstanceRenewedEvent out = mock(EurekaInstanceRenewedEvent.class); + when(out.getInstanceInfo()).thenReturn(instanceInfo); + + return out; + } + + @Test + public void testListen() { + listener.listen(createEvent("host:service:instance")); + verify(notifier, times(1)).serviceUpdated("service"); + listener.listen(createEvent("unknown format")); + verify(notifier, times(1)).serviceUpdated(null); + } + +} diff --git a/discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java b/discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java new file mode 100644 index 0000000000..eeaa18dd13 --- /dev/null +++ b/discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java @@ -0,0 +1,73 @@ +package com.ca.mfaas.discovery;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import com.netflix.appinfo.InstanceInfo; +import com.netflix.eureka.EurekaServerContext; +import com.netflix.eureka.EurekaServerContextHolder; +import com.netflix.eureka.registry.AwsInstanceRegistry; +import com.netflix.eureka.registry.PeerAwareInstanceRegistry; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.springframework.web.client.RestTemplate; + +import java.util.Arrays; +import java.util.List; + +import static org.mockito.Mockito.*; + +@RunWith(JUnit4.class) +public class GatewayNotifierTest { + + private PeerAwareInstanceRegistry registry; + + @Before + public void setUp() { + EurekaServerContext context = mock(EurekaServerContext.class); + registry = mock(AwsInstanceRegistry.class); + when(context.getRegistry()).thenReturn(registry); + EurekaServerContextHolder.initialize(context); + } + + private InstanceInfo createInstanceInfo(String ipAddr, int port, int securePort) { + InstanceInfo out = mock(InstanceInfo.class); + when(out.getIPAddr()).thenReturn(ipAddr); + when(out.getPort()).thenReturn(port); + when(out.getSecurePort()).thenReturn(securePort); + return out; + } + + @Test + public void testServiceUpdated() { + RestTemplate restTemplate = mock(RestTemplate.class); + GatewayNotifier gatewayNotifier = new GatewayNotifier(restTemplate); + + verify(restTemplate, never()).delete(anyString()); + + List instances = Arrays.asList( + createInstanceInfo("127.0.0.1", 1000, 1433), + createInstanceInfo("192.168.0.1", 1000, 0) + ); + + when(registry.getInstancesById("gateway")).thenReturn(instances); + + gatewayNotifier.serviceUpdated("testService"); + verify(restTemplate, times(1)).delete("https://127.0.0.1:1433/cache/services/testService"); + verify(restTemplate, times(1)).delete("http://192.168.0.1:1000/cache/services/testService"); + + gatewayNotifier.serviceUpdated(null); + verify(restTemplate, times(1)).delete("https://127.0.0.1:1433/cache/services"); + verify(restTemplate, times(1)).delete("http://192.168.0.1:1000/cache/services"); + + verify(restTemplate, times(4)).delete(anyString()); + } + +} diff --git a/discovery-service/src/test/java/com/ca/mfaas/discovery/metadata/MetadataDefaultsServiceTest.java b/discovery-service/src/test/java/com/ca/mfaas/discovery/metadata/MetadataDefaultsServiceTest.java index 22f6626e6c..a20bd7b964 100644 --- a/discovery-service/src/test/java/com/ca/mfaas/discovery/metadata/MetadataDefaultsServiceTest.java +++ b/discovery-service/src/test/java/com/ca/mfaas/discovery/metadata/MetadataDefaultsServiceTest.java @@ -10,6 +10,7 @@ package com.ca.mfaas.discovery.metadata; import com.ca.mfaas.discovery.EurekaInstanceRegisteredListener; +import com.ca.mfaas.discovery.GatewayNotifier; import com.ca.mfaas.discovery.staticdef.ServiceDefinitionProcessor; import com.ca.mfaas.discovery.staticdef.StaticRegistrationResult; import com.ca.mfaas.discovery.staticdef.StaticServicesRegistrationService; @@ -21,6 +22,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; +import org.mockito.Mock; import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.cloud.netflix.eureka.server.event.EurekaInstanceRegisteredEvent; @@ -57,6 +59,9 @@ public class MetadataDefaultsServiceTest { @Spy private ServiceDefinitionProcessorMock serviceDefinitionProcessor; + @Mock + private GatewayNotifier gatewayNotifier; + private PeerAwareInstanceRegistry mockRegistry; @Before diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/config/CacheConfig.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/config/CacheConfig.java index 4370f7996e..fade04acbf 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/config/CacheConfig.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/config/CacheConfig.java @@ -9,10 +9,12 @@ */ package com.ca.mfaas.gateway.config; +import com.ca.mfaas.cache.CompositeKeyGenerator; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.ehcache.EhCacheCacheManager; import org.springframework.cache.ehcache.EhCacheManagerFactoryBean; +import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; @@ -24,6 +26,8 @@ @Configuration public class CacheConfig { + public static final String COMPOSITE_KEY_GENERATOR = "compositeKeyGenerator"; + @Bean public CacheManager cacheManager() { return new EhCacheCacheManager(ehCacheCacheManager().getObject()); @@ -37,4 +41,9 @@ public EhCacheManagerFactoryBean ehCacheCacheManager() { return cmfb; } + @Bean(CacheConfig.COMPOSITE_KEY_GENERATOR) + public KeyGenerator getCompositeKeyGenerator() { + return new CompositeKeyGenerator(); + } + } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/AuthController.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/AuthController.java index 4a35c3522b..17ee0a87f3 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/AuthController.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/AuthController.java @@ -12,6 +12,7 @@ import com.ca.mfaas.gateway.security.service.AuthenticationService; import lombok.AllArgsConstructor; import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; @@ -21,14 +22,19 @@ * by gateway to distribute state of authentication between nodes. */ @AllArgsConstructor -@RestController("/auth") +@RestController +@RequestMapping("/auth") public class AuthController { private final AuthenticationService authenticationService; - @DeleteMapping("/invalidate/**") + @DeleteMapping(path = "/invalidate/**") public Boolean invalidateJwtToken(HttpServletRequest request) { - final String jwtToken = request.getRequestURI().split(request.getContextPath() + "/invalidate/")[1]; + final String path = "/auth/invalidate/"; + final String uri = request.getRequestURI(); + final int index = uri.indexOf(path); + + final String jwtToken = (index >= 0) ? uri.substring(index + path.length()) : ""; return authenticationService.invalidateJwtToken(jwtToken, false); } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/CacheServiceController.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/CacheServiceController.java new file mode 100644 index 0000000000..2608f9ef7c --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/CacheServiceController.java @@ -0,0 +1,43 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.controllers; + +import com.ca.mfaas.gateway.security.service.ServiceCacheEvict; +import lombok.AllArgsConstructor; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * This controller allows control the caches about services. The main purpose is to evict cached data + * about services when a update happened in discovery service. Discovery service notifies about any + * change to be sure that cache on gateway is still valid. + */ +@AllArgsConstructor +@RestController +@RequestMapping("/cache/services") +public class CacheServiceController { + + private final List toEvict; + + @DeleteMapping(path = "") + public void evictAll() { + toEvict.forEach(ServiceCacheEvict::evictCacheAllService); + } + + @DeleteMapping(path = "/{serviceId}") + public void evict(@PathVariable("serviceId") String serviceId) { + toEvict.forEach(s -> s.evictCacheService(serviceId)); + } + +} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilter.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilter.java index c56ef80a2f..2f02b55a81 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilter.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilter.java @@ -10,7 +10,7 @@ package com.ca.mfaas.gateway.filters.pre; import com.ca.apiml.security.common.token.TokenAuthentication; -import com.ca.mfaas.gateway.security.service.ServiceAuthenticationService; +import com.ca.mfaas.gateway.security.service.ServiceAuthenticationServiceImpl; import com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; @@ -29,7 +29,7 @@ public class ServiceAuthenticationFilter extends ZuulFilter { @Autowired - private ServiceAuthenticationService serviceAuthenticationService; + private ServiceAuthenticationServiceImpl serviceAuthenticationService; @Override public String filterType() { diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java index 99b0649bae..006cf757a3 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java @@ -39,7 +39,7 @@ import java.util.Collections; import java.util.List; -import static com.ca.mfaas.gateway.security.service.ServiceAuthenticationService.AUTHENTICATION_COMMAND_KEY; +import static com.ca.mfaas.gateway.security.service.ServiceAuthenticationServiceImpl.AUTHENTICATION_COMMAND_KEY; import static org.springframework.cloud.netflix.ribbon.RibbonUtils.updateToSecureConnectionIfNeeded; @Slf4j diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java index 6679d6b001..ed39ea892f 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java @@ -15,6 +15,7 @@ import com.ca.apiml.security.common.token.TokenExpireException; import com.ca.apiml.security.common.token.TokenNotValidException; import com.ca.mfaas.constants.ApimlConstants; +import com.ca.mfaas.util.EurekaUtils; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.EurekaClient; import io.jsonwebtoken.Claims; @@ -109,12 +110,7 @@ public Boolean invalidateJwtToken(String jwtToken, boolean distribute) { for (final InstanceInfo instanceInfo : (List) discoveryClient.getInstancesById("gateway")) { if (StringUtils.equals(myInstanceId, instanceInfo.getInstanceId())) continue; - final String url; - if (instanceInfo.getSecurePort() == 0) { - url = "http://" + instanceInfo.getIPAddr() + ":" + instanceInfo.getPort() + "/auth/invalidate/{}"; - } else { - url = "https://" + instanceInfo.getIPAddr() + ":" + instanceInfo.getSecurePort() + "/auth/invalidate/{}"; - } + final String url = EurekaUtils.getUrl(instanceInfo) + "/auth/invalidate/{}"; restTemplate.delete(url, jwtToken); } } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationService.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java similarity index 71% rename from gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationService.java rename to gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java index 038360eb19..6dd97b6b69 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationService.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java @@ -12,13 +12,18 @@ import com.ca.apiml.security.common.auth.Authentication; import com.ca.apiml.security.common.auth.AuthenticationScheme; import com.ca.apiml.security.common.token.QueryResponse; +import com.ca.mfaas.cache.CompositeKey; +import com.ca.mfaas.gateway.config.CacheConfig; import com.ca.mfaas.gateway.security.service.schema.AbstractAuthenticationScheme; import com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand; import com.ca.mfaas.gateway.security.service.schema.AuthenticationSchemeFactory; +import com.ca.mfaas.gateway.security.service.schema.ServiceAuthenticationService; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.EurekaClient; import com.netflix.zuul.context.RequestContext; import lombok.AllArgsConstructor; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; @@ -52,15 +57,19 @@ */ @Service @AllArgsConstructor -public class ServiceAuthenticationService { +public class ServiceAuthenticationServiceImpl implements ServiceAuthenticationService { public static final String AUTHENTICATION_COMMAND_KEY = "zoweAuthenticationCommand"; + private static final String CACHE_BY_SERVICE_ID = "serviceAuthenticationByServiceId"; + private static final String CACHE_BY_AUTHENTICATION = "serviceAuthenticationByAuthentication"; + private final LoadBalancerAuthenticationCommand loadBalancerCommand = new LoadBalancerAuthenticationCommand(); private final EurekaClient discoveryClient; private final AuthenticationSchemeFactory authenticationSchemeFactory; private final AuthenticationService authenticationService; + private final CacheManager cacheManager; protected Authentication getAuthentication(InstanceInfo instanceInfo) { final Map metadata = instanceInfo.getMetadata(); @@ -79,16 +88,18 @@ protected AuthenticationService getAuthenticationService() { return authenticationService; } - @CacheEvict(value = "serviceAuthenticationByAuthentication", condition = "#result != null && #result.isExpired()") - @Cacheable("serviceAuthenticationByAuthentication") + @Override + @CacheEvict(value = CACHE_BY_AUTHENTICATION, condition = "#result != null && #result.isExpired()") + @Cacheable(CACHE_BY_AUTHENTICATION) public AuthenticationCommand getAuthenticationCommand(Authentication authentication, String jwtToken) { final AbstractAuthenticationScheme scheme = authenticationSchemeFactory.getSchema(authentication.getScheme()); final QueryResponse queryResponse = authenticationService.parseJwtToken(jwtToken); return scheme.createCommand(authentication, queryResponse); } - @CacheEvict(value = "serviceAuthenticationByServiceId", condition = "#result != null && #result.isExpired()") - @Cacheable("serviceAuthenticationByServiceId") + @Override + @CacheEvict(value = CACHE_BY_SERVICE_ID, condition = "#result != null && #result.isExpired()") + @Cacheable(value = CACHE_BY_SERVICE_ID, keyGenerator = CacheConfig.COMPOSITE_KEY_GENERATOR) public AuthenticationCommand getAuthenticationCommand(String serviceId, String jwtToken) { final List instances = discoveryClient.getInstancesById(serviceId); @@ -111,6 +122,41 @@ public AuthenticationCommand getAuthenticationCommand(String serviceId, String j return getAuthenticationCommand(found, jwtToken); } + @Override + @CacheEvict(value = CACHE_BY_SERVICE_ID, allEntries = true) + public void evictCacheAllService() { + // evict all cached data accessible by serviceId + } + + /** + * Method evicts all records in cache serviceAuthenticationByServiceId, where is as key serviceId or records where + * is impossible to known, which service is belongs to. + * + * @param serviceId Id of service to evict + */ + @Override + public void evictCacheService(String serviceId) { + final Cache cache = cacheManager.getCache(CACHE_BY_SERVICE_ID); + if (cache == null) throw new IllegalArgumentException("Unknown cache " + CACHE_BY_SERVICE_ID); + final Object nativeCache = cache.getNativeCache(); + if (nativeCache instanceof net.sf.ehcache.Cache) { + final net.sf.ehcache.Cache ehCache = (net.sf.ehcache.Cache) nativeCache; + + for (final Object key : ehCache.getKeys()) { + if (key instanceof CompositeKey) { + // if entry is compositeKey and first param is different, skip it (be sure this is not to evict) + final CompositeKey compositeKey = ((CompositeKey) key); + if (!compositeKey.equals(0, serviceId)) continue; + } + // if key is not composite key (unknown for evict) or has same serviceId, evict record + ehCache.remove(key); + } + } else { + // in case of using different cache manager, evict all records for sure + cache.clear(); + } + } + public class UniversalAuthenticationCommand extends AuthenticationCommand { private static final long serialVersionUID = -2980076158001292742L; @@ -124,7 +170,7 @@ public void apply(InstanceInfo instanceInfo) { final Authentication auth = getAuthentication(instanceInfo); final HttpServletRequest request = RequestContext.getCurrentContext().getRequest(); - final String jwtToken = getAuthenticationService().getJwtTokenFromRequest(request).get(); + final String jwtToken = getAuthenticationService().getJwtTokenFromRequest(request).orElse(null); final AuthenticationCommand cmd = getAuthenticationCommand(auth, jwtToken); cmd.apply(null); diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceCacheEvict.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceCacheEvict.java new file mode 100644 index 0000000000..c8a2a86266 --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceCacheEvict.java @@ -0,0 +1,30 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.security.service; + +/** + * This interface is implemented by beans where is using cache for services. If set of services will be + * changed in discovery service, it will call those methods to evict caches. It is necessary to avoid + * using old data after a change. + */ +public interface ServiceCacheEvict { + + /** + * Evict all cached data for specific serviceId + * @param serviceId Id of service with a change (to evict) + */ + public void evictCacheService(String serviceId); + + /** + * Evict all entries in caches about services and its instances + */ + public void evictCacheAllService(); + +} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ServiceAuthenticationService.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ServiceAuthenticationService.java new file mode 100644 index 0000000000..512ec98e4c --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ServiceAuthenticationService.java @@ -0,0 +1,38 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.security.service.schema; + +import com.ca.apiml.security.common.auth.Authentication; +import com.ca.mfaas.gateway.security.service.ServiceCacheEvict; + +/** + * Interface with base method to get AuthenticationCommand by serviceId or Authentication. + * + * {@see com.ca.mfaas.gateway.security.service.ServiceAuthenticationServiceImpl} + */ +public interface ServiceAuthenticationService extends ServiceCacheEvict { + + /** + * Get or create command to service's authentication using known Authentication object and jwtToken of current user + * @param authentication Object describing authentication to the service + * @param jwtToken JWT security token of user (authentication can depends on user privilege) + * @return authentication command to update request in ZUUL + */ + public AuthenticationCommand getAuthenticationCommand(Authentication authentication, String jwtToken); + + /** + * Get or create command to service's authentication using serviceId and jwtToken of current user + * @param serviceId ID of service to call + * @param jwtToken JWT security token of user (authentication can depends on user privilege) + * @return authentication command to update request in ZUUL (or lazy command to be updated in load balancer) + */ + public AuthenticationCommand getAuthenticationCommand(String serviceId, String jwtToken); + +} diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/AuthControllerTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/AuthControllerTest.java index bfd4092bba..940bd508e5 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/AuthControllerTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/AuthControllerTest.java @@ -41,14 +41,15 @@ public void setUp() { @Test public void invalidateJwtToken() throws Exception { when(authenticationService.invalidateJwtToken("a/b", false)).thenReturn(Boolean.TRUE); - this.mockMvc.perform(delete("/invalidate/a/b")).andExpect(status().isOk()).andExpect(content().string("true")); + this.mockMvc.perform(delete("/auth/invalidate/a/b")).andExpect(status().isOk()).andExpect(content().string("true")); when(authenticationService.invalidateJwtToken("abcde", false)).thenReturn(Boolean.TRUE); - this.mockMvc.perform(delete("/invalidate/abcde")).andExpect(status().isOk()).andExpect(content().string("true")); + this.mockMvc.perform(delete("/auth/invalidate/abcde")).andExpect(status().isOk()).andExpect(content().string("true")); - this.mockMvc.perform(delete("/invalidate/xyz")).andExpect(status().isOk()).andExpect(content().string("false")); + this.mockMvc.perform(delete("/auth/invalidate/xyz")).andExpect(status().isOk()).andExpect(content().string("false")); verify(authenticationService, times(1)).invalidateJwtToken("abcde", false); verify(authenticationService, times(1)).invalidateJwtToken("a/b", false); } + } diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/CacheServiceControllerTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/CacheServiceControllerTest.java new file mode 100644 index 0000000000..5e1cb13372 --- /dev/null +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/CacheServiceControllerTest.java @@ -0,0 +1,67 @@ +package com.ca.mfaas.gateway.controllers;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import com.ca.mfaas.gateway.security.service.ServiceCacheEvict; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.Arrays; + +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringJUnit4ClassRunner.class) +public class CacheServiceControllerTest { + + private MockMvc mockMvc; + + @Mock + private ServiceCacheEvict service1; + + @Mock + private ServiceCacheEvict service2; + + private CacheServiceController cacheServiceController; + + @Before + public void setUp() { + cacheServiceController = new CacheServiceController(Arrays.asList(service1, service2)); + mockMvc = MockMvcBuilders.standaloneSetup(cacheServiceController).build(); + } + + @Test + public void testEvictAll() throws Exception { + verify(service1, never()).evictCacheAllService(); + verify(service2, never()).evictCacheAllService(); + + this.mockMvc.perform(delete("/cache/services")).andExpect(status().isOk()); + + verify(service1, times(1)).evictCacheAllService(); + verify(service2, times(1)).evictCacheAllService(); + } + + @Test + public void testEvict() throws Exception { + verify(service1, never()).evictCacheService(any()); + verify(service2, never()).evictCacheService(any()); + + this.mockMvc.perform(delete("/cache/services/service01")).andExpect(status().isOk()); + + verify(service1, times(1)).evictCacheService("service01"); + verify(service2, times(1)).evictCacheService("service01"); + } + +} diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java index 0188e0cfd7..458eaa6c76 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java @@ -9,7 +9,7 @@ */ import com.ca.apiml.security.common.token.TokenAuthentication; -import com.ca.mfaas.gateway.security.service.ServiceAuthenticationService; +import com.ca.mfaas.gateway.security.service.ServiceAuthenticationServiceImpl; import com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand; import com.netflix.zuul.context.RequestContext; import org.junit.Before; @@ -28,7 +28,7 @@ public class ServiceAuthenticationFilterTest { @Mock - private ServiceAuthenticationService serviceAuthenticationService; + private ServiceAuthenticationServiceImpl serviceAuthenticationService; @InjectMocks private ServiceAuthenticationFilter serviceAuthenticationFilter; diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java similarity index 73% rename from gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceTest.java rename to gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java index 0319fb7b81..b2a1e2ba7b 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java @@ -12,9 +12,7 @@ import com.ca.apiml.security.common.auth.AuthenticationScheme; import com.ca.apiml.security.common.token.QueryResponse; import com.ca.mfaas.gateway.config.CacheConfig; -import com.ca.mfaas.gateway.security.service.schema.AbstractAuthenticationScheme; -import com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand; -import com.ca.mfaas.gateway.security.service.schema.AuthenticationSchemeFactory; +import com.ca.mfaas.gateway.security.service.schema.*; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.EurekaClient; import com.netflix.zuul.context.RequestContext; @@ -22,10 +20,10 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.InjectMocks; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.ContextConfiguration; @@ -37,16 +35,16 @@ import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_APPLID; import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_SCHEME; -import static com.ca.mfaas.gateway.security.service.ServiceAuthenticationService.AUTHENTICATION_COMMAND_KEY; +import static com.ca.mfaas.gateway.security.service.ServiceAuthenticationServiceImpl.AUTHENTICATION_COMMAND_KEY; import static org.junit.Assert.*; import static org.mockito.Mockito.*; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = { - ServiceAuthenticationServiceTest.Context.class, + ServiceAuthenticationServiceImplTest.Context.class, CacheConfig.class }) -public class ServiceAuthenticationServiceTest { +public class ServiceAuthenticationServiceImplTest { @Autowired private EurekaClient discoveryClient; @@ -58,13 +56,23 @@ public class ServiceAuthenticationServiceTest { private AuthenticationService authenticationService; @Autowired - @InjectMocks private ServiceAuthenticationService serviceAuthenticationService; + @Autowired + private CacheManager cacheManager; + + /** + * secondary instance to check protected methods + */ + private ServiceAuthenticationServiceImpl serviceAuthenticationServiceImpl; + @Before public void init() { MockitoAnnotations.initMocks(this); RequestContext.testSetCurrentContext(null); + serviceAuthenticationService.evictCacheAllService(); + + serviceAuthenticationServiceImpl = new ServiceAuthenticationServiceImpl(discoveryClient, authenticationSchemeFactory, authenticationService, cacheManager); } private InstanceInfo createInstanceInfo(String instanceId, String scheme, String applid) { @@ -94,16 +102,16 @@ public void testGetAuthentication() { InstanceInfo ii; ii = createInstanceInfo("instance1", "bypass", "applid"); - assertEquals(new Authentication(AuthenticationScheme.BYPASS, "applid"), serviceAuthenticationService.getAuthentication(ii)); + assertEquals(new Authentication(AuthenticationScheme.BYPASS, "applid"), serviceAuthenticationServiceImpl.getAuthentication(ii)); ii = createInstanceInfo("instance2", "zoweJwt", "applid2"); - assertEquals(new Authentication(AuthenticationScheme.ZOWE_JWT, "applid2"), serviceAuthenticationService.getAuthentication(ii)); + assertEquals(new Authentication(AuthenticationScheme.ZOWE_JWT, "applid2"), serviceAuthenticationServiceImpl.getAuthentication(ii)); ii = createInstanceInfo("instance2", "httpBasicPassTicket", null); - assertEquals(new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, null), serviceAuthenticationService.getAuthentication(ii)); + assertEquals(new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, null), serviceAuthenticationServiceImpl.getAuthentication(ii)); ii = createInstanceInfo("instance2", (AuthenticationScheme) null, null); - assertEquals(new Authentication(), serviceAuthenticationService.getAuthentication(ii)); + assertEquals(new Authentication(), serviceAuthenticationServiceImpl.getAuthentication(ii)); } @Test @@ -157,7 +165,7 @@ public void testGetAuthenticationCommandByServiceId() { InstanceInfo ii3 = createInstanceInfo("inst01", a3); InstanceInfo ii4 = createInstanceInfo("inst01", a4); - ServiceAuthenticationService sas = spy(serviceAuthenticationService); + ServiceAuthenticationService sas = spy(serviceAuthenticationServiceImpl); when(authenticationSchemeFactory.getSchema(any())).thenReturn(mock(AbstractAuthenticationScheme.class)); @@ -174,19 +182,19 @@ public void testGetAuthenticationCommandByServiceId() { // multiple different instances reset(discoveryClient); when(discoveryClient.getInstancesById("svr03")).thenReturn(Arrays.asList(ii1, ii2)); - assertTrue(sas.getAuthenticationCommand("svr03", "jwt03") instanceof ServiceAuthenticationService.LoadBalancerAuthenticationCommand); + assertTrue(sas.getAuthenticationCommand("svr03", "jwt03") instanceof ServiceAuthenticationServiceImpl.LoadBalancerAuthenticationCommand); reset(discoveryClient); when(discoveryClient.getInstancesById("svr03")).thenReturn(Arrays.asList(ii1, ii3)); - assertTrue(sas.getAuthenticationCommand("svr03", "jwt03") instanceof ServiceAuthenticationService.LoadBalancerAuthenticationCommand); + assertTrue(sas.getAuthenticationCommand("svr03", "jwt03") instanceof ServiceAuthenticationServiceImpl.LoadBalancerAuthenticationCommand); reset(discoveryClient); when(discoveryClient.getInstancesById("svr03")).thenReturn(Arrays.asList(ii1, ii4)); - assertTrue(sas.getAuthenticationCommand("svr03", "jwt03") instanceof ServiceAuthenticationService.LoadBalancerAuthenticationCommand); + assertTrue(sas.getAuthenticationCommand("svr03", "jwt03") instanceof ServiceAuthenticationServiceImpl.LoadBalancerAuthenticationCommand); reset(discoveryClient); when(discoveryClient.getInstancesById("svr03")).thenReturn(Arrays.asList(ii1, ii2, ii3, ii4)); - assertTrue(sas.getAuthenticationCommand("svr03", "jwt03") instanceof ServiceAuthenticationService.LoadBalancerAuthenticationCommand); + assertTrue(sas.getAuthenticationCommand("svr03", "jwt03") instanceof ServiceAuthenticationServiceImpl.LoadBalancerAuthenticationCommand); } @Test @@ -205,6 +213,7 @@ public void testGetAuthenticationCommandByServiceIdCache() { assertSame(ac1, serviceAuthenticationService.getAuthenticationCommand("s1", "jwt")); verify(discoveryClient, times(1)).getInstancesById("s1"); + serviceAuthenticationService.evictCacheAllService(); Mockito.reset(aas1); when(aas1.getScheme()).thenReturn(AuthenticationScheme.HTTP_BASIC_PASSTICKET); when(aas1.createCommand(eq(new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applid1")), any())) @@ -217,7 +226,7 @@ public void testGetAuthenticationCommandByServiceIdCache() { @Test public void testUniversalAuthenticationCommand() { - ServiceAuthenticationService.UniversalAuthenticationCommand uac = serviceAuthenticationService.new UniversalAuthenticationCommand(); + ServiceAuthenticationServiceImpl.UniversalAuthenticationCommand uac = serviceAuthenticationServiceImpl.new UniversalAuthenticationCommand(); try { uac.apply(null); @@ -244,39 +253,53 @@ public void testUniversalAuthenticationCommand() { @Test public void testLoadBalancerAuthenticationCommand() { - ServiceAuthenticationService.LoadBalancerAuthenticationCommand lbac = serviceAuthenticationService.new LoadBalancerAuthenticationCommand(); + ServiceAuthenticationServiceImpl.LoadBalancerAuthenticationCommand lbac = serviceAuthenticationServiceImpl.new LoadBalancerAuthenticationCommand(); RequestContext requestContext = new RequestContext(); RequestContext.testSetCurrentContext(requestContext); assertNull(requestContext.get(AUTHENTICATION_COMMAND_KEY)); lbac.apply(null); - assertTrue(requestContext.get(AUTHENTICATION_COMMAND_KEY) instanceof ServiceAuthenticationService.UniversalAuthenticationCommand); + assertTrue(requestContext.get(AUTHENTICATION_COMMAND_KEY) instanceof ServiceAuthenticationServiceImpl.UniversalAuthenticationCommand); } - @Configuration - public static class Context { - - @Bean - public EurekaClient getDiscoveryClient() { - return mock(EurekaClient.class); - } - - @Bean - public AuthenticationSchemeFactory getAuthenticationSchemeFactory() { - return mock(AuthenticationSchemeFactory.class); - } - - @Bean - public AuthenticationService getAuthenticationService() { - return mock(AuthenticationService.class); - } - - @Bean - public ServiceAuthenticationService getServiceAuthenticationService() { - return new ServiceAuthenticationService(getDiscoveryClient(), getAuthenticationSchemeFactory(), getAuthenticationService()); - } - + @Test + public void testEvictCacheService() { + AuthenticationCommand command = AuthenticationCommand.EMPTY; + Authentication auth = new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applicationId0001"); + doReturn(Collections.singletonList(createInstanceInfo("instance0001", auth))).when(discoveryClient).getInstancesById("service0001"); + doReturn(Collections.singletonList(createInstanceInfo("instance0002", auth))).when(discoveryClient).getInstancesById("service0002"); + doReturn(new ByPassScheme()).when(authenticationSchemeFactory).getSchema(auth.getScheme()); + + verify(discoveryClient, never()).getInstancesById("service0001"); + verify(discoveryClient, never()).getInstancesById("service0002"); + + assertSame(command, serviceAuthenticationService.getAuthenticationCommand("service0001", "jwt01")); + assertSame(command, serviceAuthenticationService.getAuthenticationCommand("service0001", "jwt01")); + verify(discoveryClient, times(1)).getInstancesById("service0001"); + verify(discoveryClient, never()).getInstancesById("service0002"); + + assertSame(command, serviceAuthenticationService.getAuthenticationCommand("service0001", "jwt02")); + assertSame(command, serviceAuthenticationService.getAuthenticationCommand("service0002", "jwt01")); + verify(discoveryClient, times(2)).getInstancesById("service0001"); + verify(discoveryClient, times(1)).getInstancesById("service0002"); + + serviceAuthenticationService.evictCacheService("service0001"); + + assertSame(command, serviceAuthenticationService.getAuthenticationCommand("service0001", "jwt01")); + verify(discoveryClient, times(3)).getInstancesById("service0001"); + assertSame(command, serviceAuthenticationService.getAuthenticationCommand("service0001", "jwt02")); + verify(discoveryClient, times(4)).getInstancesById("service0001"); + assertSame(command, serviceAuthenticationService.getAuthenticationCommand("service0002", "jwt01")); + verify(discoveryClient, times(1)).getInstancesById("service0002"); + + serviceAuthenticationService.evictCacheAllService(); + assertSame(command, serviceAuthenticationService.getAuthenticationCommand("service0001", "jwt01")); + verify(discoveryClient, times(5)).getInstancesById("service0001"); + assertSame(command, serviceAuthenticationService.getAuthenticationCommand("service0001", "jwt02")); + verify(discoveryClient, times(6)).getInstancesById("service0001"); + assertSame(command, serviceAuthenticationService.getAuthenticationCommand("service0002", "jwt01")); + verify(discoveryClient, times(2)).getInstancesById("service0002"); } public class AuthenticationCommandTest extends AuthenticationCommand { @@ -300,4 +323,29 @@ public void apply(InstanceInfo instanceInfo) { } + @Configuration + public static class Context { + + @Bean + public EurekaClient getDiscoveryClient() { + return mock(EurekaClient.class); + } + + @Bean + public AuthenticationSchemeFactory getAuthenticationSchemeFactory() { + return mock(AuthenticationSchemeFactory.class); + } + + @Bean + public AuthenticationService getAuthenticationService() { + return mock(AuthenticationService.class); + } + + @Bean + public ServiceAuthenticationService getServiceAuthenticationService(@Autowired CacheManager cacheManager) { + return new ServiceAuthenticationServiceImpl(getDiscoveryClient(), getAuthenticationSchemeFactory(), getAuthenticationService(), cacheManager); + } + + } + } From 305831a3a5a96a7d3c54f97346c2523e9f95c3b0 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Mon, 6 Jan 2020 12:30:00 +0100 Subject: [PATCH 015/122] Add missing setup step for new users Signed-off-by: Petr Plavjanik --- .gitignore | 1 + README.md | 48 ++++++++++++++++++++++++++++++++++-------------- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 79f7026bb5..a3d460486b 100644 --- a/.gitignore +++ b/.gitignore @@ -86,3 +86,4 @@ api-catalog-ui/frontend/debug.log debug.log api-layer.tar.gz +/*.log diff --git a/README.md b/README.md index 9f19697b35..a16f1fc816 100644 --- a/README.md +++ b/README.md @@ -17,25 +17,39 @@ Following platform is required to run the API Mediation Layer: Following tools are required to build and develop API Mediation Layer: -Nodejs and npm are required to be installed globally to be able to build the API Catalog ui +* Node.js and npm are required to be installed globally to be able to build the API Catalog UI ## Quick start 1. Install the package manager `pnpm` globally in order to build the project: - - npm add -g pnpm - -2. Install `concurrently` globally: - - npm install -g concurrently - -3. Build all modules: - ./gradlew build + ```sh + npm add -g pnpm + ``` -4. Run all service on local machine: +2. Install npm packages for the UI: - npm run api-layer + ```sh + cd api-catalog-ui/frontend/; pnpm install; cd ../.. + ``` + +3. Install `concurrently` globally: + + ```sh + npm install -g concurrently + ``` + +4. Build all modules: + + ```sh + ./gradlew build + ``` + +5. Run all service on local machine: + + ```sh + npm run api-layer + ``` ## Authentication service @@ -44,12 +58,12 @@ The API Mediation Layer uses a dummy authentication provides as a default securi ### (Optional) z/OSMF Authentication Perform the following steps to use the real authentication service: - + 1. Configure a valid z/OSMF instance using the following sample configuration `config/local/api-defs/zosmf-sample.yml`. 2. Modify [gateway-service.yml](config/local/gateway-service.yml) with a z/OSMF configuration -``` +```yaml apiml: security: auth: @@ -71,7 +85,9 @@ Unit tests for Java and TypeScript modules are executed as a part of the build p For the code coverage of all modules, run: +```sh ./gradlew coverage +``` The code coverage for new code should be higher that 60% and should not be decreased for existing code. @@ -79,11 +95,15 @@ The reports in HTML format are stored `build/reports/jacoco/test/html/index.html For the code coverage of a single Java module (for example `discovery-service`), run: +```sh ./gradlew :discovery-service:jacocoTestReport +``` You can an individual test class by: +```sh ./gradlew :discovery-service:test --tests com.ca.mfaas.discovery.staticdef.ServiceDefinitionProcessorTest +``` ## Run integration tests From 273e1c8d68742651dfab7ef28b3c174db7ba9414 Mon Sep 17 00:00:00 2001 From: JirkaAichler Date: Mon, 6 Jan 2020 14:03:27 +0100 Subject: [PATCH 016/122] Add /ticket endpoint to generate PassTickets --- .../config/AuthConfigurationProperties.java | 1 + .../common/service/PassTicketService.java | 3 +- gateway-service/build.gradle | 7 +- .../config/ComponentsConfiguration.java | 9 ++ .../config/SecurityConfiguration.java | 19 ++- .../gateway/security/query/QueryFilter.java | 5 +- .../ApplicationNameNotFoundException.java | 24 ++++ .../ticket/SuccessfulTicketHandler.java | 86 +++++++++++++ .../security/ticket/TicketRequest.java | 24 ++++ .../security/ticket/TicketResponse.java | 25 ++++ .../main/resources/gateway-log-messages.yml | 8 +- .../security/query/QueryFilterTest.java | 9 +- .../ticket/SuccessfulTicketHandlerTest.java | 83 +++++++++++++ .../src/test/resources/gateway-messages.yml | 116 +++++++++++++++++- 14 files changed, 405 insertions(+), 14 deletions(-) create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/ApplicationNameNotFoundException.java create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandler.java create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/TicketRequest.java create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/TicketResponse.java create mode 100644 gateway-service/src/test/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandlerTest.java diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/config/AuthConfigurationProperties.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/config/AuthConfigurationProperties.java index 1b1c4a81c0..81a178e638 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/config/AuthConfigurationProperties.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/config/AuthConfigurationProperties.java @@ -31,6 +31,7 @@ public class AuthConfigurationProperties { // General properties private String gatewayLoginEndpoint = "/api/v1/gateway/auth/login"; private String gatewayQueryEndpoint = "/api/v1/gateway/auth/query"; + private String gatewayTicketEndpoint = "/api/v1/gateway/auth/ticket"; private String serviceLoginEndpoint = "/auth/login"; private String serviceLogoutEndpoint = "/auth/logout"; diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java index ee26377564..9d42458fe3 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java @@ -45,6 +45,7 @@ public boolean isUsingRacImplementation() { } public static class DefaultPassTicketImpl implements IRRPassTicket { + public static final String ZOWE_DUMMY_PASSTICKET = "ZoweDummyPassTicket"; @Override public void evaluate(String userId, String applId, String passTicket) { @@ -57,7 +58,7 @@ public void evaluate(String userId, String applId, String passTicket) { @Override public String generate(String userId, String applId) { - return null; + return ZOWE_DUMMY_PASSTICKET; } } diff --git a/gateway-service/build.gradle b/gateway-service/build.gradle index 47daadb672..b9a95f8791 100644 --- a/gateway-service/build.gradle +++ b/gateway-service/build.gradle @@ -18,10 +18,10 @@ springBoot { properties { // Generate extra build info: additionalProperties = [ - by: System.properties['user.name'], + by : System.properties['user.name'], operatingSystem: "${System.properties['os.name']} (${System.properties['os.version']})", - number: System.getenv('BUILD_NUMBER') ? System.getenv('BUILD_NUMBER') : "n/a", - machine: InetAddress.localHost.hostName + number : System.getenv('BUILD_NUMBER') ? System.getenv('BUILD_NUMBER') : "n/a", + machine : InetAddress.localHost.hostName ] } } @@ -49,7 +49,6 @@ dependencies { compile libraries.spring_cloud_starter_ribbon compile libraries.jetty_websocket_client compile libraries.jjwt - compile libraries.spring_boot_starter_cache compile libraries.eh_cache compileOnly libraries.lombok diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/config/ComponentsConfiguration.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/config/ComponentsConfiguration.java index ba383e4f28..d3e9eef420 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/config/ComponentsConfiguration.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/config/ComponentsConfiguration.java @@ -9,6 +9,7 @@ */ package com.ca.mfaas.gateway.security.config; +import com.ca.apiml.security.common.service.PassTicketService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -26,4 +27,12 @@ public class ComponentsConfiguration { public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(10); } + + /** + * Used to generate PassTickets + */ + @Bean + public PassTicketService passTicketService() { + return new PassTicketService(); + } } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/config/SecurityConfiguration.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/config/SecurityConfiguration.java index 3251dfaec2..eaa69bef0a 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/config/SecurityConfiguration.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/config/SecurityConfiguration.java @@ -17,6 +17,7 @@ import com.ca.mfaas.gateway.security.query.QueryFilter; import com.ca.mfaas.gateway.security.query.SuccessfulQueryHandler; import com.ca.mfaas.gateway.security.service.AuthenticationService; +import com.ca.mfaas.gateway.security.ticket.SuccessfulTicketHandler; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; @@ -50,6 +51,7 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { private final AuthConfigurationProperties authConfigurationProperties; private final HandlerInitializer handlerInitializer; private final SuccessfulQueryHandler successfulQueryHandler; + private final SuccessfulTicketHandler successfulTicketHandler; private final AuthProviderInitializer authProviderInitializer; @Override @@ -88,10 +90,11 @@ protected void configure(HttpSecurity http) throws Exception { .antMatchers("/application/health", "/application/info").permitAll() .antMatchers("/application/**").authenticated() - // add filters - login + query + // add filters - login, query, ticket .and() .addFilterBefore(loginFilter(authConfigurationProperties.getGatewayLoginEndpoint()), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(queryFilter(authConfigurationProperties.getGatewayQueryEndpoint()), UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(ticketFilter(authConfigurationProperties.getGatewayTicketEndpoint()), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(basicFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(cookieFilter(), UsernamePasswordAuthenticationFilter.class); } @@ -118,6 +121,20 @@ private QueryFilter queryFilter(String queryEndpoint) throws Exception { successfulQueryHandler, handlerInitializer.getAuthenticationFailureHandler(), authenticationService, + HttpMethod.GET, + authenticationManager()); + } + + /** + * Processes /ticket requests + */ + private QueryFilter ticketFilter(String ticketEndpoint) throws Exception { + return new QueryFilter( + ticketEndpoint, + successfulTicketHandler, + handlerInitializer.getAuthenticationFailureHandler(), + authenticationService, + HttpMethod.POST, authenticationManager()); } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/query/QueryFilter.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/query/QueryFilter.java index 8f2c725418..e9ffb797d1 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/query/QueryFilter.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/query/QueryFilter.java @@ -35,17 +35,20 @@ public class QueryFilter extends AbstractAuthenticationProcessingFilter { private final AuthenticationSuccessHandler successHandler; private final AuthenticationFailureHandler failureHandler; private final AuthenticationService authenticationService; + private final HttpMethod httpMethod; public QueryFilter( String authEndpoint, AuthenticationSuccessHandler successHandler, AuthenticationFailureHandler failureHandler, AuthenticationService authenticationService, + HttpMethod httpMethod, AuthenticationManager authenticationManager) { super(authEndpoint); this.successHandler = successHandler; this.failureHandler = failureHandler; this.authenticationService = authenticationService; + this.httpMethod = httpMethod; this.setAuthenticationManager(authenticationManager); } @@ -60,7 +63,7 @@ public QueryFilter( */ @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) { - if (!request.getMethod().equals(HttpMethod.GET.name())) { + if (!request.getMethod().equals(httpMethod.name())) { throw new AuthMethodNotSupportedException(request.getMethod()); } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/ApplicationNameNotFoundException.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/ApplicationNameNotFoundException.java new file mode 100644 index 0000000000..f49fd4ef00 --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/ApplicationNameNotFoundException.java @@ -0,0 +1,24 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.security.ticket; + +/** + * Exception thrown when applicationName parameter was not provided + */ +public class ApplicationNameNotFoundException extends Exception { + + public ApplicationNameNotFoundException(String message) { + super(message); + } + + public ApplicationNameNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandler.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandler.java new file mode 100644 index 0000000000..e94d83b202 --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandler.java @@ -0,0 +1,86 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.security.ticket; + +import com.ca.apiml.security.common.service.PassTicketService; +import com.ca.apiml.security.common.token.TokenAuthentication; +import com.ca.mfaas.message.api.ApiMessageView; +import com.ca.mfaas.message.core.MessageService; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.apache.commons.lang3.StringUtils; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Handles the successful /ticket request + */ +@Component +@RequiredArgsConstructor +public class SuccessfulTicketHandler implements AuthenticationSuccessHandler { + private final ObjectMapper mapper; + private final PassTicketService passTicketService; + private final MessageService messageService; + + /** + * Set the ticket response containing PassTicket + * + * @param request the http request + * @param response the http response + * @param authentication the successful authentication + */ + @SneakyThrows + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); + + try { + response.setStatus(HttpStatus.OK.value()); + mapper.writeValue(response.getWriter(), getTicketResponse(request, authentication)); + } catch (ApplicationNameNotFoundException e) { + response.setStatus(HttpStatus.BAD_REQUEST.value()); + ApiMessageView messageView = messageService.createMessage("apiml.security.ticket.invalidApplicationName").mapToView(); + mapper.writeValue(response.getWriter(), messageView); + } + + response.getWriter().flush(); + if (!response.isCommitted()) { + throw new IOException("Authentication response has not been committed."); + } + } + + private TicketResponse getTicketResponse(HttpServletRequest request, Authentication authentication) throws ApplicationNameNotFoundException { + TokenAuthentication tokenAuthentication = (TokenAuthentication) authentication; + String userId = tokenAuthentication.getPrincipal(); + + String applicationName; + try { + applicationName = mapper.readValue(request.getInputStream(), TicketRequest.class).getApplicationName(); + } catch (IOException e) { + throw new ApplicationNameNotFoundException("Ticket object has wrong format."); + } + + if (StringUtils.isBlank(applicationName)) { + throw new ApplicationNameNotFoundException("ApplicationName not provided."); + } + + String ticket = passTicketService.generate(userId, applicationName); + + return new TicketResponse(tokenAuthentication.getCredentials(), userId, applicationName, ticket); + } +} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/TicketRequest.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/TicketRequest.java new file mode 100644 index 0000000000..9ae1544e6e --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/TicketRequest.java @@ -0,0 +1,24 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.security.ticket; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents /ticket JSON request with application id + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TicketRequest { + private String applicationName; +} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/TicketResponse.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/TicketResponse.java new file mode 100644 index 0000000000..0228a6a202 --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/TicketResponse.java @@ -0,0 +1,25 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.security.ticket; + +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * Represents /ticket JSON response with the ticket information + */ +@Data +@AllArgsConstructor +public class TicketResponse { + private String token; + private String userId; + private String applicationName; + private String ticket; +} diff --git a/gateway-service/src/main/resources/gateway-log-messages.yml b/gateway-service/src/main/resources/gateway-log-messages.yml index 22c0cda5c3..304068aba1 100644 --- a/gateway-service/src/main/resources/gateway-log-messages.yml +++ b/gateway-service/src/main/resources/gateway-log-messages.yml @@ -147,4 +147,10 @@ messages: reason: "No authorization token is provided." action: "Provide a valid authorization token." - + # Ticket messages (140 - 150) + - key: apiml.security.ticket.invalidApplicationName + number: ZWEAG140 + type: ERROR + text: "The 'applicationName' parameter name is missing." + reason: "The application name is not provided." + action: "Provide the 'applicationName' parameter." diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/query/QueryFilterTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/query/QueryFilterTest.java index 34e7748d99..36454de0cf 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/query/QueryFilterTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/query/QueryFilterTest.java @@ -22,8 +22,8 @@ import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.http.HttpMethod; -import javax.ws.rs.HttpMethod; import java.util.Optional; import static org.mockito.ArgumentMatchers.any; @@ -56,13 +56,14 @@ public void setup() { authenticationSuccessHandler, authenticationFailureHandler, authenticationService, + HttpMethod.GET, authenticationManager); } @Test public void shouldCallAuthenticationManagerAuthenticate() { httpServletRequest = new MockHttpServletRequest(); - httpServletRequest.setMethod(HttpMethod.GET); + httpServletRequest.setMethod(HttpMethod.GET.name()); httpServletResponse = new MockHttpServletResponse(); queryFilter.setAuthenticationManager(authenticationManager); when(authenticationService.getJwtTokenFromRequest(any())).thenReturn( @@ -77,7 +78,7 @@ public void shouldCallAuthenticationManagerAuthenticate() { @Test(expected = AuthMethodNotSupportedException.class) public void shouldRejectHttpMethods() { httpServletRequest = new MockHttpServletRequest(); - httpServletRequest.setMethod(HttpMethod.POST); + httpServletRequest.setMethod(HttpMethod.POST.name()); httpServletResponse = new MockHttpServletResponse(); queryFilter.attemptAuthentication(httpServletRequest, httpServletResponse); @@ -86,7 +87,7 @@ public void shouldRejectHttpMethods() { @Test(expected = TokenNotProvidedException.class) public void shouldRejectIfTokenIsNotPresent() { httpServletRequest = new MockHttpServletRequest(); - httpServletRequest.setMethod(HttpMethod.GET); + httpServletRequest.setMethod(HttpMethod.GET.name()); httpServletResponse = new MockHttpServletResponse(); when(authenticationService.getJwtTokenFromRequest(any())).thenReturn( Optional.empty() diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandlerTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandlerTest.java new file mode 100644 index 0000000000..a32807179a --- /dev/null +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandlerTest.java @@ -0,0 +1,83 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.security.ticket; + +import com.ca.apiml.security.common.service.PassTicketService; +import com.ca.apiml.security.common.token.TokenAuthentication; + +import com.ca.mfaas.message.core.MessageService; +import com.ca.mfaas.message.yaml.YamlMessageService; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import java.io.UnsupportedEncodingException; + +import static com.ca.apiml.security.common.service.PassTicketService.DefaultPassTicketImpl.ZOWE_DUMMY_PASSTICKET; +import static org.junit.Assert.*; + +public class SuccessfulTicketHandlerTest { + private static final String TOKEN = "token"; + private static final String USER = "user"; + private static final String APPLICATION_NAME = "app"; + + private final MockHttpServletRequest httpServletRequest = new MockHttpServletRequest(); + private final MockHttpServletResponse httpServletResponse = new MockHttpServletResponse(); + private final ObjectMapper mapper = new ObjectMapper(); + private final MessageService messageService = new YamlMessageService("/gateway-messages.yml"); + private final PassTicketService passTicketService = new PassTicketService(); + private final SuccessfulTicketHandler successfulTicketHandlerHandler = new SuccessfulTicketHandler(mapper, passTicketService, messageService); + private final TokenAuthentication tokenAuthentication = new TokenAuthentication(USER, TOKEN); + + @Before + public void setUp() { + passTicketService.init(); + + httpServletResponse.setStatus(HttpStatus.EXPECTATION_FAILED.value()); + assertNotEquals(HttpStatus.OK.value(), httpServletResponse.getStatus()); + } + + @Test + public void shouldReturnDummyPassTicket() throws JsonProcessingException, UnsupportedEncodingException { + httpServletRequest.setContent(mapper.writeValueAsBytes(new TicketRequest(APPLICATION_NAME))); + + successfulTicketHandlerHandler.onAuthenticationSuccess(httpServletRequest, httpServletResponse, tokenAuthentication); + + assertEquals(MediaType.APPLICATION_JSON_UTF8_VALUE, httpServletResponse.getContentType()); + assertEquals(HttpStatus.OK.value(), httpServletResponse.getStatus()); + String expectedResponse = mapper.writeValueAsString(new TicketResponse(TOKEN, USER, APPLICATION_NAME, ZOWE_DUMMY_PASSTICKET)); + assertEquals(expectedResponse, httpServletResponse.getContentAsString()); + assertTrue(httpServletResponse.isCommitted()); + } + + @Test + public void shouldFailWhenNoApplicationName() throws UnsupportedEncodingException, JsonProcessingException { + successfulTicketHandlerHandler.onAuthenticationSuccess(httpServletRequest, httpServletResponse, tokenAuthentication); + + assertEquals(MediaType.APPLICATION_JSON_UTF8_VALUE, httpServletResponse.getContentType()); + assertEquals(HttpStatus.BAD_REQUEST.value(), httpServletResponse.getStatus()); + assertTrue(httpServletResponse.getContentAsString().contains("ZWEAG140E")); + assertTrue(httpServletResponse.isCommitted()); + + httpServletRequest.setContent(mapper.writeValueAsBytes(new TicketRequest(""))); + httpServletResponse.setStatus(HttpStatus.EXPECTATION_FAILED.value()); + successfulTicketHandlerHandler.onAuthenticationSuccess(httpServletRequest, httpServletResponse, tokenAuthentication); + + assertEquals(MediaType.APPLICATION_JSON_UTF8_VALUE, httpServletResponse.getContentType()); + assertEquals(HttpStatus.BAD_REQUEST.value(), httpServletResponse.getStatus()); + assertTrue(httpServletResponse.getContentAsString().contains("ZWEAG140E")); + assertTrue(httpServletResponse.isCommitted()); + } +} diff --git a/gateway-service/src/test/resources/gateway-messages.yml b/gateway-service/src/test/resources/gateway-messages.yml index b855872b71..304068aba1 100644 --- a/gateway-service/src/test/resources/gateway-messages.yml +++ b/gateway-service/src/test/resources/gateway-messages.yml @@ -1,44 +1,156 @@ messages: - # Security messages (100 - 199) - # General messages (100 - 120) + # Info messages + # 000-099 + + # General messages + # 100-199 + + # HTTP,Protocol messages + # 400-499 + + # TLS,Certificate messages + # 500-599 + + # Various messages + # 600-699 + + # Service specific messages + # 700-999 + + - key: apiml.gateway.instanceNotFound + number: ZWEAG700 + type: ERROR + text: "No instance of the service '%s' found. Routing will not be available." + reason: "The Gateway could not find an instance of the service from the Discovery Service." + action: "Check that the service was successfully registered to the Discovery Service and wait for Spring Cloud to refresh the routes definitions" + + - key: apiml.gateway.jwtInitConfigError + number: ZWEAG704 + type: ERROR + text: "Configuration error '%s' when trying to read jwt secret: %s" + reason: "A problem occurred while trying to read the jwt secret key from the keystore." + action: "Review the mandatory fields used in the configuration such as the keystore location path, the keystore and key password, and the keystore type." + + - key: apiml.gateway.jwtKeyMissing + number: ZWEAG705 + type: ERROR + text: "Failed to load public or private key from key with alias '%s' in the keystore '%s'." + reason: "Failed to load a public or private key from the keystore during JWT Token initialization." + action: "Check that the key alias is specified and correct. Verify that the keys are present in the keystore." + + # Legacy messages + - key: apiml.security.generic number: ZWEAG100 type: ERROR text: "Authentication exception: '%s' for URL '%s'" + reason: "A generic failure occurred during authentication." + action: "Refer to specific authentication exception details for troubleshooting." - key: apiml.security.invalidMethod number: ZWEAG101 type: ERROR text: "Authentication method '%s' is not supported for URL '%s'" + reason: "The HTTP request method is not supported by the URL." + action: "Use the correct HTTP request method supported by the URL." - key: apiml.gateway.security.invalidToken number: ZWEAG102 type: ERROR text: "Token is not valid" + reason: "The JWT token is not valid" + action: "Provide a valid token." - key: apiml.gateway.security.expiredToken number: ZWEAG103 type: ERROR text: "Token is expired" + reason: "The JWT token has expired" + action: "Obtain new token by performing an authentication request." + + - key: apiml.security.serviceUnavailable + number: ZWEAG104 + type: ERROR + text: "Authentication service is not available at URL '%s'. Error returned: '%s'" + reason: "The authentication service is not available." + action: "Make sure that the authentication service is running and is accessible by the URL provided in the message." + + - key: apiml.security.authRequired + number: ZWEAG105 + type: ERROR + text: "Authentication is required for URL '%s'" + reason: "Authentication is required." + action: "Provide valid authentication." + + - key: apiml.security.loginEndpointInDummyMode + number: ZWEAG106 + type: WARNING + text: "Login endpoint is running in the dummy mode. Use credentials user/user to login. Do not use this option in the production environment." + reason: "The authentication is running in dummy mode." + action: "Do not use this option in the production environment." + + - key: apiml.security.invalidAuthenticationProvider + number: ZWEAG107 + type: WARNING + text: "Incorrect value: apiml.security.auth.provider = '%s'. Authentication provider is not set correctly. Default 'zosmf' authentication provider is used." + reason: "An incorrect value of the apiml.security.auth.provider parameter is set in the configuration." + action: "Ensure that the value of apiml.security.auth.provider is set either to 'dummy' if you want to use dummy mode, or to 'zosmf' if you want to use the z/OSMF authentication provider." + + - key: apiml.security.zosmfInstanceNotFound + number: ZWEAG108 + type: ERROR + text: "z/OSMF instance '%s' not found or incorrectly configured." + reason: "The Gateway could not find the z/OSMF instance from the Discovery Service." + action: "Ensure that the z/OSMF instance is configured correctly and that it is successfully registered to the Discovery Service." + + - key: apiml.security.zosmfDomainIsEmpty + number: ZWEAG109 + type: ERROR + text: "z/OSMF response does not contain field '%s'." + reason: "The z/OSMF domain cannot be read." + action: "Review the z/OSMF domain value contained in the response received from the 'zosmf/info' REST endpoint." + + - key: apiml.security.errorParsingZosmfResponse + number: ZWEAG110 + type: ERROR + text: "Error parsing z/OSMF response. Error returned: '%s" + reason: "An error occurred while parsing the z/OSMF JSON response." + action: "Check the JSON response received from the 'zosmf/info' REST endpoint." # Login messages (120 - 130) - key: apiml.security.login.invalidCredentials number: ZWEAG120 type: ERROR text: "Invalid username or password for URL '%s'" + reason: "The username or password are invalid." + action: "Provide a valid username and password." - key: apiml.security.login.invalidInput number: ZWEAG121 type: ERROR text: "Authorization header is missing, or request body is missing or invalid for URL '%s'" + reason: "The authorization header is missing, or the request body is missing or invalid." + action: "Provide valid authentication." # Query messages (130 - 140) - key: apiml.security.query.invalidToken number: ZWEAG130 type: ERROR text: "Token is not valid for URL '%s'" + reason: "The token is not valid." + action: "Provide a valid token." - key: apiml.security.query.tokenNotProvided number: ZWEAG131 type: ERROR text: "No authorization token provided for URL '%s'" + reason: "No authorization token is provided." + action: "Provide a valid authorization token." + + # Ticket messages (140 - 150) + - key: apiml.security.ticket.invalidApplicationName + number: ZWEAG140 + type: ERROR + text: "The 'applicationName' parameter name is missing." + reason: "The application name is not provided." + action: "Provide the 'applicationName' parameter." From 87c20ba884215967e18f107926f6636832dca35f Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Mon, 6 Jan 2020 14:14:03 +0100 Subject: [PATCH 017/122] PassTicket integration test Signed-off-by: Petr Plavjanik --- discoverable-client/build.gradle | 1 + .../client/api/PassTicketTestController.java | 66 +++++++++++++++++ integration-tests/README.md | 41 ++++++----- .../mfaas/gatewayservice/PassTicketTest.java | 72 +++++++++++++++++++ 4 files changed, 164 insertions(+), 16 deletions(-) create mode 100644 discoverable-client/src/main/java/com/ca/mfaas/client/api/PassTicketTestController.java create mode 100644 integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java diff --git a/discoverable-client/build.gradle b/discoverable-client/build.gradle index aaf06c5cfe..f4479fc75a 100644 --- a/discoverable-client/build.gradle +++ b/discoverable-client/build.gradle @@ -33,6 +33,7 @@ gitProperties { dependencies { compile(project(':integration-enabler-spring-v2')) compile(project(':apiml-common')) + compile(project(':apiml-security-common')) compile libraries.gson compile libraries.spring_boot_starter_actuator diff --git a/discoverable-client/src/main/java/com/ca/mfaas/client/api/PassTicketTestController.java b/discoverable-client/src/main/java/com/ca/mfaas/client/api/PassTicketTestController.java new file mode 100644 index 0000000000..c4c87e5abb --- /dev/null +++ b/discoverable-client/src/main/java/com/ca/mfaas/client/api/PassTicketTestController.java @@ -0,0 +1,66 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.client.api; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import javax.annotation.PostConstruct; + +import com.ca.apiml.security.common.service.IRRPassTicket; +import com.ca.apiml.security.common.service.PassTicketService.DefaultPassTicketImpl; +import com.ca.mfaas.util.ClassOrDefaultProxyUtils; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; + +/** + * Controller for testing PassTickets. + */ +@RestController +@Api(tags = { "Test Operations" }, description = "Operations for APIML Testing") +public class PassTicketTestController { + @Value("${apiml.service.applId:ZOWEAPPL}") + private String applId; + + private IRRPassTicket irrPassTicket; + + @PostConstruct + public void init() { + this.irrPassTicket = ClassOrDefaultProxyUtils.createProxy( + IRRPassTicket.class, + "com.ibm.eserver.zos.racf.IRRPassTicket", + DefaultPassTicketImpl::new + ); + } + + /** + * Validates the PassTicket in authorization header. + */ + @GetMapping(value = "/api/v1/passticketTest") + @ApiOperation(value = "Validate that the PassTicket in Authorization header is valid", tags = { "Test Operations" }) + public void passticketTest(@RequestHeader("authorization") String authorization) { + if (authorization != null && authorization.toLowerCase().startsWith("basic")) { + String base64Credentials = authorization.substring("Basic".length()).trim(); + String credentials = new String(Base64.getDecoder().decode(base64Credentials), StandardCharsets.UTF_8); + String[] values = credentials.split(":", 2); + String userId = values[0]; + String passTicket = values[1]; + irrPassTicket.evaluate(userId, applId, passTicket); + return; + } + throw new IllegalArgumentException("Missing Basic authorization header"); + } +} diff --git a/integration-tests/README.md b/integration-tests/README.md index 19a21fdaf0..5b8e7dcb8c 100644 --- a/integration-tests/README.md +++ b/integration-tests/README.md @@ -1,36 +1,37 @@ -## Integration Tests +# Integration Tests -### Introduction +## Introduction Integration tests are meant to test a functionality that requires multiple running services. We recommend you test most test cases using unit tests in the module. Use integration tests -only when necessary. +only when necessary. Integration tests require running instances of all services. These services must be started by the user. The integration test suite detects when services are started up and are ready to begin testing. -### General Quick start +## General Quick start -Perform a general quick start to execute tests within the pipeline. +Perform a general quick start to execute tests within the pipeline. **Follow these steps:** 1. Deploy and run all services. 2. Run the following shell script: - ```shell + + ```sh ./gradlew runIntegrationTests -Dcredentials.user= -Dcredentials.password= - ``` + ``` 3. (Optional) Change the host/port/scheme for the Gateway and Discovery Service with the following shell script: - ```shell + ```sh ./gradlew runIntegrationTests -Dcredentials.user= -Dcredentials.password= -Ddiscovery.host= -Ddiscovery.port= -Dgateway.host= -Dgateway.port= -Dgateway.scheme=https ``` -### Localhost Quick start +## Localhost Quick start Perform a Localhost Quick start when you need to run the tests on your local machine. @@ -42,13 +43,15 @@ Perform a Localhost Quick start when you need to run the tests on your local mac ```shell ./gradlew runLocalIntegrationTests - ``` + ``` + +3. (Optional) Run all local tests including all sample services with the following shell script: -3. (Optional) Run all local tests including all sample services with the following shell script: ```shell ./gradlew runAllLocalIntegrationTests ``` -### Manual testing of Discovery Service in HTTP mode + +## Manual testing of Discovery Service in HTTP mode The Discovery Service in HTTP mode is not integrated within the pipeline. You can perfom tests of the Discovery Service in HTTP mode manually. @@ -59,14 +62,14 @@ The Discovery Service in HTTP mode is not integrated within the pipeline. You ca 3. Run Discovery Service and verify that you can login into the Discovery Service homepage by using basic authentication with Eureka credentials. 4. Run your service and check that it is registered to Eureka. -### Running all tests (including slow) +## Running all tests (including slow) -Run special integration tests for tests that need to be performed slowly such as when you need to test timeouts. +Run special integration tests for tests that need to be performed slowly such as when you need to test timeouts. -**Note:** Executing these slow steps with other tests causes +**Note:** Executing these slow steps with other tests causes the entire test suite to take longer to execute. -Slow tests are annotated using @Category(SlowTests.class) as in the following example: +Slow tests are annotated using @Category(SlowTests.class) as in the following example: ```java @Test @@ -80,3 +83,9 @@ Start the suite of slow tests by executing the following shell script: ```shell ./gradlew :integrationTests:runAllIntegrationTests ``` + +## Running a Specific Test + +```sh +./gradlew :integration-tests:runIntegrationTests --tests com.ca.mfaas.gatewayservice.PassTicketTest +``` diff --git a/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java new file mode 100644 index 0000000000..f69359d881 --- /dev/null +++ b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java @@ -0,0 +1,72 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gatewayservice; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static org.apache.http.HttpStatus.SC_NO_CONTENT; +import static org.apache.http.HttpStatus.SC_OK; +import static org.hamcrest.Matchers.isEmptyString; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNot.not; +import static org.hamcrest.core.IsNull.notNullValue; +import static org.junit.Assert.assertThat; + +import com.ca.apiml.security.common.login.LoginRequest; +import com.ca.mfaas.util.config.ConfigReader; + +import org.junit.Before; +import org.junit.Test; + +import io.restassured.RestAssured; +import io.restassured.http.Cookie; + +public class PassTicketTest { + private final static String SCHEME = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration() + .getScheme(); + private final static String HOST = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration() + .getHost(); + private final static int PORT = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration().getPort(); + private final static String GATEWAY_BASE_PATH = "/api/v1/gateway"; + private final static String STATICCLIENT_BASE_PATH = "/api/v1/staticclient"; + private final static String LOGIN_ENDPOINT = "/auth/login"; + private final static String PASSTICKET_TEST_ENDPOINT = "/passticketTest"; + private final static String COOKIE_NAME = "apimlAuthenticationToken"; + private final static String USERNAME = ConfigReader.environmentConfiguration().getCredentials().getUser(); + private final static String PASSWORD = ConfigReader.environmentConfiguration().getCredentials().getPassword(); + + @Before + public void setUp() { + RestAssured.port = PORT; + RestAssured.useRelaxedHTTPSValidation(); + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); + } + + private String login() { + LoginRequest loginRequest = new LoginRequest(USERNAME, PASSWORD); + + Cookie cookie = given().contentType(JSON).body(loginRequest).when() + .post(String.format("%s://%s:%d%s%s", SCHEME, HOST, PORT, GATEWAY_BASE_PATH, LOGIN_ENDPOINT)).then() + .statusCode(is(SC_NO_CONTENT)).cookie(COOKIE_NAME, not(isEmptyString())).extract() + .detailedCookie(COOKIE_NAME); + + assertThat(cookie.getValue(), is(notNullValue())); + + return cookie.getValue(); + } + + @Test + public void accessServiceWithCorrectPassTicket() { + String jwt = login(); + given().cookie(COOKIE_NAME, jwt).when().get( + String.format("%s://%s:%d%s%s", SCHEME, HOST, PORT, STATICCLIENT_BASE_PATH, PASSTICKET_TEST_ENDPOINT)) + .then().statusCode(is(SC_OK)); + } +} From a4d015739fe675eca95ded8faa6ccf4578ebcbbf Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Mon, 6 Jan 2020 15:57:18 +0100 Subject: [PATCH 018/122] wip! Making the integration test work Signed-off-by: Petr Plavjanik --- .../common/service/PassTicketService.java | 5 ++- .../pre/ServiceAuthenticationFilter.java | 41 +++++++++++-------- .../ServiceAuthenticationServiceImpl.java | 4 +- integration-tests/README.md | 2 +- 4 files changed, 29 insertions(+), 23 deletions(-) diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java index 9d42458fe3..30a486b2d6 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java @@ -52,8 +52,9 @@ public void evaluate(String userId, String applId, String passTicket) { if (userId == null) throw new IllegalArgumentException("Parameter userId is empty"); if (applId == null) throw new IllegalArgumentException("Parameter applId is empty"); if (passTicket == null) throw new IllegalArgumentException("Parameter passTicket is empty"); - - throw new IllegalStateException("This implementation only for testing purpose"); + if (!passTicket.equals(ZOWE_DUMMY_PASSTICKET)) { + throw new IllegalArgumentException("Invalid PassTicket"); + } } @Override diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilter.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilter.java index 2f02b55a81..c048402732 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilter.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilter.java @@ -9,28 +9,36 @@ */ package com.ca.mfaas.gateway.filters.pre; -import com.ca.apiml.security.common.token.TokenAuthentication; +import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_DECORATION_FILTER_ORDER; +import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE; +import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SERVICE_ID_KEY; + +import java.util.Optional; + +import com.ca.mfaas.gateway.security.service.AuthenticationService; import com.ca.mfaas.gateway.security.service.ServiceAuthenticationServiceImpl; import com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; -import org.springframework.beans.factory.annotation.Autowired; -import java.security.Principal; - -import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.*; +import org.springframework.beans.factory.annotation.Autowired; /** - * This filter is responsible for customization request to clients from security point of view. In this filter is - * fetched AuthenticationCommand which support target security. In case it is possible decide now (all instances - * use the same authentication) it will modify immediately. Otherwise in request params will be set a command to - * load balancer. The request will be modified after specific instance will be selected. + * This filter is responsible for customization request to clients from security + * point of view. In this filter is fetched AuthenticationCommand which support + * target security. In case it is possible decide now (all instances use the + * same authentication) it will modify immediately. Otherwise in request params + * will be set a command to load balancer. The request will be modified after + * specific instance will be selected. */ public class ServiceAuthenticationFilter extends ZuulFilter { @Autowired private ServiceAuthenticationServiceImpl serviceAuthenticationService; + @Autowired + private AuthenticationService authenticationService; + @Override public String filterType() { return PRE_TYPE; @@ -50,17 +58,14 @@ public boolean shouldFilter() { public Object run() { final RequestContext context = RequestContext.getCurrentContext(); - String jwtToken = null; - final Principal principal = RequestContext.getCurrentContext().getRequest().getUserPrincipal(); - if (principal instanceof TokenAuthentication) { - jwtToken = ((TokenAuthentication) principal).getCredentials(); + Optional jwtToken = authenticationService.getJwtTokenFromRequest(context.getRequest()); + if (jwtToken.isPresent()) { + final String serviceId = (String) context.get(SERVICE_ID_KEY); + final AuthenticationCommand cmd = serviceAuthenticationService.getAuthenticationCommand(serviceId, + jwtToken.get()); + cmd.apply(null); } - final String serviceId = (String) context.get(SERVICE_ID_KEY); - - final AuthenticationCommand cmd = serviceAuthenticationService.getAuthenticationCommand(serviceId, jwtToken); - cmd.apply(null); - return null; } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java index 6dd97b6b69..dd4c5c3c67 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java @@ -101,7 +101,7 @@ public AuthenticationCommand getAuthenticationCommand(Authentication authenticat @CacheEvict(value = CACHE_BY_SERVICE_ID, condition = "#result != null && #result.isExpired()") @Cacheable(value = CACHE_BY_SERVICE_ID, keyGenerator = CacheConfig.COMPOSITE_KEY_GENERATOR) public AuthenticationCommand getAuthenticationCommand(String serviceId, String jwtToken) { - final List instances = discoveryClient.getInstancesById(serviceId); + final List instances = discoveryClient.getApplication(serviceId).getInstances(); Authentication found = null; for (final InstanceInfo instance : instances) { @@ -111,7 +111,7 @@ public AuthenticationCommand getAuthenticationCommand(String serviceId, String j // this is the first record found = auth; } else if (!found.equals(auth)) { - // if next record is different, authentication cannot be determinated before load balancer + // if next record is different, authentication cannot be determined before load balancer return loadBalancerCommand; } } diff --git a/integration-tests/README.md b/integration-tests/README.md index 5b8e7dcb8c..38d92c03f4 100644 --- a/integration-tests/README.md +++ b/integration-tests/README.md @@ -81,7 +81,7 @@ Slow tests are annotated using @Category(SlowTests.class) as in the following ex Start the suite of slow tests by executing the following shell script: ```shell -./gradlew :integrationTests:runAllIntegrationTests +./gradlew :integration-tests:runAllIntegrationTests ``` ## Running a Specific Test From 7bc174d4d89bb928f217377c3e13e87f8f59df2b Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Tue, 7 Jan 2020 09:21:20 +0100 Subject: [PATCH 019/122] Import the right HttpServletRequest class Signed-off-by: Petr Plavjanik --- .../ServiceAuthenticationServiceImplTest.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java index b2a1e2ba7b..0af2365a5c 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java @@ -1,12 +1,12 @@ package com.ca.mfaas.gateway.security.service;/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ import com.ca.apiml.security.common.auth.Authentication; import com.ca.apiml.security.common.auth.AuthenticationScheme; @@ -16,7 +16,6 @@ import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.EurekaClient; import com.netflix.zuul.context.RequestContext; -import org.apache.catalina.servlet4preview.http.HttpServletRequest; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -33,6 +32,8 @@ import java.time.LocalDate; import java.util.*; +import javax.servlet.http.HttpServletRequest; + import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_APPLID; import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_SCHEME; import static com.ca.mfaas.gateway.security.service.ServiceAuthenticationServiceImpl.AUTHENTICATION_COMMAND_KEY; From 3cc69ed0f0a086637e46249382d33a4438646ee7 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Tue, 7 Jan 2020 10:59:13 +0100 Subject: [PATCH 020/122] Disable default Spring Security Configuration for discoverable-client Signed-off-by: Petr Plavjanik --- .../DiscoverableClientSampleApplication.java | 1 - .../configuration/WebSecurityConfig.java | 32 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 discoverable-client/src/main/java/com/ca/mfaas/client/configuration/WebSecurityConfig.java diff --git a/discoverable-client/src/main/java/com/ca/mfaas/client/DiscoverableClientSampleApplication.java b/discoverable-client/src/main/java/com/ca/mfaas/client/DiscoverableClientSampleApplication.java index 71d30d1809..c7e6eac065 100644 --- a/discoverable-client/src/main/java/com/ca/mfaas/client/DiscoverableClientSampleApplication.java +++ b/discoverable-client/src/main/java/com/ca/mfaas/client/DiscoverableClientSampleApplication.java @@ -31,7 +31,6 @@ @ComponentScan(value = { "com.ca.mfaas.client", "com.ca.mfaas.enable", - "com.ca.mfaas.product.security", "com.ca.mfaas.product.web" }) public class DiscoverableClientSampleApplication implements ApplicationListener { diff --git a/discoverable-client/src/main/java/com/ca/mfaas/client/configuration/WebSecurityConfig.java b/discoverable-client/src/main/java/com/ca/mfaas/client/configuration/WebSecurityConfig.java new file mode 100644 index 0000000000..d2260a4ad2 --- /dev/null +++ b/discoverable-client/src/main/java/com/ca/mfaas/client/configuration/WebSecurityConfig.java @@ -0,0 +1,32 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package com.ca.mfaas.client.configuration; + +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.stereotype.Component; + +@Component +@EnableWebSecurity +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + public void configure(WebSecurity web) throws Exception { + web.ignoring().anyRequest(); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.authorizeRequests().anyRequest().permitAll(); + } +} From 58a190956ce222e0970b96cdfe3b33221a514c2f Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Tue, 7 Jan 2020 10:59:54 +0100 Subject: [PATCH 021/122] Remove extra passTicketService bean creation Signed-off-by: Petr Plavjanik --- .../gateway/security/config/ComponentsConfiguration.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/config/ComponentsConfiguration.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/config/ComponentsConfiguration.java index d3e9eef420..df24fea2c5 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/config/ComponentsConfiguration.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/config/ComponentsConfiguration.java @@ -9,7 +9,6 @@ */ package com.ca.mfaas.gateway.security.config; -import com.ca.apiml.security.common.service.PassTicketService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -28,11 +27,4 @@ public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(10); } - /** - * Used to generate PassTickets - */ - @Bean - public PassTicketService passTicketService() { - return new PassTicketService(); - } } From 5948bb7a0b09e6e2dd1d3c9a1f86380b8ee55788 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Tue, 7 Jan 2020 11:00:56 +0100 Subject: [PATCH 022/122] wip - Ignore broken tests Signed-off-by: Petr Plavjanik --- .../pre/ServiceAuthenticationFilterTest.java | 18 ++++++++++-------- .../ServiceAuthenticationServiceImplTest.java | 4 ++++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java index 458eaa6c76..7340ad4c3d 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java @@ -1,18 +1,19 @@ package com.ca.mfaas.gateway.filters.pre;/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ import com.ca.apiml.security.common.token.TokenAuthentication; import com.ca.mfaas.gateway.security.service.ServiceAuthenticationServiceImpl; import com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand; import com.netflix.zuul.context.RequestContext; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; @@ -42,6 +43,7 @@ public void init() { } @Test + @Ignore("TODO: Pavel") public void testRun() { HttpServletRequest request = mock(HttpServletRequest.class); when(request.getUserPrincipal()).thenReturn(new TokenAuthentication("user", "token")); diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java index 0af2365a5c..af1c1acb3e 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java @@ -17,6 +17,7 @@ import com.netflix.discovery.EurekaClient; import com.netflix.zuul.context.RequestContext; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; @@ -154,6 +155,7 @@ public void testGetAuthenticationCommand() { } @Test + @Ignore("TODO: Pavel") public void testGetAuthenticationCommandByServiceId() { AuthenticationCommand ok = new AuthenticationCommandTest(false); Authentication a1 = new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applid01"); @@ -199,6 +201,7 @@ public void testGetAuthenticationCommandByServiceId() { } @Test + @Ignore("TODO: Pavel") public void testGetAuthenticationCommandByServiceIdCache() { InstanceInfo ii1 = createInstanceInfo("i1", AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applid1"); AuthenticationCommand ac1 = new AuthenticationCommandTest(true); @@ -265,6 +268,7 @@ public void testLoadBalancerAuthenticationCommand() { } @Test + @Ignore("TODO: Pavel") public void testEvictCacheService() { AuthenticationCommand command = AuthenticationCommand.EMPTY; Authentication auth = new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applicationId0001"); From ccd203dcfdd4e55df05f0757dcad579558c642e5 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Tue, 7 Jan 2020 11:01:08 +0100 Subject: [PATCH 023/122] Refactor PassTicket tests Signed-off-by: Petr Plavjanik --- .../mfaas/gatewayservice/PassTicketTest.java | 32 +++---------------- .../mfaas/gatewayservice/SecurityUtils.java | 13 ++++++-- 2 files changed, 14 insertions(+), 31 deletions(-) diff --git a/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java index f69359d881..bd700cc0e2 100644 --- a/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java +++ b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java @@ -9,24 +9,18 @@ */ package com.ca.mfaas.gatewayservice; +import static com.ca.mfaas.gatewayservice.SecurityUtils.GATEWAY_TOKEN_COOKIE_NAME; +import static com.ca.mfaas.gatewayservice.SecurityUtils.gatewayToken; import static io.restassured.RestAssured.given; -import static io.restassured.http.ContentType.JSON; -import static org.apache.http.HttpStatus.SC_NO_CONTENT; import static org.apache.http.HttpStatus.SC_OK; -import static org.hamcrest.Matchers.isEmptyString; import static org.hamcrest.core.Is.is; -import static org.hamcrest.core.IsNot.not; -import static org.hamcrest.core.IsNull.notNullValue; -import static org.junit.Assert.assertThat; -import com.ca.apiml.security.common.login.LoginRequest; import com.ca.mfaas.util.config.ConfigReader; import org.junit.Before; import org.junit.Test; import io.restassured.RestAssured; -import io.restassured.http.Cookie; public class PassTicketTest { private final static String SCHEME = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration() @@ -34,13 +28,8 @@ public class PassTicketTest { private final static String HOST = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration() .getHost(); private final static int PORT = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration().getPort(); - private final static String GATEWAY_BASE_PATH = "/api/v1/gateway"; private final static String STATICCLIENT_BASE_PATH = "/api/v1/staticclient"; - private final static String LOGIN_ENDPOINT = "/auth/login"; private final static String PASSTICKET_TEST_ENDPOINT = "/passticketTest"; - private final static String COOKIE_NAME = "apimlAuthenticationToken"; - private final static String USERNAME = ConfigReader.environmentConfiguration().getCredentials().getUser(); - private final static String PASSWORD = ConfigReader.environmentConfiguration().getCredentials().getPassword(); @Before public void setUp() { @@ -49,23 +38,10 @@ public void setUp() { RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); } - private String login() { - LoginRequest loginRequest = new LoginRequest(USERNAME, PASSWORD); - - Cookie cookie = given().contentType(JSON).body(loginRequest).when() - .post(String.format("%s://%s:%d%s%s", SCHEME, HOST, PORT, GATEWAY_BASE_PATH, LOGIN_ENDPOINT)).then() - .statusCode(is(SC_NO_CONTENT)).cookie(COOKIE_NAME, not(isEmptyString())).extract() - .detailedCookie(COOKIE_NAME); - - assertThat(cookie.getValue(), is(notNullValue())); - - return cookie.getValue(); - } - @Test public void accessServiceWithCorrectPassTicket() { - String jwt = login(); - given().cookie(COOKIE_NAME, jwt).when().get( + String jwt = gatewayToken(); + given().cookie(GATEWAY_TOKEN_COOKIE_NAME, jwt).when().get( String.format("%s://%s:%d%s%s", SCHEME, HOST, PORT, STATICCLIENT_BASE_PATH, PASSTICKET_TEST_ENDPOINT)) .then().statusCode(is(SC_OK)); } diff --git a/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/SecurityUtils.java b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/SecurityUtils.java index 6e50d32401..548a1e8903 100644 --- a/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/SecurityUtils.java +++ b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/SecurityUtils.java @@ -41,7 +41,7 @@ public class SecurityUtils { final static String ZOSMF_TOKEN = "LtpaToken2"; - private final static String GATEWAY_TOKEN = "apimlAuthenticationToken"; + public final static String GATEWAY_TOKEN_COOKIE_NAME = "apimlAuthenticationToken"; public final static String GATEWAY_LOGIN_ENDPOINT = "/auth/login"; public final static String GATEWAY_BASE_PATH = "/api/v1/gateway"; private final static String ZOSMF_LOGIN_ENDPOINT = "/zosmf/info"; @@ -57,7 +57,14 @@ public class SecurityUtils { private final static String zosmfHost = zosmfServiceConfiguration.getHost(); private final static int zosmfPort = zosmfServiceConfiguration.getPort(); + private final static String USERNAME = ConfigReader.environmentConfiguration().getCredentials().getUser(); + private final static String PASSWORD = ConfigReader.environmentConfiguration().getCredentials().getPassword(); + //@formatter:off + public static String gatewayToken() { + return gatewayToken(USERNAME, PASSWORD); + } + public static String gatewayToken(String username, String password) { LoginRequest loginRequest = new LoginRequest(username, password); @@ -68,8 +75,8 @@ public static String gatewayToken(String username, String password) { .post(String.format("%s://%s:%d%s%s", gatewayScheme, gatewayHost, gatewayPort, GATEWAY_BASE_PATH, GATEWAY_LOGIN_ENDPOINT)) .then() .statusCode(is(SC_NO_CONTENT)) - .cookie(GATEWAY_TOKEN, not(isEmptyString())) - .extract().cookie(GATEWAY_TOKEN); + .cookie(GATEWAY_TOKEN_COOKIE_NAME, not(isEmptyString())) + .extract().cookie(GATEWAY_TOKEN_COOKIE_NAME); } public static String zosmfToken(String username, String password) { From bd04e95fbae4d2790ebb86053f2581aae683a059 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Tue, 7 Jan 2020 11:51:43 +0100 Subject: [PATCH 024/122] Removing documentation that has been migrated to docs-site Signed-off-by: Petr Plavjanik --- passticket/docs/api-mediation-passtickets.md | 129 ------------------- 1 file changed, 129 deletions(-) delete mode 100644 passticket/docs/api-mediation-passtickets.md diff --git a/passticket/docs/api-mediation-passtickets.md b/passticket/docs/api-mediation-passtickets.md deleted file mode 100644 index 61d28e9305..0000000000 --- a/passticket/docs/api-mediation-passtickets.md +++ /dev/null @@ -1,129 +0,0 @@ -# Enabling PassTicket creation for API Services that Accept PassTickets - -**Note**: This is a draft documentation that needs to be migrated to after the functionality is completed. - -- [Enabling PassTicket creation for API Services that Accept PassTickets](#enabling-passticket-creation-for-api-services-that-accept-passtickets) - - [How to Enable PassTicket Support](#how-to-enable-passticket-support) - - [Enable the Zowe started task user ID to generate PassTickets for the API service](#enable-the-zowe-started-task-user-id-to-generate-passtickets-for-the-api-service) - - [ACF2](#acf2) - - [RACF](#racf) - - [TopSecret](#topsecret) - - [API Services that Register Dynamically into API ML](#api-services-that-register-dynamically-into-api-ml) - - [API Services that Register Dynamically into API ML but do not Provide Metadata](#api-services-that-register-dynamically-into-api-ml-but-do-not-provide-metadata) - - [API Services that are Defined using Static YAML Definition](#api-services-that-are-defined-using-static-yaml-definition) - - [What Developers Need to with API Services that Register Dynamically into API ML](#what-developers-need-to-with-api-services-that-register-dynamically-into-api-ml) - -The API Mediation Layer provides transparent authentication using PassTickets for API service that accept them. - -It means that API clients can use the Zowe JWT obtained from [authentication endpoint](https://docs.zowe.org/stable/extend/extend-apiml/api-mediation-security.html#authentication-for-api-ml-services) of the API gateway to access such API service even if the API service -does not support the JWT token. - -If the API client provide a valid Zowe JWT token, the API gateway generates a valid PassTicket and uses that to access the API service. -The user ID and password are provided in the Authorization header of the HTTP requests using the -[Basic authentication scheme](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#Basic_authentication_scheme). - -## How to Enable PassTicket Support - -You need to do following for each service that requires PassTickets: - -- Ensure that PassTickets are enabled for every user who might require access to API service - - Follow documentation of your API service that explains how to active support for PassTickets - - Remember the value of the APPLID of the API service -- Enable the Zowe started task user ID to generate PassTickets for the API service -- Enable the PassTicket support in the API gateway for your API service - -### Enable the Zowe started task user ID to generate PassTickets for the API service - -Following variables are used in the commands: - -- `` is the APPLID value that is used by the API service for PassTicket support (e.g. `OMVSAPPL`) - -- ``is Zowe started task user ID permission - -#### ACF2 - -Grant the Zowe started task user ID permission to generate PassTickets for users of that API service. For example: - -```txt -ACF -SET RESOURCE(PTK) -RECKEY IRRPTAUTH ADD(.- UID() SERVICE(UPDATE,READ) ALLOW) -F ACF2,REBUILD(PTK),CLASS(P) -END -``` - -#### RACF - -To enable PassTicket creation for API service users, define the profile `IRRPTAUTH..*` in the `PTKTDATA` class and set the universal access authority to NONE and grant the Zowe started task user ID permission to generate PassTickets for users of that API service. For example: - -```txt -RDEFINE PTKTDATA IRRPTAUTH..* UACC(NONE) -PERMIT IRRPTAUTH..* CL(PTKTDATA) ID() ACCESS(UPDATE) -SETROPTS RACLIST(PTKTDATA) REFRESH -``` - -#### TopSecret - -Grant the Zowe started task user ID permission to generate PassTickets for users of that API service. For example: - -```txt -TSS PERMIT() PTKTDATA(IRRPTAUTH..) ACCESS(READ,UPDATE) -TSS REFRESH -``` - -### API Services that Register Dynamically into API ML - -The API services that support Zowe API Mediation Layer and use dynamic registration into Discovery Service provide metadata -that enable PassTicket support. - -As a user of the API you are not require to do anything in this case. All required information is provided by the API service automatically. - -### API Services that Register Dynamically into API ML but do not Provide Metadata - -Some services that can use PassTickets do not provide the metadata yet. For such service you can provide them -extenally in the same files as for the static YAML definitons. - -Add following section to a YAML file with static definition. - -```yaml -additionalServiceMetadata: - : - authentication: - scheme: httpBasicPassTicket - applid: -``` - -`` is the service ID of the service where you want to add metadata. - -### API Services that are Defined using Static YAML Definition - -Add the following metadata to the same level as the `serviceId`, for example: - -```yaml - - serviceId: ... - authentication: - scheme: httpBasicPassTicket - applid: TSTAPPL -``` - -The fields are explained below. - -## What Developers Need to with API Services that Register Dynamically into API ML - -As a developer of this application, you need to provide additional metadata. -This metadata tell API gateway that it needs to use PassTickets and how to generate them. - -```yaml -authentication: - scheme: httpBasicPassTicket - applid: -``` - -`httpBasicPassTicket` is the value that means that HTTP Basic authentication scheme is used with PassTickets. - -`` if the APPLID value that is used by the API service for PassTicket support (e.g. `OMVSAPPL`). - -The other values of `authentication.scheme` that are supported: - -- `bypass` (default) - API gateway does not modify authentication headers for the API service. -- `zoweJwt` - The Zowe JWT token is expected. API gateway does not modify but can process it. From bc640436bbc5fda3140e7d5becab63c34df83e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Tue, 7 Jan 2020 12:23:35 +0100 Subject: [PATCH 025/122] mapping exception on Proxy (+ using in PassTickerService) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../AbstractIRRPassTicketException.java | 96 +++++++++ .../common/service/IRRPassTicket.java | 12 +- .../IRRPassTicketEvaluationException.java | 28 +++ .../IRRPassTicketGenerationException.java | 28 +++ .../common/service/PassTicketService.java | 18 +- .../common/service/PassTicketServiceTest.java | 4 +- .../mfaas/util/ClassOrDefaultProxyUtils.java | 203 +++++++++++++++++- .../util/ClassOrDefaultProxyUtilsTest.java | 116 +++++++++- .../pre/ServiceAuthenticationFilter.java | 11 +- .../GatewayRibbonLoadBalancingHttpClient.java | 6 +- .../ServiceAuthenticationServiceImpl.java | 6 +- .../schema/AbstractAuthenticationScheme.java | 2 +- .../service/schema/AuthenticationCommand.java | 2 +- .../schema/AuthenticationSchemeFactory.java | 2 +- .../schema/HttpBasicPassTicketScheme.java | 5 +- .../schema/ServiceAuthenticationService.java | 4 +- .../pre/ServiceAuthenticationFilterTest.java | 4 +- .../ServiceAuthenticationServiceImplTest.java | 10 +- .../schema/AuthenticationCommandTest.java | 2 +- .../AuthenticationSchemeFactoryTest.java | 4 +- .../schema/HttpBasicPassTicketSchemeTest.java | 2 +- .../service/schema/ZosmfSchemeTest.java | 2 +- 22 files changed, 512 insertions(+), 55 deletions(-) create mode 100644 apiml-security-common/src/main/java/com/ca/apiml/security/common/service/AbstractIRRPassTicketException.java create mode 100644 apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicketEvaluationException.java create mode 100644 apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicketGenerationException.java diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/AbstractIRRPassTicketException.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/AbstractIRRPassTicketException.java new file mode 100644 index 0000000000..5c2693873f --- /dev/null +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/AbstractIRRPassTicketException.java @@ -0,0 +1,96 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.apiml.security.common.service; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.LinkedList; +import java.util.List; + +/** + * Abstact exception from IRR passticket service. It collect common values about exception + */ +@AllArgsConstructor +@Getter +public abstract class AbstractIRRPassTicketException extends Exception { + + private static final long serialVersionUID = -6233392272992529775L; + + protected final int safRc; + protected final int racfRsn; + protected final int racfRc; + + public List getErrorCodes() { + return ErrorCode.getErrorCode(this); + } + + protected String getMessage(String baseMessage) { + final StringBuilder sb = new StringBuilder(); + + sb.append(baseMessage).append('\n'); + for (ErrorCode ec : getErrorCodes()) { + sb.append('\t').append(ec.getMessage()).append('\n'); + } + + return sb.toString(); + } + + @AllArgsConstructor + @Getter + public enum ErrorCode { + + ERR_0_0_0(0, 0, 0, "The service was successful."), + ERR_4_0_0(4, 0, 0, "RACF is not installed."), + ERR_8_8_0(8, 8, 0, "Invalid function code."), + ERR_8_8_4(8, 8, 4, "Parameter list error."), + ERR_8_8_8(8, 8, 8, "An internal error was encountered."), + ERR_8_8_12(8, 8, 12, "A recovery environment could not be established."), + ERR_8_8_16(8, 8, 16, "Not authorized to use this service."), + ERR_8_8_20(8, 8, 20, "High order bit was not set to indicate last parameter."), + ERR_8_12_8(8, 12, 8, "Invocation of the Security Server Network Authentication Service Program Call (PC) interface failed with a 'parameter buffer overflow' return code. This indicates an internal error in IRRSPK00."), + ERR_8_12_12(8, 12, 12, "Invocation of the Security Server Network Authentication Service Program Call (PC) interface failed with an 'unable to allocate storage' return code. The region size for the Security Server Network Authentication Service started task (SKRBKDC) should be increased."), + ERR_8_12_16(8, 12, 16, "Invocation of the Security Server Network Authentication Service Program Call (PC) interface failed with a 'local services are not available' return code. This indicates that the Security Server Network Authentication Service started task (SKRBKDC) address space has not been started or is terminating."), + ERR_8_12_20(8, 12, 20, "Invocation of the Security Server Network Authentication Service Program Call (PC) interface failed with an 'abend in the PC service routine' return code. The symptom record associated with this abend can be found in the logrec data set."), + ERR_8_12_24(8, 12, 24, "Invocation of the Security Server Network Authentication Service Program Call (PC) interface failed with an 'unable to obtain control lock' return code. This can occur if the task holding the lock is not being dispatched (for example, a dump is in progress)."), + ERR_8_16_X_1(8, 16, null, "The Security Server Network Authentication Service was not able to successfully extract the client principal name from the supplied Kerberos V5 ticket. X'nnnnnnnn' is the Kerberos return code. Refer to the Security Server Network Authentication Service documentation for more information."), + ERR_8_16_28(8, 16, 28, "Unable to generate PassTicket. Verify that the secured signon (PassTicket) function and application ID is configured properly by referring to Using PassTickets in z/OS Security Server RACF Security Administrator's Guide."), + ERR_8_16_32(8, 16, 32,"PassTicket evaluation failure. Possible reasons include:\n" + + "- PassTicket to be evaluated is not a successful PassTicket\n" + + "- The PassTicket to be evaluated was already evaluated before and replay protection is in effect.\n" + + "- No PTKTDATA profile exists to match the specified application\n" + + "- An internal error occurred."), + ERR_8_16_X_2(8, 16, null, "PassTicket evaluation extended failure. X'nnnnnnnn' is the internal reason code for the evaluation failure.") + + ; + + + private final int safRc; + private final int racfRsn; + private final Integer racfRc; + + private String message; + + public static List getErrorCode(AbstractIRRPassTicketException e) { + final List out = new LinkedList<>(); + + for (final ErrorCode ec : values()) { + if (ec.getSafRc() != e.getSafRc()) continue; + if (ec.getRacfRsn() != e.getRacfRsn()) continue; + if ((ec.getRacfRc() != null) && (ec.getRacfRc() != e.getRacfRc())) continue; + out.add(ec); + } + + return out; + } + + } + +} diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicket.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicket.java index 395d929c6b..20bd29e4ee 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicket.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicket.java @@ -18,20 +18,20 @@ public interface IRRPassTicket { * @param userId User for whom a PassTicket operation is performed. * @param applId The Application name for this PassTicket operation. The same Application must be specified when calling the evaluate method. * @param passTicket If successful, a PassTicket is returned. - * @throws IRRPassTicketGenerationException (only on mainframe) if an error occurs during the generation of PassTicket. The IRRPassTicketGeneration exception will contain the RACF return codes which identify the problem. + * @throws IRRPassTicketEvaluationException if an error occurs during the generation of PassTicket. The IRRPassTicketGeneration exception will contain the RACF return codes which identify the problem. * @throws java.lang.IllegalStateException when an internal occurs. * @throws java.lang.IllegalArgumentException when a NULL or invalid length parameter is passed. */ - public void evaluate(String userId, String applId, String passTicket); + public void evaluate(String userId, String applId, String passTicket) throws IRRPassTicketEvaluationException; /** * @param userId User for whom a PassTicket operation is performed. * @param applId The Application name for this PassTicket operation. The same Application must be specified when calling the generate method. * @return The PassTicket to evaluate for this userid and application - * @throws IRRPassTicketEvaluationException - (only on mainframe) When an error occurs during the evaluation of PassTicket. This can mean that the PassTicket is invalid, expired or has been used previously. This can also indicate that the user does not have the proper RACF authority to perform a PassTicket evaluation, or may indicate an internal error. The IRRPassTicketEvaluation exception will contain the RACF return codes which identify the problem. - * @throws java.lang.IllegalStateException - when an internal error occurs - * @throws java.lang.IllegalArgumentException - when a NULL or invalid length argument is passed P1C- AMR: Removed RETURN Parameter that existed here. This caused a Javadoc error in Java8 compilation. There is no return parameter here. + * @throws IRRPassTicketGenerationException When an error occurs during the evaluation of PassTicket. This can mean that the PassTicket is invalid, expired or has been used previously. This can also indicate that the user does not have the proper RACF authority to perform a PassTicket evaluation, or may indicate an internal error. The IRRPassTicketEvaluation exception will contain the RACF return codes which identify the problem. + * @throws java.lang.IllegalStateException when an internal error occurs + * @throws java.lang.IllegalArgumentException when a NULL or invalid length argument is passed P1C- AMR: Removed RETURN Parameter that existed here. This caused a Javadoc error in Java8 compilation. There is no return parameter here. */ - public String generate(String userId, String applId); + public String generate(String userId, String applId) throws IRRPassTicketGenerationException; } diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicketEvaluationException.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicketEvaluationException.java new file mode 100644 index 0000000000..f4f2a36a2a --- /dev/null +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicketEvaluationException.java @@ -0,0 +1,28 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.apiml.security.common.service; + +/** + * Exception on evaluation of passTicket + */ +public class IRRPassTicketEvaluationException extends AbstractIRRPassTicketException { + + private static final long serialVersionUID = -7401871844111323433L; + + public IRRPassTicketEvaluationException(int safRc, int racfRsn, int racfRc) { + super(safRc, racfRsn, racfRc); + } + + @Override + public String getMessage() { + return getMessage("Error on evaluation of PassTicket"); + } + +} diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicketGenerationException.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicketGenerationException.java new file mode 100644 index 0000000000..b8c7ba80d5 --- /dev/null +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicketGenerationException.java @@ -0,0 +1,28 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.apiml.security.common.service; + +/** + * Exception on generation of passTicket + */ +public class IRRPassTicketGenerationException extends AbstractIRRPassTicketException { + + private static final long serialVersionUID = -8944250582222779122L; + + public IRRPassTicketGenerationException(int safRc, int racfRsn, int racfRc) { + super(safRc, racfRsn, racfRc); + } + + @Override + public String getMessage() { + return getMessage("Error on generation of PassTicket"); + } + +} diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java index ee26377564..9835ddde54 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java @@ -27,19 +27,29 @@ public void init() { this.irrPassTicket = ClassOrDefaultProxyUtils.createProxy( IRRPassTicket.class, "com.ibm.eserver.zos.racf.IRRPassTicket", - DefaultPassTicketImpl::new + DefaultPassTicketImpl::new, + new ClassOrDefaultProxyUtils.ByMethodName( + "com.ibm.eserver.zos.racf.IRRPassTicketEvaluationException", + IRRPassTicketEvaluationException.class, + "getSafRc", "getRacfRsn", "getRacfRc" + ), + new ClassOrDefaultProxyUtils.ByMethodName( + "com.ibm.eserver.zos.racf.IRRPassTicketGenerationException", + IRRPassTicketGenerationException.class, + "getSafRc", "getRacfRsn", "getRacfRc" + ) ); } - public void evaluate(String userId, String applId, String passTicket) { + public void evaluate(String userId, String applId, String passTicket) throws IRRPassTicketEvaluationException { irrPassTicket.evaluate(userId, applId, passTicket); } - public String generate(String userId, String applId) { + public String generate(String userId, String applId) throws IRRPassTicketGenerationException { return irrPassTicket.generate(userId, applId); } - public boolean isUsingRacImplementation() { + public boolean isUsingSafImplementation() { ClassOrDefaultProxyUtils.ClassOrDefaultProxyState stateInterface = (ClassOrDefaultProxyUtils.ClassOrDefaultProxyState) irrPassTicket; return stateInterface.isUsingBaseImplementation(); } diff --git a/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java b/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java index c6faa7330b..d3d97df753 100644 --- a/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java +++ b/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java @@ -51,7 +51,7 @@ public String generate(String userId, String applId) { @Test @Order(2) - public void testCalledMethod() { + public void testCalledMethod() throws IRRPassTicketEvaluationException, IRRPassTicketGenerationException { evaluated = null; passTicketService.evaluate("userId", "applId", "passTicket"); assertEquals("userId-applId-passTicket", evaluated); @@ -63,7 +63,7 @@ public void testCalledMethod() { } @Test - public void testProxy() { + public void testProxy() throws IRRPassTicketGenerationException { IRRPassTicket irrPassTicket = ClassOrDefaultProxyUtils.createProxy( IRRPassTicket.class, "notExistingClass", diff --git a/common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java b/common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java index 56f82252ca..3f152382af 100644 --- a/common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java +++ b/common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java @@ -10,17 +10,17 @@ package com.ca.mfaas.util; import lombok.AllArgsConstructor; +import lombok.Data; import lombok.Value; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; +import java.lang.reflect.*; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.function.Supplier; /** @@ -65,9 +65,10 @@ public final class ClassOrDefaultProxyUtils { * @param implementationClassName Full name of prefer implementation * @param defaultImplementation Supplier to fetch implementation to use, if the prefer one is missing * @param Common interface for prefer and default implementation + * @param exceptionMappings handlers to map exception to custom class * @return Proxy object implementing interfaceClass and ClassOrDefaultProxyState */ - public static T createProxy(Class interfaceClass, String implementationClassName, Supplier defaultImplementation) { + public static T createProxy(Class interfaceClass, String implementationClassName, Supplier defaultImplementation, ExceptionMapping ... exceptionMappings) { ObjectUtil.requireNotNull(interfaceClass, "interfaceClass can't be null"); ObjectUtil.requireNotEmpty(implementationClassName, "implementationClassName can't be empty"); ObjectUtil.requireNotNull(defaultImplementation, "defaultImplementation can't be null"); @@ -75,19 +76,19 @@ public static T createProxy(Class interfaceClass, String implementationCl try { final Class implementationClazz = Class.forName(implementationClassName); final Object implementation = implementationClazz.getDeclaredConstructor().newInstance(); - return makeProxy(interfaceClass, implementation, true); + return makeProxy(interfaceClass, implementation, true, exceptionMappings); } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { log.warn("Implementation {} is not available, it will continue with default one {} : " + e.getLocalizedMessage(), implementationClassName, defaultImplementation); } - return makeProxy(interfaceClass, defaultImplementation.get(), false); + return makeProxy(interfaceClass, defaultImplementation.get(), false, exceptionMappings); } - private static T makeProxy(Class interfaceClass, Object implementation, boolean usingBaseImplementation) { + private static T makeProxy(Class interfaceClass, Object implementation, boolean usingBaseImplementation, ExceptionMapping ... exceptionMappings) { return (T) Proxy.newProxyInstance( ClassOrDefaultProxyUtils.class.getClassLoader(), new Class[] {interfaceClass, ClassOrDefaultProxyUtils.ClassOrDefaultProxyState.class}, - new MethodInvocationHandler(implementation, interfaceClass, usingBaseImplementation)); + new MethodInvocationHandler(implementation, interfaceClass, usingBaseImplementation, exceptionMappings)); } /** @@ -109,6 +110,9 @@ public interface ClassOrDefaultProxyState { } + /** + * Handler of proxy. This class prepare mapping of call and then invoke target method. + */ private static class MethodInvocationHandler implements InvocationHandler, ClassOrDefaultProxyState { private final Map mapping = new HashMap<>(); @@ -116,11 +120,21 @@ private static class MethodInvocationHandler implements InvocationHandler, Class private final boolean usingBaseImplementation; private final Object implementation; private final Class interfaceClass; + private final ExceptionMapping[] exceptionMappings; - public MethodInvocationHandler(Object implementation, Class interfaceClass, boolean usingBaseImplementation) { + public MethodInvocationHandler(Object implementation, Class interfaceClass, boolean usingBaseImplementation, ExceptionMapping ... exceptionMappings) { this.usingBaseImplementation = usingBaseImplementation; this.implementation = implementation; this.interfaceClass = interfaceClass; + this.exceptionMappings = exceptionMappings; + + if (this.usingBaseImplementation) { + for (ExceptionMapping exceptionMapping : exceptionMappings) { + if (!exceptionMapping.isInitialized()) { + log.error("Mapping of exception {} is not initialized", exceptionMapping); + } + } + } this.initMapping(); } @@ -202,7 +216,13 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl try { return endPoint.invoke(args); } catch (InvocationTargetException ite) { - throw ite.getCause(); + // thrown exception in proxied object + Throwable t = ite.getCause(); + // if there is a mapping of exceptions, apply it to use custom Exception + for (ExceptionMapping em : exceptionMappings) { + em.apply(t); + } + throw t; } } @@ -216,6 +236,10 @@ public boolean isUsingBaseImplementation() { return usingBaseImplementation; } + /** + * Object define instance of object where should be invoked the method and contains also method instance. It is + * prepare before to make invoke in the fastest way. + */ @Value @AllArgsConstructor public static final class EndPoint { @@ -231,4 +255,161 @@ public Object invoke(Object[] args) throws InvocationTargetException, IllegalAcc } + /** + * Interface to controll an exception mapping. It offer base method to make controll on exception conversion + * from Handler side. + * @param Target exception (which is thrown in case of mapping) + */ + public interface ExceptionMapping { + + /** + * Method indicate if mapping was created well (to controll from proxy handler) + * @return true if mapping is ready to use otherwise false + */ + public boolean isInitialized(); + + /** + * Method will check if t is able to map. If yes, make mapping and throw new (mapped) expception + * @param t Original exception + * @throws T Type of mapped exception to throw in case of mapping is right to type of t + */ + public void apply(Throwable t) throws T; + + } + + /** + * This exception mapper is based on getter in source exception. It allows to get from zero to N getters without + * argument in source exception and use them as arguments in target constructor. + * + * You have to define same count of getter names (with same result type) as a contructor in target exception. The + * order has to be also same. + * + * @param Type of target exception + */ + @AllArgsConstructor + @Data + public static class ByMethodName implements ExceptionMapping { + + private final String sourceExceptionClassName; + private final Function mappingFunction; + + /** + * Constructor define all required values to make a mapping + * @param sourceExceptionClassName Name of source exception's class type {@see Class#getName()} + * @param targetExceptionClass Type of exception to be thrown in case of successful mapping + * @param methodNames names of getter without arguments in sources exception, which results will be use as constructor parameters + */ + public ByMethodName(String sourceExceptionClassName, Class targetExceptionClass, String...methodNames) { + this.sourceExceptionClassName = sourceExceptionClassName; + this.mappingFunction = getMappingFunction(sourceExceptionClassName, targetExceptionClass, methodNames); + } + + /** + * Find method with name and no arguments in the class hierarchy + * @param clazz - base method to lookup + * @param methodName - name of method + * @return found method with name methodName and no arguments or null + */ + private Method findMethod(Class clazz, String methodName) { + if (clazz == Object.class) return null; + + try { + return clazz.getDeclaredMethod(methodName); + } catch (NoSuchMethodException e) { + return findMethod(clazz.getSuperclass(), methodName); + } + } + + /** + * Method find constructor in target exception, prepare lambdas to get values from source exception and return + * function to comvert source exception to new (target) one + * @param sourceExceptionClassName name of exception to map + * @param targetExceptionClass exception which could be construct after mapping + * @param methodNames names of methods without argument on source exception to get values into constructor to create target exception + * @return function to mapping of exception + */ + private Function getMappingFunction(String sourceExceptionClassName, Class targetExceptionClass, String...methodNames ) { + // find source exception + final Class eClass; + try { + eClass = (Class) Class.forName(sourceExceptionClassName); + } catch (ClassNotFoundException e) { + log.debug("Exception {} is not available, it will not be mapped into {} : " + e.getLocalizedMessage(), sourceExceptionClassName, targetExceptionClass); + return null; + } + + // find arguments of constructor and methods by names, methods should be without any arguments + final List> argClasses = new LinkedList<>(); + final List> mapFunctions = new LinkedList<>(); + for (String methodName : methodNames) { + final Method method = findMethod(eClass, methodName); + if (method == null) { + log.debug("Cannot find method {} in {} to map exceptions", methodName, sourceExceptionClassName); + return null; + } + argClasses.add(method.getReturnType()); + mapFunctions.add(x -> { + try { + return method.invoke(x); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new IllegalArgumentException(e); + } + }); + } + + // find the constructor and store functions to invoke then + try { + return getMappingFunction((Constructor) targetExceptionClass.getConstructor(argClasses.toArray(new Class[0])), mapFunctions); + } catch (NoSuchMethodException e) { + log.debug("Cannot find constructor on {} with {}", sourceExceptionClassName, argClasses); + return null; + } + } + + /** + * Method will create lambda to fully conversion of source exception + * @param constructor constructor to use (right count and type of arguments) + * @param mapFunctions list of lambdas to get results from source exception + * @return lambda function to convert exception + */ + private Function getMappingFunction(Constructor constructor, List> mapFunctions) { + return x -> { + try { + return constructor.newInstance( + mapFunctions.stream().map(y -> y.apply(x)).toArray() + ); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException("Cannot construct exception " + constructor.getDeclaringClass(), e); + } + }; + } + + @Override + public boolean isInitialized() { + return mappingFunction != null; + } + + /** + * Check if this mapping is right for type of t + * @param t source exception + * @return true if mapping is for type of t, otherwise false + */ + private boolean isMatching(Throwable t) { + return StringUtils.equals(t.getClass().getName(), sourceExceptionClassName); + } + + @Override + public void apply(Throwable t) throws T { + if (!isMatching(t)) return; + + throw mappingFunction.apply(t); + } + + @Override + public String toString() { + return "{ExceptionMapping [sourceExceptionClassName = " + sourceExceptionClassName + "]}"; + } + + } + } diff --git a/common-service-core/src/test/java/com/ca/mfaas/util/ClassOrDefaultProxyUtilsTest.java b/common-service-core/src/test/java/com/ca/mfaas/util/ClassOrDefaultProxyUtilsTest.java index dc303f09aa..822627e44c 100644 --- a/common-service-core/src/test/java/com/ca/mfaas/util/ClassOrDefaultProxyUtilsTest.java +++ b/common-service-core/src/test/java/com/ca/mfaas/util/ClassOrDefaultProxyUtilsTest.java @@ -1,10 +1,3 @@ -package com.ca.mfaas.util; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -import static org.junit.Assert.*; - /* * This program and the accompanying materials are made available under the terms of the * Eclipse Public License v2.0 which accompanies this distribution, and is available at @@ -14,6 +7,18 @@ * * Copyright Contributors to the Zowe Project. */ +package com.ca.mfaas.util; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.lang.reflect.UndeclaredThrowableException; + +import static org.junit.Assert.*; + @RunWith(JUnit4.class) public class ClassOrDefaultProxyUtilsTest { @@ -60,6 +65,65 @@ public void testOverride() { assertEquals(TestImplementation2.class, ((ClassOrDefaultProxyUtils.ClassOrDefaultProxyState) ti).getImplementationClass()); } + @Test + public void testExceptionMapping() { + TestInterfaceException tie; + + tie = ClassOrDefaultProxyUtils.createProxy( + TestInterfaceException.class, + TestImplementationException.class.getName(), + () -> { + fail("Test failed, missing implementation for test"); + return null; + }, + new ClassOrDefaultProxyUtils.ByMethodName<>( + TestSourceException.class.getName(), + TestTargetException.class, + "getParam1", "getParam2", "getParam3" + ) + ); + + Object testObject = new Object(); + try { + tie.doSomething(testObject, "testString", 123); + fail(); + } catch (TestTargetException e) { + assertSame(testObject, e.getObject()); + assertEquals("testString", e.getString()); + assertEquals(123, e.getNumber()); + } + } + + @Test + public void testExceptionMappingUnkwnown() { + TestInterfaceException tie; + + tie = ClassOrDefaultProxyUtils.createProxy( + TestInterfaceException.class, + TestImplementationException.class.getName(), + () -> { + fail("Test failed, missing implementation for test"); + return null; + }, + new ClassOrDefaultProxyUtils.ByMethodName<>( + "Unkwnown", + TestTargetException.class, + "getParam1", "getParam2", "getParam3" + ) + ); + + Object testObject = new Object(); + try { + tie.doSomething(testObject, "testString", 123); + fail(); + } catch (TestTargetException e) { + // there is missing mapping, it will throw source exception + fail(); + } catch (UndeclaredThrowableException e) { + assertTrue(e.getCause() instanceof TestSourceException); + } + } + public interface TestInterface1Super { public String method1(); @@ -131,4 +195,42 @@ public Class getImplementationClass() { } + @Getter + @AllArgsConstructor + public static class TestSourceException extends Exception { + + private static final long serialVersionUID = -5824895837948769100L; + + private final Object param1; + private final String param2; + private final int param3; + + } + + @Getter + @AllArgsConstructor + public static class TestTargetException extends Exception { + + private static final long serialVersionUID = -3566209353622503908L; + + private final Object object; + private final String string; + private final int number; + + } + + public interface TestInterfaceException { + + public void doSomething(Object param1, String param2, int param3) throws TestTargetException; + + } + + public static class TestImplementationException { + + public void doSomething(Object param1, String param2, int param3) throws TestSourceException { + throw new TestSourceException(param1, param2, param3); + } + + } + } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilter.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilter.java index 2f02b55a81..0491d168b8 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilter.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilter.java @@ -14,7 +14,10 @@ import com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; +import com.netflix.zuul.exception.ZuulException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.netflix.zuul.util.ZuulRuntimeException; +import org.springframework.http.HttpStatus; import java.security.Principal; @@ -58,8 +61,12 @@ public Object run() { final String serviceId = (String) context.get(SERVICE_ID_KEY); - final AuthenticationCommand cmd = serviceAuthenticationService.getAuthenticationCommand(serviceId, jwtToken); - cmd.apply(null); + try { + final AuthenticationCommand cmd = serviceAuthenticationService.getAuthenticationCommand(serviceId, jwtToken); + cmd.apply(null); + } catch (Exception e) { + throw new ZuulRuntimeException(new ZuulException(e, HttpStatus.INTERNAL_SERVER_ERROR.value(), String.valueOf(e))); + } return null; } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java index 006cf757a3..92a7904f1d 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java @@ -172,7 +172,11 @@ public void onStartWithServer(ExecutionContext context, ExecutionInfo in // in context is a command, it means update of authentication is waiting for select an instance final Server.MetaInfo metaInfo = info.getServer().getMetaInfo(); final InstanceInfo instanceInfo = getInstanceInfo(metaInfo.getServiceIdForDiscovery(), metaInfo.getInstanceId()); - cmd.apply(instanceInfo); + try { + cmd.apply(instanceInfo); + } catch (Exception e) { + throw new AbortExecutionException(String.valueOf(e), e); + } } } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java index 6dd97b6b69..a17d809263 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java @@ -91,7 +91,7 @@ protected AuthenticationService getAuthenticationService() { @Override @CacheEvict(value = CACHE_BY_AUTHENTICATION, condition = "#result != null && #result.isExpired()") @Cacheable(CACHE_BY_AUTHENTICATION) - public AuthenticationCommand getAuthenticationCommand(Authentication authentication, String jwtToken) { + public AuthenticationCommand getAuthenticationCommand(Authentication authentication, String jwtToken) throws Exception { final AbstractAuthenticationScheme scheme = authenticationSchemeFactory.getSchema(authentication.getScheme()); final QueryResponse queryResponse = authenticationService.parseJwtToken(jwtToken); return scheme.createCommand(authentication, queryResponse); @@ -100,7 +100,7 @@ public AuthenticationCommand getAuthenticationCommand(Authentication authenticat @Override @CacheEvict(value = CACHE_BY_SERVICE_ID, condition = "#result != null && #result.isExpired()") @Cacheable(value = CACHE_BY_SERVICE_ID, keyGenerator = CacheConfig.COMPOSITE_KEY_GENERATOR) - public AuthenticationCommand getAuthenticationCommand(String serviceId, String jwtToken) { + public AuthenticationCommand getAuthenticationCommand(String serviceId, String jwtToken) throws Exception { final List instances = discoveryClient.getInstancesById(serviceId); Authentication found = null; @@ -164,7 +164,7 @@ public class UniversalAuthenticationCommand extends AuthenticationCommand { protected UniversalAuthenticationCommand() {} @Override - public void apply(InstanceInfo instanceInfo) { + public void apply(InstanceInfo instanceInfo) throws Exception { if (instanceInfo == null) throw new NullPointerException("Argument instanceInfo is required"); final Authentication auth = getAuthentication(instanceInfo); diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AbstractAuthenticationScheme.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AbstractAuthenticationScheme.java index f6c72bf01e..49d7d40985 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AbstractAuthenticationScheme.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AbstractAuthenticationScheme.java @@ -32,7 +32,7 @@ public interface AbstractAuthenticationScheme { * @param authentication DTO describing details about authentication * @param token User's parsed (Zowe's) JWT token */ - public AuthenticationCommand createCommand(Authentication authentication, QueryResponse token); + public AuthenticationCommand createCommand(Authentication authentication, QueryResponse token) throws Exception; /** * Define implementation, which will be use in case no scheme is defined. diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationCommand.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationCommand.java index 33e93dea2a..c616569510 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationCommand.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationCommand.java @@ -45,6 +45,6 @@ public boolean isExpired() { * In all other case call apply(null). * @param instanceInfo Specific instanceIf if it is needed */ - public abstract void apply(InstanceInfo instanceInfo); + public abstract void apply(InstanceInfo instanceInfo) throws Exception; } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactory.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactory.java index c915004b21..d82283efa1 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactory.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactory.java @@ -78,7 +78,7 @@ public AbstractAuthenticationScheme getSchema(AuthenticationScheme scheme) { return output; } - public AuthenticationCommand getAuthenticationCommand(Authentication authentication) { + public AuthenticationCommand getAuthenticationCommand(Authentication authentication) throws Exception { final AbstractAuthenticationScheme scheme; if ((authentication == null) || (authentication.getScheme() == null)) { scheme = defaultScheme; diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketScheme.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketScheme.java index 02037146e0..56ab438d36 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketScheme.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketScheme.java @@ -11,6 +11,7 @@ import com.ca.apiml.security.common.auth.Authentication; import com.ca.apiml.security.common.auth.AuthenticationScheme; +import com.ca.apiml.security.common.service.IRRPassTicketGenerationException; import com.ca.apiml.security.common.service.PassTicketService; import com.ca.apiml.security.common.token.QueryResponse; import com.netflix.appinfo.InstanceInfo; @@ -33,7 +34,7 @@ public class HttpBasicPassTicketScheme implements AbstractAuthenticationScheme { private final PassTicketService passTicketService; - @Value("${apiml.security.auth.passTicket.timeout:600}") + @Value("${apiml.security.auth.passTicket.timeout:540}") private Integer timeout; public HttpBasicPassTicketScheme(@Autowired PassTicketService passTicketService) { @@ -46,7 +47,7 @@ public AuthenticationScheme getScheme() { } @Override - public AuthenticationCommand createCommand(Authentication authentication, QueryResponse token) { + public AuthenticationCommand createCommand(Authentication authentication, QueryResponse token) throws IRRPassTicketGenerationException { final long before = System.currentTimeMillis(); final String applId = authentication.getApplid(); diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ServiceAuthenticationService.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ServiceAuthenticationService.java index 512ec98e4c..8cb7f879c9 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ServiceAuthenticationService.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ServiceAuthenticationService.java @@ -25,7 +25,7 @@ public interface ServiceAuthenticationService extends ServiceCacheEvict { * @param jwtToken JWT security token of user (authentication can depends on user privilege) * @return authentication command to update request in ZUUL */ - public AuthenticationCommand getAuthenticationCommand(Authentication authentication, String jwtToken); + public AuthenticationCommand getAuthenticationCommand(Authentication authentication, String jwtToken) throws Exception; /** * Get or create command to service's authentication using serviceId and jwtToken of current user @@ -33,6 +33,6 @@ public interface ServiceAuthenticationService extends ServiceCacheEvict { * @param jwtToken JWT security token of user (authentication can depends on user privilege) * @return authentication command to update request in ZUUL (or lazy command to be updated in load balancer) */ - public AuthenticationCommand getAuthenticationCommand(String serviceId, String jwtToken); + public AuthenticationCommand getAuthenticationCommand(String serviceId, String jwtToken) throws Exception; } diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java index 458eaa6c76..bf2e0d4491 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java @@ -37,12 +37,12 @@ public class ServiceAuthenticationFilterTest { private AuthenticationCommand command; @Before - public void init() { + public void init() throws Exception { when(serviceAuthenticationService.getAuthenticationCommand(anyString(), any())).thenReturn(command); } @Test - public void testRun() { + public void testRun() throws Exception { HttpServletRequest request = mock(HttpServletRequest.class); when(request.getUserPrincipal()).thenReturn(new TokenAuthentication("user", "token")); RequestContext requestContext = mock(RequestContext.class); diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java index b2a1e2ba7b..2304eacfe4 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java @@ -115,7 +115,7 @@ public void testGetAuthentication() { } @Test - public void testGetAuthenticationCommand() { + public void testGetAuthenticationCommand() throws Exception { AbstractAuthenticationScheme schemeBeanMock = mock(AbstractAuthenticationScheme.class); // token1 - valid QueryResponse qr1 = new QueryResponse("domain", "userId", @@ -153,7 +153,7 @@ public void testGetAuthenticationCommand() { } @Test - public void testGetAuthenticationCommandByServiceId() { + public void testGetAuthenticationCommandByServiceId() throws Exception { AuthenticationCommand ok = new AuthenticationCommandTest(false); Authentication a1 = new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applid01"); Authentication a2 = new Authentication(AuthenticationScheme.ZOWE_JWT, null); @@ -198,7 +198,7 @@ public void testGetAuthenticationCommandByServiceId() { } @Test - public void testGetAuthenticationCommandByServiceIdCache() { + public void testGetAuthenticationCommandByServiceIdCache() throws Exception { InstanceInfo ii1 = createInstanceInfo("i1", AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applid1"); AuthenticationCommand ac1 = new AuthenticationCommandTest(true); AuthenticationCommand ac2 = new AuthenticationCommandTest(false); @@ -225,7 +225,7 @@ public void testGetAuthenticationCommandByServiceIdCache() { } @Test - public void testUniversalAuthenticationCommand() { + public void testUniversalAuthenticationCommand() throws Exception { ServiceAuthenticationServiceImpl.UniversalAuthenticationCommand uac = serviceAuthenticationServiceImpl.new UniversalAuthenticationCommand(); try { @@ -264,7 +264,7 @@ public void testLoadBalancerAuthenticationCommand() { } @Test - public void testEvictCacheService() { + public void testEvictCacheService() throws Exception { AuthenticationCommand command = AuthenticationCommand.EMPTY; Authentication auth = new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applicationId0001"); doReturn(Collections.singletonList(createInstanceInfo("instance0001", auth))).when(discoveryClient).getInstancesById("service0001"); diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationCommandTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationCommandTest.java index 48c51e0f61..25de5e8965 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationCommandTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationCommandTest.java @@ -17,7 +17,7 @@ public class AuthenticationCommandTest { @Test - public void testEmptyCommand() { + public void testEmptyCommand() throws Exception { assertFalse(AuthenticationCommand.EMPTY.isExpired()); AuthenticationCommand.EMPTY.apply(null); } diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java index ff74947015..1c67f5191a 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java @@ -120,7 +120,7 @@ public void testGetSchema() { } @Test - public void testGetAuthenticationCommand() { + public void testGetAuthenticationCommand() throws Exception { final AbstractAuthenticationScheme byPass = spy(createScheme(AuthenticationScheme.BYPASS, true)); final AbstractAuthenticationScheme passTicket = spy(createScheme(AuthenticationScheme.HTTP_BASIC_PASSTICKET, false)); @@ -164,7 +164,7 @@ public void testGetAuthenticationCommand() { } @Test - public void testUnknownScheme() { + public void testUnknownScheme() throws Exception { AuthenticationSchemeFactory asf = new AuthenticationSchemeFactory( mock(AuthenticationService.class), Arrays.asList( diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java index bd0311303e..9586677ce3 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java @@ -45,7 +45,7 @@ public void init() { } @Test - public void testCreateCommand() { + public void testCreateCommand() throws Exception { Calendar calendar = Calendar.getInstance(); Authentication authentication = new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applid"); QueryResponse queryResponse = new QueryResponse("domain", "username", calendar.getTime(), calendar.getTime()); diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZosmfSchemeTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZosmfSchemeTest.java index 1386ab068f..bb9bccb4f4 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZosmfSchemeTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZosmfSchemeTest.java @@ -41,7 +41,7 @@ public class ZosmfSchemeTest { private ZosmfScheme zosmfScheme; @Test - public void testCreateCommand() { + public void testCreateCommand() throws Exception { Calendar calendar = Calendar.getInstance(); Authentication authentication = new Authentication(AuthenticationScheme.ZOSMF, null); QueryResponse queryResponse = new QueryResponse("domain", "username", calendar.getTime(), calendar.getTime()); From e08b82e686a8cabcfa9d548aee3b4512e4625028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Tue, 7 Jan 2020 14:16:39 +0100 Subject: [PATCH 026/122] fix build and test on security (without fixes in discoverable client) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../DiscoverableClientSampleApplication.java | 4 +- .../client/api/PassTicketTestController.java | 35 ++++------ .../ca/mfaas/discovery/GatewayNotifier.java | 11 ++- .../mfaas/discovery/GatewayNotifierTest.java | 5 +- .../GatewayRibbonLoadBalancingHttpClient.java | 8 ++- .../service/AuthenticationService.java | 7 +- .../ServiceAuthenticationServiceImpl.java | 6 +- .../ticket/SuccessfulTicketHandler.java | 3 +- .../pre/ServiceAuthenticationFilterTest.java | 18 +++-- .../service/AuthenticationServiceTest.java | 9 ++- .../ServiceAuthenticationServiceImplTest.java | 68 ++++++++++++------- 11 files changed, 111 insertions(+), 63 deletions(-) diff --git a/discoverable-client/src/main/java/com/ca/mfaas/client/DiscoverableClientSampleApplication.java b/discoverable-client/src/main/java/com/ca/mfaas/client/DiscoverableClientSampleApplication.java index c7e6eac065..57d8346976 100644 --- a/discoverable-client/src/main/java/com/ca/mfaas/client/DiscoverableClientSampleApplication.java +++ b/discoverable-client/src/main/java/com/ca/mfaas/client/DiscoverableClientSampleApplication.java @@ -31,7 +31,9 @@ @ComponentScan(value = { "com.ca.mfaas.client", "com.ca.mfaas.enable", - "com.ca.mfaas.product.web" }) + "com.ca.mfaas.product.web", + "com.ca.apiml.security.common.service" +}) public class DiscoverableClientSampleApplication implements ApplicationListener { public static void main(String[] args) { diff --git a/discoverable-client/src/main/java/com/ca/mfaas/client/api/PassTicketTestController.java b/discoverable-client/src/main/java/com/ca/mfaas/client/api/PassTicketTestController.java index c4c87e5abb..33937a8ce2 100644 --- a/discoverable-client/src/main/java/com/ca/mfaas/client/api/PassTicketTestController.java +++ b/discoverable-client/src/main/java/com/ca/mfaas/client/api/PassTicketTestController.java @@ -9,56 +9,45 @@ */ package com.ca.mfaas.client.api; -import java.nio.charset.StandardCharsets; -import java.util.Base64; - -import javax.annotation.PostConstruct; - -import com.ca.apiml.security.common.service.IRRPassTicket; -import com.ca.apiml.security.common.service.PassTicketService.DefaultPassTicketImpl; -import com.ca.mfaas.util.ClassOrDefaultProxyUtils; - +import com.ca.apiml.security.common.service.IRRPassTicketEvaluationException; +import com.ca.apiml.security.common.service.PassTicketService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.AllArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; +import java.nio.charset.StandardCharsets; +import java.util.Base64; /** * Controller for testing PassTickets. */ +@AllArgsConstructor @RestController @Api(tags = { "Test Operations" }, description = "Operations for APIML Testing") public class PassTicketTestController { + @Value("${apiml.service.applId:ZOWEAPPL}") private String applId; - private IRRPassTicket irrPassTicket; - - @PostConstruct - public void init() { - this.irrPassTicket = ClassOrDefaultProxyUtils.createProxy( - IRRPassTicket.class, - "com.ibm.eserver.zos.racf.IRRPassTicket", - DefaultPassTicketImpl::new - ); - } + private final PassTicketService passTicketService; /** * Validates the PassTicket in authorization header. */ @GetMapping(value = "/api/v1/passticketTest") @ApiOperation(value = "Validate that the PassTicket in Authorization header is valid", tags = { "Test Operations" }) - public void passticketTest(@RequestHeader("authorization") String authorization) { + public void passticketTest(@RequestHeader("authorization") String authorization) throws IRRPassTicketEvaluationException { if (authorization != null && authorization.toLowerCase().startsWith("basic")) { String base64Credentials = authorization.substring("Basic".length()).trim(); String credentials = new String(Base64.getDecoder().decode(base64Credentials), StandardCharsets.UTF_8); String[] values = credentials.split(":", 2); String userId = values[0]; String passTicket = values[1]; - irrPassTicket.evaluate(userId, applId, passTicket); + passTicketService.evaluate(userId, applId, passTicket); return; } throw new IllegalArgumentException("Missing Basic authorization header"); diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java index 4d8d0dc54e..a13be0b253 100644 --- a/discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java @@ -11,10 +11,12 @@ import com.ca.mfaas.util.EurekaUtils; import com.netflix.appinfo.InstanceInfo; +import com.netflix.discovery.shared.Application; import com.netflix.eureka.EurekaServerContext; import com.netflix.eureka.EurekaServerContextHolder; import com.netflix.eureka.registry.PeerAwareInstanceRegistry; import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; @@ -22,6 +24,7 @@ @Component @AllArgsConstructor +@Slf4j public class GatewayNotifier { private final RestTemplate restTemplate; @@ -36,7 +39,13 @@ private PeerAwareInstanceRegistry getRegistry() { public void serviceUpdated(String serviceId) { final PeerAwareInstanceRegistry registry = getRegistry(); - final List gatewayInstances = registry.getInstancesById("gateway"); + final Application application = registry.getApplication("gateway"); + if (application == null) { + log.error("Gateway application doesn't exists, cannot be notified about service change"); + return; + } + + final List gatewayInstances = application.getInstances(); for (final InstanceInfo instanceInfo : gatewayInstances) { final StringBuilder url = new StringBuilder(); diff --git a/discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java b/discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java index eeaa18dd13..8a7c59080d 100644 --- a/discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java +++ b/discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java @@ -9,6 +9,7 @@ */ import com.netflix.appinfo.InstanceInfo; +import com.netflix.discovery.shared.Application; import com.netflix.eureka.EurekaServerContext; import com.netflix.eureka.EurekaServerContextHolder; import com.netflix.eureka.registry.AwsInstanceRegistry; @@ -57,7 +58,9 @@ public void testServiceUpdated() { createInstanceInfo("192.168.0.1", 1000, 0) ); - when(registry.getInstancesById("gateway")).thenReturn(instances); + Application application = mock(Application.class); + when(application.getInstances()).thenReturn(instances); + when(registry.getApplication("gateway")).thenReturn(application); gatewayNotifier.serviceUpdated("testService"); verify(restTemplate, times(1)).delete("https://127.0.0.1:1433/cache/services/testService"); diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java index 92a7904f1d..4648747298 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java @@ -14,6 +14,7 @@ import com.netflix.client.config.CommonClientConfigKey; import com.netflix.client.config.IClientConfig; import com.netflix.discovery.EurekaClient; +import com.netflix.discovery.shared.Application; import com.netflix.loadbalancer.Server; import com.netflix.loadbalancer.reactive.ExecutionContext; import com.netflix.loadbalancer.reactive.ExecutionInfo; @@ -37,7 +38,6 @@ import java.net.URI; import java.util.Collections; -import java.util.List; import static com.ca.mfaas.gateway.security.service.ServiceAuthenticationServiceImpl.AUTHENTICATION_COMMAND_KEY; import static org.springframework.cloud.netflix.ribbon.RibbonUtils.updateToSecureConnectionIfNeeded; @@ -133,7 +133,11 @@ public InstanceInfo putInstanceInfo(String serviceId, String instanceId, Instanc @Cacheable("instanceInfoByInstanceId") public InstanceInfo getInstanceInfo(String serviceId, String instanceId) { InstanceInfo output = null; - for (final InstanceInfo instanceInfo : (List) discoveryClient.getInstancesById(serviceId)) { + + Application application = discoveryClient.getApplication(serviceId); + if (application == null) return output; + + for (final InstanceInfo instanceInfo : application.getInstances()) { if (StringUtils.equals(instanceId, instanceInfo.getInstanceId())) { // found instance, store it for output output = instanceInfo; diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java index ed39ea892f..80a2b8d236 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java @@ -18,6 +18,7 @@ import com.ca.mfaas.util.EurekaUtils; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.EurekaClient; +import com.netflix.discovery.shared.Application; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.JwtException; @@ -106,8 +107,12 @@ public Boolean invalidateJwtToken(String jwtToken, boolean distribute) { * until ehCache is not distributed, send to other instances invalidation request */ if (distribute) { + final Application application = discoveryClient.getApplication("gateway"); + // wrong state, gateway have to exists (at least this current instance), return false like unsuccessful + if (application == null) return Boolean.FALSE; + final String myInstanceId = discoveryClient.getApplicationInfoManager().getInfo().getInstanceId(); - for (final InstanceInfo instanceInfo : (List) discoveryClient.getInstancesById("gateway")) { + for (final InstanceInfo instanceInfo : application.getInstances()) { if (StringUtils.equals(myInstanceId, instanceInfo.getInstanceId())) continue; final String url = EurekaUtils.getUrl(instanceInfo) + "/auth/invalidate/{}"; diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java index a9101bd2a4..18d3124f76 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java @@ -20,6 +20,7 @@ import com.ca.mfaas.gateway.security.service.schema.ServiceAuthenticationService; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.EurekaClient; +import com.netflix.discovery.shared.Application; import com.netflix.zuul.context.RequestContext; import lombok.AllArgsConstructor; import org.springframework.cache.Cache; @@ -101,7 +102,10 @@ public AuthenticationCommand getAuthenticationCommand(Authentication authenticat @CacheEvict(value = CACHE_BY_SERVICE_ID, condition = "#result != null && #result.isExpired()") @Cacheable(value = CACHE_BY_SERVICE_ID, keyGenerator = CacheConfig.COMPOSITE_KEY_GENERATOR) public AuthenticationCommand getAuthenticationCommand(String serviceId, String jwtToken) throws Exception { - final List instances = discoveryClient.getApplication(serviceId).getInstances(); + final Application application = discoveryClient.getApplication(serviceId); + if (application == null) return AuthenticationCommand.EMPTY; + + final List instances = application.getInstances(); Authentication found = null; for (final InstanceInfo instance : instances) { diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandler.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandler.java index e94d83b202..4c1c4f571e 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandler.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandler.java @@ -9,6 +9,7 @@ */ package com.ca.mfaas.gateway.security.ticket; +import com.ca.apiml.security.common.service.IRRPassTicketGenerationException; import com.ca.apiml.security.common.service.PassTicketService; import com.ca.apiml.security.common.token.TokenAuthentication; import com.ca.mfaas.message.api.ApiMessageView; @@ -64,7 +65,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo } } - private TicketResponse getTicketResponse(HttpServletRequest request, Authentication authentication) throws ApplicationNameNotFoundException { + private TicketResponse getTicketResponse(HttpServletRequest request, Authentication authentication) throws ApplicationNameNotFoundException, IRRPassTicketGenerationException { TokenAuthentication tokenAuthentication = (TokenAuthentication) authentication; String userId = tokenAuthentication.getPrincipal(); diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java index 628f81147c..84f74f3c74 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java @@ -8,7 +8,7 @@ * Copyright Contributors to the Zowe Project. */ -import com.ca.apiml.security.common.token.TokenAuthentication; +import com.ca.mfaas.gateway.security.service.AuthenticationService; import com.ca.mfaas.gateway.security.service.ServiceAuthenticationServiceImpl; import com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand; import com.netflix.zuul.context.RequestContext; @@ -21,6 +21,8 @@ import javax.servlet.http.HttpServletRequest; +import java.util.Optional; + import static org.mockito.Mockito.*; import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SERVICE_ID_KEY; @@ -36,28 +38,32 @@ public class ServiceAuthenticationFilterTest { @Mock private AuthenticationCommand command; + @Mock + private AuthenticationService authenticationService; + @Before - public void init() { + public void init() throws Exception { when(serviceAuthenticationService.getAuthenticationCommand(anyString(), any())).thenReturn(command); } @Test public void testRun() throws Exception { HttpServletRequest request = mock(HttpServletRequest.class); - when(request.getUserPrincipal()).thenReturn(new TokenAuthentication("user", "token")); + RequestContext requestContext = mock(RequestContext.class); when(requestContext.getRequest()).thenReturn(request); when(requestContext.get(SERVICE_ID_KEY)).thenReturn("service"); RequestContext.testSetCurrentContext(requestContext); + when(authenticationService.getJwtTokenFromRequest(any())).thenReturn(Optional.of("token")); + serviceAuthenticationFilter.run(); verify(serviceAuthenticationService, times(1)).getAuthenticationCommand("service", "token"); verify(command, times(1)).apply(null); - when(request.getUserPrincipal()).thenReturn(() -> null); + when(authenticationService.getJwtTokenFromRequest(any())).thenReturn(Optional.empty()); serviceAuthenticationFilter.run(); - verify(serviceAuthenticationService, times(1)).getAuthenticationCommand("service", null); - verify(command, times(2)).apply(null); + verify(serviceAuthenticationService, times(1)).getAuthenticationCommand(anyString(), any()); } } diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/AuthenticationServiceTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/AuthenticationServiceTest.java index 6fedda4331..48d80da9a2 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/AuthenticationServiceTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/AuthenticationServiceTest.java @@ -19,6 +19,7 @@ import com.netflix.appinfo.ApplicationInfoManager; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.EurekaClient; +import com.netflix.discovery.shared.Application; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.apache.commons.lang.time.DateUtils; @@ -40,6 +41,7 @@ import java.security.PublicKey; import java.util.Arrays; import java.util.Date; +import java.util.List; import java.util.Optional; import static org.junit.Assert.*; @@ -241,11 +243,14 @@ public void invalidateToken() { when(applicationInfoManager.getInfo()).thenReturn(myInstance); when(discoveryClient.getApplicationInfoManager()).thenReturn(applicationInfoManager); - doReturn(Arrays.asList( + Application application = mock(Application.class); + List instances = Arrays.asList( createInstanceInfo("instance02", "192.168.0.1", 10000, 10433), createInstanceInfo("myInstance01", "127.0.0.0.1", 10000, 10433), createInstanceInfo("instance03", "192.168.0.2", 10001, 0) - )).when(discoveryClient).getInstancesById("gateway"); + ); + when(application.getInstances()).thenReturn(instances); + when(discoveryClient.getApplication("gateway")).thenReturn(application); authService.invalidateJwtToken(jwt1, true); assertTrue(authService.isInvalidated(jwt1)); diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java index 5537eae107..5c1c6ac35a 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java @@ -15,8 +15,8 @@ import com.ca.mfaas.gateway.security.service.schema.*; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.EurekaClient; +import com.netflix.discovery.shared.Application; import com.netflix.zuul.context.RequestContext; -import org.apache.catalina.servlet4preview.http.HttpServletRequest; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -29,12 +29,11 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import javax.servlet.http.HttpServletRequest; import java.sql.Date; import java.time.LocalDate; import java.util.*; -import javax.servlet.http.HttpServletRequest; - import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_APPLID; import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_SCHEME; import static com.ca.mfaas.gateway.security.service.ServiceAuthenticationServiceImpl.AUTHENTICATION_COMMAND_KEY; @@ -99,6 +98,12 @@ private InstanceInfo createInstanceInfo(String id, Authentication authentication return createInstanceInfo(id, authentication == null ? null : authentication.getScheme(), authentication == null ? null : authentication.getApplid()); } + private Application createApplication(InstanceInfo...instances) { + final Application out = mock(Application.class); + when(out.getInstances()).thenReturn(Arrays.asList(instances)); + return out; + } + @Test public void testGetAuthentication() { InstanceInfo ii; @@ -117,7 +122,7 @@ public void testGetAuthentication() { } @Test - public void testGetAuthenticationCommand() { + public void testGetAuthenticationCommand() throws Exception { AbstractAuthenticationScheme schemeBeanMock = mock(AbstractAuthenticationScheme.class); // token1 - valid QueryResponse qr1 = new QueryResponse("domain", "userId", @@ -167,35 +172,43 @@ public void testGetAuthenticationCommandByServiceId() throws Exception { InstanceInfo ii3 = createInstanceInfo("inst01", a3); InstanceInfo ii4 = createInstanceInfo("inst01", a4); + Application application; + ServiceAuthenticationService sas = spy(serviceAuthenticationServiceImpl); when(authenticationSchemeFactory.getSchema(any())).thenReturn(mock(AbstractAuthenticationScheme.class)); // just one instance when(sas.getAuthenticationCommand(a1, "jwt01")).thenReturn(ok); - when(discoveryClient.getInstancesById("svr01")).thenReturn(Collections.singletonList(ii1)); + application = createApplication(ii1); + when(discoveryClient.getApplication("svr01")).thenReturn(application); assertSame(ok, sas.getAuthenticationCommand("svr01", "jwt01")); // multiple same instances when(sas.getAuthenticationCommand(a1, "jwt02")).thenReturn(ok); - when(discoveryClient.getInstancesById("svr02")).thenReturn(Arrays.asList(ii1, ii1, ii1)); + application = createApplication(ii1, ii1, ii1); + when(discoveryClient.getApplication("svr02")).thenReturn(application); assertSame(ok, sas.getAuthenticationCommand("svr02", "jwt02")); // multiple different instances reset(discoveryClient); - when(discoveryClient.getInstancesById("svr03")).thenReturn(Arrays.asList(ii1, ii2)); + application = createApplication(ii1, ii2); + when(discoveryClient.getApplication("svr03")).thenReturn(application); assertTrue(sas.getAuthenticationCommand("svr03", "jwt03") instanceof ServiceAuthenticationServiceImpl.LoadBalancerAuthenticationCommand); reset(discoveryClient); - when(discoveryClient.getInstancesById("svr03")).thenReturn(Arrays.asList(ii1, ii3)); + application = createApplication(ii1, ii3); + when(discoveryClient.getApplication("svr03")).thenReturn(application); assertTrue(sas.getAuthenticationCommand("svr03", "jwt03") instanceof ServiceAuthenticationServiceImpl.LoadBalancerAuthenticationCommand); reset(discoveryClient); - when(discoveryClient.getInstancesById("svr03")).thenReturn(Arrays.asList(ii1, ii4)); + application = createApplication(ii1, ii4); + when(discoveryClient.getApplication("svr03")).thenReturn(application); assertTrue(sas.getAuthenticationCommand("svr03", "jwt03") instanceof ServiceAuthenticationServiceImpl.LoadBalancerAuthenticationCommand); reset(discoveryClient); - when(discoveryClient.getInstancesById("svr03")).thenReturn(Arrays.asList(ii1, ii2, ii3, ii4)); + application = createApplication(ii1, ii2, ii3, ii4); + when(discoveryClient.getApplication("svr03")).thenReturn(application); assertTrue(sas.getAuthenticationCommand("svr03", "jwt03") instanceof ServiceAuthenticationServiceImpl.LoadBalancerAuthenticationCommand); } @@ -207,13 +220,14 @@ public void testGetAuthenticationCommandByServiceIdCache() throws Exception { AbstractAuthenticationScheme aas1 = mock(AbstractAuthenticationScheme.class); when(aas1.getScheme()).thenReturn(AuthenticationScheme.HTTP_BASIC_PASSTICKET); - when(discoveryClient.getInstancesById("s1")).thenReturn(Collections.singletonList(ii1)); + Application application = createApplication(ii1); + when(discoveryClient.getApplication("s1")).thenReturn(application); when(authenticationSchemeFactory.getSchema(AuthenticationScheme.HTTP_BASIC_PASSTICKET)).thenReturn(aas1); when(aas1.createCommand(eq(new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applid1")), any())) .thenReturn(ac1); assertSame(ac1, serviceAuthenticationService.getAuthenticationCommand("s1", "jwt")); - verify(discoveryClient, times(1)).getInstancesById("s1"); + verify(discoveryClient, times(1)).getApplication("s1"); serviceAuthenticationService.evictCacheAllService(); Mockito.reset(aas1); @@ -221,9 +235,9 @@ public void testGetAuthenticationCommandByServiceIdCache() throws Exception { when(aas1.createCommand(eq(new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applid1")), any())) .thenReturn(ac2); assertSame(ac2, serviceAuthenticationService.getAuthenticationCommand("s1", "jwt")); - verify(discoveryClient, times(2)).getInstancesById("s1"); + verify(discoveryClient, times(2)).getApplication("s1"); assertSame(ac2, serviceAuthenticationService.getAuthenticationCommand("s1", "jwt")); - verify(discoveryClient, times(2)).getInstancesById("s1"); + verify(discoveryClient, times(2)).getApplication("s1"); } @Test @@ -278,30 +292,36 @@ public void testEvictCacheService() throws Exception { assertSame(command, serviceAuthenticationService.getAuthenticationCommand("service0001", "jwt01")); assertSame(command, serviceAuthenticationService.getAuthenticationCommand("service0001", "jwt01")); - verify(discoveryClient, times(1)).getInstancesById("service0001"); - verify(discoveryClient, never()).getInstancesById("service0002"); + verify(discoveryClient, times(1)).getApplication("service0001"); + verify(discoveryClient, never()).getApplication("service0002"); assertSame(command, serviceAuthenticationService.getAuthenticationCommand("service0001", "jwt02")); assertSame(command, serviceAuthenticationService.getAuthenticationCommand("service0002", "jwt01")); - verify(discoveryClient, times(2)).getInstancesById("service0001"); - verify(discoveryClient, times(1)).getInstancesById("service0002"); + verify(discoveryClient, times(2)).getApplication("service0001"); + verify(discoveryClient, times(1)).getApplication("service0002"); serviceAuthenticationService.evictCacheService("service0001"); assertSame(command, serviceAuthenticationService.getAuthenticationCommand("service0001", "jwt01")); - verify(discoveryClient, times(3)).getInstancesById("service0001"); + verify(discoveryClient, times(3)).getApplication("service0001"); assertSame(command, serviceAuthenticationService.getAuthenticationCommand("service0001", "jwt02")); - verify(discoveryClient, times(4)).getInstancesById("service0001"); + verify(discoveryClient, times(4)).getApplication("service0001"); assertSame(command, serviceAuthenticationService.getAuthenticationCommand("service0002", "jwt01")); - verify(discoveryClient, times(1)).getInstancesById("service0002"); + verify(discoveryClient, times(1)).getApplication("service0002"); serviceAuthenticationService.evictCacheAllService(); assertSame(command, serviceAuthenticationService.getAuthenticationCommand("service0001", "jwt01")); - verify(discoveryClient, times(5)).getInstancesById("service0001"); + verify(discoveryClient, times(5)).getApplication("service0001"); assertSame(command, serviceAuthenticationService.getAuthenticationCommand("service0001", "jwt02")); - verify(discoveryClient, times(6)).getInstancesById("service0001"); + verify(discoveryClient, times(6)).getApplication("service0001"); assertSame(command, serviceAuthenticationService.getAuthenticationCommand("service0002", "jwt01")); - verify(discoveryClient, times(2)).getInstancesById("service0002"); + verify(discoveryClient, times(2)).getApplication("service0002"); + } + + @Test + public void testNoApplication() throws Exception { + when(discoveryClient.getApplication(any())).thenReturn(null); + assertSame(AuthenticationCommand.EMPTY, serviceAuthenticationServiceImpl.getAuthenticationCommand("unknown", "jwtToken")); } public class AuthenticationCommandTest extends AuthenticationCommand { From 87af4be71d1768027a23284e43eb11d390d9c834 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Tue, 7 Jan 2020 14:53:33 +0100 Subject: [PATCH 027/122] Fix broken PassTicketTestController initialization Signed-off-by: Petr Plavjanik --- .../com/ca/mfaas/client/api/PassTicketTestController.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/discoverable-client/src/main/java/com/ca/mfaas/client/api/PassTicketTestController.java b/discoverable-client/src/main/java/com/ca/mfaas/client/api/PassTicketTestController.java index 33937a8ce2..cee4b11db8 100644 --- a/discoverable-client/src/main/java/com/ca/mfaas/client/api/PassTicketTestController.java +++ b/discoverable-client/src/main/java/com/ca/mfaas/client/api/PassTicketTestController.java @@ -13,7 +13,6 @@ import com.ca.apiml.security.common.service.PassTicketService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; -import lombok.AllArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestHeader; @@ -25,7 +24,6 @@ /** * Controller for testing PassTickets. */ -@AllArgsConstructor @RestController @Api(tags = { "Test Operations" }, description = "Operations for APIML Testing") public class PassTicketTestController { @@ -35,6 +33,10 @@ public class PassTicketTestController { private final PassTicketService passTicketService; + public PassTicketTestController(PassTicketService passTicketService) { + this.passTicketService = passTicketService; + } + /** * Validates the PassTicket in authorization header. */ From aabe4aaacd24d72bc8a8dd56d1293043a62ded12 Mon Sep 17 00:00:00 2001 From: JirkaAichler Date: Tue, 7 Jan 2020 14:56:05 +0100 Subject: [PATCH 028/122] Fix broken integration tests Signed-off-by: JirkaAichler --- .../security/common/auth/Authentication.java | 11 ++++++---- .../ServiceAuthenticationServiceImpl.java | 2 +- .../ServiceAuthenticationServiceImplTest.java | 21 ++++++++++++------- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/auth/Authentication.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/auth/Authentication.java index 330d77a932..a8e7b13539 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/auth/Authentication.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/auth/Authentication.java @@ -16,12 +16,15 @@ /** * Information about expected authentication scheme and APPLID for PassTickets generation. */ - @Data - @NoArgsConstructor - @AllArgsConstructor - public class Authentication { +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Authentication { private AuthenticationScheme scheme; private String applid; + public boolean isEmpty() { + return (scheme == null) && (applid == null); + } } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java index 18d3124f76..59811d5001 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java @@ -121,7 +121,7 @@ public AuthenticationCommand getAuthenticationCommand(String serviceId, String j } // if no instance exist, do nothing - if (found == null) return AuthenticationCommand.EMPTY; + if (found == null || found.isEmpty()) return AuthenticationCommand.EMPTY; return getAuthenticationCommand(found, jwtToken); } diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java index 5c1c6ac35a..2437ced533 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java @@ -1,4 +1,4 @@ -package com.ca.mfaas.gateway.security.service;/* +/* * This program and the accompanying materials are made available under the terms of the * Eclipse Public License v2.0 which accompanies this distribution, and is available at * https://www.eclipse.org/legal/epl-v20.html @@ -7,6 +7,7 @@ * * Copyright Contributors to the Zowe Project. */ +package com.ca.mfaas.gateway.security.service; import com.ca.apiml.security.common.auth.Authentication; import com.ca.apiml.security.common.auth.AuthenticationScheme; @@ -98,7 +99,7 @@ private InstanceInfo createInstanceInfo(String id, Authentication authentication return createInstanceInfo(id, authentication == null ? null : authentication.getScheme(), authentication == null ? null : authentication.getApplid()); } - private Application createApplication(InstanceInfo...instances) { + private Application createApplication(InstanceInfo... instances) { final Application out = mock(Application.class); when(out.getInstances()).thenReturn(Arrays.asList(instances)); return out; @@ -117,7 +118,7 @@ public void testGetAuthentication() { ii = createInstanceInfo("instance2", "httpBasicPassTicket", null); assertEquals(new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, null), serviceAuthenticationServiceImpl.getAuthentication(ii)); - ii = createInstanceInfo("instance2", (AuthenticationScheme) null, null); + ii = createInstanceInfo("instance2", (AuthenticationScheme) null, null); assertEquals(new Authentication(), serviceAuthenticationServiceImpl.getAuthentication(ii)); } @@ -126,13 +127,13 @@ public void testGetAuthenticationCommand() throws Exception { AbstractAuthenticationScheme schemeBeanMock = mock(AbstractAuthenticationScheme.class); // token1 - valid QueryResponse qr1 = new QueryResponse("domain", "userId", - Date.valueOf(LocalDate.of(1900,1, 1)), - Date.valueOf(LocalDate.of(2100,1, 1)) + Date.valueOf(LocalDate.of(1900, 1, 1)), + Date.valueOf(LocalDate.of(2100, 1, 1)) ); // token2 - expired QueryResponse qr2 = new QueryResponse("domain", "userId", - Date.valueOf(LocalDate.of(1900,1, 1)), - Date.valueOf(LocalDate.of(2000,1, 1)) + Date.valueOf(LocalDate.of(1900, 1, 1)), + Date.valueOf(LocalDate.of(2000, 1, 1)) ); AuthenticationCommand acValid = spy(new AuthenticationCommandTest(false)); AuthenticationCommand acExpired = spy(new AuthenticationCommandTest(true)); @@ -166,11 +167,13 @@ public void testGetAuthenticationCommandByServiceId() throws Exception { Authentication a2 = new Authentication(AuthenticationScheme.ZOWE_JWT, null); Authentication a3 = new Authentication(AuthenticationScheme.ZOWE_JWT, "applid01"); Authentication a4 = new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applid02"); + Authentication a5 = new Authentication(null, null); InstanceInfo ii1 = createInstanceInfo("inst01", a1); InstanceInfo ii2 = createInstanceInfo("inst01", a2); InstanceInfo ii3 = createInstanceInfo("inst01", a3); InstanceInfo ii4 = createInstanceInfo("inst01", a4); + InstanceInfo ii5 = createInstanceInfo("inst02", a5); Application application; @@ -210,6 +213,10 @@ public void testGetAuthenticationCommandByServiceId() throws Exception { application = createApplication(ii1, ii2, ii3, ii4); when(discoveryClient.getApplication("svr03")).thenReturn(application); assertTrue(sas.getAuthenticationCommand("svr03", "jwt03") instanceof ServiceAuthenticationServiceImpl.LoadBalancerAuthenticationCommand); + + reset(discoveryClient); + when(discoveryClient.getInstancesById("svr03")).thenReturn(Collections.singletonList(ii5)); + assertSame(AuthenticationCommand.EMPTY, sas.getAuthenticationCommand("svr03", "jwt03")); } @Test From 0a1efe245df13c98edf1d6b0207042137cda7941 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Tue, 7 Jan 2020 15:44:28 +0100 Subject: [PATCH 029/122] Deprecate old ZosmfFilter Signed-off-by: Petr Plavjanik --- .../java/com/ca/mfaas/gateway/filters/pre/ZosmfFilter.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ZosmfFilter.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ZosmfFilter.java index 36a93da8f0..62fb6c2ab0 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ZosmfFilter.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ZosmfFilter.java @@ -21,8 +21,11 @@ import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SERVICE_ID_KEY; /** - * Extract LTPA token from JWT token and set it as a cookie when accessing zOSMF + * Extract LTPA token from JWT token and set it as a cookie when accessing z/OSMF + * + * TODO: Remove after zowe-install-packaging is updated */ +@Deprecated public class ZosmfFilter extends ZuulFilter { private static final String ZOSMF = "zosmf"; From 14d357c9165bad7e9459632c3f786b655aa8a767 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Tue, 7 Jan 2020 16:38:29 +0100 Subject: [PATCH 030/122] PassTicketTestController MockMvc tests Signed-off-by: Petr Plavjanik --- .../client/api/PassTicketTestController.java | 9 ++-- .../api/PassTicketTestControllerTest.java | 54 +++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 discoverable-client/src/test/java/com/ca/mfaas/client/api/PassTicketTestControllerTest.java diff --git a/discoverable-client/src/main/java/com/ca/mfaas/client/api/PassTicketTestController.java b/discoverable-client/src/main/java/com/ca/mfaas/client/api/PassTicketTestController.java index cee4b11db8..f2f0413572 100644 --- a/discoverable-client/src/main/java/com/ca/mfaas/client/api/PassTicketTestController.java +++ b/discoverable-client/src/main/java/com/ca/mfaas/client/api/PassTicketTestController.java @@ -25,7 +25,7 @@ * Controller for testing PassTickets. */ @RestController -@Api(tags = { "Test Operations" }, description = "Operations for APIML Testing") +@Api(tags = { "Test Operations" }) public class PassTicketTestController { @Value("${apiml.service.applId:ZOWEAPPL}") @@ -42,7 +42,8 @@ public PassTicketTestController(PassTicketService passTicketService) { */ @GetMapping(value = "/api/v1/passticketTest") @ApiOperation(value = "Validate that the PassTicket in Authorization header is valid", tags = { "Test Operations" }) - public void passticketTest(@RequestHeader("authorization") String authorization) throws IRRPassTicketEvaluationException { + public void passticketTest(@RequestHeader("authorization") String authorization) + throws IRRPassTicketEvaluationException { if (authorization != null && authorization.toLowerCase().startsWith("basic")) { String base64Credentials = authorization.substring("Basic".length()).trim(); String credentials = new String(Base64.getDecoder().decode(base64Credentials), StandardCharsets.UTF_8); @@ -50,8 +51,8 @@ public void passticketTest(@RequestHeader("authorization") String authorization) String userId = values[0]; String passTicket = values[1]; passTicketService.evaluate(userId, applId, passTicket); - return; + } else { + throw new IllegalArgumentException("Missing Basic authorization header"); } - throw new IllegalArgumentException("Missing Basic authorization header"); } } diff --git a/discoverable-client/src/test/java/com/ca/mfaas/client/api/PassTicketTestControllerTest.java b/discoverable-client/src/test/java/com/ca/mfaas/client/api/PassTicketTestControllerTest.java new file mode 100644 index 0000000000..6b1ded0153 --- /dev/null +++ b/discoverable-client/src/test/java/com/ca/mfaas/client/api/PassTicketTestControllerTest.java @@ -0,0 +1,54 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.client.api; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Base64; + +import javax.servlet.ServletException; + +@RunWith(SpringRunner.class) +@WebMvcTest(controllers = { PassTicketTestController.class }, secure = false) +public class PassTicketTestControllerTest { + @Autowired + private MockMvc mockMvc; + + private static final String ZOWE_PASSTICKET_AUTH_HEADER = "Basic " + + Base64.getEncoder().encodeToString(("USER:ZoweDummyPassTicket").getBytes()); + + private static final String BAD_PASSTICKET_AUTH_HEADER = "Basic " + + Base64.getEncoder().encodeToString(("USER:bad").getBytes()); + + @Test + public void callToPassTicketTestEndpointWithCorrectTicket() throws Exception { + this.mockMvc.perform(get("/api/v1/passticketTest").header("Authorization", ZOWE_PASSTICKET_AUTH_HEADER)) + .andExpect(status().isOk()); + } + + @Test + public void callToPassTicketTestEndpointWithoutTicketFails() throws Exception { + this.mockMvc.perform(get("/api/v1/passticketTest")).andExpect(status().isBadRequest()); + } + + @Test(expected = ServletException.class) + public void callToPassTicketTestEndpointWitBadTicketFails() throws Exception { + this.mockMvc.perform(get("/api/v1/passticketTest").header("Authorization", BAD_PASSTICKET_AUTH_HEADER)); + } + +} From f7f4fef91517b1d672fe04d546cd3c46591430f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Tue, 7 Jan 2020 16:50:26 +0100 Subject: [PATCH 031/122] improve dummy passticket service to serve similar result to real one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../common/service/PassTicketService.java | 56 +++++++++++++-- .../common/service/PassTicketServiceTest.java | 69 +++++++++++++++++++ .../ticket/SuccessfulTicketHandlerTest.java | 6 +- 3 files changed, 121 insertions(+), 10 deletions(-) diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java index 32ce71dcfa..5d9773d6b0 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java @@ -10,9 +10,16 @@ package com.ca.apiml.security.common.service; import com.ca.mfaas.util.ClassOrDefaultProxyUtils; +import lombok.AllArgsConstructor; +import lombok.Value; +import org.apache.commons.lang.StringUtils; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; /** * This method allow to get a passTicket from RAC. @@ -55,21 +62,58 @@ public boolean isUsingSafImplementation() { } public static class DefaultPassTicketImpl implements IRRPassTicket { - public static final String ZOWE_DUMMY_PASSTICKET = "ZoweDummyPassTicket"; + + private static int id = 0; + + public static final String ZOWE_DUMMY_PASS_TICKET_PREFIX = "ZoweDummyPassTicket"; + + public static final String UNKWNOWN_USER = "unknownUser"; + public static final String UNKWNOWN_APPLID = "unknownApplId"; + + private Map> userAppToPasstickets = new HashMap<>(); @Override - public void evaluate(String userId, String applId, String passTicket) { + public void evaluate(String userId, String applId, String passTicket) throws IRRPassTicketEvaluationException { if (userId == null) throw new IllegalArgumentException("Parameter userId is empty"); if (applId == null) throw new IllegalArgumentException("Parameter applId is empty"); if (passTicket == null) throw new IllegalArgumentException("Parameter passTicket is empty"); - if (!passTicket.equals(ZOWE_DUMMY_PASSTICKET)) { - throw new IllegalArgumentException("Invalid PassTicket"); + + final Set passTickets = userAppToPasstickets.get(new UserApp(userId, applId)); + if ((passTickets == null) || !passTickets.contains(passTicket)) { + throw new IRRPassTicketEvaluationException(8, 16, 32); } } @Override - public String generate(String userId, String applId) { - return ZOWE_DUMMY_PASSTICKET; + public String generate(String userId, String applId) throws IRRPassTicketGenerationException { + if (StringUtils.equalsIgnoreCase(UNKWNOWN_USER, userId)) { + throw new IRRPassTicketGenerationException(8, 8, 16); + } + + if (StringUtils.equalsIgnoreCase(UNKWNOWN_APPLID, applId)) { + throw new IRRPassTicketGenerationException(8, 16, 28); + } + + final UserApp userApp = new UserApp(userId, applId); + final int currentId; + synchronized (DefaultPassTicketImpl.class) { + currentId = DefaultPassTicketImpl.id ++; + } + final String passTicket = ZOWE_DUMMY_PASS_TICKET_PREFIX + "_" + applId + "_" + userId + "_" + currentId; + + final Set passTickets = userAppToPasstickets.computeIfAbsent(userApp, x -> new HashSet<>()); + passTickets.add(passTicket); + + return passTicket; + } + + @AllArgsConstructor + @Value + private class UserApp { + + private final String userId; + private final String applId; + } } diff --git a/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java b/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java index d3d97df753..607dceb1f3 100644 --- a/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java +++ b/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java @@ -80,6 +80,75 @@ public void testProxy() throws IRRPassTicketGenerationException { assertEquals("success", irrPassTicket.generate("user", "applId")); } + @Test + public void testDefaultPassTicketImpl() throws IRRPassTicketEvaluationException, IRRPassTicketGenerationException { + PassTicketService.DefaultPassTicketImpl dpti = new PassTicketService.DefaultPassTicketImpl(); + + try { + dpti.evaluate("user", "applId", "passticket"); + fail(); + } catch (IRRPassTicketEvaluationException e) { + assertEquals(8, e.getSafRc()); + assertEquals(16, e.getRacfRsn()); + assertEquals(32, e.getRacfRc()); + } + + String passTicket1 = dpti.generate("user", "applId"); + String passTicket2 = dpti.generate("user", "applId"); + + assertNotNull(passTicket1); + assertNotNull(passTicket2); + assertNotEquals(passTicket1, passTicket2); + + dpti.evaluate("user", "applId", passTicket1); + dpti.evaluate("user", "applId", passTicket2); + + try { + dpti.evaluate("userx", "applId", passTicket1); + fail(); + } catch (IRRPassTicketEvaluationException e) { + // different user, should throw exception + } + + try { + dpti.evaluate("user", "applIdx", passTicket1); + fail(); + } catch (IRRPassTicketEvaluationException e) { + // different applId, should throw exception + } + + try { + dpti.generate(PassTicketService.DefaultPassTicketImpl.UNKWNOWN_USER, "anyApplId"); + fail(); + } catch (IRRPassTicketGenerationException e) { + assertEquals(8, e.getSafRc()); + assertEquals(8, e.getRacfRsn()); + assertEquals(16, e.getRacfRc()); + assertNotNull(e.getErrorCodes()); + assertEquals(1, e.getErrorCodes().size()); + assertEquals(AbstractIRRPassTicketException.ErrorCode.ERR_8_8_16, e.getErrorCodes().get(0)); + assertEquals( + "Error on generation of PassTicket\n" + + "\tNot authorized to use this service.\n" + , e.getMessage() + ); + } + + try { + dpti.generate("anyUser", PassTicketService.DefaultPassTicketImpl.UNKWNOWN_APPLID); + fail(); + } catch (IRRPassTicketGenerationException e) { + assertEquals(8, e.getSafRc()); + assertEquals(16, e.getRacfRsn()); + assertEquals(28, e.getRacfRc()); + assertNotNull(e.getErrorCodes()); + assertEquals(3, e.getErrorCodes().size()); + assertEquals(AbstractIRRPassTicketException.ErrorCode.ERR_8_16_X_1, e.getErrorCodes().get(0)); + assertEquals(AbstractIRRPassTicketException.ErrorCode.ERR_8_16_28, e.getErrorCodes().get(1)); + assertEquals(AbstractIRRPassTicketException.ErrorCode.ERR_8_16_X_2, e.getErrorCodes().get(2)); + } + } + public static class Impl implements IRRPassTicket { @Override public void evaluate(String userId, String applId, String passTicket) { diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandlerTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandlerTest.java index a32807179a..aad1e87a9f 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandlerTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandlerTest.java @@ -11,7 +11,6 @@ import com.ca.apiml.security.common.service.PassTicketService; import com.ca.apiml.security.common.token.TokenAuthentication; - import com.ca.mfaas.message.core.MessageService; import com.ca.mfaas.message.yaml.YamlMessageService; import com.fasterxml.jackson.core.JsonProcessingException; @@ -25,7 +24,7 @@ import java.io.UnsupportedEncodingException; -import static com.ca.apiml.security.common.service.PassTicketService.DefaultPassTicketImpl.ZOWE_DUMMY_PASSTICKET; +import static com.ca.apiml.security.common.service.PassTicketService.DefaultPassTicketImpl.ZOWE_DUMMY_PASS_TICKET_PREFIX; import static org.junit.Assert.*; public class SuccessfulTicketHandlerTest { @@ -57,8 +56,7 @@ public void shouldReturnDummyPassTicket() throws JsonProcessingException, Unsupp assertEquals(MediaType.APPLICATION_JSON_UTF8_VALUE, httpServletResponse.getContentType()); assertEquals(HttpStatus.OK.value(), httpServletResponse.getStatus()); - String expectedResponse = mapper.writeValueAsString(new TicketResponse(TOKEN, USER, APPLICATION_NAME, ZOWE_DUMMY_PASSTICKET)); - assertEquals(expectedResponse, httpServletResponse.getContentAsString()); + assertTrue(httpServletResponse.getContentAsString().contains(ZOWE_DUMMY_PASS_TICKET_PREFIX)); assertTrue(httpServletResponse.isCommitted()); } From d547f40f2f02bea2420638f13d16ec32de57e883 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Tue, 7 Jan 2020 17:06:26 +0100 Subject: [PATCH 032/122] Support predefined passticket in dummy PassTicketService Signed-off-by: Petr Plavjanik --- .../common/service/PassTicketService.java | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java index 5d9773d6b0..6549ab7c3a 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java @@ -31,21 +31,14 @@ public class PassTicketService { @PostConstruct public void init() { - this.irrPassTicket = ClassOrDefaultProxyUtils.createProxy( - IRRPassTicket.class, - "com.ibm.eserver.zos.racf.IRRPassTicket", - DefaultPassTicketImpl::new, - new ClassOrDefaultProxyUtils.ByMethodName( - "com.ibm.eserver.zos.racf.IRRPassTicketEvaluationException", - IRRPassTicketEvaluationException.class, - "getSafRc", "getRacfRsn", "getRacfRc" - ), - new ClassOrDefaultProxyUtils.ByMethodName( - "com.ibm.eserver.zos.racf.IRRPassTicketGenerationException", - IRRPassTicketGenerationException.class, - "getSafRc", "getRacfRsn", "getRacfRc" - ) - ); + this.irrPassTicket = ClassOrDefaultProxyUtils.createProxy(IRRPassTicket.class, + "com.ibm.eserver.zos.racf.IRRPassTicket", DefaultPassTicketImpl::new, + new ClassOrDefaultProxyUtils.ByMethodName( + "com.ibm.eserver.zos.racf.IRRPassTicketEvaluationException", + IRRPassTicketEvaluationException.class, "getSafRc", "getRacfRsn", "getRacfRc"), + new ClassOrDefaultProxyUtils.ByMethodName( + "com.ibm.eserver.zos.racf.IRRPassTicketGenerationException", + IRRPassTicketGenerationException.class, "getSafRc", "getRacfRsn", "getRacfRc")); } public void evaluate(String userId, String applId, String passTicket) throws IRRPassTicketEvaluationException { @@ -74,11 +67,19 @@ public static class DefaultPassTicketImpl implements IRRPassTicket { @Override public void evaluate(String userId, String applId, String passTicket) throws IRRPassTicketEvaluationException { - if (userId == null) throw new IllegalArgumentException("Parameter userId is empty"); - if (applId == null) throw new IllegalArgumentException("Parameter applId is empty"); - if (passTicket == null) throw new IllegalArgumentException("Parameter passTicket is empty"); + if (userId == null) + throw new IllegalArgumentException("Parameter userId is empty"); + if (applId == null) + throw new IllegalArgumentException("Parameter applId is empty"); + if (passTicket == null) + throw new IllegalArgumentException("Parameter passTicket is empty"); + + if (passTicket.equals(ZOWE_DUMMY_PASS_TICKET_PREFIX)) { + return; + } final Set passTickets = userAppToPasstickets.get(new UserApp(userId, applId)); + if ((passTickets == null) || !passTickets.contains(passTicket)) { throw new IRRPassTicketEvaluationException(8, 16, 32); } @@ -97,7 +98,7 @@ public String generate(String userId, String applId) throws IRRPassTicketGenerat final UserApp userApp = new UserApp(userId, applId); final int currentId; synchronized (DefaultPassTicketImpl.class) { - currentId = DefaultPassTicketImpl.id ++; + currentId = DefaultPassTicketImpl.id++; } final String passTicket = ZOWE_DUMMY_PASS_TICKET_PREFIX + "_" + applId + "_" + userId + "_" + currentId; From 1ef7fefc5a0cfce2be7fc10c37d4755229079355 Mon Sep 17 00:00:00 2001 From: JirkaAichler Date: Tue, 7 Jan 2020 17:34:42 +0100 Subject: [PATCH 033/122] Fix message when token is invalid + add integration test Signed-off-by: JirkaAichler --- .../service/AuthenticationService.java | 76 ++++++++----------- .../ServiceAuthenticationServiceImpl.java | 2 +- .../mfaas/gatewayservice/PassTicketTest.java | 27 ++++++- 3 files changed, 55 insertions(+), 50 deletions(-) diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java index 80a2b8d236..cec49a346e 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java @@ -47,7 +47,7 @@ @Slf4j @Service @RequiredArgsConstructor -@Scope( proxyMode = ScopedProxyMode.TARGET_CLASS ) +@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) @EnableAspectJAutoProxy(proxyTargetClass = true) public class AuthenticationService { private static final String LTPA_CLAIM_NAME = "ltpa"; @@ -94,9 +94,10 @@ public String createJwtToken(String username, String domain, String ltpaToken) { /** * Method will invalidate jwtToken. It could be called from two reasons: - * - on logout phase (distribute = true) - * - from another gateway instance to notify about change (distribute = false) - * @param jwtToken token to invalidated + * - on logout phase (distribute = true) + * - from another gateway instance to notify about change (distribute = false) + * + * @param jwtToken token to invalidated * @param distribute distribute invalidation to another instances? * @return state of invalidate (true - token was invalidated) */ @@ -130,28 +131,12 @@ public Boolean isInvalidated(String jwtToken) { @Cacheable(value = "validationJwtToken", key = "#jwtToken", condition = "#jwtToken != null") public TokenAuthentication validateJwtToken(String jwtToken) { - try { - Claims claims = Jwts.parser() - .setSigningKey(jwtSecurityInitializer.getJwtPublicKey()) - .parseClaimsJws(jwtToken) - .getBody(); - - TokenAuthentication validTokenAuthentication = new TokenAuthentication(claims.getSubject(), jwtToken); - // without a proxy cache aspect is not working, thus it is necessary get bean from application context - final boolean authenticated = !meAsProxy.isInvalidated(jwtToken); - validTokenAuthentication.setAuthenticated(authenticated); + TokenAuthentication validTokenAuthentication = new TokenAuthentication(getClaims(jwtToken).getSubject(), jwtToken); + // without a proxy cache aspect is not working, thus it is necessary get bean from application context + final boolean authenticated = !meAsProxy.isInvalidated(jwtToken); + validTokenAuthentication.setAuthenticated(authenticated); - return validTokenAuthentication; - } catch (ExpiredJwtException exception) { - log.debug("Token with id '{}' for user '{}' is expired.", exception.getClaims().getId(), exception.getClaims().getSubject()); - throw new TokenExpireException("Token is expired."); - } catch (JwtException exception) { - log.debug("Token is not valid due to: {}.", exception.getMessage()); - throw new TokenNotValidException("Token is not valid."); - } catch (Exception exception) { - log.debug("Token is not valid due to: {}.", exception.getMessage()); - throw new TokenNotValidException("An internal error occurred while validating the token therefor the token is no longer valid."); - } + return validTokenAuthentication; } /** @@ -169,14 +154,11 @@ public TokenAuthentication validateJwtToken(TokenAuthentication token) { /** * Parse the JWT token and return a {@link QueryResponse} object containing the domain, user id, date of creation and date of expiration * - * @param token the JWT token + * @param jwtToken the JWT token * @return the query response */ - public QueryResponse parseJwtToken(String token) { - Claims claims = Jwts.parser() - .setSigningKey(jwtSecurityInitializer.getJwtPublicKey()) - .parseClaimsJws(token) - .getBody(); + public QueryResponse parseJwtToken(String jwtToken) { + Claims claims = getClaims(jwtToken); return new QueryResponse( claims.get(DOMAIN_CLAIM_NAME, String.class), @@ -216,20 +198,7 @@ public Optional getJwtTokenFromRequest(HttpServletRequest request) { * @throws TokenNotValidException if the JWT token is not valid */ public String getLtpaTokenFromJwtToken(String jwtToken) { - try { - Claims claims = Jwts.parser() - .setSigningKey(jwtSecurityInitializer.getJwtPublicKey()) - .parseClaimsJws(jwtToken) - .getBody(); - - return claims.get(LTPA_CLAIM_NAME, String.class); - } catch (ExpiredJwtException exception) { - log.debug("Authentication: Token with id '{}' for user '{}' is expired", exception.getClaims().getId(), exception.getClaims().getSubject()); - throw new TokenExpireException("Token is expired"); - } catch (JwtException exception) { - log.debug("Authentication: Token is not valid due to: {}", exception.getMessage()); - throw new TokenNotValidException("Token is not valid"); - } + return getClaims(jwtToken).get(LTPA_CLAIM_NAME, String.class); } /** @@ -270,4 +239,21 @@ private long calculateExpiration(long now, String username) { return expiration; } + private Claims getClaims(String jwtToken) { + try { + return Jwts.parser() + .setSigningKey(jwtSecurityInitializer.getJwtPublicKey()) + .parseClaimsJws(jwtToken) + .getBody(); + } catch (ExpiredJwtException exception) { + log.debug("Token with id '{}' for user '{}' is expired.", exception.getClaims().getId(), exception.getClaims().getSubject()); + throw new TokenExpireException("Token is expired."); + } catch (JwtException exception) { + log.debug("Token is not valid due to: {}.", exception.getMessage()); + throw new TokenNotValidException("Token is not valid."); + } catch (Exception exception) { + log.debug("Token is not valid due to: {}.", exception.getMessage()); + throw new TokenNotValidException("An internal error occurred while validating the token therefor the token is no longer valid."); + } + } } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java index 59811d5001..adca4c2086 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java @@ -120,7 +120,7 @@ public AuthenticationCommand getAuthenticationCommand(String serviceId, String j } } - // if no instance exist, do nothing + // if no instance exist or no metadata found, do nothing if (found == null || found.isEmpty()) return AuthenticationCommand.EMPTY; return getAuthenticationCommand(found, jwtToken); diff --git a/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java index bd700cc0e2..12776e7135 100644 --- a/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java +++ b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java @@ -13,6 +13,8 @@ import static com.ca.mfaas.gatewayservice.SecurityUtils.gatewayToken; import static io.restassured.RestAssured.given; import static org.apache.http.HttpStatus.SC_OK; +import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; +import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.core.Is.is; import com.ca.mfaas.util.config.ConfigReader; @@ -24,9 +26,9 @@ public class PassTicketTest { private final static String SCHEME = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration() - .getScheme(); + .getScheme(); private final static String HOST = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration() - .getHost(); + .getHost(); private final static int PORT = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration().getPort(); private final static String STATICCLIENT_BASE_PATH = "/api/v1/staticclient"; private final static String PASSTICKET_TEST_ENDPOINT = "/passticketTest"; @@ -42,7 +44,24 @@ public void setUp() { public void accessServiceWithCorrectPassTicket() { String jwt = gatewayToken(); given().cookie(GATEWAY_TOKEN_COOKIE_NAME, jwt).when().get( - String.format("%s://%s:%d%s%s", SCHEME, HOST, PORT, STATICCLIENT_BASE_PATH, PASSTICKET_TEST_ENDPOINT)) - .then().statusCode(is(SC_OK)); + String.format("%s://%s:%d%s%s", SCHEME, HOST, PORT, STATICCLIENT_BASE_PATH, PASSTICKET_TEST_ENDPOINT)) + .then().statusCode(is(SC_OK)); } + + + @Test + //@formatter:off + public void accessServiceWithIncorrectToken() { + String jwt = "nonsense"; + String expectedMessage = "Token is not valid"; + + given() + .cookie(GATEWAY_TOKEN_COOKIE_NAME, jwt) + .when() + .get(String.format("%s://%s:%d%s%s", SCHEME, HOST, PORT, STATICCLIENT_BASE_PATH, PASSTICKET_TEST_ENDPOINT)) + .then() + .statusCode(is(SC_UNAUTHORIZED)) + .body("messages.find { it.messageNumber == 'ZWEAG102E' }.messageContent", equalTo(expectedMessage)); + } + //@formatter:on } From 350ff913c8238d6e2593184167365e2e4203dc15 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Wed, 8 Jan 2020 08:35:08 +0100 Subject: [PATCH 034/122] Use user.dir for EHCache since the /tmp is not good Signed-off-by: Petr Plavjanik --- gateway-service/src/main/resources/ehcache.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gateway-service/src/main/resources/ehcache.xml b/gateway-service/src/main/resources/ehcache.xml index 22907729fe..a4d4f0cb0f 100644 --- a/gateway-service/src/main/resources/ehcache.xml +++ b/gateway-service/src/main/resources/ehcache.xml @@ -1,6 +1,8 @@ + + From 8bd1c157798955cfa028c8852193ef9a7f02ca80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Wed, 8 Jan 2020 10:18:32 +0100 Subject: [PATCH 035/122] refactor cleaning cache by parameter into CacheUtils MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- common-service-core/build.gradle | 4 + .../java/com/ca/mfaas/util/CacheUtils.java | 80 ++++++++++++++++ .../com/ca/mfaas/util/CacheUtilsTest.java | 94 +++++++++++++++++++ .../ServiceAuthenticationServiceImpl.java | 23 +---- gradle/versions.gradle | 4 +- 5 files changed, 181 insertions(+), 24 deletions(-) create mode 100644 common-service-core/src/main/java/com/ca/mfaas/util/CacheUtils.java create mode 100644 common-service-core/src/test/java/com/ca/mfaas/util/CacheUtilsTest.java diff --git a/common-service-core/build.gradle b/common-service-core/build.gradle index 053c683e56..093b9eff22 100644 --- a/common-service-core/build.gradle +++ b/common-service-core/build.gradle @@ -10,8 +10,10 @@ dependencies { compileOnly(libraries.javax_servlet_api) compileOnly(libraries.lombok) compileOnly(libraries.spring_boot_starter_cache) + compileOnly(libraries.eh_cache) testCompile(libraries.spring_boot_starter_cache) + testCompile(libraries.eh_cache) testCompile(libraries.jackson_core) testCompile(libraries.jackson_databind) testCompile(libraries.javax_servlet_api) @@ -22,6 +24,8 @@ dependencies { testCompile(libraries.logback_classic) testCompile(libraries.tomcat_embed_core) testCompile(libraries.mockito_core) + testCompile(libraries.powermock_api_mockito2) + testCompile(libraries.power_mock_junit4) testCompileOnly(libraries.lombok) } diff --git a/common-service-core/src/main/java/com/ca/mfaas/util/CacheUtils.java b/common-service-core/src/main/java/com/ca/mfaas/util/CacheUtils.java new file mode 100644 index 0000000000..ce57a98fdb --- /dev/null +++ b/common-service-core/src/main/java/com/ca/mfaas/util/CacheUtils.java @@ -0,0 +1,80 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.util; + +import com.ca.mfaas.cache.CompositeKey; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; + +import java.util.function.Predicate; + +/** + * This utils offer base operation with cache, which can be shared to multiple codes. + */ +public final class CacheUtils { + + private CacheUtils() {} + + /** + * This method evict a part of cache by condition on key if method use composite keys. It cannot be used on cache + * with just one parameter. + * + * Method iterate over all keys in cache and evict just a part (if keyPredicate return true). It requires to use + * ehCache and {@link CompositeKey}. If there is no way how to filter the entries in the cache, it will remove all + * of them (different provider than EhCache, different type of keys). + * + * If entry is not stored as CompositeKey (ie. just one argumentm different keyGenerator) it will be removed allways, + * without any test. If cache contains other entries witch CompositeKey, they will be checked and removed only in + * case keyPredicate match. + * + * Example: + * + *
+     * @Cacheable(value = "", keyGenerator = CacheConfig.COMPOSITE_KEY_GENERATOR)
+     * public  cacheSomething(arg1, arg2, ...) {
+     *     // do something
+     *     return ;
+     * }
+     *
+     * @Autowired CacheManager cacheManager;
+     *
+     * public void evictByArg1(arg1) {
+     *     CacheUtils.evictSubset(cacheManager, "", x -> x.equals(0, arg1));
+     *     // alternatively CacheUtils.evictSubset(cacheManager, "", x -> ObjectUtils.equals(x.get(0), arg1));
+     * }
+     * 
+ * + * @param cacheManager manager collecting the cache + * @param cacheName name of cache + * @param keyPredicate condition to filter keys to evict + */ + public static void evictSubset(CacheManager cacheManager, String cacheName, Predicate keyPredicate) { + final Cache cache = cacheManager.getCache(cacheName); + if (cache == null) throw new IllegalArgumentException("Unknown cache " + cacheName); + final Object nativeCache = cache.getNativeCache(); + if (nativeCache instanceof net.sf.ehcache.Cache) { + final net.sf.ehcache.Cache ehCache = (net.sf.ehcache.Cache) nativeCache; + + for (final Object key : ehCache.getKeys()) { + if (key instanceof CompositeKey) { + // if entry is compositeKey and first param is different, skip it (be sure this is not to evict) + final CompositeKey compositeKey = ((CompositeKey) key); + if (!keyPredicate.test(compositeKey)) continue; + } + // if key is not composite key (unknown for evict) or has same serviceId, evict record + ehCache.remove(key); + } + } else { + // in case of using different cache manager, evict all records for sure + cache.clear(); + } + } + +} diff --git a/common-service-core/src/test/java/com/ca/mfaas/util/CacheUtilsTest.java b/common-service-core/src/test/java/com/ca/mfaas/util/CacheUtilsTest.java new file mode 100644 index 0000000000..5a81d7296c --- /dev/null +++ b/common-service-core/src/test/java/com/ca/mfaas/util/CacheUtilsTest.java @@ -0,0 +1,94 @@ +package com.ca.mfaas.util;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import com.ca.mfaas.cache.CompositeKey; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.stubbing.Answer; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +@RunWith(PowerMockRunner.class) +@PrepareForTest(net.sf.ehcache.Cache.class) +public class CacheUtilsTest { + + private int removeCounter; + + @Test + public void testEvictSubset() { + CacheManager cacheManager = mock(CacheManager.class); + + // cache1 is not ehCache + Cache cache1 = mock(Cache.class); + when(cacheManager.getCache("cache1")).thenReturn(cache1); + when(cache1.getNativeCache()).thenReturn(Object.class); + + Cache cache2 = mock(Cache.class); + when(cacheManager.getCache("cache2")).thenReturn(cache2); + net.sf.ehcache.Cache ehCache2 = PowerMockito.mock(net.sf.ehcache.Cache.class); + + when(cache2.getNativeCache()).thenReturn(ehCache2); + List keys = Arrays.asList( + "abc", // not composite key + new CompositeKey("test", 5), + new CompositeKey("next", 10), + new CompositeKey("next", 15) + ); + when(ehCache2.getKeys()).thenReturn(keys); + + try { + CacheUtils.evictSubset(cacheManager, "missing", x -> true); + fail(); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Unknown cache")); + assertTrue(e.getMessage().contains("missing")); + } + + // not EhCache - clean all, dont use keyPredicate + verify(cache1, never()).clear(); + CacheUtils.evictSubset(cacheManager, "cache1", x -> false); + verify(cache1, times(1)).clear(); + + final Answer answer = invocation -> { + removeCounter++; + return true; + }; + + doAnswer(answer).when(ehCache2).remove(any(Serializable.class)); + doAnswer(answer).when(ehCache2).remove((Object) any()); + + assertEquals(0, removeCounter); + // in all cases remove entries without CompositeKey + CacheUtils.evictSubset(cacheManager, "cache2", x -> false); + assertEquals(1, removeCounter); + verify(ehCache2, times(1)).remove(keys.get(0)); + + CacheUtils.evictSubset(cacheManager, "cache2", x -> x.equals(0, "test")); + assertEquals(3, removeCounter); + verify(ehCache2, times(2)).remove(keys.get(0)); + verify(ehCache2, times(1)).remove(keys.get(1)); + + CacheUtils.evictSubset(cacheManager, "cache2", x -> (Integer) x.get(1) > 10); + assertEquals(5, removeCounter); + verify(ehCache2, times(3)).remove(keys.get(0)); + verify(ehCache2, times(1)).remove(keys.get(3)); + } + +} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java index adca4c2086..3632050451 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java @@ -12,18 +12,17 @@ import com.ca.apiml.security.common.auth.Authentication; import com.ca.apiml.security.common.auth.AuthenticationScheme; import com.ca.apiml.security.common.token.QueryResponse; -import com.ca.mfaas.cache.CompositeKey; import com.ca.mfaas.gateway.config.CacheConfig; import com.ca.mfaas.gateway.security.service.schema.AbstractAuthenticationScheme; import com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand; import com.ca.mfaas.gateway.security.service.schema.AuthenticationSchemeFactory; import com.ca.mfaas.gateway.security.service.schema.ServiceAuthenticationService; +import com.ca.mfaas.util.CacheUtils; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.EurekaClient; import com.netflix.discovery.shared.Application; import com.netflix.zuul.context.RequestContext; import lombok.AllArgsConstructor; -import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; @@ -140,25 +139,7 @@ public void evictCacheAllService() { */ @Override public void evictCacheService(String serviceId) { - final Cache cache = cacheManager.getCache(CACHE_BY_SERVICE_ID); - if (cache == null) throw new IllegalArgumentException("Unknown cache " + CACHE_BY_SERVICE_ID); - final Object nativeCache = cache.getNativeCache(); - if (nativeCache instanceof net.sf.ehcache.Cache) { - final net.sf.ehcache.Cache ehCache = (net.sf.ehcache.Cache) nativeCache; - - for (final Object key : ehCache.getKeys()) { - if (key instanceof CompositeKey) { - // if entry is compositeKey and first param is different, skip it (be sure this is not to evict) - final CompositeKey compositeKey = ((CompositeKey) key); - if (!compositeKey.equals(0, serviceId)) continue; - } - // if key is not composite key (unknown for evict) or has same serviceId, evict record - ehCache.remove(key); - } - } else { - // in case of using different cache manager, evict all records for sure - cache.clear(); - } + CacheUtils.evictSubset(cacheManager, CACHE_BY_SERVICE_ID, x -> x.equals(0, serviceId)); } public class UniversalAuthenticationCommand extends AuthenticationCommand { diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 20729abe03..3c6b9be4e3 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -17,7 +17,7 @@ ext { jacksonVersion = '2.9.2' restAssuredVersion = '3.0.7' commonsValidatorVersion = "1.6" - powerMockVersion = "1.7.3" + powerMockVersion = "2.0.4" jacksonCoreVersion = "2.9.6" javaxValidationApiVersion = "2.0.1.Final" esmClientVersion = '2.0.0-SNAPSHOT' @@ -58,7 +58,6 @@ ext { spring_boot_starter_security : "org.springframework.boot:spring-boot-starter-security:${springBootVersion}", spring_boot_starter_web : "org.springframework.boot:spring-boot-starter-web:${springBootVersion}", spring_boot_starter_websocket : "org.springframework.boot:spring-boot-starter-websocket:${springBootVersion}", - spring_boot_starter_websocket : "org.springframework.boot:spring-boot-starter-websocket:${springBootVersion}", spring_boot_starter_test : "org.springframework.boot:spring-boot-starter-test:${springBootVersion}", spring_boot_starter_mobile : "org.springframework.boot:spring-boot-starter-mobile:${springBootVersion}", spring_boot_starter_web_services : "org.springframework.boot:spring-boot-starter-web-services:${springBootVersion}", @@ -109,7 +108,6 @@ ext { jackson_core : "com.fasterxml.jackson.core:jackson-core:${jacksonCoreVersion}", jackson_databind : "com.fasterxml.jackson.core:jackson-databind:${jacksonCoreVersion}", jackson_annotations : "com.fasterxml.jackson.core:jackson-annotations:${jacksonCoreVersion}", - jackson_dataformat_yaml : "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${jacksonCoreVersion}", esm_client : "dbm:esm-client:${esmClientVersion}", javax_validation : "javax.validation:validation-api:${javaxValidationApiVersion}", gson : "com.google.code.gson:gson:${gsonVersion}", From d0ac20cd60836b5c544abec30bedbb561671c952 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Wed, 8 Jan 2020 12:33:03 +0100 Subject: [PATCH 036/122] Ignore EHCache working files Signed-off-by: Petr Plavjanik --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index a3d460486b..ac9be5d9f4 100644 --- a/.gitignore +++ b/.gitignore @@ -87,3 +87,7 @@ api-catalog-ui/frontend/debug.log debug.log api-layer.tar.gz /*.log + +# EHCache +gateway-service/*.data +gateway-service/*.index From e346ce66ef6b13a463179be203729f41ea91589a Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Wed, 8 Jan 2020 14:45:50 +0100 Subject: [PATCH 037/122] Disable banner Signed-off-by: Petr Plavjanik --- .../com/ca/mfaas/apicatalog/ApiCatalogApplication.java | 2 ++ api-catalog-services/src/main/resources/banner.txt | 8 -------- .../mfaas/client/DiscoverableClientSampleApplication.java | 2 ++ discoverable-client/src/main/resources/banner.txt | 4 ---- .../ca/mfaas/discovery/DiscoveryServiceApplication.java | 2 ++ discovery-service/src/main/resources/banner.txt | 8 -------- .../java/com/ca/mfaas/gateway/GatewayApplication.java | 2 ++ gateway-service/src/main/resources/banner.txt | 8 -------- 8 files changed, 8 insertions(+), 28 deletions(-) delete mode 100644 api-catalog-services/src/main/resources/banner.txt delete mode 100644 discoverable-client/src/main/resources/banner.txt delete mode 100644 discovery-service/src/main/resources/banner.txt delete mode 100644 gateway-service/src/main/resources/banner.txt diff --git a/api-catalog-services/src/main/java/com/ca/mfaas/apicatalog/ApiCatalogApplication.java b/api-catalog-services/src/main/java/com/ca/mfaas/apicatalog/ApiCatalogApplication.java index 6068532802..8bceac00cc 100644 --- a/api-catalog-services/src/main/java/com/ca/mfaas/apicatalog/ApiCatalogApplication.java +++ b/api-catalog-services/src/main/java/com/ca/mfaas/apicatalog/ApiCatalogApplication.java @@ -13,6 +13,7 @@ import com.ca.mfaas.product.logging.annotations.EnableApimlLogger; import com.ca.mfaas.product.monitoring.LatencyUtilsConfigInitializer; import com.ca.mfaas.product.version.BuildInfo; +import org.springframework.boot.Banner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; @@ -42,6 +43,7 @@ public static void main(String[] args) { SpringApplication app = new SpringApplication(ApiCatalogApplication.class); app.addInitializers(new LatencyUtilsConfigInitializer()); app.setLogStartupInfo(false); + app.setBannerMode(Banner.Mode.OFF); new BuildInfo().logBuildInfo(); app.run(args); } diff --git a/api-catalog-services/src/main/resources/banner.txt b/api-catalog-services/src/main/resources/banner.txt deleted file mode 100644 index 78b285b48d..0000000000 --- a/api-catalog-services/src/main/resources/banner.txt +++ /dev/null @@ -1,8 +0,0 @@ - _____ _____ _____ _ _ - /\ | __ \_ _| / ____| | | | | - / \ | |__) || | | | __ _| |_ __ _| | ___ __ _ - / /\ \ | ___/ | | | | / _` | __/ _` | |/ _ \ / _` | - / ____ \| | _| |_ | |___| (_| | || (_| | | (_) | (_| | - /_/ \_\_| |_____| \_____\__,_|\__\__,_|_|\___/ \__, | - __/ | - |___/ diff --git a/discoverable-client/src/main/java/com/ca/mfaas/client/DiscoverableClientSampleApplication.java b/discoverable-client/src/main/java/com/ca/mfaas/client/DiscoverableClientSampleApplication.java index 57d8346976..4b4b450774 100644 --- a/discoverable-client/src/main/java/com/ca/mfaas/client/DiscoverableClientSampleApplication.java +++ b/discoverable-client/src/main/java/com/ca/mfaas/client/DiscoverableClientSampleApplication.java @@ -14,6 +14,7 @@ import com.ca.mfaas.product.monitoring.LatencyUtilsConfigInitializer; import com.ca.mfaas.product.service.ServiceStartupEventHandler; import com.ca.mfaas.product.version.BuildInfo; +import org.springframework.boot.Banner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.event.ApplicationReadyEvent; @@ -40,6 +41,7 @@ public static void main(String[] args) { SpringApplication app = new SpringApplication(DiscoverableClientSampleApplication.class); app.addInitializers(new LatencyUtilsConfigInitializer()); app.setLogStartupInfo(false); + app.setBannerMode(Banner.Mode.OFF); new BuildInfo().logBuildInfo(); app.run(args); } diff --git a/discoverable-client/src/main/resources/banner.txt b/discoverable-client/src/main/resources/banner.txt deleted file mode 100644 index ee1db1a1c7..0000000000 --- a/discoverable-client/src/main/resources/banner.txt +++ /dev/null @@ -1,4 +0,0 @@ - _____ __ __ __ ______ __ __ __ -| \ |__|.-----..----..-----..--.--..-----..----..---.-.| |--.| |.-----. | || ||__|.-----..-----.| |_ -| -- || ||__ --|| __|| _ || | || -__|| _|| _ || _ || || -__| | ---|| || || -__|| || _| -|_____/ |__||_____||____||_____| \___/ |_____||__| |___._||_____||__||_____| |______||__||__||_____||__|__||____| diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/DiscoveryServiceApplication.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/DiscoveryServiceApplication.java index e3f2f5a063..42b833ba0e 100644 --- a/discovery-service/src/main/java/com/ca/mfaas/discovery/DiscoveryServiceApplication.java +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/DiscoveryServiceApplication.java @@ -13,6 +13,7 @@ import com.ca.mfaas.product.monitoring.LatencyUtilsConfigInitializer; import com.ca.mfaas.product.service.ServiceStartupEventHandler; import com.ca.mfaas.product.version.BuildInfo; +import org.springframework.boot.Banner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.event.ApplicationReadyEvent; @@ -37,6 +38,7 @@ public static void main(String[] args) { SpringApplication app = new SpringApplication(DiscoveryServiceApplication.class); app.addInitializers(new LatencyUtilsConfigInitializer()); app.setLogStartupInfo(false); + app.setBannerMode(Banner.Mode.OFF); new BuildInfo().logBuildInfo(); app.run(args); } diff --git a/discovery-service/src/main/resources/banner.txt b/discovery-service/src/main/resources/banner.txt deleted file mode 100644 index fc7e90631e..0000000000 --- a/discovery-service/src/main/resources/banner.txt +++ /dev/null @@ -1,8 +0,0 @@ - _____ _ _____ _ - | __ \(_) / ____| (_) - | | | |_ ___ ___ _____ _____ _ __ _ _ | (___ ___ _ ____ ___ ___ ___ - | | | | / __|/ __/ _ \ \ / / _ \ '__| | | | \___ \ / _ \ '__\ \ / / |/ __/ _ \ - | |__| | \__ \ (_| (_) \ V / __/ | | |_| | ____) | __/ | \ V /| | (_| __/ - |_____/|_|___/\___\___/ \_/ \___|_| \__, | |_____/ \___|_| \_/ |_|\___\___| - __/ | - |___/ diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/GatewayApplication.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/GatewayApplication.java index a99a0014b3..d9c2b28f07 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/GatewayApplication.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/GatewayApplication.java @@ -14,6 +14,7 @@ import com.ca.mfaas.product.monitoring.LatencyUtilsConfigInitializer; import com.ca.mfaas.product.service.ServiceStartupEventHandler; import com.ca.mfaas.product.version.BuildInfo; +import org.springframework.boot.Banner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.event.ApplicationReadyEvent; @@ -50,6 +51,7 @@ public static void main(String[] args) { SpringApplication app = new SpringApplication(GatewayApplication.class); app.addInitializers(new LatencyUtilsConfigInitializer()); app.setLogStartupInfo(false); + app.setBannerMode(Banner.Mode.OFF); new BuildInfo().logBuildInfo(); app.run(args); } diff --git a/gateway-service/src/main/resources/banner.txt b/gateway-service/src/main/resources/banner.txt deleted file mode 100644 index 88f649124d..0000000000 --- a/gateway-service/src/main/resources/banner.txt +++ /dev/null @@ -1,8 +0,0 @@ - _____ _____ _____ _ - /\ | __ \_ _| / ____| | | - / \ | |__) || | | | __ __ _| |_ _____ ____ _ _ _ - / /\ \ | ___/ | | | | |_ |/ _` | __/ _ \ \ /\ / / _` | | | | - / ____ \| | _| |_ | |__| | (_| | || __/\ V V / (_| | |_| | - /_/ \_\_| |_____| \_____|\__,_|\__\___| \_/\_/ \__,_|\__, | - __/ | - |___/ From 1db1d359f415855731158a12bd61786b576dc9de Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Wed, 8 Jan 2020 14:46:14 +0100 Subject: [PATCH 038/122] Ignore EHCache lock Signed-off-by: Petr Plavjanik --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ac9be5d9f4..782a05eae0 100644 --- a/.gitignore +++ b/.gitignore @@ -91,3 +91,4 @@ api-layer.tar.gz # EHCache gateway-service/*.data gateway-service/*.index +.ehcache-diskstore.lock From b97dffac9e4c219ce8f4e938e9e401743bbccf5c Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Wed, 8 Jan 2020 14:48:56 +0100 Subject: [PATCH 039/122] Add PassTicket integration test with bad APPLID Signed-off-by: Petr Plavjanik --- .../common/service/PassTicketService.java | 5 +++-- .../common/service/PassTicketServiceTest.java | 20 ++++++++++--------- .../client/api/PassTicketTestController.java | 9 +++++++-- .../api/PassTicketTestControllerTest.java | 4 ++-- .../mfaas/gatewayservice/PassTicketTest.java | 19 ++++++++++++++---- package.json | 1 + 6 files changed, 39 insertions(+), 19 deletions(-) diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java index 6549ab7c3a..ad3de4d9f7 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java @@ -58,10 +58,11 @@ public static class DefaultPassTicketImpl implements IRRPassTicket { private static int id = 0; + public static final String ZOWE_DUMMY_USERID = "user"; public static final String ZOWE_DUMMY_PASS_TICKET_PREFIX = "ZoweDummyPassTicket"; public static final String UNKWNOWN_USER = "unknownUser"; - public static final String UNKWNOWN_APPLID = "unknownApplId"; + public static final String UNKWNOWN_APPLID = "XBADAPPL"; private Map> userAppToPasstickets = new HashMap<>(); @@ -74,7 +75,7 @@ public void evaluate(String userId, String applId, String passTicket) throws IRR if (passTicket == null) throw new IllegalArgumentException("Parameter passTicket is empty"); - if (passTicket.equals(ZOWE_DUMMY_PASS_TICKET_PREFIX)) { + if (userId.equals(ZOWE_DUMMY_USERID) && passTicket.startsWith(ZOWE_DUMMY_PASS_TICKET_PREFIX)) { return; } diff --git a/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java b/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java index 607dceb1f3..78b1a43c45 100644 --- a/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java +++ b/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java @@ -26,6 +26,8 @@ @ContextConfiguration public class PassTicketServiceTest { + private static final String TEST_USERID = "userId"; + @Autowired private PassTicketService passTicketService; @@ -71,13 +73,13 @@ public void testProxy() throws IRRPassTicketGenerationException { ); try { - irrPassTicket.evaluate("user", "applId", "passTicket"); + irrPassTicket.evaluate(TEST_USERID, "applId", "passTicket"); fail(); } catch (Exception e) { - assertEquals("Dummy implementation of evaluate : user x applId x passTicket", e.getMessage()); + assertEquals("Dummy implementation of evaluate : userId x applId x passTicket", e.getMessage()); } - assertEquals("success", irrPassTicket.generate("user", "applId")); + assertEquals("success", irrPassTicket.generate(TEST_USERID, "applId")); } @Test @@ -85,7 +87,7 @@ public void testDefaultPassTicketImpl() throws IRRPassTicketEvaluationException, PassTicketService.DefaultPassTicketImpl dpti = new PassTicketService.DefaultPassTicketImpl(); try { - dpti.evaluate("user", "applId", "passticket"); + dpti.evaluate(TEST_USERID, "applId", "passticket"); fail(); } catch (IRRPassTicketEvaluationException e) { assertEquals(8, e.getSafRc()); @@ -93,15 +95,15 @@ public void testDefaultPassTicketImpl() throws IRRPassTicketEvaluationException, assertEquals(32, e.getRacfRc()); } - String passTicket1 = dpti.generate("user", "applId"); - String passTicket2 = dpti.generate("user", "applId"); + String passTicket1 = dpti.generate(TEST_USERID, "applId"); + String passTicket2 = dpti.generate(TEST_USERID, "applId"); assertNotNull(passTicket1); assertNotNull(passTicket2); assertNotEquals(passTicket1, passTicket2); - dpti.evaluate("user", "applId", passTicket1); - dpti.evaluate("user", "applId", passTicket2); + dpti.evaluate(TEST_USERID, "applId", passTicket1); + dpti.evaluate(TEST_USERID, "applId", passTicket2); try { dpti.evaluate("userx", "applId", passTicket1); @@ -111,7 +113,7 @@ public void testDefaultPassTicketImpl() throws IRRPassTicketEvaluationException, } try { - dpti.evaluate("user", "applIdx", passTicket1); + dpti.evaluate(TEST_USERID, "applIdx", passTicket1); fail(); } catch (IRRPassTicketEvaluationException e) { // different applId, should throw exception diff --git a/discoverable-client/src/main/java/com/ca/mfaas/client/api/PassTicketTestController.java b/discoverable-client/src/main/java/com/ca/mfaas/client/api/PassTicketTestController.java index f2f0413572..3553d5c9bf 100644 --- a/discoverable-client/src/main/java/com/ca/mfaas/client/api/PassTicketTestController.java +++ b/discoverable-client/src/main/java/com/ca/mfaas/client/api/PassTicketTestController.java @@ -16,6 +16,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.nio.charset.StandardCharsets; @@ -29,7 +30,7 @@ public class PassTicketTestController { @Value("${apiml.service.applId:ZOWEAPPL}") - private String applId; + private String defaultApplId; private final PassTicketService passTicketService; @@ -42,7 +43,8 @@ public PassTicketTestController(PassTicketService passTicketService) { */ @GetMapping(value = "/api/v1/passticketTest") @ApiOperation(value = "Validate that the PassTicket in Authorization header is valid", tags = { "Test Operations" }) - public void passticketTest(@RequestHeader("authorization") String authorization) + public void passticketTest(@RequestHeader("authorization") String authorization, + @RequestParam(value = "applId", defaultValue = "", required = false) String applId) throws IRRPassTicketEvaluationException { if (authorization != null && authorization.toLowerCase().startsWith("basic")) { String base64Credentials = authorization.substring("Basic".length()).trim(); @@ -50,6 +52,9 @@ public void passticketTest(@RequestHeader("authorization") String authorization) String[] values = credentials.split(":", 2); String userId = values[0]; String passTicket = values[1]; + if (applId.isEmpty()) { + applId = defaultApplId; + } passTicketService.evaluate(userId, applId, passTicket); } else { throw new IllegalArgumentException("Missing Basic authorization header"); diff --git a/discoverable-client/src/test/java/com/ca/mfaas/client/api/PassTicketTestControllerTest.java b/discoverable-client/src/test/java/com/ca/mfaas/client/api/PassTicketTestControllerTest.java index 6b1ded0153..dc3cb3b914 100644 --- a/discoverable-client/src/test/java/com/ca/mfaas/client/api/PassTicketTestControllerTest.java +++ b/discoverable-client/src/test/java/com/ca/mfaas/client/api/PassTicketTestControllerTest.java @@ -30,10 +30,10 @@ public class PassTicketTestControllerTest { private MockMvc mockMvc; private static final String ZOWE_PASSTICKET_AUTH_HEADER = "Basic " - + Base64.getEncoder().encodeToString(("USER:ZoweDummyPassTicket").getBytes()); + + Base64.getEncoder().encodeToString(("user:ZoweDummyPassTicket").getBytes()); private static final String BAD_PASSTICKET_AUTH_HEADER = "Basic " - + Base64.getEncoder().encodeToString(("USER:bad").getBytes()); + + Base64.getEncoder().encodeToString(("user:bad").getBytes()); @Test public void callToPassTicketTestEndpointWithCorrectTicket() throws Exception { diff --git a/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java index 12776e7135..5f7b6a18c6 100644 --- a/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java +++ b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java @@ -14,7 +14,9 @@ import static io.restassured.RestAssured.given; import static org.apache.http.HttpStatus.SC_OK; import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; +import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR; import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.core.Is.is; import com.ca.mfaas.util.config.ConfigReader; @@ -26,9 +28,9 @@ public class PassTicketTest { private final static String SCHEME = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration() - .getScheme(); + .getScheme(); private final static String HOST = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration() - .getHost(); + .getHost(); private final static int PORT = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration().getPort(); private final static String STATICCLIENT_BASE_PATH = "/api/v1/staticclient"; private final static String PASSTICKET_TEST_ENDPOINT = "/passticketTest"; @@ -44,10 +46,19 @@ public void setUp() { public void accessServiceWithCorrectPassTicket() { String jwt = gatewayToken(); given().cookie(GATEWAY_TOKEN_COOKIE_NAME, jwt).when().get( - String.format("%s://%s:%d%s%s", SCHEME, HOST, PORT, STATICCLIENT_BASE_PATH, PASSTICKET_TEST_ENDPOINT)) - .then().statusCode(is(SC_OK)); + String.format("%s://%s:%d%s%s", SCHEME, HOST, PORT, STATICCLIENT_BASE_PATH, PASSTICKET_TEST_ENDPOINT)) + .then().statusCode(is(SC_OK)); } + @Test + public void accessServiceWithIncorrectApplId() { + String jwt = gatewayToken(); + given().cookie(GATEWAY_TOKEN_COOKIE_NAME, jwt).when() + .get(String.format("%s://%s:%d%s%s?applId=XBADAPPL", SCHEME, HOST, PORT, STATICCLIENT_BASE_PATH, + PASSTICKET_TEST_ENDPOINT)) + .then().statusCode(is(SC_INTERNAL_SERVER_ERROR)) + .body("message", containsString("No PTKTDATA profile exists to match the specified application")); + } @Test //@formatter:off diff --git a/package.json b/package.json index 974af2a3d9..c2088c2bda 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "api-catalog-service": "java -jar api-catalog-services/build/libs/api-catalog-services.jar --spring.config.additional-location=file:./config/local/api-catalog-service.yml --apiml.security.ssl.verifySslCertificatesOfServices=true", "api-catalog-service-debug": "java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=5014,suspend=n -jar api-catalog-services/build/libs/api-catalog-services.jar --spring.config.additional-location=file:./config/local/api-catalog-service.yml", "discoverable-client": "java -jar discoverable-client/build/libs/discoverable-client.jar --spring.config.additional-location=file:./config/local/discoverable-client.yml", + "discoverable-client-debug": "java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=5012,suspend=n -jar discoverable-client/build/libs/discoverable-client.jar --spring.config.additional-location=file:./config/local/discoverable-client.yml", "integration-enabler-spring-v1-sample-app": "java -jar integration-enabler-spring-v1-sample-app/build/libs/enabler-springboot-1.5.9.RELEASE-sample.jar --spring.config.location=classpath:/,file:./config/local/integration-enabler-spring-v1-sample-app.yml", "api-layer-multi": "concurrently --names \"GS1,DS1,DS2,AC1,DC1\" -c cyan,yellow,yellow,white,blue npm:gateway-service-1 npm:discovery-service-1 npm:discovery-service-2 npm:api-catalog-service-1 npm:discoverable-client-1", "gateway-service-1": "java -jar gateway-service/build/libs/gateway-service.jar --spring.config.additional-location=file:./config/local-multi/gateway-service.yml", From a3b2d5b64b0cb7738a07a9e993189811c38e412e Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Wed, 8 Jan 2020 14:55:52 +0100 Subject: [PATCH 040/122] Revert "Disable banner" This reverts commit e346ce66ef6b13a463179be203729f41ea91589a. --- .../com/ca/mfaas/apicatalog/ApiCatalogApplication.java | 2 -- api-catalog-services/src/main/resources/banner.txt | 8 ++++++++ .../mfaas/client/DiscoverableClientSampleApplication.java | 2 -- discoverable-client/src/main/resources/banner.txt | 4 ++++ .../ca/mfaas/discovery/DiscoveryServiceApplication.java | 2 -- discovery-service/src/main/resources/banner.txt | 8 ++++++++ .../java/com/ca/mfaas/gateway/GatewayApplication.java | 2 -- gateway-service/src/main/resources/banner.txt | 8 ++++++++ 8 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 api-catalog-services/src/main/resources/banner.txt create mode 100644 discoverable-client/src/main/resources/banner.txt create mode 100644 discovery-service/src/main/resources/banner.txt create mode 100644 gateway-service/src/main/resources/banner.txt diff --git a/api-catalog-services/src/main/java/com/ca/mfaas/apicatalog/ApiCatalogApplication.java b/api-catalog-services/src/main/java/com/ca/mfaas/apicatalog/ApiCatalogApplication.java index 8bceac00cc..6068532802 100644 --- a/api-catalog-services/src/main/java/com/ca/mfaas/apicatalog/ApiCatalogApplication.java +++ b/api-catalog-services/src/main/java/com/ca/mfaas/apicatalog/ApiCatalogApplication.java @@ -13,7 +13,6 @@ import com.ca.mfaas.product.logging.annotations.EnableApimlLogger; import com.ca.mfaas.product.monitoring.LatencyUtilsConfigInitializer; import com.ca.mfaas.product.version.BuildInfo; -import org.springframework.boot.Banner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; @@ -43,7 +42,6 @@ public static void main(String[] args) { SpringApplication app = new SpringApplication(ApiCatalogApplication.class); app.addInitializers(new LatencyUtilsConfigInitializer()); app.setLogStartupInfo(false); - app.setBannerMode(Banner.Mode.OFF); new BuildInfo().logBuildInfo(); app.run(args); } diff --git a/api-catalog-services/src/main/resources/banner.txt b/api-catalog-services/src/main/resources/banner.txt new file mode 100644 index 0000000000..78b285b48d --- /dev/null +++ b/api-catalog-services/src/main/resources/banner.txt @@ -0,0 +1,8 @@ + _____ _____ _____ _ _ + /\ | __ \_ _| / ____| | | | | + / \ | |__) || | | | __ _| |_ __ _| | ___ __ _ + / /\ \ | ___/ | | | | / _` | __/ _` | |/ _ \ / _` | + / ____ \| | _| |_ | |___| (_| | || (_| | | (_) | (_| | + /_/ \_\_| |_____| \_____\__,_|\__\__,_|_|\___/ \__, | + __/ | + |___/ diff --git a/discoverable-client/src/main/java/com/ca/mfaas/client/DiscoverableClientSampleApplication.java b/discoverable-client/src/main/java/com/ca/mfaas/client/DiscoverableClientSampleApplication.java index 4b4b450774..57d8346976 100644 --- a/discoverable-client/src/main/java/com/ca/mfaas/client/DiscoverableClientSampleApplication.java +++ b/discoverable-client/src/main/java/com/ca/mfaas/client/DiscoverableClientSampleApplication.java @@ -14,7 +14,6 @@ import com.ca.mfaas.product.monitoring.LatencyUtilsConfigInitializer; import com.ca.mfaas.product.service.ServiceStartupEventHandler; import com.ca.mfaas.product.version.BuildInfo; -import org.springframework.boot.Banner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.event.ApplicationReadyEvent; @@ -41,7 +40,6 @@ public static void main(String[] args) { SpringApplication app = new SpringApplication(DiscoverableClientSampleApplication.class); app.addInitializers(new LatencyUtilsConfigInitializer()); app.setLogStartupInfo(false); - app.setBannerMode(Banner.Mode.OFF); new BuildInfo().logBuildInfo(); app.run(args); } diff --git a/discoverable-client/src/main/resources/banner.txt b/discoverable-client/src/main/resources/banner.txt new file mode 100644 index 0000000000..ee1db1a1c7 --- /dev/null +++ b/discoverable-client/src/main/resources/banner.txt @@ -0,0 +1,4 @@ + _____ __ __ __ ______ __ __ __ +| \ |__|.-----..----..-----..--.--..-----..----..---.-.| |--.| |.-----. | || ||__|.-----..-----.| |_ +| -- || ||__ --|| __|| _ || | || -__|| _|| _ || _ || || -__| | ---|| || || -__|| || _| +|_____/ |__||_____||____||_____| \___/ |_____||__| |___._||_____||__||_____| |______||__||__||_____||__|__||____| diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/DiscoveryServiceApplication.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/DiscoveryServiceApplication.java index 42b833ba0e..e3f2f5a063 100644 --- a/discovery-service/src/main/java/com/ca/mfaas/discovery/DiscoveryServiceApplication.java +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/DiscoveryServiceApplication.java @@ -13,7 +13,6 @@ import com.ca.mfaas.product.monitoring.LatencyUtilsConfigInitializer; import com.ca.mfaas.product.service.ServiceStartupEventHandler; import com.ca.mfaas.product.version.BuildInfo; -import org.springframework.boot.Banner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.event.ApplicationReadyEvent; @@ -38,7 +37,6 @@ public static void main(String[] args) { SpringApplication app = new SpringApplication(DiscoveryServiceApplication.class); app.addInitializers(new LatencyUtilsConfigInitializer()); app.setLogStartupInfo(false); - app.setBannerMode(Banner.Mode.OFF); new BuildInfo().logBuildInfo(); app.run(args); } diff --git a/discovery-service/src/main/resources/banner.txt b/discovery-service/src/main/resources/banner.txt new file mode 100644 index 0000000000..fc7e90631e --- /dev/null +++ b/discovery-service/src/main/resources/banner.txt @@ -0,0 +1,8 @@ + _____ _ _____ _ + | __ \(_) / ____| (_) + | | | |_ ___ ___ _____ _____ _ __ _ _ | (___ ___ _ ____ ___ ___ ___ + | | | | / __|/ __/ _ \ \ / / _ \ '__| | | | \___ \ / _ \ '__\ \ / / |/ __/ _ \ + | |__| | \__ \ (_| (_) \ V / __/ | | |_| | ____) | __/ | \ V /| | (_| __/ + |_____/|_|___/\___\___/ \_/ \___|_| \__, | |_____/ \___|_| \_/ |_|\___\___| + __/ | + |___/ diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/GatewayApplication.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/GatewayApplication.java index d9c2b28f07..a99a0014b3 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/GatewayApplication.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/GatewayApplication.java @@ -14,7 +14,6 @@ import com.ca.mfaas.product.monitoring.LatencyUtilsConfigInitializer; import com.ca.mfaas.product.service.ServiceStartupEventHandler; import com.ca.mfaas.product.version.BuildInfo; -import org.springframework.boot.Banner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.event.ApplicationReadyEvent; @@ -51,7 +50,6 @@ public static void main(String[] args) { SpringApplication app = new SpringApplication(GatewayApplication.class); app.addInitializers(new LatencyUtilsConfigInitializer()); app.setLogStartupInfo(false); - app.setBannerMode(Banner.Mode.OFF); new BuildInfo().logBuildInfo(); app.run(args); } diff --git a/gateway-service/src/main/resources/banner.txt b/gateway-service/src/main/resources/banner.txt new file mode 100644 index 0000000000..88f649124d --- /dev/null +++ b/gateway-service/src/main/resources/banner.txt @@ -0,0 +1,8 @@ + _____ _____ _____ _ + /\ | __ \_ _| / ____| | | + / \ | |__) || | | | __ __ _| |_ _____ ____ _ _ _ + / /\ \ | ___/ | | | | |_ |/ _` | __/ _ \ \ /\ / / _` | | | | + / ____ \| | _| |_ | |__| | (_| | || __/\ V V / (_| | |_| | + /_/ \_\_| |_____| \_____|\__,_|\__\___| \_/\_/ \__,_|\__, | + __/ | + |___/ From 266c963fb987db8a97ce63c2eb7b003935a54331 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Wed, 8 Jan 2020 15:22:57 +0100 Subject: [PATCH 041/122] Dummy implementation for invalid applid in evaluation Signed-off-by: Petr Plavjanik --- .../ca/apiml/security/common/service/PassTicketService.java | 4 ++++ .../test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java | 2 +- invalidated%004awt%0054okens.data | 0 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 invalidated%004awt%0054okens.data diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java index ad3de4d9f7..d4fc45cfe9 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java @@ -75,6 +75,10 @@ public void evaluate(String userId, String applId, String passTicket) throws IRR if (passTicket == null) throw new IllegalArgumentException("Parameter passTicket is empty"); + if (StringUtils.equalsIgnoreCase(UNKWNOWN_APPLID, applId)) { + throw new IRRPassTicketEvaluationException(8, 16, 28); + } + if (userId.equals(ZOWE_DUMMY_USERID) && passTicket.startsWith(ZOWE_DUMMY_PASS_TICKET_PREFIX)) { return; } diff --git a/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java index 5f7b6a18c6..74b9d1b7f0 100644 --- a/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java +++ b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java @@ -57,7 +57,7 @@ public void accessServiceWithIncorrectApplId() { .get(String.format("%s://%s:%d%s%s?applId=XBADAPPL", SCHEME, HOST, PORT, STATICCLIENT_BASE_PATH, PASSTICKET_TEST_ENDPOINT)) .then().statusCode(is(SC_INTERNAL_SERVER_ERROR)) - .body("message", containsString("No PTKTDATA profile exists to match the specified application")); + .body("message", containsString("Unable to generate PassTicket")); } @Test diff --git a/invalidated%004awt%0054okens.data b/invalidated%004awt%0054okens.data new file mode 100644 index 0000000000..e69de29bb2 From b10dd8c4dd7fdce78050420a856bbf911202f3dc Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Wed, 8 Jan 2020 15:30:43 +0100 Subject: [PATCH 042/122] Fix the error text that is expected Signed-off-by: Petr Plavjanik --- .../test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java index 74b9d1b7f0..55e333706d 100644 --- a/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java +++ b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java @@ -57,7 +57,7 @@ public void accessServiceWithIncorrectApplId() { .get(String.format("%s://%s:%d%s%s?applId=XBADAPPL", SCHEME, HOST, PORT, STATICCLIENT_BASE_PATH, PASSTICKET_TEST_ENDPOINT)) .then().statusCode(is(SC_INTERNAL_SERVER_ERROR)) - .body("message", containsString("Unable to generate PassTicket")); + .body("message", containsString("Error on evaluation of PassTicket")); } @Test From 57f015dd4e3a30b13cd0cabf9581338dda7ce767 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Thu, 9 Jan 2020 08:58:45 +0100 Subject: [PATCH 043/122] Make HttpBasicPassTicketSchemeTest more stable Signed-off-by: Petr Plavjanik --- .../security/service/schema/HttpBasicPassTicketSchemeTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java index 9586677ce3..ca91c5cf29 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java @@ -82,7 +82,7 @@ public void testCreateCommand() throws Exception { calendar = Calendar.getInstance(); calendar.add(Calendar.SECOND, PASSTICKET_DURATION); // checking setup of expired time, JWT expired in future (more than hour), check if set date is similar to passticket timeout (5s) - assertTrue(Math.abs(calendar.getTime().getTime() - (long) ReflectionTestUtils.getField(ac, "expireAt")) < 5); + assertEquals(Math.abs(calendar.getTime().getTime() - (long) ReflectionTestUtils.getField(ac, "expireAt")), 0.0, 10.0); } } From d989da5b7d9d2189722bfef768459bf3b0b8729b Mon Sep 17 00:00:00 2001 From: JirkaAichler Date: Thu, 9 Jan 2020 10:19:28 +0100 Subject: [PATCH 044/122] Fix OpenApi 3 parser Update Gateway swagger to contain /ticket endpoint Signed-off-by: JirkaAichler --- api-catalog-services/build.gradle | 2 + .../swagger/api/ApiDocV3Service.java | 21 ++-- .../resources/apicatalog-log-messages.yml | 4 +- .../swagger/api/ApiDocV3ServiceTest.java | 21 +++- .../src/main/resources/gateway-api-doc.json | 110 ++++++++++++++++-- gradle/versions.gradle | 4 +- 6 files changed, 137 insertions(+), 25 deletions(-) diff --git a/api-catalog-services/build.gradle b/api-catalog-services/build.gradle index 3e35ba4b74..94e7cea050 100644 --- a/api-catalog-services/build.gradle +++ b/api-catalog-services/build.gradle @@ -44,6 +44,8 @@ dependencies { compile libraries.spring_retry compile libraries.swagger_core compile libraries.swagger3_core + compile libraries.swagger3_parser + compile libraries.jackson_core compile libraries.json_path compile libraries.apache_commons_lang3 compile libraries.spring_boot_starter_thymeleaf diff --git a/api-catalog-services/src/main/java/com/ca/mfaas/apicatalog/swagger/api/ApiDocV3Service.java b/api-catalog-services/src/main/java/com/ca/mfaas/apicatalog/swagger/api/ApiDocV3Service.java index 40ac7a339a..3d08ff466f 100644 --- a/api-catalog-services/src/main/java/com/ca/mfaas/apicatalog/swagger/api/ApiDocV3Service.java +++ b/api-catalog-services/src/main/java/com/ca/mfaas/apicatalog/swagger/api/ApiDocV3Service.java @@ -22,10 +22,11 @@ import io.swagger.v3.oas.models.Paths; import io.swagger.v3.oas.models.servers.Server; import io.swagger.v3.oas.models.tags.Tag; +import io.swagger.v3.parser.OpenAPIV3Parser; +import io.swagger.v3.parser.core.models.SwaggerParseResult; import lombok.extern.slf4j.Slf4j; import javax.validation.UnexpectedTypeException; -import java.io.IOException; import java.net.URI; import java.util.*; @@ -38,13 +39,17 @@ public ApiDocV3Service(GatewayClient gatewayClient) { } public String transformApiDoc(String serviceId, ApiDocInfo apiDocInfo) { - OpenAPI openAPI; + SwaggerParseResult parseResult = new OpenAPIV3Parser().readContents(apiDocInfo.getApiDocContent()); + OpenAPI openAPI = parseResult.getOpenAPI(); - try { - openAPI = Json.mapper().readValue(apiDocInfo.getApiDocContent(), OpenAPI.class); - } catch (IOException e) { - log.debug("Could not convert response body to an OpenAPI object. {}",serviceId, e); - throw new UnexpectedTypeException("Response is not an OpenAPI type object."); + if (openAPI == null) { + log.debug("Could not convert response body to an OpenAPI object for service {}. {}", serviceId, parseResult.getMessages()); + + if (parseResult.getMessages() == null) { + throw new UnexpectedTypeException("Response is not an OpenAPI type object."); + } else { + throw new UnexpectedTypeException(parseResult.getMessages().toString()); + } } boolean hidden = isHidden(openAPI.getTags()); @@ -112,7 +117,7 @@ protected void updatePaths(OpenAPI openAPI, String serviceId, ApiDocInfo apiDocI private Server getBestMatchingServer(List servers, ApiDocInfo apiDocInfo) { if (servers != null && !servers.isEmpty()) { - for (Server server: servers) { + for (Server server : servers) { String basePath = getBasePath(server.getUrl()); RoutedService route = getRoutedServiceByApiInfo(apiDocInfo, basePath); if (route != null) { diff --git a/api-catalog-services/src/main/resources/apicatalog-log-messages.yml b/api-catalog-services/src/main/resources/apicatalog-log-messages.yml index 80f646d41a..9955759c30 100644 --- a/api-catalog-services/src/main/resources/apicatalog-log-messages.yml +++ b/api-catalog-services/src/main/resources/apicatalog-log-messages.yml @@ -80,8 +80,8 @@ messages: - key: apiml.apicatalog.apidocRetrievalProblem number: ZWEAC704 type: ERROR - text: "ApiDoc retrieval problem for service %s. %s" - reason: "ApiDoc for service could not be retrieved from cache." + text: "ApiDoc retrieval problem for '%s' service. %s" + reason: "ApiDoc for service could not be retrieved." action: "Verify that the service provides a valid ApiDoc." - key: apiml.apicatalog.homePageTransformFailed diff --git a/api-catalog-services/src/test/java/com/ca/mfaas/apicatalog/swagger/api/ApiDocV3ServiceTest.java b/api-catalog-services/src/test/java/com/ca/mfaas/apicatalog/swagger/api/ApiDocV3ServiceTest.java index a86cb4528d..eaf6754fcc 100644 --- a/api-catalog-services/src/test/java/com/ca/mfaas/apicatalog/swagger/api/ApiDocV3ServiceTest.java +++ b/api-catalog-services/src/test/java/com/ca/mfaas/apicatalog/swagger/api/ApiDocV3ServiceTest.java @@ -34,6 +34,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; + import static org.hamcrest.Matchers.is; import static org.junit.Assert.*; @@ -108,13 +109,24 @@ public void givenOpenApiValidJson_whenApiDocTransform_thenCheckUpdatedValues() { } @Test - public void givenInvalidJson_whenApiDocTransform_thenThrowExeption() throws IOException { + public void givenEmptyJson_whenApiDocTransform_thenThrowException() { String invalidJson = ""; ApiInfo apiInfo = new ApiInfo("org.zowe.apicatalog", "api/v1", "3.0.0", "https://localhost:10014/apicatalog/api-doc", "https://www.zowe.org"); ApiDocInfo apiDocInfo = new ApiDocInfo(apiInfo, invalidJson, null); exceptionRule.expect(UnexpectedTypeException.class); - exceptionRule.expectMessage("Response is not an OpenAPI type object."); + exceptionRule.expectMessage("No swagger supplied"); + apiDocV3Service.transformApiDoc(SERVICE_ID, apiDocInfo); + } + + @Test + public void givenInvalidJson_whenApiDocTransform_thenThrowException() { + String invalidJson = "nonsense"; + ApiInfo apiInfo = new ApiInfo("org.zowe.apicatalog", "api/v1", "3.0.0", "https://localhost:10014/apicatalog/api-doc", "https://www.zowe.org"); + ApiDocInfo apiDocInfo = new ApiDocInfo(apiInfo, invalidJson, null); + + exceptionRule.expect(UnexpectedTypeException.class); + exceptionRule.expectMessage("attribute openapi is not of type `object`"); apiDocV3Service.transformApiDoc(SERVICE_ID, apiDocInfo); } @@ -162,9 +174,8 @@ private OpenAPI getDummyOpenApiObject(List servers, boolean apimlHidden) tag.setName(HIDDEN_TAG); openAPI.getTags().add(tag); } - - openAPI.getPaths().put("/api1", new PathItem()); - openAPI.getPaths().put("/api2", new PathItem()); + openAPI.getPaths().put("/api1", new PathItem().$ref("test")); + openAPI.getPaths().put("/api2", new PathItem().$ref("test")); return openAPI; } diff --git a/gateway-service/src/main/resources/gateway-api-doc.json b/gateway-service/src/main/resources/gateway-api-doc.json index 888df42ae6..0d7e0b9f96 100644 --- a/gateway-service/src/main/resources/gateway-api-doc.json +++ b/gateway-service/src/main/resources/gateway-api-doc.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "description": "REST API for the API Gateway service, which is a component of the API\nMediation Layer. Use this API to perform tasks such as logging in with the\nmainframe credentials and checking authorization to mainframe resources.", - "version": "1.1.2", + "version": "1.8.0", "title": "API Gateway" }, "tags": [ @@ -18,7 +18,7 @@ "Security" ], "summary": "Authenticate mainframe user credentials and return authentication token.", - "description": "Use the `/login` API to authenticate mainframe user credentials and return authentication token.\n\nThe login request requires the user credentials in one of the following formats: * Basic access authentication\n * JSON body, which provides an object with the user credentials\n\n \nThe response is an empty body and a token in a secure HttpOnly cookie named `apimlAuthenticationToken`.\n", + "description": "Use the `/login` API to authenticate mainframe user credentials and return authentication token.\n\n**Request:**\n\nThe login request requires the user credentials in one of the following formats:\n * Basic access authentication\n * JSON body, which provides an object with the user credentials\n\n**Response:**\n\nThe response is an empty body and a token in a secure HttpOnly cookie named `apimlAuthenticationToken`.\n", "operationId": "loginUsingPOST", "requestBody": { "content": { @@ -68,7 +68,7 @@ "Security" ], "summary": "Validate the authentication token.", - "description": "Use the `/query` API to validate the token and retrieve the information associated with the token.\n\nThe query request requires the token in one of the following formats: * Cookie named `apimlAuthenticationToken`.\n * Bearer authentication\n **Header example:** Authorization: Bearer *token*\n\n \nThe response is a JSON object, which contains information associated with the token.\n", + "description": "Use the `/query` API to validate the token and retrieve the information associated with the token.\n\n **HTTP Headers:**\n\nThe query request requires the token in one of the following formats:\n * Cookie named `apimlAuthenticationToken`.\n * Bearer authenticatio\n \n*Header example:* Authorization: Bearer *token*\n\n**Request payload:**\n\nThe request body is empty.\n\n**Response Payload:**\n\nThe response is a JSON object, which contains information associated with the token.\n", "operationId": "validateUsingGET", "security": [ { @@ -100,6 +100,55 @@ } } } + }, + "/ticket": { + "post": { + "tags": [ + "Security" + ], + "summary": "Generate a passticket for the user associated with a token.", + "description": "Use the `/ticket` API to request a passticket for the user associated with a token.\n\n**HTTP Headers:**\n\nThe ticket request requires the token in one of the following formats: \n * Cookie named `apimlAuthenticationToken`.\n * Bearer authentication\n \n*Header example:* Authorization: Bearer *token*\n\n**Request payload:**\n\nThe request takes one parameter, the name of the application for which the passticket should be generated. This parameter must be supplied.\n\n**Response Payload:**\n\nThe response is a JSON object, which contains information associated with the ticket.\n", + "operationId": "GenerateTicketUsingPOST", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TicketRequest" + } + } + }, + "description": "Specifies the name of the application for which the passticket should be generated." + }, + "security": [ + { + "CookieAuth": [] + }, + { + "Bearer": [] + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TicketResponse" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Not Found" + }, + "405": { + "description": "Method Not Allowed" + } + } + } } }, "servers": [ @@ -110,18 +159,18 @@ "components": { "securitySchemes": { "LoginBasicAuth": { - "type": "HTTP", - "scheme": "BASIC" + "type": "http", + "scheme": "basic" }, "Bearer": { - "type": "APIKEY", + "type": "apiKey", "name": "Authorization", - "in": "HEADER" + "in": "header" }, "CookieAuth": { - "type": "APIKEY", + "type": "apiKey", "name": "apimlAuthenticationToken", - "in": "COOKIE" + "in": "cookie" } }, "schemas": { @@ -174,6 +223,49 @@ "creation": "2019-05-13T12:47:04.000+0000", "expiration": "2019-05-14T12:47:04.000+0000" } + }, + "TicketRequest": { + "type": "object", + "title": "Application name", + "properties": { + "applicationName": { + "type": "string" + } + }, + "required": [ + "applicationName" + ], + "example": { + "applicationName": "MYAPP" + } + }, + "TicketResponse": { + "type": "object", + "title": "PassTicket", + "properties": { + "token": { + "type": "string", + "description": "Specifies the token that was supplied in the header." + }, + "userId": { + "type": "string", + "description": "Specifies the user associated with the token." + }, + "applicationName": { + "type": "string", + "description": "Specifies the application name associated with the passticket. Note that the Gateway user must be authorized to generate passtickets for this application name." + }, + "ticket": { + "type": "string", + "description": "Specifies a passticket for the pair, userId and applicationName." + } + }, + "example": { + "token": "eyJhbGciOiJSUzI1N", + "userId": "John", + "applicationName": "MYAPP", + "ticket": "LZTKEEDQ" + } } } } diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 3c6b9be4e3..f724292c5a 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -18,7 +18,7 @@ ext { restAssuredVersion = '3.0.7' commonsValidatorVersion = "1.6" powerMockVersion = "2.0.4" - jacksonCoreVersion = "2.9.6" + jacksonCoreVersion = "2.10.1" javaxValidationApiVersion = "2.0.1.Final" esmClientVersion = '2.0.0-SNAPSHOT' gsonVersion = '2.8.2' @@ -44,6 +44,7 @@ ext { springFoxVersion = '2.9.2' swaggerCoreVersion = "1.5.21" swagger3CoreVersion = "2.0.0" + swagger3ParserVersion = "2.0.17" swaggerJerseyJaxrsVersion = '1.5.10' jacksonDataformatYamlVersion = '2.9.7' jerseyVersion = '2.26' @@ -89,6 +90,7 @@ ext { springFoxWeb : "io.springfox:springfox-spring-web:${springFoxVersion}", swagger_core : "io.swagger:swagger-core:${swaggerCoreVersion}", swagger3_core : "io.swagger.core.v3:swagger-core:${swagger3CoreVersion}", + swagger3_parser : "io.swagger.parser.v3:swagger-parser-v3:${swagger3ParserVersion}", swagger_jersey2_jaxrs : "io.swagger:swagger-jersey2-jaxrs:${swaggerJerseyJaxrsVersion}", http_client : "org.apache.httpcomponents:httpclient:${httpClientVersion}", http_core : "org.apache.httpcomponents:httpcore:${httpCoreVersion}", From 6f30e3d73ab020820c51a27a23f04b06d6261354 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Thu, 9 Jan 2020 17:14:14 +0100 Subject: [PATCH 045/122] Suppress GatewayNotifier error message during startup Signed-off-by: Petr Plavjanik --- api-catalog-services/src/main/resources/application.yml | 2 +- .../mfaas/product/service/ServiceStartupEventHandler.java | 8 +++++--- .../main/java/com/ca/mfaas/discovery/GatewayNotifier.java | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/api-catalog-services/src/main/resources/application.yml b/api-catalog-services/src/main/resources/application.yml index 87a138125f..c223d60407 100644 --- a/api-catalog-services/src/main/resources/application.yml +++ b/api-catalog-services/src/main/resources/application.yml @@ -22,6 +22,7 @@ logging: level: ROOT: INFO com.ca.mfaas: INFO + com.ca.mfaas.discovery.GatewayNotifier: OFF org.springframework: WARN com.netflix: WARN com.netflix.discovery: ERROR @@ -30,7 +31,6 @@ logging: com.netflix.discovery.DiscoveryClient: OFF org.springframework.boot.web.embedded.tomcat.TomcatWebServer: INFO - # New Config org.apache: WARN #org.apache.catalina, org.apache.coyote, org.apache.tomcat org.eclipse.jetty: WARN diff --git a/apiml-common/src/main/java/com/ca/mfaas/product/service/ServiceStartupEventHandler.java b/apiml-common/src/main/java/com/ca/mfaas/product/service/ServiceStartupEventHandler.java index fcd8dfcabc..9327f73ce1 100644 --- a/apiml-common/src/main/java/com/ca/mfaas/product/service/ServiceStartupEventHandler.java +++ b/apiml-common/src/main/java/com/ca/mfaas/product/service/ServiceStartupEventHandler.java @@ -20,19 +20,21 @@ public class ServiceStartupEventHandler { public static final int DEFAULT_DELAY_FACTOR = 5; - private final ApimlLogger apimlLog = ApimlLogger.of(ServiceStartupEventHandler.class, YamlMessageServiceInstance.getInstance()); + private final ApimlLogger apimlLog = ApimlLogger.of(ServiceStartupEventHandler.class, + YamlMessageServiceInstance.getInstance()); @SuppressWarnings("squid:S1172") public void onServiceStartup(String serviceName, int delayFactor) { long uptime = ManagementFactory.getRuntimeMXBean().getUptime(); - apimlLog.log("apiml.common.serviceStarted",serviceName, uptime / 1000.0); + apimlLog.log("apiml.common.serviceStarted", serviceName, uptime / 1000.0); new java.util.Timer().schedule(new java.util.TimerTask() { @Override public void run() { LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); String[] names = new String[] { "com.netflix.discovery.DiscoveryClient", - "com.netflix.discovery.shared.transport.decorator.RedirectingEurekaHttpClient" }; + "com.netflix.discovery.shared.transport.decorator.RedirectingEurekaHttpClient", + "com.ca.mfaas.discovery.GatewayNotifier" }; for (String name : names) { Logger logger = loggerContext.getLogger(name); logger.setLevel(Level.ERROR); diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java index a13be0b253..cdfc86fb74 100644 --- a/discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java @@ -41,7 +41,7 @@ public void serviceUpdated(String serviceId) { final PeerAwareInstanceRegistry registry = getRegistry(); final Application application = registry.getApplication("gateway"); if (application == null) { - log.error("Gateway application doesn't exists, cannot be notified about service change"); + log.error("Gateway service is not available so it cannot be notified about changes"); return; } From 6ddbb9840771d0799aa1c4913fcd315f11117d7a Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Thu, 9 Jan 2020 17:17:34 +0100 Subject: [PATCH 046/122] Only warnings and errors for EHCache Signed-off-by: Petr Plavjanik --- api-catalog-services/src/main/resources/application.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api-catalog-services/src/main/resources/application.yml b/api-catalog-services/src/main/resources/application.yml index c223d60407..bb4f35512c 100644 --- a/api-catalog-services/src/main/resources/application.yml +++ b/api-catalog-services/src/main/resources/application.yml @@ -30,6 +30,7 @@ logging: com.netflix.discovery.shared.transport.decorator.RedirectingEurekaHttpClient: OFF com.netflix.discovery.DiscoveryClient: OFF org.springframework.boot.web.embedded.tomcat.TomcatWebServer: INFO + net.sf.ehcache: WARN # New Config org.apache: WARN #org.apache.catalina, org.apache.coyote, org.apache.tomcat @@ -184,6 +185,7 @@ logging: org.apache.http: DEBUG com.netflix: INFO springfox: INFO + net.sf.ehcache: INFO --- spring: From 511f53032fb4fc7c40d9cfc1133889347c9a0f73 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Thu, 9 Jan 2020 17:40:18 +0100 Subject: [PATCH 047/122] fixup! Signed-off-by: Petr Plavjanik --- api-catalog-services/src/main/resources/application.yml | 1 - discovery-service/src/main/resources/application.yml | 4 ++++ gateway-service/src/main/resources/application.yml | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/api-catalog-services/src/main/resources/application.yml b/api-catalog-services/src/main/resources/application.yml index bb4f35512c..7f816ad82b 100644 --- a/api-catalog-services/src/main/resources/application.yml +++ b/api-catalog-services/src/main/resources/application.yml @@ -22,7 +22,6 @@ logging: level: ROOT: INFO com.ca.mfaas: INFO - com.ca.mfaas.discovery.GatewayNotifier: OFF org.springframework: WARN com.netflix: WARN com.netflix.discovery: ERROR diff --git a/discovery-service/src/main/resources/application.yml b/discovery-service/src/main/resources/application.yml index 376bef20b3..a7e66e1b80 100644 --- a/discovery-service/src/main/resources/application.yml +++ b/discovery-service/src/main/resources/application.yml @@ -2,6 +2,7 @@ logging: level: ROOT: INFO com.ca.mfaas: INFO + com.ca.mfaas.discovery.GatewayNotifier: OFF org.springframework: WARN com.netflix: WARN com.netflix.discovery: ERROR @@ -10,6 +11,7 @@ logging: com.netflix.discovery.DiscoveryClient: OFF org.springframework.boot.web.embedded.tomcat.TomcatWebServer: INFO com.sun.jersey.server.impl.application.WebApplicationImpl: WARN + net.sf.ehcache: WARN # New Config org.apache: WARN #org.apache.catalina, org.apache.coyote, org.apache.tomcat @@ -126,11 +128,13 @@ logging: level: ROOT: INFO com.ca.mfaas: DEBUG + com.ca.mfaas.discovery.GatewayNotifier: INFO org.springframework: INFO org.apache: INFO org.apache.http: DEBUG com.netflix: INFO com.sun.jersey.server.impl.application.WebApplicationImpl: INFO + net.sf.ehcache: INFO --- spring: diff --git a/gateway-service/src/main/resources/application.yml b/gateway-service/src/main/resources/application.yml index 7ddce827a8..b65792a5ce 100644 --- a/gateway-service/src/main/resources/application.yml +++ b/gateway-service/src/main/resources/application.yml @@ -14,6 +14,7 @@ logging: com.ca.mfaas.gateway.error: INFO org.eclipse.jetty: WARN org.springframework.web.servlet.PageNotFound: ERROR + net.sf.ehcache: WARN # New Config org.apache: WARN #org.apache.catalina, org.apache.coyote, org.apache.tomcat @@ -205,6 +206,7 @@ logging: com.netflix: INFO org.hibernate: INFO org.springframework.web.servlet.PageNotFound: WARN + net.sf.ehcache: INFO --- spring: From 6a5b8c480486138a4f7b475d7875846c2516d025 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Thu, 9 Jan 2020 18:18:14 +0100 Subject: [PATCH 048/122] Numberred error message for GatewayNotifier Signed-off-by: Petr Plavjanik --- .../com/ca/mfaas/discovery/GatewayNotifier.java | 16 ++++++++++++---- .../main/resources/discovery-log-messages.yml | 7 +++++++ .../ca/mfaas/discovery/GatewayNotifierTest.java | 4 +++- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java index cdfc86fb74..ed83573d95 100644 --- a/discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java @@ -9,13 +9,14 @@ */ package com.ca.mfaas.discovery; +import com.ca.mfaas.message.core.MessageService; +import com.ca.mfaas.message.log.ApimlLogger; import com.ca.mfaas.util.EurekaUtils; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.shared.Application; import com.netflix.eureka.EurekaServerContext; import com.netflix.eureka.EurekaServerContextHolder; import com.netflix.eureka.registry.PeerAwareInstanceRegistry; -import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; @@ -23,12 +24,18 @@ import java.util.List; @Component -@AllArgsConstructor @Slf4j public class GatewayNotifier { + private final ApimlLogger logger; + private final RestTemplate restTemplate; + public GatewayNotifier(RestTemplate restTemplate, MessageService messageService) { + this.restTemplate = restTemplate; + this.logger = ApimlLogger.of(GatewayNotifier.class, messageService); + } + private EurekaServerContext getServerContext() { return EurekaServerContextHolder.getInstance().getServerContext(); } @@ -41,7 +48,7 @@ public void serviceUpdated(String serviceId) { final PeerAwareInstanceRegistry registry = getRegistry(); final Application application = registry.getApplication("gateway"); if (application == null) { - log.error("Gateway service is not available so it cannot be notified about changes"); + logger.log("apiml.discovery.errorNotifyingGateway"); return; } @@ -50,7 +57,8 @@ public void serviceUpdated(String serviceId) { for (final InstanceInfo instanceInfo : gatewayInstances) { final StringBuilder url = new StringBuilder(); url.append(EurekaUtils.getUrl(instanceInfo)).append("/cache/services"); - if (serviceId != null) url.append('/').append(serviceId); + if (serviceId != null) + url.append('/').append(serviceId); restTemplate.delete(url.toString()); } } diff --git a/discovery-service/src/main/resources/discovery-log-messages.yml b/discovery-service/src/main/resources/discovery-log-messages.yml index 7e7db9e7ad..9bb2e77334 100644 --- a/discovery-service/src/main/resources/discovery-log-messages.yml +++ b/discovery-service/src/main/resources/discovery-log-messages.yml @@ -60,4 +60,11 @@ messages: - An I/O error occurred while attempting to read the static API definition directory." action: "Review the static API definition directory definition and its contents on the storage. The static definition directories are specified as a parameter to launch a Discovery service jar. The property key is: `apiml.discovery.staticApiDefinitionsDirectories`" + - key: apiml.discovery.errorNotifyingGateway + number: ZWEAD704 + type: ERROR + text: "Gateway Service is not available so it cannot be notified about changes in Discovery Service" + reason: "Gateway Service is probably misconfigured or failed to start from another reason." + action: "Review the log of Gateway Service and its configuration." + # Legacy messages diff --git a/discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java b/discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java index 8a7c59080d..eae83073d8 100644 --- a/discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java +++ b/discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java @@ -8,6 +8,7 @@ * Copyright Contributors to the Zowe Project. */ +import com.ca.mfaas.message.core.MessageService; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.shared.Application; import com.netflix.eureka.EurekaServerContext; @@ -49,7 +50,8 @@ private InstanceInfo createInstanceInfo(String ipAddr, int port, int securePort) @Test public void testServiceUpdated() { RestTemplate restTemplate = mock(RestTemplate.class); - GatewayNotifier gatewayNotifier = new GatewayNotifier(restTemplate); + MessageService messageService = mock(MessageService.class); + GatewayNotifier gatewayNotifier = new GatewayNotifier(restTemplate, messageService); verify(restTemplate, never()).delete(anyString()); From eb9c65be734a8c629b9c7c5deea0ce8512c883f1 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Fri, 10 Jan 2020 07:38:47 +0100 Subject: [PATCH 049/122] Use @depracated tag as SonarQube recommends Signed-off-by: Petr Plavjanik --- .../main/java/com/ca/mfaas/gateway/filters/pre/ZosmfFilter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ZosmfFilter.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ZosmfFilter.java index 62fb6c2ab0..9c6593d413 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ZosmfFilter.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ZosmfFilter.java @@ -23,7 +23,7 @@ /** * Extract LTPA token from JWT token and set it as a cookie when accessing z/OSMF * - * TODO: Remove after zowe-install-packaging is updated + * @deprecated TODO: Remove after zowe-install-packaging is updated */ @Deprecated public class ZosmfFilter extends ZuulFilter { From d4d10c23d85fe9eaa2e64d41bffef467dbd3fa30 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Fri, 10 Jan 2020 08:38:14 +0100 Subject: [PATCH 050/122] User a dedicated exception for exception mapping errors Signed-off-by: Petr Plavjanik --- .../mfaas/util/ClassOrDefaultProxyUtils.java | 2 +- .../ca/mfaas/util/ExceptionMappingError.java | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 common-service-core/src/main/java/com/ca/mfaas/util/ExceptionMappingError.java diff --git a/common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java b/common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java index 3f152382af..082905d56b 100644 --- a/common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java +++ b/common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java @@ -379,7 +379,7 @@ private Function getMappingFunction(Constructor constructor, Li mapFunctions.stream().map(y -> y.apply(x)).toArray() ); } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException("Cannot construct exception " + constructor.getDeclaringClass(), e); + throw new ExceptionMappingError("Cannot construct exception " + constructor.getDeclaringClass(), e); } }; } diff --git a/common-service-core/src/main/java/com/ca/mfaas/util/ExceptionMappingError.java b/common-service-core/src/main/java/com/ca/mfaas/util/ExceptionMappingError.java new file mode 100644 index 0000000000..3f2a7a7336 --- /dev/null +++ b/common-service-core/src/main/java/com/ca/mfaas/util/ExceptionMappingError.java @@ -0,0 +1,18 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.util; + +public class ExceptionMappingError extends RuntimeException { + private static final long serialVersionUID = 7278565687932741451L; + + public ExceptionMappingError(String message, Throwable cause) { + super(message, cause); + } +} From a010935454c17c25045950863e95328863d76d3c Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Fri, 10 Jan 2020 08:49:45 +0100 Subject: [PATCH 051/122] Use dedicated exception for authentication problems in PassTicket service Signed-off-by: Petr Plavjanik --- .../common/service/PassTicketService.java | 2 +- .../invalidated%004awt%0054okens.data | 0 .../service/AuthenticationException.java | 18 +++++++++++++++ .../ServiceAuthenticationServiceImpl.java | 21 ++++++++++-------- .../schema/AbstractAuthenticationScheme.java | 3 ++- .../schema/AuthenticationSchemeFactory.java | 3 ++- .../schema/HttpBasicPassTicketScheme.java | 19 +++++++++++----- .../schema/ServiceAuthenticationService.java | 5 +++-- invalidated%004awt%0054okens.index | Bin 0 -> 4 bytes 9 files changed, 52 insertions(+), 19 deletions(-) create mode 100644 gateway-service/ehcache_auto_created8494010272322209455diskstore/invalidated%004awt%0054okens.data create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationException.java create mode 100644 invalidated%004awt%0054okens.index diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java index d4fc45cfe9..62569d06c8 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java @@ -22,7 +22,7 @@ import java.util.Set; /** - * This method allow to get a passTicket from RAC. + * This method allow to get a PassTicket from SAF. */ @Service public class PassTicketService { diff --git a/gateway-service/ehcache_auto_created8494010272322209455diskstore/invalidated%004awt%0054okens.data b/gateway-service/ehcache_auto_created8494010272322209455diskstore/invalidated%004awt%0054okens.data new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationException.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationException.java new file mode 100644 index 0000000000..a6763f6e42 --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationException.java @@ -0,0 +1,18 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.security.service; + +public class AuthenticationException extends Exception { + private static final long serialVersionUID = -5152411541425940337L; + + public AuthenticationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java index 3632050451..4b611f0f32 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java @@ -9,6 +9,14 @@ */ package com.ca.mfaas.gateway.security.service; +import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_APPLID; +import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_SCHEME; + +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + import com.ca.apiml.security.common.auth.Authentication; import com.ca.apiml.security.common.auth.AuthenticationScheme; import com.ca.apiml.security.common.token.QueryResponse; @@ -22,18 +30,13 @@ import com.netflix.discovery.EurekaClient; import com.netflix.discovery.shared.Application; import com.netflix.zuul.context.RequestContext; -import lombok.AllArgsConstructor; + import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; -import javax.servlet.http.HttpServletRequest; -import java.util.List; -import java.util.Map; - -import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_APPLID; -import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_SCHEME; +import lombok.AllArgsConstructor; /** * This bean is responsible for "translating" security to specific service. It decorate request with security data for @@ -91,7 +94,7 @@ protected AuthenticationService getAuthenticationService() { @Override @CacheEvict(value = CACHE_BY_AUTHENTICATION, condition = "#result != null && #result.isExpired()") @Cacheable(CACHE_BY_AUTHENTICATION) - public AuthenticationCommand getAuthenticationCommand(Authentication authentication, String jwtToken) throws Exception { + public AuthenticationCommand getAuthenticationCommand(Authentication authentication, String jwtToken) throws AuthenticationException { final AbstractAuthenticationScheme scheme = authenticationSchemeFactory.getSchema(authentication.getScheme()); final QueryResponse queryResponse = authenticationService.parseJwtToken(jwtToken); return scheme.createCommand(authentication, queryResponse); @@ -100,7 +103,7 @@ public AuthenticationCommand getAuthenticationCommand(Authentication authenticat @Override @CacheEvict(value = CACHE_BY_SERVICE_ID, condition = "#result != null && #result.isExpired()") @Cacheable(value = CACHE_BY_SERVICE_ID, keyGenerator = CacheConfig.COMPOSITE_KEY_GENERATOR) - public AuthenticationCommand getAuthenticationCommand(String serviceId, String jwtToken) throws Exception { + public AuthenticationCommand getAuthenticationCommand(String serviceId, String jwtToken) throws AuthenticationException { final Application application = discoveryClient.getApplication(serviceId); if (application == null) return AuthenticationCommand.EMPTY; diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AbstractAuthenticationScheme.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AbstractAuthenticationScheme.java index 49d7d40985..fa7887ff1a 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AbstractAuthenticationScheme.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AbstractAuthenticationScheme.java @@ -12,6 +12,7 @@ import com.ca.apiml.security.common.auth.Authentication; import com.ca.apiml.security.common.auth.AuthenticationScheme; import com.ca.apiml.security.common.token.QueryResponse; +import com.ca.mfaas.gateway.security.service.AuthenticationException; /** * This is abstract class for any processor which support service's authentication. They are called from ZUUL filters @@ -32,7 +33,7 @@ public interface AbstractAuthenticationScheme { * @param authentication DTO describing details about authentication * @param token User's parsed (Zowe's) JWT token */ - public AuthenticationCommand createCommand(Authentication authentication, QueryResponse token) throws Exception; + public AuthenticationCommand createCommand(Authentication authentication, QueryResponse token) throws AuthenticationException; /** * Define implementation, which will be use in case no scheme is defined. diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactory.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactory.java index d82283efa1..322840e294 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactory.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactory.java @@ -12,6 +12,7 @@ import com.ca.apiml.security.common.auth.Authentication; import com.ca.apiml.security.common.auth.AuthenticationScheme; import com.ca.apiml.security.common.token.QueryResponse; +import com.ca.mfaas.gateway.security.service.AuthenticationException; import com.ca.mfaas.gateway.security.service.AuthenticationService; import com.netflix.zuul.context.RequestContext; import org.springframework.beans.factory.annotation.Autowired; @@ -78,7 +79,7 @@ public AbstractAuthenticationScheme getSchema(AuthenticationScheme scheme) { return output; } - public AuthenticationCommand getAuthenticationCommand(Authentication authentication) throws Exception { + public AuthenticationCommand getAuthenticationCommand(Authentication authentication) throws AuthenticationException { final AbstractAuthenticationScheme scheme; if ((authentication == null) || (authentication.getScheme() == null)) { scheme = defaultScheme; diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketScheme.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketScheme.java index 56ab438d36..4015951380 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketScheme.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketScheme.java @@ -14,6 +14,7 @@ import com.ca.apiml.security.common.service.IRRPassTicketGenerationException; import com.ca.apiml.security.common.service.PassTicketService; import com.ca.apiml.security.common.token.QueryResponse; +import com.ca.mfaas.gateway.security.service.AuthenticationException; import com.netflix.appinfo.InstanceInfo; import com.netflix.zuul.context.RequestContext; import lombok.EqualsAndHashCode; @@ -26,8 +27,8 @@ import java.util.Base64; /** - * This bean support PassTicket. Bean is responsible for getting Passticket from RAC and generating new authentication - * header in request. + * This bean support PassTicket. Bean is responsible for getting PassTicket from + * SAF and generating new authentication header in request. */ @Component public class HttpBasicPassTicketScheme implements AbstractAuthenticationScheme { @@ -47,13 +48,21 @@ public AuthenticationScheme getScheme() { } @Override - public AuthenticationCommand createCommand(Authentication authentication, QueryResponse token) throws IRRPassTicketGenerationException { + public AuthenticationCommand createCommand(Authentication authentication, QueryResponse token) + throws AuthenticationException { final long before = System.currentTimeMillis(); final String applId = authentication.getApplid(); final String userId = token.getUserId(); - final String passTicket = passTicketService.generate(userId, applId); - final String encoded = Base64.getEncoder().encodeToString((userId + ":" + passTicket).getBytes(StandardCharsets.UTF_8)); + String passTicket; + try { + passTicket = passTicketService.generate(userId, applId); + } catch (IRRPassTicketGenerationException e) { + throw new AuthenticationException( + String.format("Could not generate PassTicket for user ID %s and APPLID %s", userId, applId), e); + } + final String encoded = Base64.getEncoder() + .encodeToString((userId + ":" + passTicket).getBytes(StandardCharsets.UTF_8)); final String value = "Basic " + encoded; final long expiredAt = Math.min(before + timeout * 1000, token.getExpiration().getTime()); diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ServiceAuthenticationService.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ServiceAuthenticationService.java index 8cb7f879c9..2901616f67 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ServiceAuthenticationService.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ServiceAuthenticationService.java @@ -10,6 +10,7 @@ package com.ca.mfaas.gateway.security.service.schema; import com.ca.apiml.security.common.auth.Authentication; +import com.ca.mfaas.gateway.security.service.AuthenticationException; import com.ca.mfaas.gateway.security.service.ServiceCacheEvict; /** @@ -25,7 +26,7 @@ public interface ServiceAuthenticationService extends ServiceCacheEvict { * @param jwtToken JWT security token of user (authentication can depends on user privilege) * @return authentication command to update request in ZUUL */ - public AuthenticationCommand getAuthenticationCommand(Authentication authentication, String jwtToken) throws Exception; + public AuthenticationCommand getAuthenticationCommand(Authentication authentication, String jwtToken) throws AuthenticationException; /** * Get or create command to service's authentication using serviceId and jwtToken of current user @@ -33,6 +34,6 @@ public interface ServiceAuthenticationService extends ServiceCacheEvict { * @param jwtToken JWT security token of user (authentication can depends on user privilege) * @return authentication command to update request in ZUUL (or lazy command to be updated in load balancer) */ - public AuthenticationCommand getAuthenticationCommand(String serviceId, String jwtToken) throws Exception; + public AuthenticationCommand getAuthenticationCommand(String serviceId, String jwtToken) throws AuthenticationException; } diff --git a/invalidated%004awt%0054okens.index b/invalidated%004awt%0054okens.index new file mode 100644 index 0000000000000000000000000000000000000000..711006c3d3b5c6d50049e3f48311f3dbe372803d GIT binary patch literal 4 LcmZ4UmVp%j1%Lsc literal 0 HcmV?d00001 From d5ea878da70fba0411275015c9f230267ea148f6 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Fri, 10 Jan 2020 09:26:05 +0100 Subject: [PATCH 052/122] Delete temporary files Signed-off-by: Petr Plavjanik --- .gitignore | 4 ++-- .../invalidated%004awt%0054okens.data | 0 invalidated%004awt%0054okens.data | 0 invalidated%004awt%0054okens.index | Bin 4 -> 0 bytes 4 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 gateway-service/ehcache_auto_created8494010272322209455diskstore/invalidated%004awt%0054okens.data delete mode 100644 invalidated%004awt%0054okens.data delete mode 100644 invalidated%004awt%0054okens.index diff --git a/.gitignore b/.gitignore index 782a05eae0..4f9d400ad2 100644 --- a/.gitignore +++ b/.gitignore @@ -89,6 +89,6 @@ api-layer.tar.gz /*.log # EHCache -gateway-service/*.data -gateway-service/*.index +gateway-service/**/*.data +gateway-service/**/*.index .ehcache-diskstore.lock diff --git a/gateway-service/ehcache_auto_created8494010272322209455diskstore/invalidated%004awt%0054okens.data b/gateway-service/ehcache_auto_created8494010272322209455diskstore/invalidated%004awt%0054okens.data deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/invalidated%004awt%0054okens.data b/invalidated%004awt%0054okens.data deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/invalidated%004awt%0054okens.index b/invalidated%004awt%0054okens.index deleted file mode 100644 index 711006c3d3b5c6d50049e3f48311f3dbe372803d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4 LcmZ4UmVp%j1%Lsc From 62ca288fb2d4bdb2d19f624c76c8cfa89427b4ba Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Fri, 10 Jan 2020 09:31:40 +0100 Subject: [PATCH 053/122] Correct order of expected and actual in the test Signed-off-by: Petr Plavjanik --- .../security/service/schema/HttpBasicPassTicketSchemeTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java index ca91c5cf29..3d244cc5dd 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java @@ -82,7 +82,7 @@ public void testCreateCommand() throws Exception { calendar = Calendar.getInstance(); calendar.add(Calendar.SECOND, PASSTICKET_DURATION); // checking setup of expired time, JWT expired in future (more than hour), check if set date is similar to passticket timeout (5s) - assertEquals(Math.abs(calendar.getTime().getTime() - (long) ReflectionTestUtils.getField(ac, "expireAt")), 0.0, 10.0); + assertEquals(0.0, Math.abs(calendar.getTime().getTime() - (long) ReflectionTestUtils.getField(ac, "expireAt")), 10.0); } } From 0c6883c17be7a82ec50ff64f351a9ae4f284b9d4 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Fri, 10 Jan 2020 09:33:02 +0100 Subject: [PATCH 054/122] Use dedicated authentication exception Signed-off-by: Petr Plavjanik --- .../security/service/ServiceAuthenticationServiceImpl.java | 2 +- .../gateway/security/service/schema/AuthenticationCommand.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java index 4b611f0f32..fe13a145e3 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java @@ -152,7 +152,7 @@ public class UniversalAuthenticationCommand extends AuthenticationCommand { protected UniversalAuthenticationCommand() {} @Override - public void apply(InstanceInfo instanceInfo) throws Exception { + public void apply(InstanceInfo instanceInfo) throws AuthenticationException { if (instanceInfo == null) throw new NullPointerException("Argument instanceInfo is required"); final Authentication auth = getAuthentication(instanceInfo); diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationCommand.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationCommand.java index c616569510..cd65742f32 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationCommand.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationCommand.java @@ -10,6 +10,7 @@ package com.ca.mfaas.gateway.security.service.schema; import com.ca.mfaas.cache.EntryExpiration; +import com.ca.mfaas.gateway.security.service.AuthenticationException; import com.netflix.appinfo.InstanceInfo; import java.io.Serializable; @@ -45,6 +46,6 @@ public boolean isExpired() { * In all other case call apply(null). * @param instanceInfo Specific instanceIf if it is needed */ - public abstract void apply(InstanceInfo instanceInfo) throws Exception; + public abstract void apply(InstanceInfo instanceInfo) throws AuthenticationException; } From e6c66dba50e02cdaf0682c30414b300183e4ea16 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Mon, 13 Jan 2020 07:58:54 +0100 Subject: [PATCH 055/122] Make ServiceStartupEventHandler use the delayFactor parameter Signed-off-by: Petr Plavjanik --- .../ca/mfaas/product/service/ServiceStartupEventHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apiml-common/src/main/java/com/ca/mfaas/product/service/ServiceStartupEventHandler.java b/apiml-common/src/main/java/com/ca/mfaas/product/service/ServiceStartupEventHandler.java index 9327f73ce1..ee337e3ed1 100644 --- a/apiml-common/src/main/java/com/ca/mfaas/product/service/ServiceStartupEventHandler.java +++ b/apiml-common/src/main/java/com/ca/mfaas/product/service/ServiceStartupEventHandler.java @@ -20,6 +20,7 @@ public class ServiceStartupEventHandler { public static final int DEFAULT_DELAY_FACTOR = 5; + private final ApimlLogger apimlLog = ApimlLogger.of(ServiceStartupEventHandler.class, YamlMessageServiceInstance.getInstance()); @@ -40,6 +41,6 @@ public void run() { logger.setLevel(Level.ERROR); } } - }, uptime * 5); + }, uptime * delayFactor); } } From bc6a6269972d8739369ea0485e6f2c5c1871a7ae Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Tue, 14 Jan 2020 08:00:20 +0100 Subject: [PATCH 056/122] Remove proposal word from finalized metadata Signed-off-by: Petr Plavjanik --- config/local/api-defs/staticclient.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/local/api-defs/staticclient.yml b/config/local/api-defs/staticclient.yml index bd0991c1ab..1aedebca71 100644 --- a/config/local/api-defs/staticclient.yml +++ b/config/local/api-defs/staticclient.yml @@ -77,7 +77,7 @@ services: gatewayUrl: api/v1 version: 1.0.0 -# Proposal - Additional metadata that will be added to existing dynamically registered services: +# Additional metadata that will be added to existing dynamically registered services: additionalServiceMetadata: - serviceId: staticclient # The staticclient service metadata will be extended mode: UPDATE # How to update UPDATE=only missing, FORCE_UPDATE=update all set values From b2f9f9246e577469d83614d3ec9b131ba953cba8 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Tue, 14 Jan 2020 09:31:00 +0100 Subject: [PATCH 057/122] Remove non-robust test with hardcoded version Signed-off-by: Petr Plavjanik --- .../cypress/integration/e2e/detail-page/detail-page.test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/api-catalog-ui/frontend/cypress/integration/e2e/detail-page/detail-page.test.js b/api-catalog-ui/frontend/cypress/integration/e2e/detail-page/detail-page.test.js index ff4833b536..2b6cc745d2 100644 --- a/api-catalog-ui/frontend/cypress/integration/e2e/detail-page/detail-page.test.js +++ b/api-catalog-ui/frontend/cypress/integration/e2e/detail-page/detail-page.test.js @@ -90,8 +90,6 @@ describe('>>> Detail page test', () => { cy.contains('Service Homepage').should('exist'); - cy.get('pre.version').should('contain', '1.1.2'); - cy.get('pre.version').should('contain', 'OAS3'); cy.contains('Swagger/OpenAPI JSON Document').should('exist'); From 8ad2b7609dfb04e3c2f42e748390d2be4ebced80 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Tue, 14 Jan 2020 09:31:10 +0100 Subject: [PATCH 058/122] Typo fix Signed-off-by: Petr Plavjanik --- .../ca/mfaas/gatewayservice/PageRedirectionTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PageRedirectionTest.java b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PageRedirectionTest.java index 52e183e1d1..8a336ab906 100644 --- a/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PageRedirectionTest.java +++ b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PageRedirectionTest.java @@ -65,7 +65,7 @@ public PageRedirectionTest() throws URISyntaxException { public void apiRouteOfDiscoverableClient() { String apiRelativeUrl = "/api/v1"; String location = String.format("%s://%s:%d%s%s%s", dcScheme, dcHost, dcPort, BASE_URL, apiRelativeUrl, "/greeting"); - String trasformedLocation = String.format("%s://%s:%d%s%s%s", gatewayScheme, gatewayHost, gatewayPort, API_PREFIX, "/" + SERVICE_ID, "/greeting"); + String transformedLocation = String.format("%s://%s:%d%s%s%s", gatewayScheme, gatewayHost, gatewayPort, API_PREFIX, "/" + SERVICE_ID, "/greeting"); RedirectLocation redirectLocation = new RedirectLocation(location); @@ -76,7 +76,7 @@ public void apiRouteOfDiscoverableClient() { .post(requestUrl) .then() .statusCode(is(HttpStatus.TEMPORARY_REDIRECT.value())) - .header(LOCATION, trasformedLocation); + .header(LOCATION, transformedLocation); } /** @@ -87,7 +87,7 @@ public void wsRouteOfDiscoverableClient() { String wsRelativeUrl = "/ws"; String location = String.format("%s://%s:%d%s%s", dcScheme, dcHost, dcPort, BASE_URL, wsRelativeUrl); String wsPrefix = "/ws/v1"; - String trasformedLocation = String.format("%s://%s:%d%s%s", gatewayScheme, gatewayHost, gatewayPort, wsPrefix, "/" + SERVICE_ID); + String transformedLocation = String.format("%s://%s:%d%s%s", gatewayScheme, gatewayHost, gatewayPort, wsPrefix, "/" + SERVICE_ID); RedirectLocation redirectLocation = new RedirectLocation(location); @@ -98,7 +98,7 @@ public void wsRouteOfDiscoverableClient() { .post(requestUrl) .then() .statusCode(is(HttpStatus.TEMPORARY_REDIRECT.value())) - .header(LOCATION, trasformedLocation); + .header(LOCATION, transformedLocation); } /** @@ -108,7 +108,7 @@ public void wsRouteOfDiscoverableClient() { public void uiRouteOfDiscoverableClient() { String location = String.format("%s://%s:%d%s", dcScheme, dcHost, dcPort, BASE_URL); String uiPrefix = "/ui/v1"; - String trasformedLocation = String.format("%s://%s:%d%s%s", gatewayScheme, gatewayHost, gatewayPort, uiPrefix, "/" + SERVICE_ID); + String transformedLocation = String.format("%s://%s:%d%s%s", gatewayScheme, gatewayHost, gatewayPort, uiPrefix, "/" + SERVICE_ID); RedirectLocation redirectLocation = new RedirectLocation(location); @@ -119,7 +119,7 @@ public void uiRouteOfDiscoverableClient() { .post(requestUrl) .then() .statusCode(is(HttpStatus.TEMPORARY_REDIRECT.value())) - .header(LOCATION, trasformedLocation); + .header(LOCATION, transformedLocation); } /** From 3db9bb555857f5847cf2b61639ed1862fa723cb8 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Tue, 14 Jan 2020 16:22:01 +0100 Subject: [PATCH 059/122] PassTicket diagram with API ML Signed-off-by: Petr Plavjanik --- passticket/passticket.wsd | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 passticket/passticket.wsd diff --git a/passticket/passticket.wsd b/passticket/passticket.wsd new file mode 100644 index 0000000000..48a08563a9 --- /dev/null +++ b/passticket/passticket.wsd @@ -0,0 +1,15 @@ +@startuml +archimate #business "User" as user <> +archimate #application "API Client" as apiClient <> +archimate #technology "Zowe API Gateway" as GW <> +archimate #application "z/OSMF" as zosmf <> +archimate #application "Zowe API Service" as zoweApiService <> +archimate #application "z/OS API Service" as apiService <> + +apiClient --> GW: 1. logins and obtains Zowe JWT +apiClient --> GW: 2. provides Zowe JWT to API calls +user --> apiClient: provides credentials to API client +GW --> zoweApiService: provides Zowe JWT +GW --> apiService: provides PassTicket +GW --> zosmf: provides z/OSMF LTPA +@enduml From 065d35ce80aa1c7468cb13b36cc4c137fe9badef Mon Sep 17 00:00:00 2001 From: JirkaAichler Date: Thu, 16 Jan 2020 14:28:14 +0100 Subject: [PATCH 060/122] Add certificate protection for /ticket endpoint Add integration tests for /ticket endpoint Fix error handling Signed-off-by: JirkaAichler --- .../error/AbstractExceptionHandler.java | 7 +- .../common/error/AuthExceptionHandler.java | 7 + .../error/InvalidCertificateException.java | 22 ++ .../AbstractIRRPassTicketException.java | 92 ++++--- .../IRRPassTicketEvaluationException.java | 7 +- .../IRRPassTicketGenerationException.java | 6 +- .../common/service/PassTicketService.java | 29 ++- .../common}/ticket/TicketRequest.java | 2 +- .../common}/ticket/TicketResponse.java | 4 +- .../security-common-log-messages.yml | 8 +- .../common/service/PassTicketServiceTest.java | 31 +-- .../config/SecurityConfiguration.java | 17 ++ .../gateway/security/query/QueryFilter.java | 13 + .../ApplicationNameNotFoundException.java | 4 - .../ticket/SuccessfulTicketHandler.java | 7 + .../src/main/resources/application.yml | 1 + .../main/resources/gateway-log-messages.yml | 7 + .../security/query/QueryFilterTest.java | 29 ++- .../ticket/SuccessfulTicketHandlerTest.java | 1 + .../EurekaInstancesIntegrationTest.java | 44 +--- .../mfaas/gatewayservice/PassTicketTest.java | 228 ++++++++++++++++-- .../gatewayservice/QueryIntegrationTest.java | 2 +- .../ca/mfaas/util/config/ConfigReader.java | 18 +- .../DiscoverableClientConfiguration.java | 24 ++ .../util/config/EnvironmentConfiguration.java | 1 + .../resources/environment-configuration.yml | 2 + .../security-client-log-messages.yml | 7 - 27 files changed, 445 insertions(+), 175 deletions(-) create mode 100644 apiml-security-common/src/main/java/com/ca/apiml/security/common/error/InvalidCertificateException.java rename {gateway-service/src/main/java/com/ca/mfaas/gateway/security => apiml-security-common/src/main/java/com/ca/apiml/security/common}/ticket/TicketRequest.java (92%) rename {gateway-service/src/main/java/com/ca/mfaas/gateway/security => apiml-security-common/src/main/java/com/ca/apiml/security/common}/ticket/TicketResponse.java (86%) create mode 100644 integration-tests/src/test/java/com/ca/mfaas/util/config/DiscoverableClientConfiguration.java diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/error/AbstractExceptionHandler.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/error/AbstractExceptionHandler.java index a6515023bd..ba9e6d8856 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/error/AbstractExceptionHandler.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/error/AbstractExceptionHandler.java @@ -17,6 +17,7 @@ import org.springframework.http.MediaType; import com.ca.mfaas.message.log.ApimlLogger; import com.ca.mfaas.product.logging.annotations.InjectApimlLogger; + import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -37,7 +38,7 @@ public abstract class AbstractExceptionHandler { protected final ObjectMapper mapper; @InjectApimlLogger - private ApimlLogger apimlLog = ApimlLogger.empty(); + private final ApimlLogger apimlLog = ApimlLogger.empty(); /** * Entry method that takes care of an exception passed to it @@ -78,8 +79,8 @@ protected void writeErrorResponse(ApiMessageView message, HttpStatus status, Htt try { mapper.writeValue(response.getWriter(), message); } catch (IOException e) { - apimlLog.log("apiml.security.errorWrittingResponse", e.getMessage()); - throw new ServletException("Error writting response", e); + apimlLog.log("apiml.security.errorWritingResponse", e.getMessage()); + throw new ServletException("Error writing response", e); } } } diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/error/AuthExceptionHandler.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/error/AuthExceptionHandler.java index 7d7fbdfcc7..3988f38401 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/error/AuthExceptionHandler.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/error/AuthExceptionHandler.java @@ -63,6 +63,8 @@ public void handleException(HttpServletRequest request, HttpServletResponse resp handleTokenNotProvided(request, response, ex); } else if (ex instanceof TokenExpireException) { handleTokenExpire(request, response, ex); + } else if (ex instanceof InvalidCertificateException) { + handleInvalidCertificate(response, ex); } else if (ex instanceof AuthenticationException) { handleAuthenticationException(request, response, ex); } else { @@ -108,6 +110,11 @@ private void handleTokenExpire(HttpServletRequest request, HttpServletResponse r writeErrorResponse(ErrorType.TOKEN_EXPIRED.getErrorMessageKey(), HttpStatus.UNAUTHORIZED, request, response); } + private void handleInvalidCertificate(HttpServletResponse response, RuntimeException ex) { + log.debug(ERROR_MESSAGE_400, ex.getMessage()); + response.setStatus(HttpStatus.FORBIDDEN.value()); + } + //500 private void handleAuthenticationException(HttpServletRequest request, HttpServletResponse response, RuntimeException ex) throws ServletException { log.debug(ERROR_MESSAGE_500, ex.getMessage()); diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/error/InvalidCertificateException.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/error/InvalidCertificateException.java new file mode 100644 index 0000000000..246227800a --- /dev/null +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/error/InvalidCertificateException.java @@ -0,0 +1,22 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.apiml.security.common.error; + +import org.springframework.security.core.AuthenticationException; + +/** + * This exception is thrown in case a certificate is not provided or is invalid + */ +public class InvalidCertificateException extends AuthenticationException { + + public InvalidCertificateException(String method) { + super(method); + } +} diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/AbstractIRRPassTicketException.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/AbstractIRRPassTicketException.java index 5c2693873f..e191b8823f 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/AbstractIRRPassTicketException.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/AbstractIRRPassTicketException.java @@ -11,12 +11,10 @@ import lombok.AllArgsConstructor; import lombok.Getter; - -import java.util.LinkedList; -import java.util.List; +import org.springframework.http.HttpStatus; /** - * Abstact exception from IRR passticket service. It collect common values about exception + * Abstract exception from IRR passticket service. It collect common values about exception */ @AllArgsConstructor @Getter @@ -25,72 +23,66 @@ public abstract class AbstractIRRPassTicketException extends Exception { private static final long serialVersionUID = -6233392272992529775L; protected final int safRc; - protected final int racfRsn; protected final int racfRc; + protected final int racfRsn; - public List getErrorCodes() { + public ErrorCode getErrorCode() { return ErrorCode.getErrorCode(this); } protected String getMessage(String baseMessage) { - final StringBuilder sb = new StringBuilder(); - - sb.append(baseMessage).append('\n'); - for (ErrorCode ec : getErrorCodes()) { - sb.append('\t').append(ec.getMessage()).append('\n'); - } + return baseMessage + ' ' + getErrorCode().getMessage(); + } - return sb.toString(); + public HttpStatus getHttpStatus() { + return getErrorCode().getHttpStatus(); } @AllArgsConstructor @Getter public enum ErrorCode { - ERR_0_0_0(0, 0, 0, "The service was successful."), - ERR_4_0_0(4, 0, 0, "RACF is not installed."), - ERR_8_8_0(8, 8, 0, "Invalid function code."), - ERR_8_8_4(8, 8, 4, "Parameter list error."), - ERR_8_8_8(8, 8, 8, "An internal error was encountered."), - ERR_8_8_12(8, 8, 12, "A recovery environment could not be established."), - ERR_8_8_16(8, 8, 16, "Not authorized to use this service."), - ERR_8_8_20(8, 8, 20, "High order bit was not set to indicate last parameter."), - ERR_8_12_8(8, 12, 8, "Invocation of the Security Server Network Authentication Service Program Call (PC) interface failed with a 'parameter buffer overflow' return code. This indicates an internal error in IRRSPK00."), - ERR_8_12_12(8, 12, 12, "Invocation of the Security Server Network Authentication Service Program Call (PC) interface failed with an 'unable to allocate storage' return code. The region size for the Security Server Network Authentication Service started task (SKRBKDC) should be increased."), - ERR_8_12_16(8, 12, 16, "Invocation of the Security Server Network Authentication Service Program Call (PC) interface failed with a 'local services are not available' return code. This indicates that the Security Server Network Authentication Service started task (SKRBKDC) address space has not been started or is terminating."), - ERR_8_12_20(8, 12, 20, "Invocation of the Security Server Network Authentication Service Program Call (PC) interface failed with an 'abend in the PC service routine' return code. The symptom record associated with this abend can be found in the logrec data set."), - ERR_8_12_24(8, 12, 24, "Invocation of the Security Server Network Authentication Service Program Call (PC) interface failed with an 'unable to obtain control lock' return code. This can occur if the task holding the lock is not being dispatched (for example, a dump is in progress)."), - ERR_8_16_X_1(8, 16, null, "The Security Server Network Authentication Service was not able to successfully extract the client principal name from the supplied Kerberos V5 ticket. X'nnnnnnnn' is the Kerberos return code. Refer to the Security Server Network Authentication Service documentation for more information."), - ERR_8_16_28(8, 16, 28, "Unable to generate PassTicket. Verify that the secured signon (PassTicket) function and application ID is configured properly by referring to Using PassTickets in z/OS Security Server RACF Security Administrator's Guide."), - ERR_8_16_32(8, 16, 32,"PassTicket evaluation failure. Possible reasons include:\n" + - "- PassTicket to be evaluated is not a successful PassTicket\n" + - "- The PassTicket to be evaluated was already evaluated before and replay protection is in effect.\n" + - "- No PTKTDATA profile exists to match the specified application\n" + - "- An internal error occurred."), - ERR_8_16_X_2(8, 16, null, "PassTicket evaluation extended failure. X'nnnnnnnn' is the internal reason code for the evaluation failure.") - - ; - - - private final int safRc; - private final int racfRsn; + ERR_0_0_0(0, 0, 0, HttpStatus.INTERNAL_SERVER_ERROR, "The service was successful."), + ERR_4_0_0(4, 0, 0, HttpStatus.INTERNAL_SERVER_ERROR, "RACF is not installed."), + ERR_8_8_0(8, 8, 0, HttpStatus.INTERNAL_SERVER_ERROR, "Invalid function code."), + ERR_8_8_4(8, 8, 4, HttpStatus.INTERNAL_SERVER_ERROR, "Parameter list error."), + ERR_8_8_8(8, 8, 8, HttpStatus.INTERNAL_SERVER_ERROR, "An internal error was encountered."), + ERR_8_8_12(8, 8, 12, HttpStatus.INTERNAL_SERVER_ERROR, "A recovery environment could not be established."), + ERR_8_8_16(8, 8, 16, HttpStatus.INTERNAL_SERVER_ERROR, "Not authorized to use this service."), + ERR_8_8_20(8, 8, 20, HttpStatus.INTERNAL_SERVER_ERROR, "High order bit was not set to indicate last parameter."), + ERR_8_12_8(8, 12, 8, HttpStatus.INTERNAL_SERVER_ERROR, "Invocation of the Security Server Network Authentication Service Program Call (PC) interface failed with a 'parameter buffer overflow' return code. This indicates an internal error in IRRSPK00."), + ERR_8_12_12(8, 12, 12, HttpStatus.INTERNAL_SERVER_ERROR, "Invocation of the Security Server Network Authentication Service Program Call (PC) interface failed with an 'unable to allocate storage' return code. The region size for the Security Server Network Authentication Service started task (SKRBKDC) should be increased."), + ERR_8_12_16(8, 12, 16, HttpStatus.INTERNAL_SERVER_ERROR, "Invocation of the Security Server Network Authentication Service Program Call (PC) interface failed with a 'local services are not available' return code. This indicates that the Security Server Network Authentication Service started task (SKRBKDC) address space has not been started or is terminating."), + ERR_8_12_20(8, 12, 20, HttpStatus.INTERNAL_SERVER_ERROR, "Invocation of the Security Server Network Authentication Service Program Call (PC) interface failed with an 'abend in the PC service routine' return code. The symptom record associated with this abend can be found in the logrec data set."), + ERR_8_12_24(8, 12, 24, HttpStatus.INTERNAL_SERVER_ERROR, "Invocation of the Security Server Network Authentication Service Program Call (PC) interface failed with an 'unable to obtain control lock' return code. This can occur if the task holding the lock is not being dispatched (for example, a dump is in progress)."), + ERR_8_16_28(8, 16, 28, HttpStatus.BAD_REQUEST, "Unable to generate PassTicket. Verify that the secured signon (PassTicket) function and application ID is configured properly by referring to Using PassTickets in z/OS Security Server RACF Security Administrator's Guide."), + ERR_8_16_32(8, 16, 32, HttpStatus.INTERNAL_SERVER_ERROR, "PassTicket evaluation failure. Possible reasons include: " + + "PassTicket to be evaluated is not a successful PassTicket. " + + "The PassTicket to be evaluated was already evaluated before and replay protection is in effect. " + + "No PTKTDATA profile exists to match the specified application " + + "An internal error occurred."), + ERR_8_16_X(8, 16, null, HttpStatus.INTERNAL_SERVER_ERROR, "PassTicket evaluation extended failure. X'nnnnnnnn' is the internal reason code for the evaluation failure."), + ERR_UNKNOWN(null, null, null, HttpStatus.INTERNAL_SERVER_ERROR, "The Saf Auth Service returned unknown exception."); + + + private final Integer safRc; private final Integer racfRc; + private final Integer racfRsn; - private String message; + private final HttpStatus httpStatus; - public static List getErrorCode(AbstractIRRPassTicketException e) { - final List out = new LinkedList<>(); + private String message; + public static ErrorCode getErrorCode(AbstractIRRPassTicketException e) { for (final ErrorCode ec : values()) { - if (ec.getSafRc() != e.getSafRc()) continue; - if (ec.getRacfRsn() != e.getRacfRsn()) continue; - if ((ec.getRacfRc() != null) && (ec.getRacfRc() != e.getRacfRc())) continue; - out.add(ec); + if (ec.getSafRc() != null && ec.getSafRc() == e.getSafRc() && + ec.getRacfRc() != null && ec.getRacfRc() == e.getRacfRc() && + ec.getRacfRsn() != null && ec.getRacfRsn() == e.getRacfRsn()) { + return ec; + } } - return out; + return ERR_UNKNOWN; } - } - } diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicketEvaluationException.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicketEvaluationException.java index f4f2a36a2a..ae51cb3a3d 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicketEvaluationException.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicketEvaluationException.java @@ -16,13 +16,12 @@ public class IRRPassTicketEvaluationException extends AbstractIRRPassTicketExcep private static final long serialVersionUID = -7401871844111323433L; - public IRRPassTicketEvaluationException(int safRc, int racfRsn, int racfRc) { - super(safRc, racfRsn, racfRc); + public IRRPassTicketEvaluationException(int safRc, int racfRc, int racfRsn) { + super(safRc, racfRc, racfRsn); } @Override public String getMessage() { - return getMessage("Error on evaluation of PassTicket"); + return getMessage("Error on evaluation of PassTicket:"); } - } diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicketGenerationException.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicketGenerationException.java index b8c7ba80d5..0e71e3ae8d 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicketGenerationException.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicketGenerationException.java @@ -16,13 +16,13 @@ public class IRRPassTicketGenerationException extends AbstractIRRPassTicketExcep private static final long serialVersionUID = -8944250582222779122L; - public IRRPassTicketGenerationException(int safRc, int racfRsn, int racfRc) { - super(safRc, racfRsn, racfRc); + public IRRPassTicketGenerationException(int safRc, int racfRc, int racfRsn) { + super(safRc, racfRc, racfRsn); } @Override public String getMessage() { - return getMessage("Error on generation of PassTicket"); + return getMessage("Error on generation of PassTicket:"); } } diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java index 62569d06c8..2d8f7084b2 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java @@ -32,13 +32,13 @@ public class PassTicketService { @PostConstruct public void init() { this.irrPassTicket = ClassOrDefaultProxyUtils.createProxy(IRRPassTicket.class, - "com.ibm.eserver.zos.racf.IRRPassTicket", DefaultPassTicketImpl::new, - new ClassOrDefaultProxyUtils.ByMethodName( - "com.ibm.eserver.zos.racf.IRRPassTicketEvaluationException", - IRRPassTicketEvaluationException.class, "getSafRc", "getRacfRsn", "getRacfRc"), - new ClassOrDefaultProxyUtils.ByMethodName( - "com.ibm.eserver.zos.racf.IRRPassTicketGenerationException", - IRRPassTicketGenerationException.class, "getSafRc", "getRacfRsn", "getRacfRc")); + "com.ibm.eserver.zos.racf.IRRPassTicket", DefaultPassTicketImpl::new, + new ClassOrDefaultProxyUtils.ByMethodName<>( + "com.ibm.eserver.zos.racf.IRRPassTicketEvaluationException", + IRRPassTicketEvaluationException.class, "getSafRc", "getRacfRc", "getRacfRsn"), + new ClassOrDefaultProxyUtils.ByMethodName<>( + "com.ibm.eserver.zos.racf.IRRPassTicketGenerationException", + IRRPassTicketGenerationException.class, "getSafRc", "getRacfRc", "getRacfRsn")); } public void evaluate(String userId, String applId, String passTicket) throws IRRPassTicketEvaluationException { @@ -61,10 +61,10 @@ public static class DefaultPassTicketImpl implements IRRPassTicket { public static final String ZOWE_DUMMY_USERID = "user"; public static final String ZOWE_DUMMY_PASS_TICKET_PREFIX = "ZoweDummyPassTicket"; - public static final String UNKWNOWN_USER = "unknownUser"; - public static final String UNKWNOWN_APPLID = "XBADAPPL"; + public static final String UNKNOWN_USER = "unknownUser"; + public static final String UNKNOWN_APPLID = "XBADAPPL"; - private Map> userAppToPasstickets = new HashMap<>(); + private final Map> userAppToPasstickets = new HashMap<>(); @Override public void evaluate(String userId, String applId, String passTicket) throws IRRPassTicketEvaluationException { @@ -75,7 +75,7 @@ public void evaluate(String userId, String applId, String passTicket) throws IRR if (passTicket == null) throw new IllegalArgumentException("Parameter passTicket is empty"); - if (StringUtils.equalsIgnoreCase(UNKWNOWN_APPLID, applId)) { + if (StringUtils.equalsIgnoreCase(UNKNOWN_APPLID, applId)) { throw new IRRPassTicketEvaluationException(8, 16, 28); } @@ -92,11 +92,11 @@ public void evaluate(String userId, String applId, String passTicket) throws IRR @Override public String generate(String userId, String applId) throws IRRPassTicketGenerationException { - if (StringUtils.equalsIgnoreCase(UNKWNOWN_USER, userId)) { + if (StringUtils.equalsIgnoreCase(UNKNOWN_USER, userId)) { throw new IRRPassTicketGenerationException(8, 8, 16); } - if (StringUtils.equalsIgnoreCase(UNKWNOWN_APPLID, applId)) { + if (StringUtils.equalsIgnoreCase(UNKNOWN_APPLID, applId)) { throw new IRRPassTicketGenerationException(8, 16, 28); } @@ -115,7 +115,7 @@ public String generate(String userId, String applId) throws IRRPassTicketGenerat @AllArgsConstructor @Value - private class UserApp { + private static class UserApp { private final String userId; private final String applId; @@ -123,5 +123,4 @@ private class UserApp { } } - } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/TicketRequest.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/ticket/TicketRequest.java similarity index 92% rename from gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/TicketRequest.java rename to apiml-security-common/src/main/java/com/ca/apiml/security/common/ticket/TicketRequest.java index 9ae1544e6e..f48350ab13 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/TicketRequest.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/ticket/TicketRequest.java @@ -7,7 +7,7 @@ * * Copyright Contributors to the Zowe Project. */ -package com.ca.mfaas.gateway.security.ticket; +package com.ca.apiml.security.common.ticket; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/TicketResponse.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/ticket/TicketResponse.java similarity index 86% rename from gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/TicketResponse.java rename to apiml-security-common/src/main/java/com/ca/apiml/security/common/ticket/TicketResponse.java index 0228a6a202..b63d50feac 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/TicketResponse.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/ticket/TicketResponse.java @@ -7,15 +7,17 @@ * * Copyright Contributors to the Zowe Project. */ -package com.ca.mfaas.gateway.security.ticket; +package com.ca.apiml.security.common.ticket; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; /** * Represents /ticket JSON response with the ticket information */ @Data +@NoArgsConstructor @AllArgsConstructor public class TicketResponse { private String token; diff --git a/apiml-security-common/src/main/resources/security-common-log-messages.yml b/apiml-security-common/src/main/resources/security-common-log-messages.yml index 8f4b4055bb..9eb05268e3 100644 --- a/apiml-security-common/src/main/resources/security-common-log-messages.yml +++ b/apiml-security-common/src/main/resources/security-common-log-messages.yml @@ -5,8 +5,14 @@ messages: # General messages # 100-199 + - key: apiml.security.expiredToken + number: ZWEAT100 + type: ERROR + text: "Token is expired for URL '%s'" + reason: "The validity of the token is expired." + action: "Obtain new token by performing an authentication request." - - key: apiml.security.errorWrittingResponse + - key: apiml.security.errorWritingResponse number: ZWEAT103 type: ERROR text: "Could not write response: %s" diff --git a/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java b/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java index 78b1a43c45..a29e9e6e74 100644 --- a/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java +++ b/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java @@ -91,8 +91,8 @@ public void testDefaultPassTicketImpl() throws IRRPassTicketEvaluationException, fail(); } catch (IRRPassTicketEvaluationException e) { assertEquals(8, e.getSafRc()); - assertEquals(16, e.getRacfRsn()); - assertEquals(32, e.getRacfRc()); + assertEquals(16, e.getRacfRc()); + assertEquals(32, e.getRacfRsn()); } String passTicket1 = dpti.generate(TEST_USERID, "applId"); @@ -120,34 +120,29 @@ public void testDefaultPassTicketImpl() throws IRRPassTicketEvaluationException, } try { - dpti.generate(PassTicketService.DefaultPassTicketImpl.UNKWNOWN_USER, "anyApplId"); + dpti.generate(PassTicketService.DefaultPassTicketImpl.UNKNOWN_USER, "anyApplId"); fail(); } catch (IRRPassTicketGenerationException e) { assertEquals(8, e.getSafRc()); - assertEquals(8, e.getRacfRsn()); - assertEquals(16, e.getRacfRc()); - assertNotNull(e.getErrorCodes()); - assertEquals(1, e.getErrorCodes().size()); - assertEquals(AbstractIRRPassTicketException.ErrorCode.ERR_8_8_16, e.getErrorCodes().get(0)); + assertEquals(8, e.getRacfRc()); + assertEquals(16, e.getRacfRsn()); + assertNotNull(e.getErrorCode()); + assertEquals(AbstractIRRPassTicketException.ErrorCode.ERR_8_8_16, e.getErrorCode()); assertEquals( - "Error on generation of PassTicket\n" + - "\tNot authorized to use this service.\n" + "Error on generation of PassTicket: Not authorized to use this service." , e.getMessage() ); } try { - dpti.generate("anyUser", PassTicketService.DefaultPassTicketImpl.UNKWNOWN_APPLID); + dpti.generate("anyUser", PassTicketService.DefaultPassTicketImpl.UNKNOWN_APPLID); fail(); } catch (IRRPassTicketGenerationException e) { assertEquals(8, e.getSafRc()); - assertEquals(16, e.getRacfRsn()); - assertEquals(28, e.getRacfRc()); - assertNotNull(e.getErrorCodes()); - assertEquals(3, e.getErrorCodes().size()); - assertEquals(AbstractIRRPassTicketException.ErrorCode.ERR_8_16_X_1, e.getErrorCodes().get(0)); - assertEquals(AbstractIRRPassTicketException.ErrorCode.ERR_8_16_28, e.getErrorCodes().get(1)); - assertEquals(AbstractIRRPassTicketException.ErrorCode.ERR_8_16_X_2, e.getErrorCodes().get(2)); + assertEquals(16, e.getRacfRc()); + assertEquals(28, e.getRacfRsn()); + assertNotNull(e.getErrorCode()); + assertEquals(AbstractIRRPassTicketException.ErrorCode.ERR_8_16_28, e.getErrorCode()); } } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/config/SecurityConfiguration.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/config/SecurityConfiguration.java index eaa69bef0a..53b0d115e6 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/config/SecurityConfiguration.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/config/SecurityConfiguration.java @@ -27,9 +27,13 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutHandler; +import java.util.Collections; + /** * Security configuration for Gateway *

@@ -78,6 +82,13 @@ protected void configure(HttpSecurity http) throws Exception { .authorizeRequests() .antMatchers(HttpMethod.POST, authConfigurationProperties.getGatewayLoginEndpoint()).permitAll() + // ticket endpoint + .and() + .authorizeRequests() + .antMatchers(HttpMethod.POST, authConfigurationProperties.getGatewayTicketEndpoint()).authenticated() + .and() + .x509().userDetailsService(x509UserDetailsService()) + // logout endpoint .and() .logout() @@ -122,6 +133,7 @@ private QueryFilter queryFilter(String queryEndpoint) throws Exception { handlerInitializer.getAuthenticationFailureHandler(), authenticationService, HttpMethod.GET, + false, authenticationManager()); } @@ -135,6 +147,7 @@ private QueryFilter ticketFilter(String ticketEndpoint) throws Exception { handlerInitializer.getAuthenticationFailureHandler(), authenticationService, HttpMethod.POST, + true, authenticationManager()); } @@ -167,4 +180,8 @@ private LogoutHandler logoutHandler() { authenticationService.invalidateJwtToken(x, true) ); } + + private UserDetailsService x509UserDetailsService() { + return username -> new User("gatewayClient", "", Collections.emptyList()); + } } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/query/QueryFilter.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/query/QueryFilter.java index e9ffb797d1..d9d1fe4328 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/query/QueryFilter.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/query/QueryFilter.java @@ -10,6 +10,7 @@ package com.ca.mfaas.gateway.security.query; import com.ca.apiml.security.common.error.AuthMethodNotSupportedException; +import com.ca.apiml.security.common.error.InvalidCertificateException; import com.ca.apiml.security.common.token.TokenAuthentication; import com.ca.apiml.security.common.token.TokenNotProvidedException; import com.ca.mfaas.gateway.security.service.AuthenticationService; @@ -27,6 +28,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.security.cert.X509Certificate; /** * Filter for /query endpoint requests with JWT token. @@ -36,6 +38,7 @@ public class QueryFilter extends AbstractAuthenticationProcessingFilter { private final AuthenticationFailureHandler failureHandler; private final AuthenticationService authenticationService; private final HttpMethod httpMethod; + private final boolean protectedByCertificate; public QueryFilter( String authEndpoint, @@ -43,12 +46,14 @@ public QueryFilter( AuthenticationFailureHandler failureHandler, AuthenticationService authenticationService, HttpMethod httpMethod, + boolean protectedByCertificate, AuthenticationManager authenticationManager) { super(authEndpoint); this.successHandler = successHandler; this.failureHandler = failureHandler; this.authenticationService = authenticationService; this.httpMethod = httpMethod; + this.protectedByCertificate = protectedByCertificate; this.setAuthenticationManager(authenticationManager); } @@ -67,6 +72,14 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ throw new AuthMethodNotSupportedException(request.getMethod()); } + // Must be already authenticated by certificate + if (protectedByCertificate && + (SecurityContextHolder.getContext().getAuthentication() == null || + !(SecurityContextHolder.getContext().getAuthentication().getCredentials() instanceof X509Certificate) || + !SecurityContextHolder.getContext().getAuthentication().isAuthenticated())) { + throw new InvalidCertificateException("Invalid certificate."); + } + String token = authenticationService.getJwtTokenFromRequest(request) .orElseThrow(() -> new TokenNotProvidedException("Authorization token not provided.")); diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/ApplicationNameNotFoundException.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/ApplicationNameNotFoundException.java index f49fd4ef00..44353c0ca8 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/ApplicationNameNotFoundException.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/ApplicationNameNotFoundException.java @@ -17,8 +17,4 @@ public class ApplicationNameNotFoundException extends Exception { public ApplicationNameNotFoundException(String message) { super(message); } - - public ApplicationNameNotFoundException(String message, Throwable cause) { - super(message, cause); - } } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandler.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandler.java index 4c1c4f571e..bd0466f2c5 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandler.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandler.java @@ -11,6 +11,8 @@ import com.ca.apiml.security.common.service.IRRPassTicketGenerationException; import com.ca.apiml.security.common.service.PassTicketService; +import com.ca.apiml.security.common.ticket.TicketRequest; +import com.ca.apiml.security.common.ticket.TicketResponse; import com.ca.apiml.security.common.token.TokenAuthentication; import com.ca.mfaas.message.api.ApiMessageView; import com.ca.mfaas.message.core.MessageService; @@ -57,6 +59,11 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo response.setStatus(HttpStatus.BAD_REQUEST.value()); ApiMessageView messageView = messageService.createMessage("apiml.security.ticket.invalidApplicationName").mapToView(); mapper.writeValue(response.getWriter(), messageView); + } catch (IRRPassTicketGenerationException e) { + response.setStatus(e.getHttpStatus().value()); + ApiMessageView messageView = messageService.createMessage("apiml.security.ticket.generateFailed", + e.getErrorCode().getMessage()).mapToView(); + mapper.writeValue(response.getWriter(), messageView); } response.getWriter().flush(); diff --git a/gateway-service/src/main/resources/application.yml b/gateway-service/src/main/resources/application.yml index b65792a5ce..8eb6b1b998 100644 --- a/gateway-service/src/main/resources/application.yml +++ b/gateway-service/src/main/resources/application.yml @@ -70,6 +70,7 @@ server: port: ${apiml.service.port} ssl: enabled: true + clientAuth: want protocol: TLSv1.2 enabled-protocols: TLSv1.2 ciphers: ${apiml.security.ssl.ciphers} diff --git a/gateway-service/src/main/resources/gateway-log-messages.yml b/gateway-service/src/main/resources/gateway-log-messages.yml index 304068aba1..b3a33db0d6 100644 --- a/gateway-service/src/main/resources/gateway-log-messages.yml +++ b/gateway-service/src/main/resources/gateway-log-messages.yml @@ -154,3 +154,10 @@ messages: text: "The 'applicationName' parameter name is missing." reason: "The application name is not provided." action: "Provide the 'applicationName' parameter." + + - key: apiml.security.ticket.generateFailed + number: ZWEAG141 + type: ERROR + text: "The generation of the PassTicket failed. Reason: %s" + reason: "An error occurred in the SAF Auth Service. Review the reason in the error message." + action: "Supply a valid user and application name, and check that corresponding permissions have been set up." diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/query/QueryFilterTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/query/QueryFilterTest.java index 36454de0cf..8a5b387dd7 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/query/QueryFilterTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/query/QueryFilterTest.java @@ -10,6 +10,8 @@ package com.ca.mfaas.gateway.security.query; import com.ca.apiml.security.common.error.AuthMethodNotSupportedException; +import com.ca.apiml.security.common.error.InvalidCertificateException; +import com.ca.apiml.security.common.token.TokenAuthentication; import com.ca.apiml.security.common.token.TokenNotProvidedException; import com.ca.mfaas.gateway.security.service.AuthenticationService; import org.junit.Before; @@ -20,15 +22,15 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.context.SecurityContextImpl; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.http.HttpMethod; import java.util.Optional; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; @RunWith(MockitoJUnitRunner.class) public class QueryFilterTest { @@ -57,6 +59,7 @@ public void setup() { authenticationFailureHandler, authenticationService, HttpMethod.GET, + false, authenticationManager); } @@ -95,4 +98,24 @@ public void shouldRejectIfTokenIsNotPresent() { queryFilter.attemptAuthentication(httpServletRequest, httpServletResponse); } + + @Test(expected = InvalidCertificateException.class) + public void shouldRejectIfNotAuthenticatedByCertficate() { + httpServletRequest = new MockHttpServletRequest(); + httpServletRequest.setMethod(HttpMethod.GET.name()); + httpServletResponse = new MockHttpServletResponse(); + TokenAuthentication authentication = new TokenAuthentication("token"); + authentication.setAuthenticated(true); + SecurityContextHolder.setContext(new SecurityContextImpl(authentication)); + + QueryFilter protectedQueryFilter = new QueryFilter("TEST_ENDPOINT", + authenticationSuccessHandler, + authenticationFailureHandler, + authenticationService, + HttpMethod.GET, + true, + authenticationManager); + + protectedQueryFilter.attemptAuthentication(httpServletRequest, httpServletResponse); + } } diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandlerTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandlerTest.java index aad1e87a9f..d16dc0df0f 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandlerTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandlerTest.java @@ -10,6 +10,7 @@ package com.ca.mfaas.gateway.security.ticket; import com.ca.apiml.security.common.service.PassTicketService; +import com.ca.apiml.security.common.ticket.TicketRequest; import com.ca.apiml.security.common.token.TokenAuthentication; import com.ca.mfaas.message.core.MessageService; import com.ca.mfaas.message.yaml.YamlMessageService; diff --git a/integration-tests/src/test/java/com/ca/mfaas/discoveryservice/EurekaInstancesIntegrationTest.java b/integration-tests/src/test/java/com/ca/mfaas/discoveryservice/EurekaInstancesIntegrationTest.java index 650d28b921..591a62b298 100644 --- a/integration-tests/src/test/java/com/ca/mfaas/discoveryservice/EurekaInstancesIntegrationTest.java +++ b/integration-tests/src/test/java/com/ca/mfaas/discoveryservice/EurekaInstancesIntegrationTest.java @@ -12,33 +12,20 @@ import com.ca.mfaas.gatewayservice.SecurityUtils; import com.ca.mfaas.util.config.ConfigReader; import com.ca.mfaas.util.config.DiscoveryServiceConfiguration; -import com.ca.mfaas.util.config.TlsConfiguration; -import com.netflix.discovery.shared.transport.jersey.SSLSocketFactoryAdapter; import io.restassured.RestAssured; -import io.restassured.config.SSLConfig; import io.restassured.path.xml.XmlPath; import io.restassured.response.Response; import org.apache.http.HttpHeaders; import org.apache.http.HttpStatus; import org.apache.http.client.utils.URIBuilder; -import org.apache.http.conn.ssl.NoopHostnameVerifier; -import org.apache.http.conn.ssl.SSLConnectionSocketFactory; -import org.apache.http.ssl.SSLContexts; import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import javax.net.ssl.SSLContext; -import java.io.File; -import java.io.IOException; import java.net.URI; -import java.security.KeyManagementException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.UnrecoverableKeyException; -import java.security.cert.CertificateException; import java.util.*; +import static com.ca.mfaas.gatewayservice.SecurityUtils.getConfiguredSslConfig; import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.*; import static org.hamcrest.collection.IsMapContaining.hasEntry; @@ -52,9 +39,7 @@ public class EurekaInstancesIntegrationTest { private static final String DISCOVERY_REALM = "API Mediation Discovery Service realm"; - private DiscoveryServiceConfiguration discoveryServiceConfiguration; - private TlsConfiguration tlsConfiguration; private final static String COOKIE = "apimlAuthenticationToken"; private String scheme; private String username; @@ -66,7 +51,6 @@ public class EurekaInstancesIntegrationTest { @Before public void setUp() { discoveryServiceConfiguration = ConfigReader.environmentConfiguration().getDiscoveryServiceConfiguration(); - tlsConfiguration = ConfigReader.environmentConfiguration().getTlsConfiguration(); scheme = discoveryServiceConfiguration.getScheme(); username = ConfigReader.environmentConfiguration().getCredentials().getUser(); password = ConfigReader.environmentConfiguration().getCredentials().getPassword(); @@ -368,32 +352,6 @@ public void shouldSeeEurekaReplicasIfRegistered() throws Exception { } } - private SSLConfig getConfiguredSslConfig() { - try { - SSLContext sslContext = SSLContexts.custom() - .loadKeyMaterial( - new File(tlsConfiguration.getKeyStore()), - getCharArray(tlsConfiguration.getKeyStorePassword()), - getCharArray(tlsConfiguration.getKeyPassword()), - (aliases, socket) -> tlsConfiguration.getKeyAlias()) - .loadTrustMaterial( - new File(tlsConfiguration.getTrustStore()), - getCharArray(tlsConfiguration.getTrustStorePassword())) - .build(); - - SSLSocketFactoryAdapter sslSocketFactory = new SSLSocketFactoryAdapter(new SSLConnectionSocketFactory(sslContext, - NoopHostnameVerifier.INSTANCE)); - return SSLConfig.sslConfig().with().sslSocketFactory(sslSocketFactory); - } catch (KeyManagementException | UnrecoverableKeyException | NoSuchAlgorithmException | KeyStoreException - | CertificateException | IOException e) { - throw new RuntimeException(e.getMessage(), e); - } - } - - private char[] getCharArray(String value) { - return value != null ? value.toCharArray() : null; - } - private URI getDiscoveryUriWithPath(String path) throws Exception { return new URIBuilder() .setScheme(scheme) diff --git a/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java index 55e333706d..f90a3cd1e8 100644 --- a/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java +++ b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/PassTicketTest.java @@ -9,31 +9,41 @@ */ package com.ca.mfaas.gatewayservice; -import static com.ca.mfaas.gatewayservice.SecurityUtils.GATEWAY_TOKEN_COOKIE_NAME; -import static com.ca.mfaas.gatewayservice.SecurityUtils.gatewayToken; +import static com.ca.apiml.security.common.service.PassTicketService.DefaultPassTicketImpl.UNKNOWN_APPLID; +import static com.ca.mfaas.gatewayservice.SecurityUtils.*; import static io.restassured.RestAssured.given; -import static org.apache.http.HttpStatus.SC_OK; -import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; -import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR; +import static io.restassured.http.ContentType.JSON; +import static org.apache.http.HttpStatus.*; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertEquals; +import com.ca.apiml.security.common.ticket.TicketRequest; +import com.ca.apiml.security.common.ticket.TicketResponse; import com.ca.mfaas.util.config.ConfigReader; +import lombok.extern.slf4j.Slf4j; import org.junit.Before; import org.junit.Test; import io.restassured.RestAssured; +@Slf4j public class PassTicketTest { private final static String SCHEME = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration() - .getScheme(); + .getScheme(); private final static String HOST = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration() - .getHost(); + .getHost(); private final static int PORT = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration().getPort(); + private final static String USERNAME = ConfigReader.environmentConfiguration().getCredentials().getUser(); + private final static String APPLICATION_NAME = ConfigReader.environmentConfiguration() + .getDiscoverableClientConfiguration().getApplId(); private final static String STATICCLIENT_BASE_PATH = "/api/v1/staticclient"; + private final static String DISCOVERABLECLIENT_BASE_PATH = "/api/v1/discoverableclient"; private final static String PASSTICKET_TEST_ENDPOINT = "/passticketTest"; + private final static String TICKET_ENDPOINT = "/api/v1/gateway/auth/ticket"; + private final static String COOKIE = "apimlAuthenticationToken"; @Before public void setUp() { @@ -46,22 +56,22 @@ public void setUp() { public void accessServiceWithCorrectPassTicket() { String jwt = gatewayToken(); given().cookie(GATEWAY_TOKEN_COOKIE_NAME, jwt).when().get( - String.format("%s://%s:%d%s%s", SCHEME, HOST, PORT, STATICCLIENT_BASE_PATH, PASSTICKET_TEST_ENDPOINT)) - .then().statusCode(is(SC_OK)); + String.format("%s://%s:%d%s%s", SCHEME, HOST, PORT, STATICCLIENT_BASE_PATH, PASSTICKET_TEST_ENDPOINT)) + .then().statusCode(is(SC_OK)); } @Test public void accessServiceWithIncorrectApplId() { String jwt = gatewayToken(); given().cookie(GATEWAY_TOKEN_COOKIE_NAME, jwt).when() - .get(String.format("%s://%s:%d%s%s?applId=XBADAPPL", SCHEME, HOST, PORT, STATICCLIENT_BASE_PATH, - PASSTICKET_TEST_ENDPOINT)) - .then().statusCode(is(SC_INTERNAL_SERVER_ERROR)) - .body("message", containsString("Error on evaluation of PassTicket")); + .get(String.format("%s://%s:%d%s%s?applId=XBADAPPL", SCHEME, HOST, PORT, STATICCLIENT_BASE_PATH, + PASSTICKET_TEST_ENDPOINT)) + .then().statusCode(is(SC_INTERNAL_SERVER_ERROR)) + .body("message", containsString("Error on evaluation of PassTicket")); } - @Test //@formatter:off + @Test public void accessServiceWithIncorrectToken() { String jwt = "nonsense"; String expectedMessage = "Token is not valid"; @@ -74,5 +84,195 @@ public void accessServiceWithIncorrectToken() { .statusCode(is(SC_UNAUTHORIZED)) .body("messages.find { it.messageNumber == 'ZWEAG102E' }.messageContent", equalTo(expectedMessage)); } + + /* + * /ticket endpoint tests + */ + + @Test + public void doTicketWithValidCookieAndCertificate() { + RestAssured.config = RestAssured.config().sslConfig(getConfiguredSslConfig()); + String jwt = gatewayToken(); + log.info(APPLICATION_NAME); + TicketRequest ticketRequest = new TicketRequest(APPLICATION_NAME); + + // Generate ticket + TicketResponse ticketResponse = given() + .contentType(JSON) + .body(ticketRequest) + .cookie(COOKIE, jwt) + .when() + .post(String.format("%s://%s:%d%s", SCHEME, HOST, PORT, TICKET_ENDPOINT)) + .then() + .statusCode(is(SC_OK)) + .extract().body().as(TicketResponse.class); + + assertEquals(jwt, ticketResponse.getToken()); + assertEquals(USERNAME, ticketResponse.getUserId()); + assertEquals(APPLICATION_NAME, ticketResponse.getApplicationName()); + + // Validate ticket + given() + .auth().preemptive().basic(USERNAME, ticketResponse.getTicket()) + .param("appliId", APPLICATION_NAME) + .when() + .get(String.format("%s://%s:%d%s%s", SCHEME, HOST, PORT, DISCOVERABLECLIENT_BASE_PATH, PASSTICKET_TEST_ENDPOINT)) + .then() + .statusCode(is(SC_OK)); + } + + @Test + public void doTicketWithValidHeaderAndCertificate() { + RestAssured.config = RestAssured.config().sslConfig(getConfiguredSslConfig()); + String jwt = gatewayToken(); + TicketRequest ticketRequest = new TicketRequest(APPLICATION_NAME); + + //Generate ticket + TicketResponse ticketResponse = given() + .contentType(JSON) + .body(ticketRequest) + .header("Authorization", "Bearer " + jwt) + .when() + .post(String.format("%s://%s:%d%s", SCHEME, HOST, PORT, TICKET_ENDPOINT)) + .then() + .statusCode(is(SC_OK)) + .extract().body().as(TicketResponse.class); + + assertEquals(jwt, ticketResponse.getToken()); + assertEquals(USERNAME, ticketResponse.getUserId()); + assertEquals(APPLICATION_NAME, ticketResponse.getApplicationName()); + + // Validate ticket + given() + .auth().preemptive().basic(USERNAME, ticketResponse.getTicket()) + .param("appliId", APPLICATION_NAME) + .when() + .get(String.format("%s://%s:%d%s%s", SCHEME, HOST, PORT, DISCOVERABLECLIENT_BASE_PATH, PASSTICKET_TEST_ENDPOINT)) + .then() + .statusCode(is(SC_OK)); + } + + @Test + public void doTicketWithInvalidMethod() { + String expectedMessage = "Authentication method 'GET' is not supported for URL '" + TICKET_ENDPOINT + "'"; + + RestAssured.config = RestAssured.config().sslConfig(getConfiguredSslConfig()); + TicketRequest ticketRequest = new TicketRequest(APPLICATION_NAME); + + given() + .contentType(JSON) + .body(ticketRequest) + .when() + .get(String.format("%s://%s:%d%s", SCHEME, HOST, PORT, TICKET_ENDPOINT)) + .then() + .statusCode(is(SC_METHOD_NOT_ALLOWED)) + .body("messages.find { it.messageNumber == 'ZWEAG101E' }.messageContent", equalTo(expectedMessage)); + } + + @Test + public void doTicketWithoutCertificate() { + String jwt = gatewayToken(); + TicketRequest ticketRequest = new TicketRequest(APPLICATION_NAME); + + given() + .contentType(JSON) + .body(ticketRequest) + .cookie(COOKIE, jwt) + .when() + .post(String.format("%s://%s:%d%s", SCHEME, HOST, PORT, TICKET_ENDPOINT)) + .then() + .statusCode(is(SC_FORBIDDEN)); + } + + @Test + public void doTicketWithoutToken() { + String expectedMessage = "No authorization token provided for URL '" + TICKET_ENDPOINT + "'"; + + RestAssured.config = RestAssured.config().sslConfig(getConfiguredSslConfig()); + TicketRequest ticketRequest = new TicketRequest(APPLICATION_NAME); + + given() + .contentType(JSON) + .body(ticketRequest) + .when() + .post(String.format("%s://%s:%d%s", SCHEME, HOST, PORT, TICKET_ENDPOINT)) + .then() + .statusCode(is(SC_UNAUTHORIZED)) + .body("messages.find { it.messageNumber == 'ZWEAG131E' }.messageContent", equalTo(expectedMessage)); + } + + @Test + public void doTicketWithInvalidCookie() { + String jwt = "invalidToken"; + String expectedMessage = "Token is not valid for URL '" + TICKET_ENDPOINT + "'"; + + RestAssured.config = RestAssured.config().sslConfig(getConfiguredSslConfig()); + TicketRequest ticketRequest = new TicketRequest(APPLICATION_NAME); + + given() + .contentType(JSON) + .body(ticketRequest) + .cookie(COOKIE, jwt) + .when() + .post(String.format("%s://%s:%d%s", SCHEME, HOST, PORT, TICKET_ENDPOINT)) + .then() + .statusCode(is(SC_UNAUTHORIZED)) + .body("messages.find { it.messageNumber == 'ZWEAG130E' }.messageContent", equalTo(expectedMessage)); + } + + @Test + public void doTicketWithInvalidHeader() { + String jwt = "invalidToken"; + String expectedMessage = "Token is not valid for URL '" + TICKET_ENDPOINT + "'"; + + RestAssured.config = RestAssured.config().sslConfig(getConfiguredSslConfig()); + TicketRequest ticketRequest = new TicketRequest(APPLICATION_NAME); + + given() + .contentType(JSON) + .body(ticketRequest) + .header("Authorization", "Bearer " + jwt) + .when() + .post(String.format("%s://%s:%d%s", SCHEME, HOST, PORT, TICKET_ENDPOINT)) + .then() + .statusCode(is(SC_UNAUTHORIZED)) + .body("messages.find { it.messageNumber == 'ZWEAG130E' }.messageContent", equalTo(expectedMessage)); + } + + @Test + public void doTicketWithoutApplicationName() { + String expectedMessage = "The 'applicationName' parameter name is missing."; + + RestAssured.config = RestAssured.config().sslConfig(getConfiguredSslConfig()); + String jwt = gatewayToken(); + + given() + .cookie(COOKIE, jwt) + .when() + .post(String.format("%s://%s:%d%s", SCHEME, HOST, PORT, TICKET_ENDPOINT)) + .then() + .statusCode(is(SC_BAD_REQUEST)) + .body("messages.find { it.messageNumber == 'ZWEAG140E' }.messageContent", equalTo(expectedMessage)); + } + + @Test + public void doTicketWithInvalidApplicationName() { + String expectedMessage = "The generation of the PassTicket failed. Reason: Unable to generate PassTicket. Verify that the secured signon (PassTicket) function and application ID is configured properly by referring to Using PassTickets in z/OS Security Server RACF Security Administrator's Guide."; + + RestAssured.config = RestAssured.config().sslConfig(getConfiguredSslConfig()); + String jwt = gatewayToken(); + TicketRequest ticketRequest = new TicketRequest(UNKNOWN_APPLID); + + given() + .contentType(JSON) + .body(ticketRequest) + .cookie(COOKIE, jwt) + .when() + .post(String.format("%s://%s:%d%s", SCHEME, HOST, PORT, TICKET_ENDPOINT)) + .then() + .statusCode(is(SC_BAD_REQUEST)) + .body("messages.find { it.messageNumber == 'ZWEAG141E' }.messageContent", equalTo(expectedMessage)); + + } //@formatter:on } diff --git a/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/QueryIntegrationTest.java b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/QueryIntegrationTest.java index c4a4ecb6c7..6bc185662f 100644 --- a/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/QueryIntegrationTest.java +++ b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/QueryIntegrationTest.java @@ -76,7 +76,7 @@ public void doQueryWithInvalidTokenFromHeader() { .get(String.format("%s://%s:%d%s%s", SCHEME, HOST, PORT, BASE_PATH, QUERY_ENDPOINT)) .then() .statusCode(is(SC_UNAUTHORIZED)) - .body( + .body( "messages.find { it.messageNumber == 'ZWEAG130E' }.messageContent", equalTo(expectedMessage) ); } diff --git a/integration-tests/src/test/java/com/ca/mfaas/util/config/ConfigReader.java b/integration-tests/src/test/java/com/ca/mfaas/util/config/ConfigReader.java index 047a3ea076..a905b8a798 100644 --- a/integration-tests/src/test/java/com/ca/mfaas/util/config/ConfigReader.java +++ b/integration-tests/src/test/java/com/ca/mfaas/util/config/ConfigReader.java @@ -15,12 +15,10 @@ import java.io.File; import java.io.IOException; +import java.util.Objects; @Slf4j public class ConfigReader { - private ConfigReader() { - } - private static volatile EnvironmentConfiguration instance; public static EnvironmentConfiguration environmentConfiguration() { @@ -29,7 +27,7 @@ public static EnvironmentConfiguration environmentConfiguration() { if (instance == null) { final String configFileName = "environment-configuration.yml"; ClassLoader classLoader = ClassLoader.getSystemClassLoader(); - File configFile = new File(classLoader.getResource(configFileName).getFile()); + File configFile = new File(Objects.requireNonNull(classLoader.getResource(configFileName)).getFile()); ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); EnvironmentConfiguration configuration; try { @@ -40,6 +38,7 @@ public static EnvironmentConfiguration environmentConfiguration() { GatewayServiceConfiguration gatewayServiceConfiguration = new GatewayServiceConfiguration("https", "localhost", 10010, 1); DiscoveryServiceConfiguration discoveryServiceConfiguration = new DiscoveryServiceConfiguration("https", "eureka", "password", "localhost", 10011, 1); + DiscoverableClientConfiguration discoverableClientConfiguration = new DiscoverableClientConfiguration("ZOWEAPPL"); TlsConfiguration tlsConfiguration = TlsConfiguration.builder() .keyAlias("localhost") @@ -53,9 +52,15 @@ public static EnvironmentConfiguration environmentConfiguration() { .build(); ZosmfServiceConfiguration zosmfServiceConfiguration = new ZosmfServiceConfiguration("https", "ca32.ca.com", 1443); - configuration = new EnvironmentConfiguration(credentials, gatewayServiceConfiguration, discoveryServiceConfiguration, tlsConfiguration, zosmfServiceConfiguration); - + configuration = new EnvironmentConfiguration( + credentials, + gatewayServiceConfiguration, + discoveryServiceConfiguration, + discoverableClientConfiguration, + tlsConfiguration, + zosmfServiceConfiguration); } + configuration.getCredentials().setUser(System.getProperty("credentials.user", configuration.getCredentials().getUser())); configuration.getCredentials().setPassword(System.getProperty("credentials.password", configuration.getCredentials().getPassword())); @@ -71,7 +76,6 @@ public static EnvironmentConfiguration environmentConfiguration() { configuration.getDiscoveryServiceConfiguration().setPort(Integer.parseInt(System.getProperty("discovery.port", String.valueOf(configuration.getDiscoveryServiceConfiguration().getPort())))); configuration.getDiscoveryServiceConfiguration().setInstances(Integer.parseInt(System.getProperty("discovery.instances", String.valueOf(configuration.getDiscoveryServiceConfiguration().getInstances())))); - setTlsConfigurationFromSystemProperties(configuration); instance = configuration; diff --git a/integration-tests/src/test/java/com/ca/mfaas/util/config/DiscoverableClientConfiguration.java b/integration-tests/src/test/java/com/ca/mfaas/util/config/DiscoverableClientConfiguration.java new file mode 100644 index 0000000000..b7d9f78e50 --- /dev/null +++ b/integration-tests/src/test/java/com/ca/mfaas/util/config/DiscoverableClientConfiguration.java @@ -0,0 +1,24 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.util.config; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Configuration parameters for DiscoverableClient + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DiscoverableClientConfiguration { + private String applId; +} diff --git a/integration-tests/src/test/java/com/ca/mfaas/util/config/EnvironmentConfiguration.java b/integration-tests/src/test/java/com/ca/mfaas/util/config/EnvironmentConfiguration.java index 1dc56475de..4906780a27 100644 --- a/integration-tests/src/test/java/com/ca/mfaas/util/config/EnvironmentConfiguration.java +++ b/integration-tests/src/test/java/com/ca/mfaas/util/config/EnvironmentConfiguration.java @@ -20,6 +20,7 @@ public class EnvironmentConfiguration { private Credentials credentials; private GatewayServiceConfiguration gatewayServiceConfiguration; private DiscoveryServiceConfiguration discoveryServiceConfiguration; + private DiscoverableClientConfiguration discoverableClientConfiguration; private TlsConfiguration tlsConfiguration; private ZosmfServiceConfiguration zosmfServiceConfiguration; } diff --git a/integration-tests/src/test/resources/environment-configuration.yml b/integration-tests/src/test/resources/environment-configuration.yml index b6c18bd12a..a59fc96cc3 100644 --- a/integration-tests/src/test/resources/environment-configuration.yml +++ b/integration-tests/src/test/resources/environment-configuration.yml @@ -14,6 +14,8 @@ discoveryServiceConfiguration: host: localhost port: 10011 instances: 1 +discoverableClientConfiguration: + applId: ZOWEAPPL tlsConfiguration: keyAlias: localhost keyPassword: password diff --git a/security-service-client-spring/src/main/resources/security-client-log-messages.yml b/security-service-client-spring/src/main/resources/security-client-log-messages.yml index 82a8614fec..9b4869edc3 100644 --- a/security-service-client-spring/src/main/resources/security-client-log-messages.yml +++ b/security-service-client-spring/src/main/resources/security-client-log-messages.yml @@ -15,13 +15,6 @@ messages: reason: "The HTTP request method is not supported for the URL." action: "Use the correct HTTP request method that is supported for the URL." - - key: apiml.security.expiredToken - number: ZWEAS102 - type: ERROR - text: "Token is expired for URL '%s'" - reason: "The validity of the token is expired." - action: "Obtain new token by performing an authentication request." - - key: apiml.security.gatewayNotAvailable number: ZWEAS103 type: ERROR From ee378a04d874dde243e41af552e6c0e253d9ff3b Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Thu, 16 Jan 2020 22:38:44 +0100 Subject: [PATCH 061/122] Remove redundant JUnit4 definitions Signed-off-by: Petr Plavjanik --- .../apiml/security/common/auth/AuthenticationSchemeTest.java | 3 --- .../com/ca/apiml/security/common/token/QueryResponseTest.java | 3 --- .../java/com/ca/mfaas/cache/CompositeKeyGeneratorTest.java | 3 --- .../src/test/java/com/ca/mfaas/cache/CompositeKeyTest.java | 3 --- .../java/com/ca/mfaas/util/ClassOrDefaultProxyUtilsTest.java | 3 --- .../src/test/java/com/ca/mfaas/util/EurekaUtilsTest.java | 3 --- .../src/test/java/com/ca/mfaas/util/FileUtilsTest.java | 3 --- .../test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java | 3 --- .../security/service/schema/AuthenticationCommandTest.java | 3 --- .../service/schema/AuthenticationSchemeFactoryTest.java | 3 --- .../gateway/security/service/schema/ByPassSchemeTest.java | 3 --- 11 files changed, 33 deletions(-) diff --git a/apiml-security-common/src/test/java/com/ca/apiml/security/common/auth/AuthenticationSchemeTest.java b/apiml-security-common/src/test/java/com/ca/apiml/security/common/auth/AuthenticationSchemeTest.java index 8c7ed544e8..ba0db615ca 100644 --- a/apiml-security-common/src/test/java/com/ca/apiml/security/common/auth/AuthenticationSchemeTest.java +++ b/apiml-security-common/src/test/java/com/ca/apiml/security/common/auth/AuthenticationSchemeTest.java @@ -9,11 +9,8 @@ */ import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; import static org.junit.Assert.*; -@RunWith(JUnit4.class) public class AuthenticationSchemeTest { @Test diff --git a/apiml-security-common/src/test/java/com/ca/apiml/security/common/token/QueryResponseTest.java b/apiml-security-common/src/test/java/com/ca/apiml/security/common/token/QueryResponseTest.java index f6d4fa4b08..02ae7ee38e 100644 --- a/apiml-security-common/src/test/java/com/ca/apiml/security/common/token/QueryResponseTest.java +++ b/apiml-security-common/src/test/java/com/ca/apiml/security/common/token/QueryResponseTest.java @@ -9,8 +9,6 @@ */ import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; import java.util.Calendar; import java.util.Date; @@ -18,7 +16,6 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -@RunWith(JUnit4.class) public class QueryResponseTest { @Test diff --git a/common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyGeneratorTest.java b/common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyGeneratorTest.java index 4a83d8f4a0..b57f968438 100644 --- a/common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyGeneratorTest.java +++ b/common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyGeneratorTest.java @@ -9,14 +9,11 @@ */ import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; import java.lang.reflect.Method; import static org.junit.Assert.*; -@RunWith(JUnit4.class) public class CompositeKeyGeneratorTest { @Test diff --git a/common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyTest.java b/common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyTest.java index 8965e3cb65..647d02de4e 100644 --- a/common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyTest.java +++ b/common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyTest.java @@ -9,12 +9,9 @@ */ import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; import static org.junit.Assert.*; -@RunWith(JUnit4.class) public class CompositeKeyTest { @Test diff --git a/common-service-core/src/test/java/com/ca/mfaas/util/ClassOrDefaultProxyUtilsTest.java b/common-service-core/src/test/java/com/ca/mfaas/util/ClassOrDefaultProxyUtilsTest.java index 822627e44c..5332abd61e 100644 --- a/common-service-core/src/test/java/com/ca/mfaas/util/ClassOrDefaultProxyUtilsTest.java +++ b/common-service-core/src/test/java/com/ca/mfaas/util/ClassOrDefaultProxyUtilsTest.java @@ -12,14 +12,11 @@ import lombok.AllArgsConstructor; import lombok.Getter; import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; import java.lang.reflect.UndeclaredThrowableException; import static org.junit.Assert.*; -@RunWith(JUnit4.class) public class ClassOrDefaultProxyUtilsTest { private static String voidResponse; diff --git a/common-service-core/src/test/java/com/ca/mfaas/util/EurekaUtilsTest.java b/common-service-core/src/test/java/com/ca/mfaas/util/EurekaUtilsTest.java index 6dbe8351cc..41c6772b62 100644 --- a/common-service-core/src/test/java/com/ca/mfaas/util/EurekaUtilsTest.java +++ b/common-service-core/src/test/java/com/ca/mfaas/util/EurekaUtilsTest.java @@ -10,15 +10,12 @@ import com.netflix.appinfo.InstanceInfo; import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -@RunWith(JUnit4.class) public class EurekaUtilsTest { @Test diff --git a/common-service-core/src/test/java/com/ca/mfaas/util/FileUtilsTest.java b/common-service-core/src/test/java/com/ca/mfaas/util/FileUtilsTest.java index b20fdd70b5..5672ab08d2 100644 --- a/common-service-core/src/test/java/com/ca/mfaas/util/FileUtilsTest.java +++ b/common-service-core/src/test/java/com/ca/mfaas/util/FileUtilsTest.java @@ -11,8 +11,6 @@ import org.junit.*; import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; import java.io.File; import java.io.IOException; @@ -22,7 +20,6 @@ import static org.junit.Assert.assertNotNull; -@RunWith(JUnit4.class) public class FileUtilsTest { private static final String RELATIVE_PATH_NAME = "Documents/apiml"; private static final String CONFIG_PATH = System.getProperty("user.home").replace("\\", "/") + File.separator + RELATIVE_PATH_NAME; diff --git a/discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java b/discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java index eae83073d8..799bfe094a 100644 --- a/discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java +++ b/discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java @@ -17,8 +17,6 @@ import com.netflix.eureka.registry.PeerAwareInstanceRegistry; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; import org.springframework.web.client.RestTemplate; import java.util.Arrays; @@ -26,7 +24,6 @@ import static org.mockito.Mockito.*; -@RunWith(JUnit4.class) public class GatewayNotifierTest { private PeerAwareInstanceRegistry registry; diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationCommandTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationCommandTest.java index 25de5e8965..b369f6d584 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationCommandTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationCommandTest.java @@ -9,11 +9,8 @@ */ import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; import static org.junit.Assert.assertFalse; -@RunWith(JUnit4.class) public class AuthenticationCommandTest { @Test diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java index 1c67f5191a..38782325e1 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java @@ -15,8 +15,6 @@ import com.netflix.zuul.context.RequestContext; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; import javax.servlet.http.HttpServletRequest; import java.util.Arrays; @@ -26,7 +24,6 @@ import static org.junit.Assert.*; import static org.mockito.Mockito.*; -@RunWith(JUnit4.class) public class AuthenticationSchemeFactoryTest { private static final AuthenticationCommand COMMAND = mock(AuthenticationCommand.class); diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ByPassSchemeTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ByPassSchemeTest.java index 9f327c4f06..72fc277257 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ByPassSchemeTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ByPassSchemeTest.java @@ -9,12 +9,9 @@ */ import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; -@RunWith(JUnit4.class) public class ByPassSchemeTest { @Test From a67e2e41cf9cdcafd5b1188a028f419a3a1428c1 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Thu, 16 Jan 2020 23:01:23 +0100 Subject: [PATCH 062/122] Making ConvertAuthTokenInUriToCookieFilterTest more stable Signed-off-by: Petr Plavjanik --- ...nvertAuthTokenInUriToCookieFilterTest.java | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/post/ConvertAuthTokenInUriToCookieFilterTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/post/ConvertAuthTokenInUriToCookieFilterTest.java index 931489add5..79bf49d2a2 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/post/ConvertAuthTokenInUriToCookieFilterTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/post/ConvertAuthTokenInUriToCookieFilterTest.java @@ -11,7 +11,6 @@ import com.ca.apiml.security.common.config.AuthConfigurationProperties; import com.netflix.zuul.context.RequestContext; -import org.junit.Before; import org.junit.Test; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -26,40 +25,48 @@ public class ConvertAuthTokenInUriToCookieFilterTest { private final AuthConfigurationProperties authConfigurationProperties = new AuthConfigurationProperties(); - private final ConvertAuthTokenInUriToCookieFilter filter = new ConvertAuthTokenInUriToCookieFilter(authConfigurationProperties); + private final ConvertAuthTokenInUriToCookieFilter filter = new ConvertAuthTokenInUriToCookieFilter( + authConfigurationProperties); - @Before - public void setUp() { + private RequestContext getMockRequestContext() { RequestContext ctx = RequestContext.getCurrentContext(); ctx.clear(); ctx.setResponse(new MockHttpServletResponse()); + return ctx; + } + + private void runFilter(RequestContext ctx) { + synchronized (ctx) { + this.filter.run(); + } } @Test public void doesNotDoAnythingWhenThereIsNoParam() { - final RequestContext ctx = RequestContext.getCurrentContext(); - this.filter.run(); + RequestContext ctx = getMockRequestContext(); + runFilter(ctx); assertFalse(ctx.getResponse().getHeaderNames().contains("Set-Cookie")); } @Test public void doesNotDoAnythingWhenThereIsAnotherParam() { - final RequestContext ctx = RequestContext.getCurrentContext(); + RequestContext ctx = getMockRequestContext(); Map> params = new HashMap<>(); params.put("someParameter", Collections.singletonList("value")); ctx.setRequestQueryParams(params); - this.filter.run(); + runFilter(ctx); assertFalse(ctx.getResponse().getHeaderNames().contains("Set-Cookie")); } @Test public void setsCookieForCorrectParameter() { - final RequestContext ctx = RequestContext.getCurrentContext(); + RequestContext ctx = getMockRequestContext(); ctx.setRequest(new MockHttpServletRequest("GET", "/api/v1/service")); Map> params = new HashMap<>(); - params.put(authConfigurationProperties.getCookieProperties().getCookieName(), Collections.singletonList("token")); + params.put(authConfigurationProperties.getCookieProperties().getCookieName(), + Collections.singletonList("token")); ctx.setRequestQueryParams(params); - this.filter.run(); + runFilter(ctx); assertTrue(ctx.getResponse().getHeaders("Set-Cookie").toString().contains("apimlAuthenticationToken=token")); assertEquals("Location", ctx.getZuulResponseHeaders().get(0).first()); assertEquals("http://localhost/api/v1/service", ctx.getZuulResponseHeaders().get(0).second()); @@ -67,12 +74,13 @@ public void setsCookieForCorrectParameter() { @Test public void setsLocationToDashboardForApiCatalog() { - final RequestContext ctx = RequestContext.getCurrentContext(); + RequestContext ctx = getMockRequestContext(); ctx.setRequest(new MockHttpServletRequest("GET", "/api/v1/apicatalog/")); Map> params = new HashMap<>(); - params.put(authConfigurationProperties.getCookieProperties().getCookieName(), Collections.singletonList("token")); + params.put(authConfigurationProperties.getCookieProperties().getCookieName(), + Collections.singletonList("token")); ctx.setRequestQueryParams(params); - this.filter.run(); + runFilter(ctx); assertTrue(ctx.getResponse().getHeaders("Set-Cookie").toString().contains("apimlAuthenticationToken=token")); assertEquals("Location", ctx.getZuulResponseHeaders().get(0).first()); assertEquals("http://localhost/api/v1/apicatalog/#/dashboard", ctx.getZuulResponseHeaders().get(0).second()); From 38a2c4ab8ea3c466fe2769f6ab098896f1fa35af Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Fri, 17 Jan 2020 10:08:54 +0100 Subject: [PATCH 063/122] Publish gateway-service unit tests report Signed-off-by: Petr Plavjanik --- Jenkinsfile | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index c98dce2e4b..86c360ca27 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -282,6 +282,14 @@ pipeline { always { junit allowEmptyResults: true, testResults: '**/test-results/**/*.xml' archiveArtifacts '.change_class' + publishHTML (target: [ + allowMissing: true, + alwaysLinkToLastBuild: true, + keepAll: true, + reportDir: 'gateway-service/build/reports/tests/test', + reportFiles: 'index.html', + reportName: "Unit Tests Report - gateway-service" + ]) } success { From 2483d80fb2790a1823bb415cdaf8cc6c67ac9a12 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Fri, 17 Jan 2020 10:09:04 +0100 Subject: [PATCH 064/122] wip! Signed-off-by: Petr Plavjanik --- .../filters/post/ConvertAuthTokenInUriToCookieFilterTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/post/ConvertAuthTokenInUriToCookieFilterTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/post/ConvertAuthTokenInUriToCookieFilterTest.java index 79bf49d2a2..fd9a8c5707 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/post/ConvertAuthTokenInUriToCookieFilterTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/post/ConvertAuthTokenInUriToCookieFilterTest.java @@ -45,6 +45,9 @@ private void runFilter(RequestContext ctx) { public void doesNotDoAnythingWhenThereIsNoParam() { RequestContext ctx = getMockRequestContext(); runFilter(ctx); + System.err.println(ctx); + System.err.println(ctx.getResponse()); + System.err.println(ctx.getResponse().getHeaderNames()); assertFalse(ctx.getResponse().getHeaderNames().contains("Set-Cookie")); } From 22d473f332c56c5f580750ff184d2388c7c0d84e Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Fri, 17 Jan 2020 12:36:46 +0100 Subject: [PATCH 065/122] Only one testSetCurrentContext at a time Signed-off-by: Petr Plavjanik --- ...nvertAuthTokenInUriToCookieFilterTest.java | 44 ++++++------------- .../pre/ServiceAuthenticationFilterTest.java | 22 +++++----- .../ServiceAuthenticationServiceImplTest.java | 3 +- .../AuthenticationSchemeFactoryTest.java | 22 +++++----- .../schema/HttpBasicPassTicketSchemeTest.java | 6 ++- .../service/schema/ZosmfSchemeTest.java | 6 ++- .../utils/CurrentRequestContextTest.java | 41 +++++++++++++++++ 7 files changed, 88 insertions(+), 56 deletions(-) create mode 100644 gateway-service/src/test/java/com/ca/mfaas/gateway/utils/CurrentRequestContextTest.java diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/post/ConvertAuthTokenInUriToCookieFilterTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/post/ConvertAuthTokenInUriToCookieFilterTest.java index fd9a8c5707..88026c341e 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/post/ConvertAuthTokenInUriToCookieFilterTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/post/ConvertAuthTokenInUriToCookieFilterTest.java @@ -9,67 +9,50 @@ */ package com.ca.mfaas.gateway.filters.post; -import com.ca.apiml.security.common.config.AuthConfigurationProperties; -import com.netflix.zuul.context.RequestContext; -import org.junit.Test; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import static org.junit.Assert.*; +import com.ca.apiml.security.common.config.AuthConfigurationProperties; +import com.ca.mfaas.gateway.utils.CurrentRequestContextTest; -public class ConvertAuthTokenInUriToCookieFilterTest { +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; + +public class ConvertAuthTokenInUriToCookieFilterTest extends CurrentRequestContextTest { private final AuthConfigurationProperties authConfigurationProperties = new AuthConfigurationProperties(); private final ConvertAuthTokenInUriToCookieFilter filter = new ConvertAuthTokenInUriToCookieFilter( authConfigurationProperties); - private RequestContext getMockRequestContext() { - RequestContext ctx = RequestContext.getCurrentContext(); - ctx.clear(); - ctx.setResponse(new MockHttpServletResponse()); - return ctx; - } - - private void runFilter(RequestContext ctx) { - synchronized (ctx) { - this.filter.run(); - } - } - @Test public void doesNotDoAnythingWhenThereIsNoParam() { - RequestContext ctx = getMockRequestContext(); - runFilter(ctx); - System.err.println(ctx); - System.err.println(ctx.getResponse()); - System.err.println(ctx.getResponse().getHeaderNames()); + this.filter.run(); assertFalse(ctx.getResponse().getHeaderNames().contains("Set-Cookie")); } @Test public void doesNotDoAnythingWhenThereIsAnotherParam() { - RequestContext ctx = getMockRequestContext(); Map> params = new HashMap<>(); params.put("someParameter", Collections.singletonList("value")); ctx.setRequestQueryParams(params); - runFilter(ctx); + this.filter.run(); assertFalse(ctx.getResponse().getHeaderNames().contains("Set-Cookie")); } @Test public void setsCookieForCorrectParameter() { - RequestContext ctx = getMockRequestContext(); ctx.setRequest(new MockHttpServletRequest("GET", "/api/v1/service")); Map> params = new HashMap<>(); params.put(authConfigurationProperties.getCookieProperties().getCookieName(), Collections.singletonList("token")); ctx.setRequestQueryParams(params); - runFilter(ctx); + this.filter.run(); assertTrue(ctx.getResponse().getHeaders("Set-Cookie").toString().contains("apimlAuthenticationToken=token")); assertEquals("Location", ctx.getZuulResponseHeaders().get(0).first()); assertEquals("http://localhost/api/v1/service", ctx.getZuulResponseHeaders().get(0).second()); @@ -77,13 +60,12 @@ public void setsCookieForCorrectParameter() { @Test public void setsLocationToDashboardForApiCatalog() { - RequestContext ctx = getMockRequestContext(); ctx.setRequest(new MockHttpServletRequest("GET", "/api/v1/apicatalog/")); Map> params = new HashMap<>(); params.put(authConfigurationProperties.getCookieProperties().getCookieName(), Collections.singletonList("token")); ctx.setRequestQueryParams(params); - runFilter(ctx); + this.filter.run(); assertTrue(ctx.getResponse().getHeaders("Set-Cookie").toString().contains("apimlAuthenticationToken=token")); assertEquals("Location", ctx.getZuulResponseHeaders().get(0).first()); assertEquals("http://localhost/api/v1/apicatalog/#/dashboard", ctx.getZuulResponseHeaders().get(0).second()); diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java index 84f74f3c74..e8b1cf8944 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java @@ -1,16 +1,18 @@ -package com.ca.mfaas.gateway.filters.pre;/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +*/ +package com.ca.mfaas.gateway.filters.pre; import com.ca.mfaas.gateway.security.service.AuthenticationService; import com.ca.mfaas.gateway.security.service.ServiceAuthenticationServiceImpl; import com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand; +import com.ca.mfaas.gateway.utils.CurrentRequestContextTest; import com.netflix.zuul.context.RequestContext; import org.junit.Before; import org.junit.Test; @@ -27,7 +29,7 @@ import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SERVICE_ID_KEY; @RunWith(MockitoJUnitRunner.class) -public class ServiceAuthenticationFilterTest { +public class ServiceAuthenticationFilterTest extends CurrentRequestContextTest { @Mock private ServiceAuthenticationServiceImpl serviceAuthenticationService; diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java index 2437ced533..cebde95c75 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java @@ -14,6 +14,7 @@ import com.ca.apiml.security.common.token.QueryResponse; import com.ca.mfaas.gateway.config.CacheConfig; import com.ca.mfaas.gateway.security.service.schema.*; +import com.ca.mfaas.gateway.utils.CurrentRequestContextTest; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.EurekaClient; import com.netflix.discovery.shared.Application; @@ -46,7 +47,7 @@ ServiceAuthenticationServiceImplTest.Context.class, CacheConfig.class }) -public class ServiceAuthenticationServiceImplTest { +public class ServiceAuthenticationServiceImplTest extends CurrentRequestContextTest { @Autowired private EurekaClient discoveryClient; diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java index 38782325e1..995f7d4d10 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java @@ -1,17 +1,19 @@ -package com.ca.mfaas.gateway.security.service.schema;/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ +/* +* This program and the accompanying materials are made available under the terms of the +* Eclipse Public License v2.0 which accompanies this distribution, and is available at +* https://www.eclipse.org/legal/epl-v20.html +* +* SPDX-License-Identifier: EPL-2.0 +* +* Copyright Contributors to the Zowe Project. +*/ +package com.ca.mfaas.gateway.security.service.schema; import com.ca.apiml.security.common.auth.Authentication; import com.ca.apiml.security.common.auth.AuthenticationScheme; import com.ca.apiml.security.common.token.QueryResponse; import com.ca.mfaas.gateway.security.service.AuthenticationService; +import com.ca.mfaas.gateway.utils.CurrentRequestContextTest; import com.netflix.zuul.context.RequestContext; import org.junit.Before; import org.junit.Test; @@ -24,7 +26,7 @@ import static org.junit.Assert.*; import static org.mockito.Mockito.*; -public class AuthenticationSchemeFactoryTest { +public class AuthenticationSchemeFactoryTest extends CurrentRequestContextTest { private static final AuthenticationCommand COMMAND = mock(AuthenticationCommand.class); diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java index 3d244cc5dd..5537e108b2 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java @@ -1,4 +1,4 @@ -package com.ca.mfaas.gateway.security.service.schema;/* +/* * This program and the accompanying materials are made available under the terms of the * Eclipse Public License v2.0 which accompanies this distribution, and is available at * https://www.eclipse.org/legal/epl-v20.html @@ -7,11 +7,13 @@ * * Copyright Contributors to the Zowe Project. */ +package com.ca.mfaas.gateway.security.service.schema; import com.ca.apiml.security.common.auth.Authentication; import com.ca.apiml.security.common.auth.AuthenticationScheme; import com.ca.apiml.security.common.service.PassTicketService; import com.ca.apiml.security.common.token.QueryResponse; +import com.ca.mfaas.gateway.utils.CurrentRequestContextTest; import com.netflix.zuul.context.RequestContext; import org.junit.Before; import org.junit.Test; @@ -29,7 +31,7 @@ import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) -public class HttpBasicPassTicketSchemeTest { +public class HttpBasicPassTicketSchemeTest extends CurrentRequestContextTest { private final int PASSTICKET_DURATION = 300; diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZosmfSchemeTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZosmfSchemeTest.java index bb9bccb4f4..678ea09021 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZosmfSchemeTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZosmfSchemeTest.java @@ -1,4 +1,4 @@ -package com.ca.mfaas.gateway.security.service.schema;/* +/* * This program and the accompanying materials are made available under the terms of the * Eclipse Public License v2.0 which accompanies this distribution, and is available at * https://www.eclipse.org/legal/epl-v20.html @@ -7,12 +7,14 @@ * * Copyright Contributors to the Zowe Project. */ +package com.ca.mfaas.gateway.security.service.schema; import com.ca.apiml.security.common.auth.Authentication; import com.ca.apiml.security.common.auth.AuthenticationScheme; import com.ca.apiml.security.common.token.QueryResponse; import com.ca.apiml.security.common.token.TokenNotValidException; import com.ca.mfaas.gateway.security.service.AuthenticationService; +import com.ca.mfaas.gateway.utils.CurrentRequestContextTest; import com.netflix.zuul.context.RequestContext; import io.jsonwebtoken.JwtException; import org.junit.Test; @@ -32,7 +34,7 @@ import static org.mockito.Mockito.*; @RunWith(MockitoJUnitRunner.class) -public class ZosmfSchemeTest { +public class ZosmfSchemeTest extends CurrentRequestContextTest { @Mock private AuthenticationService authenticationService; diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/utils/CurrentRequestContextTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/utils/CurrentRequestContextTest.java new file mode 100644 index 0000000000..a2adb45559 --- /dev/null +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/utils/CurrentRequestContextTest.java @@ -0,0 +1,41 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.utils; + +import java.util.concurrent.locks.ReentrantLock; + +import com.netflix.zuul.context.RequestContext; + +import org.junit.After; +import org.junit.Before; +import org.springframework.mock.web.MockHttpServletResponse; + +/** + * Makes sure that only one test class derived from this class is using + * RequestContext.getCurrentContext at a time + */ +public class CurrentRequestContextTest { + private final ReentrantLock currentRequestContext = new ReentrantLock(); + + protected RequestContext ctx; + + @Before + public void setUp() { + currentRequestContext.lock(); + ctx = RequestContext.getCurrentContext(); + ctx.clear(); + ctx.setResponse(new MockHttpServletResponse()); + } + + @After + public void tearDown() { + currentRequestContext.unlock(); + } +} From 3bcd24bc7a99ad69cfc762f9082ba6c6d870bb7b Mon Sep 17 00:00:00 2001 From: JirkaAichler Date: Fri, 17 Jan 2020 14:15:46 +0100 Subject: [PATCH 066/122] Fix few minor Sonar issues Signed-off-by: JirkaAichler --- .../mfaas/util/ClassOrDefaultProxyUtils.java | 20 ++++++++-------- discoverable-client/build.gradle | 1 - .../GatewayRibbonLoadBalancingHttpClient.java | 23 +++++++++++-------- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java b/common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java index 082905d56b..20588de885 100644 --- a/common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java +++ b/common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java @@ -100,13 +100,13 @@ public interface ClassOrDefaultProxyState { * * @return class which is now proxied. It could be one of implementationClassName or defaultImplementation */ - public Class getImplementationClass(); + Class getImplementationClass(); /** * * @return true if proxy use the original class, false if is using default (dummy) class */ - public boolean isUsingBaseImplementation(); + boolean isUsingBaseImplementation(); } @@ -256,24 +256,24 @@ public Object invoke(Object[] args) throws InvocationTargetException, IllegalAcc } /** - * Interface to controll an exception mapping. It offer base method to make controll on exception conversion + * Interface to control an exception mapping. It offer base method to make control on exception conversion * from Handler side. * @param Target exception (which is thrown in case of mapping) */ public interface ExceptionMapping { /** - * Method indicate if mapping was created well (to controll from proxy handler) + * Method indicate if mapping was created well (to control from proxy handler) * @return true if mapping is ready to use otherwise false */ - public boolean isInitialized(); + boolean isInitialized(); /** - * Method will check if t is able to map. If yes, make mapping and throw new (mapped) expception + * Method will check if t is able to map. If yes, make mapping and throw new (mapped) exception * @param t Original exception * @throws T Type of mapped exception to throw in case of mapping is right to type of t */ - public void apply(Throwable t) throws T; + void apply(Throwable t) throws T; } @@ -281,7 +281,7 @@ public interface ExceptionMapping { * This exception mapper is based on getter in source exception. It allows to get from zero to N getters without * argument in source exception and use them as arguments in target constructor. * - * You have to define same count of getter names (with same result type) as a contructor in target exception. The + * You have to define same count of getter names (with same result type) as a constructor in target exception. The * order has to be also same. * * @param Type of target exception @@ -322,7 +322,7 @@ private Method findMethod(Class clazz, String methodName) { /** * Method find constructor in target exception, prepare lambdas to get values from source exception and return - * function to comvert source exception to new (target) one + * function to convert source exception to new (target) one * @param sourceExceptionClassName name of exception to map * @param targetExceptionClass exception which could be construct after mapping * @param methodNames names of methods without argument on source exception to get values into constructor to create target exception @@ -359,7 +359,7 @@ private Function getMappingFunction(String sourceExceptionClassNam // find the constructor and store functions to invoke then try { - return getMappingFunction((Constructor) targetExceptionClass.getConstructor(argClasses.toArray(new Class[0])), mapFunctions); + return getMappingFunction(targetExceptionClass.getConstructor(argClasses.toArray(new Class[0])), mapFunctions); } catch (NoSuchMethodException e) { log.debug("Cannot find constructor on {} with {}", sourceExceptionClassName, argClasses); return null; diff --git a/discoverable-client/build.gradle b/discoverable-client/build.gradle index f4479fc75a..eec6792482 100644 --- a/discoverable-client/build.gradle +++ b/discoverable-client/build.gradle @@ -32,7 +32,6 @@ gitProperties { dependencies { compile(project(':integration-enabler-spring-v2')) - compile(project(':apiml-common')) compile(project(':apiml-security-common')) compile libraries.gson diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java index 74da6a7ce2..738787132d 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java @@ -52,8 +52,9 @@ public class GatewayRibbonLoadBalancingHttpClient extends RibbonLoadBalancingHtt /** * Ribbon load balancer - * @param secureHttpClient custom http client for our certificates - * @param config configuration details + * + * @param secureHttpClient custom http client for our certificates + * @param config configuration details * @param serverIntrospector introspector */ public GatewayRibbonLoadBalancingHttpClient(CloseableHttpClient secureHttpClient, IClientConfig config, ServerIntrospector serverIntrospector, EurekaClient discoveryClient) { @@ -114,8 +115,9 @@ public RibbonApacheHttpResponse execute(RibbonApacheHttpRequest request, IClient /** * This methods write specific InstanceInfo into cache - * @param serviceId serviceId of instance - * @param instanceId ID of instance + * + * @param serviceId serviceId of instance + * @param instanceId ID of instance * @param instanceInfo instanceInfo to store * @return cached instanceInfo object */ @@ -127,7 +129,8 @@ public InstanceInfo putInstanceInfo(String serviceId, String instanceId, Instanc /** * Get the InstanceInfo by id. For searching is used serviceId. It method found another instances it will * cache them for next using - * @param serviceId service to call + * + * @param serviceId service to call * @param instanceId selected instance of service * @return instance with matching service and instanceId */ @@ -136,7 +139,7 @@ public InstanceInfo getInstanceInfo(String serviceId, String instanceId) { InstanceInfo output = null; Application application = discoveryClient.getApplication(serviceId); - if (application == null) return output; + if (application == null) return null; for (final InstanceInfo instanceInfo : application.getInstances()) { if (StringUtils.equals(instanceId, instanceInfo.getInstanceId())) { @@ -158,20 +161,20 @@ protected void customizeLoadBalancerCommandBuilder(RibbonApacheHttpRequest reque super.customizeLoadBalancerCommandBuilder(request, config, builder); /* - * add into builder listener to work with request immediatelly when instance if selected - * it is helpfull for selecting {@com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand} in + * add into builder listener to work with request immediately when instance if selected + * it is helpful for selecting {@com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand} in * case there is multiple instances with same serviceId which have different authentication. Therefor it * is necessary wait for selection of instance to apply right authentication command. */ builder.withListeners(Collections.singletonList(new ExecutionListener() { @Override - public void onExecutionStart(ExecutionContext context) throws AbortExecutionException { + public void onExecutionStart(ExecutionContext context) { // dont needed yet } @Override - public void onStartWithServer(ExecutionContext context, ExecutionInfo info) throws AbortExecutionException { + public void onStartWithServer(ExecutionContext context, ExecutionInfo info) { final AuthenticationCommand cmd = (AuthenticationCommand) RequestContext.getCurrentContext().get(AUTHENTICATION_COMMAND_KEY); if (cmd != null) { // in context is a command, it means update of authentication is waiting for select an instance From 959fafef23456e945340de8ef576d421fdd471a7 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Fri, 17 Jan 2020 15:09:44 +0100 Subject: [PATCH 067/122] fixup! Signed-off-by: Petr Plavjanik --- .../com/ca/mfaas/gateway/utils/CurrentRequestContextTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/utils/CurrentRequestContextTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/utils/CurrentRequestContextTest.java index a2adb45559..a2e19dba0d 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/utils/CurrentRequestContextTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/utils/CurrentRequestContextTest.java @@ -22,7 +22,7 @@ * RequestContext.getCurrentContext at a time */ public class CurrentRequestContextTest { - private final ReentrantLock currentRequestContext = new ReentrantLock(); + private final static ReentrantLock currentRequestContext = new ReentrantLock(); protected RequestContext ctx; From 15b912db0fa3435f3397ebd07615bfd2cb03fd2d Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Fri, 17 Jan 2020 19:06:28 +0100 Subject: [PATCH 068/122] Disable parallel tests Signed-off-by: Petr Plavjanik --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 3d0f77f572..b3a8c72ca4 100644 --- a/build.gradle +++ b/build.gradle @@ -80,7 +80,7 @@ subprojects { } tasks.withType(Test) { - maxParallelForks = Runtime.runtime.availableProcessors() + maxParallelForks = 1 } } @@ -193,4 +193,4 @@ if (hasProperty('buildScan')) { termsOfServiceUrl = 'https://gradle.com/terms-of-service' termsOfServiceAgree = 'yes' } -} \ No newline at end of file +} From e91d0635c9048f7fe746b9c85a70e4e7ecc271a5 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Fri, 17 Jan 2020 19:14:45 +0100 Subject: [PATCH 069/122] Disable parallel builds Signed-off-by: Petr Plavjanik --- gradle.properties | 2 +- integration-tests/build.gradle | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/gradle.properties b/gradle.properties index dd59e32864..41c7b2dfee 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,4 +25,4 @@ nodejsVersion=8.11.1 projectRoot=${project.projectDir} org.gradle.daemon=false -org.gradle.parallel=true +org.gradle.parallel=false diff --git a/integration-tests/build.gradle b/integration-tests/build.gradle index ecaac80a2f..233ef2e477 100644 --- a/integration-tests/build.gradle +++ b/integration-tests/build.gradle @@ -43,10 +43,6 @@ jar { baseName = "integration-tests" } -testlogger { - theme 'standard-parallel' -} - test.enabled = false apply plugin : 'java' From ad4ff417f7519e84ab6643b8ad220ffc9ab1e320 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Fri, 17 Jan 2020 19:55:50 +0100 Subject: [PATCH 070/122] fixup! Signed-off-by: Petr Plavjanik --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 86c360ca27..872922bd5e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -80,7 +80,7 @@ properties(opts) pipeline { agent { - label 'apiml-jenkins-agent-swarm' + label 'apiml-jenkins-agent' } options { From 46400039b7d6832e0b6ea2edb82942038a055d22 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Sat, 18 Jan 2020 12:02:54 +0100 Subject: [PATCH 071/122] fixup! Signed-off-by: Petr Plavjanik --- ...nvertAuthTokenInUriToCookieFilterTest.java | 4 +- .../pre/ServiceAuthenticationFilterTest.java | 5 +- .../ServiceAuthenticationServiceImplTest.java | 54 ++++++++++++++----- .../AuthenticationSchemeFactoryTest.java | 36 +++++++------ .../schema/HttpBasicPassTicketSchemeTest.java | 21 +++++--- .../service/schema/ZosmfSchemeTest.java | 30 +++++++---- .../utils/CleanCurrentRequestContextTest.java | 29 ++++++++++ .../utils/CurrentRequestContextTest.java | 15 +++--- 8 files changed, 135 insertions(+), 59 deletions(-) create mode 100644 gateway-service/src/test/java/com/ca/mfaas/gateway/utils/CleanCurrentRequestContextTest.java diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/post/ConvertAuthTokenInUriToCookieFilterTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/post/ConvertAuthTokenInUriToCookieFilterTest.java index 88026c341e..948b01fa6e 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/post/ConvertAuthTokenInUriToCookieFilterTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/post/ConvertAuthTokenInUriToCookieFilterTest.java @@ -19,12 +19,12 @@ import java.util.Map; import com.ca.apiml.security.common.config.AuthConfigurationProperties; -import com.ca.mfaas.gateway.utils.CurrentRequestContextTest; +import com.ca.mfaas.gateway.utils.CleanCurrentRequestContextTest; import org.junit.Test; import org.springframework.mock.web.MockHttpServletRequest; -public class ConvertAuthTokenInUriToCookieFilterTest extends CurrentRequestContextTest { +public class ConvertAuthTokenInUriToCookieFilterTest extends CleanCurrentRequestContextTest { private final AuthConfigurationProperties authConfigurationProperties = new AuthConfigurationProperties(); private final ConvertAuthTokenInUriToCookieFilter filter = new ConvertAuthTokenInUriToCookieFilter( diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java index e8b1cf8944..efc543f3c0 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java @@ -12,8 +12,9 @@ import com.ca.mfaas.gateway.security.service.AuthenticationService; import com.ca.mfaas.gateway.security.service.ServiceAuthenticationServiceImpl; import com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand; -import com.ca.mfaas.gateway.utils.CurrentRequestContextTest; +import com.ca.mfaas.gateway.utils.CleanCurrentRequestContextTest; import com.netflix.zuul.context.RequestContext; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -29,7 +30,7 @@ import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SERVICE_ID_KEY; @RunWith(MockitoJUnitRunner.class) -public class ServiceAuthenticationFilterTest extends CurrentRequestContextTest { +public class ServiceAuthenticationFilterTest extends CleanCurrentRequestContextTest { @Mock private ServiceAuthenticationServiceImpl serviceAuthenticationService; diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java index cebde95c75..6ae4841b1d 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java @@ -9,16 +9,51 @@ */ package com.ca.mfaas.gateway.security.service; +import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_APPLID; +import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_SCHEME; +import static com.ca.mfaas.gateway.security.service.ServiceAuthenticationServiceImpl.AUTHENTICATION_COMMAND_KEY; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.sql.Date; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import javax.servlet.http.HttpServletRequest; + import com.ca.apiml.security.common.auth.Authentication; import com.ca.apiml.security.common.auth.AuthenticationScheme; import com.ca.apiml.security.common.token.QueryResponse; import com.ca.mfaas.gateway.config.CacheConfig; -import com.ca.mfaas.gateway.security.service.schema.*; +import com.ca.mfaas.gateway.security.service.schema.AbstractAuthenticationScheme; +import com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand; +import com.ca.mfaas.gateway.security.service.schema.AuthenticationSchemeFactory; +import com.ca.mfaas.gateway.security.service.schema.ByPassScheme; +import com.ca.mfaas.gateway.security.service.schema.ServiceAuthenticationService; import com.ca.mfaas.gateway.utils.CurrentRequestContextTest; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.EurekaClient; import com.netflix.discovery.shared.Application; import com.netflix.zuul.context.RequestContext; + +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -31,17 +66,6 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import javax.servlet.http.HttpServletRequest; -import java.sql.Date; -import java.time.LocalDate; -import java.util.*; - -import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_APPLID; -import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_SCHEME; -import static com.ca.mfaas.gateway.security.service.ServiceAuthenticationServiceImpl.AUTHENTICATION_COMMAND_KEY; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; - @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = { ServiceAuthenticationServiceImplTest.Context.class, @@ -71,6 +95,7 @@ public class ServiceAuthenticationServiceImplTest extends CurrentRequestContextT @Before public void init() { + lockAndClearRequestContext(); MockitoAnnotations.initMocks(this); RequestContext.testSetCurrentContext(null); serviceAuthenticationService.evictCacheAllService(); @@ -78,6 +103,11 @@ public void init() { serviceAuthenticationServiceImpl = new ServiceAuthenticationServiceImpl(discoveryClient, authenticationSchemeFactory, authenticationService, cacheManager); } + @After + public void tearDown() { + unlockRequestContext(); + } + private InstanceInfo createInstanceInfo(String instanceId, String scheme, String applid) { InstanceInfo out = mock(InstanceInfo.class); diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java index 995f7d4d10..cab4ff1e99 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java @@ -9,24 +9,33 @@ */ package com.ca.mfaas.gateway.security.service.schema; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Date; +import java.util.Optional; + +import javax.servlet.http.HttpServletRequest; + import com.ca.apiml.security.common.auth.Authentication; import com.ca.apiml.security.common.auth.AuthenticationScheme; import com.ca.apiml.security.common.token.QueryResponse; import com.ca.mfaas.gateway.security.service.AuthenticationService; -import com.ca.mfaas.gateway.utils.CurrentRequestContextTest; +import com.ca.mfaas.gateway.utils.CleanCurrentRequestContextTest; import com.netflix.zuul.context.RequestContext; -import org.junit.Before; -import org.junit.Test; -import javax.servlet.http.HttpServletRequest; -import java.util.Arrays; -import java.util.Date; -import java.util.Optional; - -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import org.junit.Test; -public class AuthenticationSchemeFactoryTest extends CurrentRequestContextTest { +public class AuthenticationSchemeFactoryTest extends CleanCurrentRequestContextTest { private static final AuthenticationCommand COMMAND = mock(AuthenticationCommand.class); @@ -49,11 +58,6 @@ public AuthenticationCommand createCommand(Authentication authentication, QueryR }; } - @Before - public void init() { - RequestContext.testSetCurrentContext(null); - } - @Test public void testInit() { // happy day diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java index 5537e108b2..4916515077 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java @@ -9,12 +9,23 @@ */ package com.ca.mfaas.gateway.security.service.schema; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +import java.util.Calendar; + +import javax.servlet.http.HttpServletRequest; + import com.ca.apiml.security.common.auth.Authentication; import com.ca.apiml.security.common.auth.AuthenticationScheme; import com.ca.apiml.security.common.service.PassTicketService; import com.ca.apiml.security.common.token.QueryResponse; -import com.ca.mfaas.gateway.utils.CurrentRequestContextTest; +import com.ca.mfaas.gateway.utils.CleanCurrentRequestContextTest; import com.netflix.zuul.context.RequestContext; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -24,14 +35,8 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.test.util.ReflectionTestUtils; -import javax.servlet.http.HttpServletRequest; -import java.util.Calendar; - -import static org.junit.Assert.*; -import static org.mockito.Mockito.when; - @RunWith(MockitoJUnitRunner.class) -public class HttpBasicPassTicketSchemeTest extends CurrentRequestContextTest { +public class HttpBasicPassTicketSchemeTest extends CleanCurrentRequestContextTest { private final int PASSTICKET_DURATION = 300; diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZosmfSchemeTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZosmfSchemeTest.java index 678ea09021..1c6406f528 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZosmfSchemeTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZosmfSchemeTest.java @@ -9,14 +9,29 @@ */ package com.ca.mfaas.gateway.security.service.schema; +import static com.ca.mfaas.gateway.security.service.schema.ZosmfScheme.ZosmfCommand.COOKIE_HEADER; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Calendar; +import java.util.Optional; + +import javax.servlet.http.HttpServletRequest; + import com.ca.apiml.security.common.auth.Authentication; import com.ca.apiml.security.common.auth.AuthenticationScheme; import com.ca.apiml.security.common.token.QueryResponse; import com.ca.apiml.security.common.token.TokenNotValidException; import com.ca.mfaas.gateway.security.service.AuthenticationService; -import com.ca.mfaas.gateway.utils.CurrentRequestContextTest; +import com.ca.mfaas.gateway.utils.CleanCurrentRequestContextTest; import com.netflix.zuul.context.RequestContext; -import io.jsonwebtoken.JwtException; + import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; @@ -24,17 +39,10 @@ import org.mockito.junit.MockitoJUnitRunner; import org.springframework.mock.web.MockHttpServletRequest; -import javax.servlet.http.HttpServletRequest; -import java.util.Calendar; -import java.util.Optional; - -import static com.ca.mfaas.gateway.security.service.schema.ZosmfScheme.ZosmfCommand.COOKIE_HEADER; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.*; +import io.jsonwebtoken.JwtException; @RunWith(MockitoJUnitRunner.class) -public class ZosmfSchemeTest extends CurrentRequestContextTest { +public class ZosmfSchemeTest extends CleanCurrentRequestContextTest { @Mock private AuthenticationService authenticationService; diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/utils/CleanCurrentRequestContextTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/utils/CleanCurrentRequestContextTest.java new file mode 100644 index 0000000000..de03c48518 --- /dev/null +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/utils/CleanCurrentRequestContextTest.java @@ -0,0 +1,29 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.utils; + +import org.junit.After; +import org.junit.Before; + +/** + * Makes sure that only one test class derived from this class is using + * RequestContext.getCurrentContext at a time. + */ +public class CleanCurrentRequestContextTest extends CurrentRequestContextTest { + @Before + public void setup() { + this.lockAndClearRequestContext(); + } + + @After + public void tearDown() { + this.unlockRequestContext(); + } +} diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/utils/CurrentRequestContextTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/utils/CurrentRequestContextTest.java index a2e19dba0d..4a66187511 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/utils/CurrentRequestContextTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/utils/CurrentRequestContextTest.java @@ -13,29 +13,28 @@ import com.netflix.zuul.context.RequestContext; -import org.junit.After; -import org.junit.Before; import org.springframework.mock.web.MockHttpServletResponse; /** - * Makes sure that only one test class derived from this class is using - * RequestContext.getCurrentContext at a time + * Provides functions to lock, clear, and unlock CurrentRequestContext in a test + * so there are no race conditions in parallel test execution. */ public class CurrentRequestContextTest { private final static ReentrantLock currentRequestContext = new ReentrantLock(); protected RequestContext ctx; - @Before - public void setUp() { + public void lockAndClearRequestContext() { currentRequestContext.lock(); + RequestContext.testSetCurrentContext(null); ctx = RequestContext.getCurrentContext(); ctx.clear(); ctx.setResponse(new MockHttpServletResponse()); } - @After - public void tearDown() { + public void unlockRequestContext() { + RequestContext.testSetCurrentContext(null); + ctx.clear(); currentRequestContext.unlock(); } } From 69d8fb9281a0a62a04a6adf2ba791f932e00aea3 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Sat, 18 Jan 2020 12:46:09 +0100 Subject: [PATCH 072/122] Revert build agent change in Jenkinsfile This reverts commit ad4ff417f7519e84ab6643b8ad220ffc9ab1e320. --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 872922bd5e..86c360ca27 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -80,7 +80,7 @@ properties(opts) pipeline { agent { - label 'apiml-jenkins-agent' + label 'apiml-jenkins-agent-swarm' } options { From 5a26b27520560b8011c502ea55deee893e924bc1 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Sat, 18 Jan 2020 12:52:30 +0100 Subject: [PATCH 073/122] Build agent change in Jenkinsfile to prevent TomcatHttpsTest failures This reverts commit 69d8fb9281a0a62a04a6adf2ba791f932e00aea3. --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 86c360ca27..872922bd5e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -80,7 +80,7 @@ properties(opts) pipeline { agent { - label 'apiml-jenkins-agent-swarm' + label 'apiml-jenkins-agent' } options { From deaec3b21ce2c8fd3c5e9ef94b86bb9e9fdf5c88 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Sat, 18 Jan 2020 13:07:34 +0100 Subject: [PATCH 074/122] Clean javax.net.ssl setting before TomcatHttpsTests Signed-off-by: Petr Plavjanik --- Jenkinsfile | 2 +- .../src/test/java/com/ca/mfaas/tomcat/TomcatHttpsTest.java | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 872922bd5e..86c360ca27 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -80,7 +80,7 @@ properties(opts) pipeline { agent { - label 'apiml-jenkins-agent' + label 'apiml-jenkins-agent-swarm' } options { diff --git a/common-service-core/src/test/java/com/ca/mfaas/tomcat/TomcatHttpsTest.java b/common-service-core/src/test/java/com/ca/mfaas/tomcat/TomcatHttpsTest.java index 02ba363500..330812be28 100644 --- a/common-service-core/src/test/java/com/ca/mfaas/tomcat/TomcatHttpsTest.java +++ b/common-service-core/src/test/java/com/ca/mfaas/tomcat/TomcatHttpsTest.java @@ -24,6 +24,7 @@ import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.util.EntityUtils; +import org.junit.Before; import org.junit.Test; import java.io.IOException; @@ -37,6 +38,12 @@ public class TomcatHttpsTest { private static final String EXPECTED_HTTPS_CONFIG_ERROR_NOT_THROWN = "excepted HttpsConfigError exception not thrown"; private static final String UNABLE_TO_FIND_CERTIFICATION_PATH_MESSAGE = "unable to find valid certification path"; + @Before + public void setUp() { + System.clearProperty("javax.net.ssl.keyStore"); + System.clearProperty("javax.net.ssl.trustStore"); + } + @Test public void correctConfigurationShouldWork() throws IOException, LifecycleException { HttpsConfig httpsConfig = SecurityTestUtils.correctHttpsSettings().build(); From 42ca0a4fb8fb068384812bf5747619316a1fca0f Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Sat, 18 Jan 2020 15:04:12 +0100 Subject: [PATCH 075/122] Suggested by Jack Signed-off-by: Petr Plavjanik --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 86c360ca27..872922bd5e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -80,7 +80,7 @@ properties(opts) pipeline { agent { - label 'apiml-jenkins-agent-swarm' + label 'apiml-jenkins-agent' } options { From d9e9a04201ddbe3fb9e49fa0bde3e7b39807d216 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Sat, 18 Jan 2020 15:55:16 +0100 Subject: [PATCH 076/122] Use swarm agent after Jack's fix Signed-off-by: Petr Plavjanik --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 872922bd5e..86c360ca27 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -80,7 +80,7 @@ properties(opts) pipeline { agent { - label 'apiml-jenkins-agent' + label 'apiml-jenkins-agent-swarm' } options { From cc425634fa419fd8b02eaf731f12419346dfd957 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Sat, 18 Jan 2020 16:49:45 +0100 Subject: [PATCH 077/122] wip! Signed-off-by: Petr Plavjanik --- .../test/java/com/ca/mfaas/tomcat/TomcatHttpsTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/common-service-core/src/test/java/com/ca/mfaas/tomcat/TomcatHttpsTest.java b/common-service-core/src/test/java/com/ca/mfaas/tomcat/TomcatHttpsTest.java index 330812be28..490528be44 100644 --- a/common-service-core/src/test/java/com/ca/mfaas/tomcat/TomcatHttpsTest.java +++ b/common-service-core/src/test/java/com/ca/mfaas/tomcat/TomcatHttpsTest.java @@ -27,12 +27,15 @@ import org.junit.Before; import org.junit.Test; +import lombok.extern.slf4j.Slf4j; + import java.io.IOException; import javax.net.ssl.SSLHandshakeException; import com.ca.mfaas.security.SecurityTestUtils; +@Slf4j public class TomcatHttpsTest { private static final String EXPECTED_SSL_HANDSHAKE_EXCEPTION_NOT_THROWN = "excepted SSLHandshakeException exception not thrown"; private static final String EXPECTED_HTTPS_CONFIG_ERROR_NOT_THROWN = "excepted HttpsConfigError exception not thrown"; @@ -41,7 +44,11 @@ public class TomcatHttpsTest { @Before public void setUp() { System.clearProperty("javax.net.ssl.keyStore"); + System.clearProperty("javax.net.ssl.keyStorePassword"); + System.clearProperty("javax.net.ssl.keyStoreType"); System.clearProperty("javax.net.ssl.trustStore"); + System.clearProperty("javax.net.ssl.trustStorePassword"); + System.clearProperty("javax.net.ssl.keyStoreType"); } @Test @@ -57,6 +64,7 @@ public void noTrustStoreShouldFail() throws IOException, LifecycleException { startTomcatAndDoHttpsRequest(httpsConfig); fail(EXPECTED_SSL_HANDSHAKE_EXCEPTION_NOT_THROWN); } catch (SSLHandshakeException e) { // NOSONAR + log.info("SSLHandshakeException: {}", e, e); assertTrue(e.getMessage().contains(UNABLE_TO_FIND_CERTIFICATION_PATH_MESSAGE)); } } From ac6cba721392623554ab400ab473df403c52b77f Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Sun, 19 Jan 2020 14:00:16 +0100 Subject: [PATCH 078/122] fixup! Signed-off-by: Petr Plavjanik --- .../src/test/java/com/ca/mfaas/tomcat/TomcatHttpsTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common-service-core/src/test/java/com/ca/mfaas/tomcat/TomcatHttpsTest.java b/common-service-core/src/test/java/com/ca/mfaas/tomcat/TomcatHttpsTest.java index 490528be44..68e142a08d 100644 --- a/common-service-core/src/test/java/com/ca/mfaas/tomcat/TomcatHttpsTest.java +++ b/common-service-core/src/test/java/com/ca/mfaas/tomcat/TomcatHttpsTest.java @@ -48,7 +48,7 @@ public void setUp() { System.clearProperty("javax.net.ssl.keyStoreType"); System.clearProperty("javax.net.ssl.trustStore"); System.clearProperty("javax.net.ssl.trustStorePassword"); - System.clearProperty("javax.net.ssl.keyStoreType"); + System.clearProperty("javax.net.ssl.trustStoreType"); } @Test From bd8b23ab27ae2d305809a51828cadd84d19c1bc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Mon, 20 Jan 2020 16:03:01 +0100 Subject: [PATCH 079/122] integration test about deploying new version with different Authentication, fixes of caches and load balancer authentication command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../common/service/PassTicketService.java | 6 + .../java/com/ca/mfaas/util/EurekaUtils.java | 4 +- .../com/ca/mfaas/util/EurekaUtilsTest.java | 12 +- .../discovery/ApimlInstanceRegistry.java | 165 +++++ ...va => EurekaInstanceCanceledListener.java} | 17 +- .../ca/mfaas/discovery/GatewayNotifier.java | 2 +- .../mfaas/discovery/config/EurekaConfig.java | 41 ++ .../src/main/resources/application.yml | 2 + ...> EurekaInstanceCanceledListenerTest.java} | 28 +- .../discovery/EurekaSecuredEndpointsTest.java | 4 +- .../mfaas/discovery/GatewayNotifierTest.java | 18 +- .../StaticApiRestControllerTest.java | 16 +- docs/registry-communication.md | 231 +++++++ .../ca/mfaas/gateway/GatewayApplication.java | 1 + .../gateway/cache/ServiceCacheEvictor.java | 96 +++ .../gateway/config/DiscoveryClientConfig.java | 79 +++ .../controllers/CacheServiceController.java | 4 + .../discovery/ApimlDiscoveryClient.java | 70 ++ .../gateway/filters/pre/ZosmfFilter.java | 3 +- .../ribbon/ApimlZoneAwareLoadBalancer.java | 47 ++ .../gateway/ribbon/GatewayRibbonConfig.java | 23 + .../GatewayRibbonLoadBalancingHttpClient.java | 21 + .../gateway/routing/ApimlRouteLocator.java | 4 +- .../ServiceAuthenticationServiceImpl.java | 20 +- .../cache/ServiceCacheEvictorTest.java | 67 ++ .../CacheServiceControllerTest.java | 10 +- .../service/AuthenticationServiceTest.java | 14 +- integration-tests/build.gradle | 10 + .../AuthenticationOnDeploymentTest.java | 140 ++++ .../ca/mfaas/util/service/DiscoveryUtils.java | 121 ++++ .../mfaas/util/service/RequestVerifier.java | 132 ++++ .../ca/mfaas/util/service/VirtualService.java | 621 ++++++++++++++++++ 32 files changed, 1957 insertions(+), 72 deletions(-) create mode 100644 discovery-service/src/main/java/com/ca/mfaas/discovery/ApimlInstanceRegistry.java rename discovery-service/src/main/java/com/ca/mfaas/discovery/{EurekaInstanceRenewedListener.java => EurekaInstanceCanceledListener.java} (60%) create mode 100644 discovery-service/src/main/java/com/ca/mfaas/discovery/config/EurekaConfig.java rename discovery-service/src/test/java/com/ca/mfaas/discovery/{EurekaInstanceRenewedListenerTest.java => EurekaInstanceCanceledListenerTest.java} (51%) create mode 100644 docs/registry-communication.md create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/cache/ServiceCacheEvictor.java create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/config/DiscoveryClientConfig.java create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/discovery/ApimlDiscoveryClient.java create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/ApimlZoneAwareLoadBalancer.java create mode 100644 gateway-service/src/test/java/com/ca/mfaas/gateway/cache/ServiceCacheEvictorTest.java create mode 100644 integration-tests/src/test/java/com/ca/mfaas/gatewayservice/AuthenticationOnDeploymentTest.java create mode 100644 integration-tests/src/test/java/com/ca/mfaas/util/service/DiscoveryUtils.java create mode 100644 integration-tests/src/test/java/com/ca/mfaas/util/service/RequestVerifier.java create mode 100644 integration-tests/src/test/java/com/ca/mfaas/util/service/VirtualService.java diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java index 2d8f7084b2..752b1f4ffd 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java @@ -61,6 +61,7 @@ public static class DefaultPassTicketImpl implements IRRPassTicket { public static final String ZOWE_DUMMY_USERID = "user"; public static final String ZOWE_DUMMY_PASS_TICKET_PREFIX = "ZoweDummyPassTicket"; + public static final String DUMMY_USER = "user"; public static final String UNKNOWN_USER = "unknownUser"; public static final String UNKNOWN_APPLID = "XBADAPPL"; @@ -100,6 +101,10 @@ public String generate(String userId, String applId) throws IRRPassTicketGenerat throw new IRRPassTicketGenerationException(8, 16, 28); } + if (StringUtils.equalsIgnoreCase(DUMMY_USER, userId)) { + return ZOWE_DUMMY_PASS_TICKET_PREFIX; + } + final UserApp userApp = new UserApp(userId, applId); final int currentId; synchronized (DefaultPassTicketImpl.class) { @@ -123,4 +128,5 @@ private static class UserApp { } } + } diff --git a/common-service-core/src/main/java/com/ca/mfaas/util/EurekaUtils.java b/common-service-core/src/main/java/com/ca/mfaas/util/EurekaUtils.java index a640736db3..550601b5fc 100644 --- a/common-service-core/src/main/java/com/ca/mfaas/util/EurekaUtils.java +++ b/common-service-core/src/main/java/com/ca/mfaas/util/EurekaUtils.java @@ -43,9 +43,9 @@ public static final String getServiceIdFromInstanceId(String instanceId) { */ public static final String getUrl(InstanceInfo instanceInfo) { if (instanceInfo.getSecurePort() == 0) { - return "http://" + instanceInfo.getIPAddr() + ":" + instanceInfo.getPort(); + return "http://" + instanceInfo.getHostName() + ":" + instanceInfo.getPort(); } else { - return "https://" + instanceInfo.getIPAddr() + ":" + instanceInfo.getSecurePort(); + return "https://" + instanceInfo.getHostName() + ":" + instanceInfo.getSecurePort(); } } diff --git a/common-service-core/src/test/java/com/ca/mfaas/util/EurekaUtilsTest.java b/common-service-core/src/test/java/com/ca/mfaas/util/EurekaUtilsTest.java index 41c6772b62..e895ff72a8 100644 --- a/common-service-core/src/test/java/com/ca/mfaas/util/EurekaUtilsTest.java +++ b/common-service-core/src/test/java/com/ca/mfaas/util/EurekaUtilsTest.java @@ -29,9 +29,9 @@ public void test() { assertNull(EurekaUtils.getServiceIdFromInstanceId("abc")); } - private InstanceInfo createInstanceInfo(String ip, int port, int securePort) { + private InstanceInfo createInstanceInfo(String host, int port, int securePort) { InstanceInfo out = mock(InstanceInfo.class); - when(out.getIPAddr()).thenReturn(ip); + when(out.getHostName()).thenReturn(host); when(out.getPort()).thenReturn(port); when(out.getSecurePort()).thenReturn(securePort); return out; @@ -39,11 +39,11 @@ private InstanceInfo createInstanceInfo(String ip, int port, int securePort) { @Test public void testGetUrl() { - InstanceInfo ii1 = createInstanceInfo("127.0.0.1", 80, 0); - InstanceInfo ii2 = createInstanceInfo("192.168.0.1", 80, 443); + InstanceInfo ii1 = createInstanceInfo("hostname1", 80, 0); + InstanceInfo ii2 = createInstanceInfo("locahost", 80, 443); - assertEquals("http://127.0.0.1:80", EurekaUtils.getUrl(ii1)); - assertEquals("https://192.168.0.1:443", EurekaUtils.getUrl(ii2)); + assertEquals("http://hostname1:80", EurekaUtils.getUrl(ii1)); + assertEquals("https://locahost:443", EurekaUtils.getUrl(ii2)); } } diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/ApimlInstanceRegistry.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/ApimlInstanceRegistry.java new file mode 100644 index 0000000000..ddd704c9e0 --- /dev/null +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/ApimlInstanceRegistry.java @@ -0,0 +1,165 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.discovery; + +import com.netflix.appinfo.InstanceInfo; +import com.netflix.discovery.EurekaClient; +import com.netflix.discovery.EurekaClientConfig; +import com.netflix.eureka.EurekaServerConfig; +import com.netflix.eureka.registry.AbstractInstanceRegistry; +import com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl; +import com.netflix.eureka.resources.ServerCodecs; +import org.springframework.cloud.netflix.eureka.server.InstanceRegistry; +import org.springframework.cloud.netflix.eureka.server.InstanceRegistryProperties; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * This implementation of instance registry is solving known problem in Eureka. Discovery service notify about change + * in services before it does it. From this reason listener can try to use services before they are really registered. + * + * At least implementation with reflection of register and cancel should be removed after finish task in Eureka: + * + * #2659 Race condition with registration events in Eureka server + * https://github.com/spring-cloud/spring-cloud-netflix/issues/2659 + */ +public class ApimlInstanceRegistry extends InstanceRegistry { + + private static final String EXCEPTION_MESSAGE = "Implementation of InstanceRegistry changed, please verify fix of order sending events"; + + private MethodHandle handleRegistrationMethod; + private MethodHandle handlerResolveInstanceLeaseDurationMethod; + private MethodHandle handleCancelationMethod; + + private MethodHandle register2ArgsMethodHandle; + private MethodHandle register3ArgsMethodHandle; + private MethodHandle cancelMethodHandle; + + public ApimlInstanceRegistry( + EurekaServerConfig serverConfig, + EurekaClientConfig clientConfig, + ServerCodecs serverCodecs, + EurekaClient eurekaClient, + InstanceRegistryProperties instanceRegistryProperties + ) { + super(serverConfig, clientConfig, serverCodecs, eurekaClient, + instanceRegistryProperties.getExpectedNumberOfClientsSendingRenews(), + instanceRegistryProperties.getDefaultOpenForTrafficCount() + ); + + init(); + } + + /** + * Prepare method handlers to overridden methods to reimplement methods in InstanceRegistry, which contains a race + * condition problem. Handlers are faster than reflection, close to bytecode. + */ + private void init() { + try { + Method registrationMethod = + InstanceRegistry.class.getDeclaredMethod("handleRegistration", + InstanceInfo.class, int.class, boolean.class + ); + registrationMethod.setAccessible(true); + handleRegistrationMethod = MethodHandles.lookup().unreflect(registrationMethod); + + Method cancelationMethod = + InstanceRegistry.class.getDeclaredMethod("handleCancelation", + String.class, String.class, boolean.class + ); + cancelationMethod.setAccessible(true); + handleCancelationMethod = MethodHandles.lookup().unreflect(cancelationMethod); + + Method resolveInstanceLeaseDurationMethod = + InstanceRegistry.class.getDeclaredMethod("resolveInstanceLeaseDuration", + InstanceInfo.class + ); + resolveInstanceLeaseDurationMethod.setAccessible(true); + handlerResolveInstanceLeaseDurationMethod = MethodHandles.lookup().unreflect(resolveInstanceLeaseDurationMethod); + + Constructor lookupConstructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class); + lookupConstructor.setAccessible(true); + MethodHandles.Lookup lookup = lookupConstructor.newInstance(PeerAwareInstanceRegistryImpl.class); + + register2ArgsMethodHandle = + lookup.findSpecial( + PeerAwareInstanceRegistryImpl.class, + "register", + MethodType.methodType(void.class, InstanceInfo.class, boolean.class), + PeerAwareInstanceRegistryImpl.class + ); + + cancelMethodHandle = + lookup.findSpecial( + PeerAwareInstanceRegistryImpl.class, + "cancel", + MethodType.methodType(boolean.class, String.class, String.class, boolean.class), + PeerAwareInstanceRegistryImpl.class + ); + + lookup = lookupConstructor.newInstance(AbstractInstanceRegistry.class); + + register3ArgsMethodHandle = + lookup.findSpecial( + AbstractInstanceRegistry.class, + "register", + MethodType.methodType(void.class, InstanceInfo.class, int.class, boolean.class), + AbstractInstanceRegistry.class + ); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | InstantiationException e) { + throw new IllegalArgumentException(EXCEPTION_MESSAGE, e); + } + } + + protected int resolveInstanceLeaseDurationRewritten(final InstanceInfo info) { + try { + return (int) handlerResolveInstanceLeaseDurationMethod.invokeWithArguments(this, info); + } catch (Throwable t) { + throw new IllegalArgumentException(EXCEPTION_MESSAGE, t); + } + } + + @Override + public void register(InstanceInfo info, int leaseDuration, boolean isReplication) { + try { + register3ArgsMethodHandle.invokeWithArguments(this, info, leaseDuration, isReplication); + handleRegistrationMethod.invokeWithArguments(this, info, leaseDuration, isReplication); + } catch (Throwable t) { + throw new IllegalArgumentException(EXCEPTION_MESSAGE, t); + } + } + + @Override + public void register(final InstanceInfo info, final boolean isReplication) { + try { + register2ArgsMethodHandle.invokeWithArguments(this, info, isReplication); + handleRegistrationMethod.invokeWithArguments(this, info, resolveInstanceLeaseDurationRewritten(info), isReplication); + } catch (Throwable t) { + throw new IllegalArgumentException(EXCEPTION_MESSAGE, t); + } + } + + @Override + public boolean cancel(String appName, String serverId, boolean isReplication) { + try { + final boolean out = (boolean) cancelMethodHandle.invokeWithArguments(this, appName, serverId, isReplication); + handleCancelationMethod.invokeWithArguments(this, appName, serverId, isReplication); + return out; + } catch (Throwable t) { + throw new IllegalArgumentException(EXCEPTION_MESSAGE, t); + } + } + +} diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/EurekaInstanceRenewedListener.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/EurekaInstanceCanceledListener.java similarity index 60% rename from discovery-service/src/main/java/com/ca/mfaas/discovery/EurekaInstanceRenewedListener.java rename to discovery-service/src/main/java/com/ca/mfaas/discovery/EurekaInstanceCanceledListener.java index 55be0f5756..7fc968735a 100644 --- a/discovery-service/src/main/java/com/ca/mfaas/discovery/EurekaInstanceRenewedListener.java +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/EurekaInstanceCanceledListener.java @@ -11,25 +11,24 @@ import com.ca.mfaas.util.EurekaUtils; import lombok.RequiredArgsConstructor; -import org.springframework.cloud.netflix.eureka.server.event.EurekaInstanceRenewedEvent; +import org.springframework.cloud.netflix.eureka.server.event.EurekaInstanceCanceledEvent; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; - /** - * Listener of updated services. + * Called by Eureka when the service instance is unregistered (/cancelled) */ @Component @RequiredArgsConstructor -public class EurekaInstanceRenewedListener { +public class EurekaInstanceCanceledListener { private final GatewayNotifier gatewayNotifier; + /** + * Translates service instance Eureka metadata from older versions to the current version + */ @EventListener - public void listen(EurekaInstanceRenewedEvent event) { - final String instanceId = event.getInstanceInfo().getInstanceId(); - final String serviceId = EurekaUtils.getServiceIdFromInstanceId(instanceId); - // ie. update instance can have different authentication, this is reason to evict caches on gateway - gatewayNotifier.serviceUpdated(serviceId); + public void listen(EurekaInstanceCanceledEvent event) { + gatewayNotifier.serviceUpdated(EurekaUtils.getServiceIdFromInstanceId(event.getServerId())); } } diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java index ed83573d95..31630bc6db 100644 --- a/discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java @@ -46,7 +46,7 @@ private PeerAwareInstanceRegistry getRegistry() { public void serviceUpdated(String serviceId) { final PeerAwareInstanceRegistry registry = getRegistry(); - final Application application = registry.getApplication("gateway"); + final Application application = registry.getApplication("GATEWAY"); if (application == null) { logger.log("apiml.discovery.errorNotifyingGateway"); return; diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/config/EurekaConfig.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/config/EurekaConfig.java new file mode 100644 index 0000000000..baf90bc839 --- /dev/null +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/config/EurekaConfig.java @@ -0,0 +1,41 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.discovery.config; + +import com.ca.mfaas.discovery.ApimlInstanceRegistry; +import com.netflix.discovery.EurekaClient; +import com.netflix.discovery.EurekaClientConfig; +import com.netflix.eureka.EurekaServerConfig; +import com.netflix.eureka.resources.ServerCodecs; +import org.springframework.cloud.netflix.eureka.server.InstanceRegistryProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +/** + * Configuration to rewrite default Eureka's implementation with custom one + */ +@Configuration +public class EurekaConfig { + + @Bean + @Primary + public ApimlInstanceRegistry getApimlInstanceRegistry( + EurekaServerConfig serverConfig, + EurekaClientConfig clientConfig, + ServerCodecs serverCodecs, + EurekaClient eurekaClient, + InstanceRegistryProperties instanceRegistryProperties) + { + eurekaClient.getApplications(); // force initialization + return new ApimlInstanceRegistry(serverConfig, clientConfig, serverCodecs, eurekaClient, instanceRegistryProperties); + } + +} diff --git a/discovery-service/src/main/resources/application.yml b/discovery-service/src/main/resources/application.yml index a7e66e1b80..27861ee5b2 100644 --- a/discovery-service/src/main/resources/application.yml +++ b/discovery-service/src/main/resources/application.yml @@ -73,6 +73,8 @@ eureka: region: default serviceUrl: defaultZone: ${apiml.discovery.allPeersUrls} + server: + useReadOnlyResponseCache: false management: endpoints: diff --git a/discovery-service/src/test/java/com/ca/mfaas/discovery/EurekaInstanceRenewedListenerTest.java b/discovery-service/src/test/java/com/ca/mfaas/discovery/EurekaInstanceCanceledListenerTest.java similarity index 51% rename from discovery-service/src/test/java/com/ca/mfaas/discovery/EurekaInstanceRenewedListenerTest.java rename to discovery-service/src/test/java/com/ca/mfaas/discovery/EurekaInstanceCanceledListenerTest.java index f9952e9103..db3a244979 100644 --- a/discovery-service/src/test/java/com/ca/mfaas/discovery/EurekaInstanceRenewedListenerTest.java +++ b/discovery-service/src/test/java/com/ca/mfaas/discovery/EurekaInstanceCanceledListenerTest.java @@ -8,37 +8,25 @@ * Copyright Contributors to the Zowe Project. */ -import com.netflix.appinfo.InstanceInfo; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.cloud.netflix.eureka.server.event.EurekaInstanceRenewedEvent; +import org.springframework.cloud.netflix.eureka.server.event.EurekaInstanceCanceledEvent; import static org.mockito.Mockito.*; -@RunWith(MockitoJUnitRunner.class) -public class EurekaInstanceRenewedListenerTest { +public class EurekaInstanceCanceledListenerTest { - @InjectMocks - private EurekaInstanceRenewedListener listener; - - @Mock - private GatewayNotifier notifier; - - private EurekaInstanceRenewedEvent createEvent(String instanceId) { - InstanceInfo instanceInfo = mock(InstanceInfo.class); - when(instanceInfo.getInstanceId()).thenReturn(instanceId); - - EurekaInstanceRenewedEvent out = mock(EurekaInstanceRenewedEvent.class); - when(out.getInstanceInfo()).thenReturn(instanceInfo); + private EurekaInstanceCanceledEvent createEvent(String serverId) { + EurekaInstanceCanceledEvent out = mock(EurekaInstanceCanceledEvent.class); + when(out.getServerId()).thenReturn(serverId); return out; } @Test public void testListen() { + GatewayNotifier notifier = mock(GatewayNotifier.class); + EurekaInstanceCanceledListener listener = new EurekaInstanceCanceledListener(notifier); + listener.listen(createEvent("host:service:instance")); verify(notifier, times(1)).serviceUpdated("service"); listener.listen(createEvent("unknown format")); diff --git a/discovery-service/src/test/java/com/ca/mfaas/discovery/EurekaSecuredEndpointsTest.java b/discovery-service/src/test/java/com/ca/mfaas/discovery/EurekaSecuredEndpointsTest.java index d11e59db07..7989117a17 100644 --- a/discovery-service/src/test/java/com/ca/mfaas/discovery/EurekaSecuredEndpointsTest.java +++ b/discovery-service/src/test/java/com/ca/mfaas/discovery/EurekaSecuredEndpointsTest.java @@ -9,6 +9,7 @@ */ package com.ca.mfaas.discovery; +import com.ca.mfaas.discovery.config.EurekaConfig; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -30,7 +31,7 @@ "eureka.client.fetchRegistry=false", "eureka.client.registerWithEureka=false" }, - classes = {DiscoveryServiceApplication.class} + classes = {DiscoveryServiceApplication.class, EurekaConfig.class} ) @AutoConfigureMockMvc public class EurekaSecuredEndpointsTest { @@ -59,4 +60,5 @@ public void shouldForbidCallForNotEurekaUser() throws Exception { .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isUnauthorized()); } + } diff --git a/discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java b/discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java index 799bfe094a..206c5a6b7b 100644 --- a/discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java +++ b/discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java @@ -36,9 +36,9 @@ public void setUp() { EurekaServerContextHolder.initialize(context); } - private InstanceInfo createInstanceInfo(String ipAddr, int port, int securePort) { + private InstanceInfo createInstanceInfo(String hostName, int port, int securePort) { InstanceInfo out = mock(InstanceInfo.class); - when(out.getIPAddr()).thenReturn(ipAddr); + when(out.getHostName()).thenReturn(hostName); when(out.getPort()).thenReturn(port); when(out.getSecurePort()).thenReturn(securePort); return out; @@ -53,21 +53,21 @@ public void testServiceUpdated() { verify(restTemplate, never()).delete(anyString()); List instances = Arrays.asList( - createInstanceInfo("127.0.0.1", 1000, 1433), - createInstanceInfo("192.168.0.1", 1000, 0) + createInstanceInfo("hostname1", 1000, 1433), + createInstanceInfo("hostname2", 1000, 0) ); Application application = mock(Application.class); when(application.getInstances()).thenReturn(instances); - when(registry.getApplication("gateway")).thenReturn(application); + when(registry.getApplication("GATEWAY")).thenReturn(application); gatewayNotifier.serviceUpdated("testService"); - verify(restTemplate, times(1)).delete("https://127.0.0.1:1433/cache/services/testService"); - verify(restTemplate, times(1)).delete("http://192.168.0.1:1000/cache/services/testService"); + verify(restTemplate, times(1)).delete("https://hostname1:1433/cache/services/testService"); + verify(restTemplate, times(1)).delete("http://hostname2:1000/cache/services/testService"); gatewayNotifier.serviceUpdated(null); - verify(restTemplate, times(1)).delete("https://127.0.0.1:1433/cache/services"); - verify(restTemplate, times(1)).delete("http://192.168.0.1:1000/cache/services"); + verify(restTemplate, times(1)).delete("https://hostname1:1433/cache/services"); + verify(restTemplate, times(1)).delete("http://hostname2:1000/cache/services"); verify(restTemplate, times(4)).delete(anyString()); } diff --git a/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/StaticApiRestControllerTest.java b/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/StaticApiRestControllerTest.java index 13fca49903..caf800498c 100644 --- a/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/StaticApiRestControllerTest.java +++ b/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/StaticApiRestControllerTest.java @@ -9,11 +9,14 @@ */ package com.ca.mfaas.discovery.staticdef; +import com.ca.mfaas.discovery.DiscoveryServiceApplication; +import com.ca.mfaas.discovery.config.EurekaConfig; import com.netflix.appinfo.InstanceInfo; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; @@ -30,8 +33,17 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @RunWith(SpringRunner.class) -@WebMvcTest +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { + "eureka.client.fetchRegistry=false", + "eureka.client.registerWithEureka=false" + }, + classes = {DiscoveryServiceApplication.class, EurekaConfig.class} +) +@AutoConfigureMockMvc public class StaticApiRestControllerTest { + private static final String CREDENTIALS = "eureka:password"; @Autowired diff --git a/docs/registry-communication.md b/docs/registry-communication.md new file mode 100644 index 0000000000..98ed08b6f8 --- /dev/null +++ b/docs/registry-communication.md @@ -0,0 +1,231 @@ +# Communication between client, discovery service and gateway + +This document was created to summarize all knowledge gathered during implementation PassTickets, where like +side-effect delays were shortcuted. + +## Client and discovery service (server) + +On the begin of communication, client should register into discovery server. Client says to server at least: +- serviceId (or applicationId) +- instanceId + - to be unique, it is usually concatenated from serviceId, hostname and port (hostname and port should + be unique itself) + - structure of instanceId usually is "${hostname}:${serviceId}:${port}", as third part could be use also + random number (or any string) + - health URL + - URL where service answers status of client + - discovery client can check this state during the time, but registration is solved via heart beats + - timeout about heartbeat + - client define, how often it will send heartbeat and what is time to unregister + - staying in discovery service is until heartbeat is not delivered in timeout or client unregister itself + - service location information + - information to callback (IP, hostname, port, securePort for HTTPS) + - VipAddress - usually same like serviceId + - information about environment (shortly AWS or own) + - status + - here in registration it replaced first heartbeat + - other information + - you can find other parameters, but their have not impact to this communication + - custom data (outer Eureka's scope) could be stored into metadata + - btw. metadata are just once data, which could be changed after registration (there exists REST method + to update them) + +After registration client should send heartbeat. Until this heartbeat is received, the client is up and it is +possible call it. + +On the end client can unregister from discovery service. This call is optional, because Eureka should solve +failover. Unregister just speed up process of client removing. If this call is missing, discovery should wait +for heartbeat's time out (keep in mind, this timeout is longer than interval to client renew via heartbeat). + +All communication is usually covered with many caches and this is reason why client cannot be used immediately +after registration. In whole system exists many cached places and it takes time to go through all of them. +Usually there is a thread pool and one thread which periodically update caches. All of them are independent by +themself. + +## Caches + +In this paragraph I will describe all caches which I found in improving of time. The main idea was to shortcut +process of registration and unregistration to allows using caches on gateway side (avoid race condition between +different settings in discovery services and gateways). It each description of cache will be also link to solution +how it is solved. + +### Discovery service & ResponseCache + +On discovery service is implemented ResponseCache. This cache is responsible from minimize call's overhead about +registered instances. If any application call discovery service, at first use the cache or create there a record +for next calls. + +This cache as default contains two spaces: read and readWrite. Other application look in read and just in case +record is missing there, it look in readWrite or create it again. Read cache is updated by internal thread which +periodically compare records in both spaces (on references level, null including) and in case of different copy +records from readWrite into read. + +Two spaces has evidently reason just for NetFlix purpose and was changed with pull +request https://github.com/Netflix/eureka/pull/544. This improvement allow to use configuration to use only +readWrite space, read will be ignored and user look directly to readWrite. This cache could be updated by +discovery service. It also evict records in there on registation and unregistration (after client register +readWrite has evicted records about service, delta and full registry, but read still contains old record). + +``` +eureka: + server: + useReadOnlyResponseCache: false +``` + +### Gateway & Discovery client + +On gateways site is discovery client which support queries about services and instances on gateway side. It will +cache all information from discovery service and the serve those old cached data. +Data are updated with thread asynchronous. It time by time fetch new registry and then you can use them. For +updating exists two ways: delta, full. + +Full update is using on the very begin. It is necessary to download all information. After that is is using very +rare because performance. Gateway could call full update, but it happens only if data are not correct to fix them. +Probably there is just one possible reason - to long delay between fetching. + +Delta update fetch only changes and project them into local cache. But delta doesn't mean different between +fetching. To save resources delta means just delta in last time interval. Discovery service store changed +instances for a time period (as default 180s). Those changes are stored in the queue which is periodically +cleaned - removed changes older than the limit (next separed thread, asynchronous). When gateway ask for delta +it will return mirror of this queue stored in ResponseCache. Gateway then detect which updates were applied in +the past and which updates are new. For that it uses version (discovery service somehow mark changes with +numbers). + +**solution** + +This cache was minimized via allowing run asynchronous fetching any time. Used classes for that: +- ApimlDiscoveryClient + - custom implementation of discovery client + - via reflection it takes reference to queue responsible for fetching of registry + - contains method ```public void fetchRegistry()```, which add new asynchronous command to fetch registry + - DiscoveryClientConfig + - configuration bean to construct custom discovery client + - DiscoveryClient also support event, especially CacheRefreshedEvent after fetching + - it is used to notify other bean to evict caches (route locators, ZUUL handle mapping, CacheNotifier) + - ServiceCacheController + - controller to accept information about service changes (ie. new instance, removed instance) + - this controller ask ApimlDiscoveryClient to make fetching (it makes delta and send event about) + - after this call process is asynchronous and cannot be directly checked (only by events) + +### Gateway & Route locators + +In gateway exists bean ApimlRouteLocator. This bean is responsible for collecting of client's routes. It means there are +available information about path and services. Those information are required for map URI to service. The most important is +the filter PreDecorationFilter. It call method ```Route getMatchingRoute(String path)``` on locator to translate URI into +information about service. Filter than store information (ie. serviceId) into ZUUL context. + +In out implementation we use custom locator, which add information about static routing. Route locators could be composite +from many. Eureka use CompositeRouteLocator which contains ApimlRouteLocator and a default. Implementation of static routing +could also make as different locator. In similar way super class of ApimlRouteLocator use ZuulProperties, this can be also use +for storing of static route. **This is only for information, could be changed in the future, now it is without any change**. + +**solution** + +Anyway this bean should evicted. It is realized via event from fetching registry (implemented in DiscoveryClientConfig) and +call on each locator method refresh(). This method call discoveryClient and then construct location mapping again. Now after +fetching new version of registry is constructed well, with new services. + +### Gateway & ZuulHandlerMapping + +This bean serve method to detect endpoint and return by it handler. Handlers are created on the begin and then just looked up +by URI. In there is mechanism of dirty data. It means, that it create handlers and they are available (dont use locators) +until they are mark as dirty. Then next call refresh all handlers by data from locators. + +**solution** + +In DiscoveryClientConfig is implemented listener of fetched registry. It will mark ZuulHandlerMapping as dirty. + +### Ribbon load balancer + +On the end of ZUUL is load balancer. For that we use Ribbon (before implementation implementation was ZoneAwareLoadBalancer). +Ribbon has also own cache it is use to have information about instances. Shortly, ZUUL give to Ribbon request and it should +send to an instance. ZUUL contains information about servers (serviceId -> 1-N instances) and information about state of load +balancing (depends on selected mechanism way to select next instance). If this cache is not evicted, Ribbon can try send +request to server which was removed, don't know any server to send or just overload an instance, because don't know about other. +Ribbon can throw many exception in this time, and it is not sure, that it retry sending in right way. + +**solution** + +Now we use as load balancer implementation ApimlZoneAwareLoadBalancer (it extends original ZoneAwareLoadBalancer). This +implementation only add method ```public void serverChanged()``` which call super class to reload information about servers, +it means about instances and their addresses. + +This is call from ServiceCacheEvictor to be sure, that before custom EhCaches are evicted and load balancer get right +information from ZUUL. + +### Service cache - our custom EhCache + +For own purpose was added EhCache, which can collect many information about processes. It is highly recommended to synchronize +state of EhCache with discovery client. If not, it is possible to use old values (ie. before registering new service's +instance with different data than old one). It can make many problems in logic (based on race condition). + +It was reason to add CacheServiceController. This controller is called from discovery service (exactly from +EurekaInstanceRegisteredListener by event EurekaInstanceRegisteredEvent). For cleaning caches gateway uses interface +ServiceCacheEvict. It means each bean can be called about any changes in registry and evict EhCache (or different cache). + +Controller evict all custom caches via interface ServiceCacheEvict and as ApimlDiscoveryClient to fetch new registry. After +than other beans are notified (see CacheRefreshedEvent from discovery client). + +This mechanism is working, but not strictly right. There is one case: + +1. instance changes in discovery client +2. gateway are notified, clean custome caches and ask for new registry fetching +3. new request accept and make a cache (again with old state) - **this is wrong** +4. fetching of registry is done, evict all Eureka caches + +For this reason there was added new bean CacheEvictor. + +#### CacheEvictor + +This bean collect all calls from CacheServiceController and is waiting for registry fetching. On this event it will clean all +custom caches (via interface ServiceCacheEvict). On the end it means that custom caches are evicted twice (before Eureka parts +and after). It fully supported right state. + +## Other improvements + +Implementation of this improvement wasn't just about caches, but discovery service contains one bug with notification. + +### Event from InstanceRegistry + +In Discovery service exist bean InstanceRegistry. This bean is call for register, renew and unregister of service. +Unfortunately, this bean contains also one problem. It notified about newly registered instances before it register it, in +similar way about unregister (cancellation) and renew. It doesnt matter about renew, but other makes problem for us. We +can clean caches before update in InstanceRegistry happened. On this topic exists issue: +``` +#2659 Race condition with registration events in Eureka server +https://github.com/spring-cloud/spring-cloud-netflix/issues/2659 +``` + +This issue takes long time and it is not good wait for implementation, for this reason was implemented ApimlInstanceRegistry. +This bean replace implementation and make notification in right order. It is via java reflection and it will be removed when +Eureka will be fixed. + +## Using caches and their evicting + +If you use anywhere custom cache, implement interface ServiceCacheEvict to evict. It offer to methods: +- public void evictCacheService(String serviceId) + - to evict only part of caches for service with serviceId + - if there is no way how to do it, you can evict all records +- public void evictCacheAllService() + - to evict all records in the caches, which can has a relationship with any service + - this method will be call very rare, only in case that, there is impossible to get serviceId (ie. wrong format of instanceId) + +## Order to clean caches + +From Instance registry is information distributed in this order: +``` +Discovery service > ResponseCache in discovery service > Discovery client in gateway > Route locators in gateway > ZUUL handler mapping +``` + +After those chain is our EhCache (because this is first time, which could cache new data) + +From user point of view after ZUUL handler mapping exists Ribbon load balancer cache + +--- + +**REST API** + +``` +There is possible to use REST API, described at https://github.com/Netflix/eureka/wiki/Eureka-REST-operations. +``` + diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/GatewayApplication.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/GatewayApplication.java index a99a0014b3..0f4e34a047 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/GatewayApplication.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/GatewayApplication.java @@ -46,6 +46,7 @@ @EnableWebSocket @EnableApiDiscovery public class GatewayApplication implements ApplicationListener { + public static void main(String[] args) { SpringApplication app = new SpringApplication(GatewayApplication.class); app.addInitializers(new LatencyUtilsConfigInitializer()); diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/cache/ServiceCacheEvictor.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/cache/ServiceCacheEvictor.java new file mode 100644 index 0000000000..18cc904a10 --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/cache/ServiceCacheEvictor.java @@ -0,0 +1,96 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.cache; + +import com.ca.mfaas.gateway.discovery.ApimlDiscoveryClient; +import com.ca.mfaas.gateway.ribbon.ApimlZoneAwareLoadBalancer; +import com.ca.mfaas.gateway.security.service.ServiceCacheEvict; +import com.netflix.discovery.CacheRefreshedEvent; +import com.netflix.discovery.EurekaEvent; +import com.netflix.discovery.EurekaEventListener; +import lombok.Value; +import org.springframework.stereotype.Component; + +import java.util.HashSet; +import java.util.List; + +/** + * This class is responsible for evicting cache after new registry is loaded. This avoid race condition. Scenario is: + * 1. discovery service changed + * a. about change is notified all gateways + * b. gateways evict caches and start with mirroring of discovery into discoveryClient + * 2. now is possible cache again data with old settings from discovery service, because fetching new is asynchronous + * 3. after make fetching this beans is notified from discovery client and evict caches again + * + * This process evict evict caches two times, because not all reason to cache is dependent only by discovery client + * updates. + */ +@Component +public class ServiceCacheEvictor implements EurekaEventListener, ServiceCacheEvict { + + private List serviceCacheEvicts; + + private boolean evictAll = false; + private HashSet toEvict = new HashSet<>(); + + private ApimlZoneAwareLoadBalancer apimlZoneAwareLoadBalancer; + + public ServiceCacheEvictor( + ApimlDiscoveryClient apimlDiscoveryClient, + List serviceCacheEvicts + ) { + apimlDiscoveryClient.registerEventListener(this); + this.serviceCacheEvicts = serviceCacheEvicts; + this.serviceCacheEvicts.remove(this); + } + + public void setApimlZoneAwareLoadBalancer(ApimlZoneAwareLoadBalancer apimlZoneAwareLoadBalancer) { + this.apimlZoneAwareLoadBalancer = apimlZoneAwareLoadBalancer; + } + + public synchronized void evictCacheService(String serviceId) { + if (evictAll) return; + toEvict.add(new ServiceRef(serviceId)); + } + + public synchronized void evictCacheAllService() { + evictAll = true; + toEvict.clear(); + } + + @Override + public synchronized void onEvent(EurekaEvent event) { + if (event instanceof CacheRefreshedEvent) { + if (!evictAll && toEvict.isEmpty()) return; + + if (evictAll) { + serviceCacheEvicts.forEach(ServiceCacheEvict::evictCacheAllService); + evictAll = false; + } else { + toEvict.forEach(ServiceRef::evict); + toEvict.clear(); + } + + apimlZoneAwareLoadBalancer.serverChanged(); + } + } + + @Value + private class ServiceRef { + + private final String serviceId; + + public void evict() { + serviceCacheEvicts.forEach(x -> x.evictCacheService(serviceId)); + } + + } + +} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/config/DiscoveryClientConfig.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/config/DiscoveryClientConfig.java new file mode 100644 index 0000000000..9fdf18735f --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/config/DiscoveryClientConfig.java @@ -0,0 +1,79 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.config; + +import com.ca.mfaas.gateway.discovery.ApimlDiscoveryClient; +import com.netflix.appinfo.ApplicationInfoManager; +import com.netflix.appinfo.EurekaInstanceConfig; +import com.netflix.appinfo.HealthCheckHandler; +import com.netflix.discovery.AbstractDiscoveryClientOptionalArgs; +import com.netflix.discovery.CacheRefreshedEvent; +import com.netflix.discovery.EurekaClientConfig; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.cloud.netflix.zuul.filters.RefreshableRouteLocator; +import org.springframework.cloud.netflix.zuul.web.ZuulHandlerMapping; +import org.springframework.cloud.util.ProxyUtils; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +/** + * This configuration override bean EurekaClient with custom ApimlDiscoveryClient. This bean offer additional method + * fetchRegistry. User can call this method to asynchronously fetch new data from discovery service. There is no time + * to fetching. + * + * Configuration also add listeners to call other beans waiting for fetch new registry. It speed up distribution of + * changes in whole gateway. + */ +@Configuration +public class DiscoveryClientConfig { + + @Autowired + private ApplicationContext context; + + @Autowired + private AbstractDiscoveryClientOptionalArgs optionalArgs; + + @Autowired + private List refreshableRouteLocators; + + @Autowired + private ZuulHandlerMapping zuulHandlerMapping; + + @Bean(destroyMethod = "shutdown") + @RefreshScope + public ApimlDiscoveryClient eurekaClient(ApplicationInfoManager manager, + EurekaClientConfig config, + EurekaInstanceConfig instance, + @Autowired(required = false) HealthCheckHandler healthCheckHandler + ) { + ApplicationInfoManager appManager; + if (AopUtils.isAopProxy(manager)) { + appManager = ProxyUtils.getTargetObject(manager); + } + else { + appManager = manager; + } + final ApimlDiscoveryClient discoveryClientClient = new ApimlDiscoveryClient(appManager, config, this.optionalArgs, this.context); + discoveryClientClient.registerHealthCheck(healthCheckHandler); + + discoveryClientClient.registerEventListener(event -> { + if (event instanceof CacheRefreshedEvent) { + refreshableRouteLocators.forEach(x -> x.refresh()); + zuulHandlerMapping.setDirty(true); + } + }); + return discoveryClientClient; + } +} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/CacheServiceController.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/CacheServiceController.java index 2608f9ef7c..6747fcb33a 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/CacheServiceController.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/CacheServiceController.java @@ -9,6 +9,7 @@ */ package com.ca.mfaas.gateway.controllers; +import com.ca.mfaas.gateway.discovery.ApimlDiscoveryClient; import com.ca.mfaas.gateway.security.service.ServiceCacheEvict; import lombok.AllArgsConstructor; import org.springframework.web.bind.annotation.DeleteMapping; @@ -29,15 +30,18 @@ public class CacheServiceController { private final List toEvict; + private final ApimlDiscoveryClient discoveryClient; @DeleteMapping(path = "") public void evictAll() { toEvict.forEach(ServiceCacheEvict::evictCacheAllService); + discoveryClient.fetchRegistry(); } @DeleteMapping(path = "/{serviceId}") public void evict(@PathVariable("serviceId") String serviceId) { toEvict.forEach(s -> s.evictCacheService(serviceId)); + discoveryClient.fetchRegistry(); } } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/discovery/ApimlDiscoveryClient.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/discovery/ApimlDiscoveryClient.java new file mode 100644 index 0000000000..43e50d6abe --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/discovery/ApimlDiscoveryClient.java @@ -0,0 +1,70 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.discovery; + +import com.netflix.appinfo.ApplicationInfoManager; +import com.netflix.discovery.AbstractDiscoveryClientOptionalArgs; +import com.netflix.discovery.DiscoveryClient; +import com.netflix.discovery.EurekaClientConfig; +import org.springframework.cloud.netflix.eureka.CloudEurekaClient; +import org.springframework.context.ApplicationEventPublisher; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.Optional; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * Custom implementation of Eureka client. It support additional feature: + * - fetchRegistry - invoke asynchronous task to update registry from discovery client immediatelly + */ +public class ApimlDiscoveryClient extends CloudEurekaClient { + + protected ScheduledExecutorService scheduler; + + protected Runnable cacheRefresh; + + public ApimlDiscoveryClient( + ApplicationInfoManager applicationInfoManager, + EurekaClientConfig config, + AbstractDiscoveryClientOptionalArgs args, + ApplicationEventPublisher publisher + ) { + super(applicationInfoManager, config, args, publisher); + init(); + } + + public void init() { + try { + // take scheduler and save in local variable to work with in the future + Field schedulerField = DiscoveryClient.class.getDeclaredField("scheduler"); + schedulerField.setAccessible(true); + scheduler = (ScheduledExecutorService) schedulerField.get(this); + + // find class with process to fetch from discovery server and construct instance for call to fetch + Optional> cacheRefreshClass = Arrays.stream(DiscoveryClient.class.getDeclaredClasses()) + .filter(x -> "CacheRefreshThread".equals(x.getSimpleName())).findFirst(); + if (!cacheRefreshClass.isPresent()) throw new NoSuchMethodException(); + Constructor cacheRefreshConstructor = cacheRefreshClass.get().getDeclaredConstructor(DiscoveryClient.class); + cacheRefreshConstructor.setAccessible(true); + cacheRefresh = (Runnable) cacheRefreshConstructor.newInstance(this); + } catch (InstantiationException | InvocationTargetException | NoSuchMethodException | IllegalAccessException | NoSuchFieldException e) { + throw new IllegalArgumentException("Implementation of discovery client was changed. Please review implementation of manual registry fetching"); + } + } + + public void fetchRegistry() { + scheduler.schedule(cacheRefresh, 0, TimeUnit.NANOSECONDS); + } + +} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ZosmfFilter.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ZosmfFilter.java index 9c6593d413..c17642d608 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ZosmfFilter.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ZosmfFilter.java @@ -12,6 +12,7 @@ import com.ca.mfaas.gateway.security.service.AuthenticationService; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; +import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import java.util.Optional; @@ -43,7 +44,7 @@ public boolean shouldFilter() { RequestContext context = RequestContext.getCurrentContext(); String serviceId = (String) context.get(SERVICE_ID_KEY); - return serviceId.toLowerCase().contains(ZOSMF); + return StringUtils.containsIgnoreCase(serviceId, ZOSMF); } @Override diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/ApimlZoneAwareLoadBalancer.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/ApimlZoneAwareLoadBalancer.java new file mode 100644 index 0000000000..9be79ddf93 --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/ApimlZoneAwareLoadBalancer.java @@ -0,0 +1,47 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.ribbon; + +import com.ca.mfaas.gateway.cache.ServiceCacheEvictor; +import com.netflix.client.config.IClientConfig; +import com.netflix.loadbalancer.*; + +/** + * Custom implementation of load balancer. This implementation register on creating into ServiceCacheEvictor. It allows + * faster reaction. When a service is registred or unregistred, discovery service call CacheServiceController. It + * cooperates with bean CacheEvictor. Purspose of CacheEvictor is to remember changed services until new instance + * registry is loaded. After that is is posible to update also load balancer, especially list of servers. Otherwise + * gateway try to send requests, but load balncer can send them to missing service or dont know about new one. + * + * @param ussually Server class + */ +public class ApimlZoneAwareLoadBalancer extends ZoneAwareLoadBalancer { + + public ApimlZoneAwareLoadBalancer( + IClientConfig clientConfig, + IRule rule, + IPing ping, + ServerList serverList, + ServerListFilter filter, + ServerListUpdater serverListUpdater, + ServiceCacheEvictor serviceCacheEvictor + ) { + super(clientConfig, rule, ping, serverList, filter, serverListUpdater); + serviceCacheEvictor.setApimlZoneAwareLoadBalancer(this); + } + + /** + * Update list of servers, to handle services in right way + */ + public void serverChanged() { + updateListOfServers(); + } + +} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonConfig.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonConfig.java index 236dd96ca1..cd424235d8 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonConfig.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonConfig.java @@ -9,10 +9,14 @@ */ package com.ca.mfaas.gateway.ribbon; +import com.ca.mfaas.gateway.cache.ServiceCacheEvictor; import com.netflix.client.config.IClientConfig; import com.netflix.discovery.EurekaClient; +import com.netflix.loadbalancer.*; import org.apache.http.impl.client.CloseableHttpClient; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.netflix.ribbon.PropertiesFactory; +import org.springframework.cloud.netflix.ribbon.RibbonClientName; import org.springframework.cloud.netflix.ribbon.ServerIntrospector; import org.springframework.cloud.netflix.ribbon.apache.RibbonLoadBalancingHttpClient; import org.springframework.context.annotation.Bean; @@ -21,6 +25,12 @@ @Configuration public class GatewayRibbonConfig { + @RibbonClientName + private String ribbonClientName = "client"; + + @Autowired + private PropertiesFactory propertiesFactory; + @Bean @Autowired public RibbonLoadBalancingHttpClient ribbonLoadBalancingHttpClient( @@ -32,4 +42,17 @@ public RibbonLoadBalancingHttpClient ribbonLoadBalancingHttpClient( return new GatewayRibbonLoadBalancingHttpClient(secureHttpClient, config, serverIntrospector, discoveryClient); } + @Bean + @Autowired + public ILoadBalancer ribbonLoadBalancer(IClientConfig config, + ServerList serverList, ServerListFilter serverListFilter, + IRule rule, IPing ping, ServerListUpdater serverListUpdater, + ServiceCacheEvictor serviceCacheEvictor) { + if (this.propertiesFactory.isSet(ILoadBalancer.class, ribbonClientName)) { + return this.propertiesFactory.get(ILoadBalancer.class, config, ribbonClientName); + } + return new ApimlZoneAwareLoadBalancer<>(config, rule, ping, serverList, + serverListFilter, serverListUpdater, serviceCacheEvictor); + } + } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java index 738787132d..f91814eb2c 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java @@ -38,6 +38,7 @@ import java.net.URI; import java.util.Collections; +import java.util.Map; import static com.ca.mfaas.gateway.security.service.ServiceAuthenticationServiceImpl.AUTHENTICATION_COMMAND_KEY; import static org.springframework.cloud.netflix.ribbon.RibbonUtils.updateToSecureConnectionIfNeeded; @@ -168,6 +169,24 @@ protected void customizeLoadBalancerCommandBuilder(RibbonApacheHttpRequest reque */ builder.withListeners(Collections.singletonList(new ExecutionListener() { + /** + * This method update current request by added values on sending in load balancer. It is used at least + * for service authentication. + * + * Now it updates only headers, but could be extended for another values in future. + * + * @param context + */ + private void updateRequestByZuulChanges(ExecutionContext context) { + final Map newHeaders = RequestContext.getCurrentContext().getZuulRequestHeaders(); + if (!newHeaders.isEmpty()) { + final RibbonApacheHttpRequest req = (RibbonApacheHttpRequest) context.getRequest(); + for (Map.Entry entry : newHeaders.entrySet()) { + req.getContext().getHeaders().add(entry.getKey(), entry.getValue()); + } + } + } + @Override public void onExecutionStart(ExecutionContext context) { // dont needed yet @@ -186,6 +205,8 @@ public void onStartWithServer(ExecutionContext context, ExecutionInfo in throw new AbortExecutionException(String.valueOf(e), e); } } + + updateRequestByZuulChanges(context); } @Override diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/routing/ApimlRouteLocator.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/routing/ApimlRouteLocator.java index 5b70efe4fd..d71c7dee99 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/routing/ApimlRouteLocator.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/routing/ApimlRouteLocator.java @@ -10,10 +10,10 @@ package com.ca.mfaas.gateway.routing; import com.ca.mfaas.eurekaservice.client.util.EurekaMetadataParser; -import com.ca.mfaas.product.routing.RoutedServices; -import com.ca.mfaas.product.routing.RoutedServicesUser; import com.ca.mfaas.message.log.ApimlLogger; import com.ca.mfaas.product.logging.annotations.InjectApimlLogger; +import com.ca.mfaas.product.routing.RoutedServices; +import com.ca.mfaas.product.routing.RoutedServicesUser; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java index fe13a145e3..f8a701f75e 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImpl.java @@ -9,14 +9,6 @@ */ package com.ca.mfaas.gateway.security.service; -import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_APPLID; -import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_SCHEME; - -import java.util.List; -import java.util.Map; - -import javax.servlet.http.HttpServletRequest; - import com.ca.apiml.security.common.auth.Authentication; import com.ca.apiml.security.common.auth.AuthenticationScheme; import com.ca.apiml.security.common.token.QueryResponse; @@ -30,13 +22,19 @@ import com.netflix.discovery.EurekaClient; import com.netflix.discovery.shared.Application; import com.netflix.zuul.context.RequestContext; - +import lombok.AllArgsConstructor; +import org.apache.commons.lang.StringUtils; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; -import lombok.AllArgsConstructor; +import javax.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.Map; + +import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_APPLID; +import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_SCHEME; /** * This bean is responsible for "translating" security to specific service. It decorate request with security data for @@ -142,7 +140,7 @@ public void evictCacheAllService() { */ @Override public void evictCacheService(String serviceId) { - CacheUtils.evictSubset(cacheManager, CACHE_BY_SERVICE_ID, x -> x.equals(0, serviceId)); + CacheUtils.evictSubset(cacheManager, CACHE_BY_SERVICE_ID, x -> StringUtils.equalsIgnoreCase((String) x.get(0), serviceId)); } public class UniversalAuthenticationCommand extends AuthenticationCommand { diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/cache/ServiceCacheEvictorTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/cache/ServiceCacheEvictorTest.java new file mode 100644 index 0000000000..be1aa8c0f9 --- /dev/null +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/cache/ServiceCacheEvictorTest.java @@ -0,0 +1,67 @@ +package com.ca.mfaas.gateway.cache;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import com.ca.mfaas.gateway.discovery.ApimlDiscoveryClient; +import com.ca.mfaas.gateway.ribbon.ApimlZoneAwareLoadBalancer; +import com.ca.mfaas.gateway.security.service.ServiceCacheEvict; +import com.netflix.discovery.CacheRefreshedEvent; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.Arrays; +import java.util.List; + +import static org.mockito.Mockito.*; + +@RunWith(JUnit4.class) +public class ServiceCacheEvictorTest { + + private ServiceCacheEvictor serviceCacheEvictor; + + private ApimlDiscoveryClient apimlDiscoveryClient = mock(ApimlDiscoveryClient.class); + + private ApimlZoneAwareLoadBalancer apimlZoneAwareLoadBalancer = mock(ApimlZoneAwareLoadBalancer.class); + + private List serviceCacheEvicts = Arrays.asList( + mock(ServiceCacheEvict.class), + mock(ServiceCacheEvict.class) + ); + + @Before + public void setUp() { + serviceCacheEvictor = new ServiceCacheEvictor(apimlDiscoveryClient, serviceCacheEvicts); + serviceCacheEvictor.setApimlZoneAwareLoadBalancer(apimlZoneAwareLoadBalancer); + } + + @Test + public void testService() { + serviceCacheEvictor.evictCacheService("service1"); + serviceCacheEvictor.evictCacheService("service1"); + serviceCacheEvictor.evictCacheService("service2"); + serviceCacheEvictor.onEvent(mock(CacheRefreshedEvent.class)); + serviceCacheEvicts.forEach(x -> { + verify(x, times(1)).evictCacheService("service1"); + verify(x, times(1)).evictCacheService("service2"); + }); + verify(apimlZoneAwareLoadBalancer, times(1)).serverChanged(); + + serviceCacheEvictor.evictCacheService("service3"); + serviceCacheEvictor.evictCacheAllService(); + serviceCacheEvictor.onEvent(mock(CacheRefreshedEvent.class)); + serviceCacheEvicts.forEach(x -> { + verify(x, never()).evictCacheService("service3"); + verify(x, times(1)).evictCacheAllService(); + }); + verify(apimlZoneAwareLoadBalancer, times(2)).serverChanged(); + } + +} diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/CacheServiceControllerTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/CacheServiceControllerTest.java index 5e1cb13372..329588cf09 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/CacheServiceControllerTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/CacheServiceControllerTest.java @@ -8,6 +8,7 @@ * Copyright Contributors to the Zowe Project. */ +import com.ca.mfaas.gateway.discovery.ApimlDiscoveryClient; import com.ca.mfaas.gateway.security.service.ServiceCacheEvict; import org.junit.Before; import org.junit.Test; @@ -34,11 +35,14 @@ public class CacheServiceControllerTest { @Mock private ServiceCacheEvict service2; + @Mock + private ApimlDiscoveryClient discoveryClient; + private CacheServiceController cacheServiceController; @Before public void setUp() { - cacheServiceController = new CacheServiceController(Arrays.asList(service1, service2)); + cacheServiceController = new CacheServiceController(Arrays.asList(service1, service2), discoveryClient); mockMvc = MockMvcBuilders.standaloneSetup(cacheServiceController).build(); } @@ -46,22 +50,26 @@ public void setUp() { public void testEvictAll() throws Exception { verify(service1, never()).evictCacheAllService(); verify(service2, never()).evictCacheAllService(); + verify(discoveryClient, never()).fetchRegistry(); this.mockMvc.perform(delete("/cache/services")).andExpect(status().isOk()); verify(service1, times(1)).evictCacheAllService(); verify(service2, times(1)).evictCacheAllService(); + verify(discoveryClient, times(1)).fetchRegistry(); } @Test public void testEvict() throws Exception { verify(service1, never()).evictCacheService(any()); verify(service2, never()).evictCacheService(any()); + verify(discoveryClient, never()).fetchRegistry(); this.mockMvc.perform(delete("/cache/services/service01")).andExpect(status().isOk()); verify(service1, times(1)).evictCacheService("service01"); verify(service2, times(1)).evictCacheService("service01"); + verify(discoveryClient, times(1)).fetchRegistry(); } } diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/AuthenticationServiceTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/AuthenticationServiceTest.java index 48d80da9a2..cb4af675c2 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/AuthenticationServiceTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/AuthenticationServiceTest.java @@ -216,10 +216,10 @@ private String createExpiredJwtToken(Key secretKey) { .compact(); } - private InstanceInfo createInstanceInfo(String instanceId, String ip, int port, int securePort) { + private InstanceInfo createInstanceInfo(String instanceId, String hostName, int port, int securePort) { InstanceInfo out = mock(InstanceInfo.class); when(out.getInstanceId()).thenReturn(instanceId); - when(out.getIPAddr()).thenReturn(ip); + when(out.getHostName()).thenReturn(hostName); when(out.getPort()).thenReturn(port); when(out.getSecurePort()).thenReturn(securePort); return out; @@ -245,9 +245,9 @@ public void invalidateToken() { Application application = mock(Application.class); List instances = Arrays.asList( - createInstanceInfo("instance02", "192.168.0.1", 10000, 10433), - createInstanceInfo("myInstance01", "127.0.0.0.1", 10000, 10433), - createInstanceInfo("instance03", "192.168.0.2", 10001, 0) + createInstanceInfo("instance02", "hostname1", 10000, 10433), + createInstanceInfo("myInstance01", "localhost", 10000, 10433), + createInstanceInfo("instance03", "hostname2", 10001, 0) ); when(application.getInstances()).thenReturn(instances); when(discoveryClient.getApplication("gateway")).thenReturn(application); @@ -257,8 +257,8 @@ public void invalidateToken() { tokenAuthentication = authService.validateJwtToken(jwt1); assertFalse(tokenAuthentication.isAuthenticated()); verify(restTemplate, times(2)).delete(anyString(), (Object[]) any()); - verify(restTemplate).delete("https://192.168.0.1:10433/auth/invalidate/{}", jwt1); - verify(restTemplate).delete("http://192.168.0.2:10001/auth/invalidate/{}", jwt1); + verify(restTemplate).delete("https://hostname1:10433/auth/invalidate/{}", jwt1); + verify(restTemplate).delete("http://hostname2:10001/auth/invalidate/{}", jwt1); } @Test diff --git a/integration-tests/build.gradle b/integration-tests/build.gradle index 233ef2e477..a871c523f0 100644 --- a/integration-tests/build.gradle +++ b/integration-tests/build.gradle @@ -37,6 +37,7 @@ dependencies { testCompile libraries.lombok testCompile libraries.jsoup testCompile libraries.rest_assured + testCompile libraries.javax_servlet_api } jar { @@ -135,3 +136,12 @@ task runAllIntegrationTests(type: Test) { } outputs.upToDateWhen { false } } + +task runTestWithoutStartupCheck(type: Test) { + group "Integration tests" + description "Run integration test without startup check" + + systemProperties System.properties + + outputs.upToDateWhen { false } +} diff --git a/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/AuthenticationOnDeploymentTest.java b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/AuthenticationOnDeploymentTest.java new file mode 100644 index 0000000000..fc6d667a92 --- /dev/null +++ b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/AuthenticationOnDeploymentTest.java @@ -0,0 +1,140 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gatewayservice; + +import com.ca.apiml.security.common.auth.Authentication; +import com.ca.apiml.security.common.auth.AuthenticationScheme; +import com.ca.apiml.security.common.service.PassTicketService; +import com.ca.mfaas.util.categories.MainframeDependentTests; +import com.ca.mfaas.util.service.RequestVerifier; +import com.ca.mfaas.util.service.VirtualService; +import io.restassured.RestAssured; +import org.apache.http.HttpHeaders; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import static com.ca.mfaas.gatewayservice.SecurityUtils.*; +import static io.restassured.RestAssured.given; +import static org.apache.http.HttpStatus.SC_OK; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertEquals; + +/** + * This test requires to allow endpoint routes on gateway (ie profile dev) + */ +@RunWith(JUnit4.class) +@Category(MainframeDependentTests.class) +public class AuthenticationOnDeploymentTest { + + private static final int TIMEOUT = 10; + + private RequestVerifier verifier; + + @Before + public void setUp() { + RestAssured.useRelaxedHTTPSValidation(); + RestAssured.config = RestAssured.config().sslConfig(getConfiguredSslConfig()); + verifier = RequestVerifier.getInstance(); + verifier.clear(); + } + + @Test + public void testMultipleAuthenticationSchemes() throws Exception { + final String jwt = gatewayToken(); + + try ( + final VirtualService service1 = new VirtualService("testService"); + final VirtualService service2 = new VirtualService("testService") + ) { + // start first instance - without passTickets + service1 + .addGetHeaderServlet(HttpHeaders.AUTHORIZATION) + .addVerifyServlet() + .start() + .waitForGatewayRegistration(1, TIMEOUT); + + + // on each gateway make a call to service + service1.getGatewayVerifyUrls().forEach(x -> + given() + .cookie(GATEWAY_TOKEN_COOKIE_NAME, jwt) + .when().get(x + "/test") + .then().statusCode(is(SC_OK)) + ); + + // verify if each gateway sent request to service + service1.getGatewayVerifyUrls().forEach(gw -> + verifier.existAndClean(service1, x -> x.getHeader(HttpHeaders.AUTHORIZATION) == null && x.getRequestURI().equals("/verify/test")) + ); + + // start second service (with passTicket authorization) + service2 + .addGetHeaderServlet(HttpHeaders.AUTHORIZATION) + .addVerifyServlet() + .setAuthentication(new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "TESTAPPL")) + .start() + .waitForGatewayRegistration(2, TIMEOUT); + + // on each gateway make calls (count same as instances) to service + service1.getGatewayVerifyUrls().forEach(x -> { + given() + .cookie(GATEWAY_TOKEN_COOKIE_NAME, jwt) + .when().get(x + "/test") + .then().statusCode(is(SC_OK)); + }); + service2.getGatewayVerifyUrls().forEach(x -> { + given() + .cookie(GATEWAY_TOKEN_COOKIE_NAME, jwt) + .when().get(x + "/test") + .then().statusCode(is(SC_OK)); + }); + + // verify if each gateway sent request to service (one with and one without passTicket) + String auth = "Basic " + Base64.getEncoder().encodeToString(("user:" + PassTicketService.DefaultPassTicketImpl.ZOWE_DUMMY_PASS_TICKET_PREFIX).getBytes(StandardCharsets.UTF_8)); + service1.getGatewayVerifyUrls().forEach(gw -> { + verifier.existAndClean(service1, x -> x.getHeader(HttpHeaders.AUTHORIZATION) == null && x.getRequestURI().equals("/verify/test")); + verifier.existAndClean(service2, x -> { + assertEquals(auth, x.getHeader(HttpHeaders.AUTHORIZATION)); + assertEquals("/verify/test", x.getRequestURI()); + return true; + }); + } + ); + + // stop first service without authentication + service1 + .unregister() + .waitForGatewayUnregistration(2, TIMEOUT) + .stop(); + + // check second service, all called second one with passTicket, same url like service1 (removed) + service1.getGatewayVerifyUrls().forEach(x -> { + given() + .cookie(GATEWAY_TOKEN_COOKIE_NAME, jwt) + .when().get(x + "/test") + .then().statusCode(is(SC_OK)); + }); + service1.getGatewayVerifyUrls().forEach(gw -> { + verifier.existAndClean(service2, x -> { + assertEquals(auth, x.getHeader(HttpHeaders.AUTHORIZATION)); + assertEquals("/verify/test", x.getRequestURI()); + return true; + }); + }); + } + } + +} diff --git a/integration-tests/src/test/java/com/ca/mfaas/util/service/DiscoveryUtils.java b/integration-tests/src/test/java/com/ca/mfaas/util/service/DiscoveryUtils.java new file mode 100644 index 0000000000..951e0bc7af --- /dev/null +++ b/integration-tests/src/test/java/com/ca/mfaas/util/service/DiscoveryUtils.java @@ -0,0 +1,121 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.util.service; + +import com.ca.mfaas.util.config.ConfigReader; +import com.ca.mfaas.util.config.DiscoveryServiceConfiguration; +import io.restassured.path.xml.XmlPath; +import io.restassured.response.ResponseBody; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.LinkedList; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static io.restassured.RestAssured.given; + +/** + * This utils serve to test base queries on discovery service to get information about registred services. This is way + * how to check count of discovery services, gateways and also any other service. + */ +public class DiscoveryUtils { + + public static final String getDiscoveryUrl() { + DiscoveryServiceConfiguration discoveryServiceConfiguration = ConfigReader.environmentConfiguration().getDiscoveryServiceConfiguration(); + return discoveryServiceConfiguration.getScheme() + "://" + discoveryServiceConfiguration.getHost() + ":" + discoveryServiceConfiguration.getPort(); + } + + public static final List getDiscoveryUrls() { + return getInstances("discovery").stream().filter(InstanceInfo.ONLY_UP).map(x -> x.getUrl()).collect(Collectors.toList()); + } + + public static final List getGatewayUrls() { + return getInstances("gateway").stream().filter(InstanceInfo.ONLY_UP).map(x -> x.getUrl()).collect(Collectors.toList()); + } + + public static final List getInstances(String serviceId) { + return getInstances(serviceId, null); + } + + public static final List getInstances(String serviceId, String instanceId) { + final List out = new LinkedList<>(); + + final StringBuilder url = new StringBuilder().append(getDiscoveryUrl()).append("/eureka"); + if (serviceId != null) { + url.append("/apps/").append(serviceId); + if (instanceId != null) url.append('/').append(instanceId); + } else if (instanceId != null) { + url.append("/instances/").append(instanceId); + } else { + url.append("/apps"); + } + + final ResponseBody body = given() + .get(url.toString()) + .body(); + + final String applicationPath; + if (serviceId == null) { + applicationPath = "applications.application"; + } else { + applicationPath = "application"; + } + + final int applicationCount = body.xmlPath().getInt(applicationPath + ".size()"); + for (int applicationId = 0; applicationId < applicationCount; applicationId++) { + final XmlPath instances = body.xmlPath().setRoot(String.format("%s[%d].instance", applicationPath, applicationId)); + + final int instanceCount = instances.getInt("size()"); + for (int instanceOrder = 0; instanceOrder < instanceCount; instanceOrder++) { + final XmlPath instance = body.xmlPath().setRoot(String.format("%s[%d].instance[%d]", applicationPath, applicationId, instanceOrder)); + final InstanceInfo instanceInfo = new InstanceInfo( + instance.getString("instanceId"), + instance.getString("hostName"), + instance.getString("app"), + instance.getString("ipAddr"), + instance.getString("status"), + instance.getInt("port"), + instance.getInt("securePort") + ); + out.add(instanceInfo); + } + } + + return out; + } + + @NoArgsConstructor + @AllArgsConstructor + @Data + public static final class InstanceInfo { + + public static final Predicate ONLY_UP = x -> "UP".equals(x.getStatus()); + + private String instanceId; + private String hostName; + private String app; + private String ipAddr; + private String status; + private Integer port, securePort; + + public String getUrl() { + if (securePort != null) { + return "https://" + hostName + ":" + securePort; + } else { + return "http://" + hostName + ":" + port; + } + } + + } + +} diff --git a/integration-tests/src/test/java/com/ca/mfaas/util/service/RequestVerifier.java b/integration-tests/src/test/java/com/ca/mfaas/util/service/RequestVerifier.java new file mode 100644 index 0000000000..20f55ce0b3 --- /dev/null +++ b/integration-tests/src/test/java/com/ca/mfaas/util/service/RequestVerifier.java @@ -0,0 +1,132 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.util.service; + +import lombok.Getter; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import java.util.*; +import java.util.function.Predicate; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +/** + * Class is using for verification request in VirtualService. If test call VerifyServlet in there, request data are + * stored to next verification (see asserts). In this class are copied values from HttpServletRequest. Not all, but + * the most important part. List of stored values could be extended. + * + * This class is design as singleton, same for virtual services in the test + */ +public class RequestVerifier { + + private static RequestVerifier instance; + + private final Map> requests = new HashMap<>(); + + private RequestVerifier() { + } + + public static RequestVerifier getInstance() { + if (instance != null) return instance; + synchronized (RequestVerifier.class) { + if (instance == null) { + instance = new RequestVerifier(); + } + return instance; + } + } + + public void clear() { + synchronized (requests) { + requests.clear(); + } + } + + public void clear(VirtualService virtualService) { + synchronized (requests) { + requests.remove(virtualService); + } + } + + public void add(VirtualService virtualService, HttpServletRequest httpServletRequest) { + synchronized (requests) { + HttpRequestCopy copy = new HttpRequestCopy(httpServletRequest); + + List list = requests.computeIfAbsent(virtualService, x -> new LinkedList<>()); + list.add(copy); + } + } + + public void existAndClean(VirtualService virtualService, Predicate verify) { + synchronized (requests) { + List list = requests.get(virtualService); + assertNotNull("No request exists", list); + for (Iterator i = list.iterator(); i.hasNext(); ) { + HttpRequestCopy req = i.next(); + if (verify.test(req)) { + i.remove(); + return; + } + } + + fail(list.isEmpty() ? "No request exists" : "Request was not found, but cache contains " + list.size() + " requests"); + } + } + + @Getter + public static class HttpRequestCopy { + + private final String authType; + private final Cookie[] cookies; + private final String pathInfo; + private final String contextPath; + private final String queryString; + private final String requestURI; + private final StringBuffer requestURL; + + private final Map> headersData = new HashMap<>(); + + private HttpRequestCopy(HttpServletRequest request) { + this.authType = request.getAuthType(); + this.cookies = request.getCookies(); + this.pathInfo = request.getPathInfo(); + this.contextPath = request.getContextPath(); + this.queryString = request.getQueryString(); + this.requestURI = request.getRequestURI(); + this.requestURL = request.getRequestURL(); + + for (final Enumeration e = request.getHeaderNames(); e.hasMoreElements(); ) { + final String headerName = e.nextElement(); + + final List headers = new LinkedList<>(); + for (final Enumeration e2 = request.getHeaders(headerName); e2.hasMoreElements(); ) { + headers.add(e2.nextElement()); + } + headersData.put(headerName.toLowerCase(), headers); + } + } + + public String getHeader(String name) { + final List list = headersData.get(name.toLowerCase()); + if ((list == null) || list.isEmpty()) return null; + return list.get(0); + } + + public Enumeration getHeaders(String name) { + List list = headersData.get(name); + if (list == null) list = Collections.emptyList(); + return Collections.enumeration(list); + } + + } + +} diff --git a/integration-tests/src/test/java/com/ca/mfaas/util/service/VirtualService.java b/integration-tests/src/test/java/com/ca/mfaas/util/service/VirtualService.java new file mode 100644 index 0000000000..77d441a0b9 --- /dev/null +++ b/integration-tests/src/test/java/com/ca/mfaas/util/service/VirtualService.java @@ -0,0 +1,621 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.util.service; + +import com.ca.apiml.security.common.auth.Authentication; +import com.ca.mfaas.util.UrlUtils; +import io.restassured.response.ResponseBody; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.startup.Tomcat; +import org.apache.commons.lang.StringUtils; +import org.apache.http.HttpStatus; +import org.json.JSONObject; +import org.springframework.boot.actuate.health.Status; +import org.springframework.http.MediaType; + +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static com.ca.mfaas.constants.EurekaMetadataDefinition.*; +import static io.restassured.RestAssured.given; +import static org.apache.http.HttpStatus.SC_NO_CONTENT; +import static org.apache.http.HttpStatus.SC_OK; +import static org.junit.Assert.*; + +/** + * This class simulate a service. You can create new instance dynamically in the test. It will register into discovery + * service and then you can call methods on it. This service also support heartBean, health check and unregistration. + * + * It is recommended to use try-with-resource to be sure, service will be unregistred on the end, ie.: + * + * try (final VirtualService service = new VirtualService("testService")) { + * service + * // add same servlet and setting of service + * .start() + * // you should wait until service is registred, usually registration is faster, but for sure - registration + * // contains many asynchronous steps + * .waitForGatewayRegistration(1, TIMEOUT); + * // use serice + * } + * + * If you want to unregister service during the test, you can do that like this: + * + * service1 + * .unregister() + * // similar to registration, unregister contains same asynchronous steps + * .waitForGatewayUnregistration(1, TIMEOUT) + * .stop(); + * + * VirtualService allow to you add custom servlets for checking, but few are implemented yet: + * - HeaderServlet + * - register with addGetHeaderServlet({header}) + * - it will response content of header with name {header} on /header/{header} + * - VerifyServlet + * - register with addVerifyServlet + * - it allows to store all request on path /verify/* and then make asserts on them + * - see also method getGatewayVerifyUrls + * - InstanceServlet + * - automatically created + * - return instanceId in the body at /application/instance + * - it is used for checking of gateways (see waitForGatewayRegistration and waitForGatewayUnregistration) + * - HealthServlet + * - automatically created + * - to check state of service from discovery service (see /application/health) + */ +@Slf4j +public class VirtualService implements AutoCloseable { + + private final String serviceId; + private String instanceId; + + private boolean registred, started; + + private Tomcat tomcat; + private Context context; + private Connector httpConnector; + private VirtualService.HealthService healthService; + + private int renewalIntervalInSecs = 10; + + private Map metadata = new HashMap(); + + private String gatewayPath; + + public VirtualService(String serviceId) throws LifecycleException { + this.serviceId = serviceId; + createTomcat(); + } + + /** + * To start tomcat and register service + * @return this instance to next command + * @throws IOException problem with socket + * @throws LifecycleException Tomcat exception + */ + public VirtualService start() throws IOException, LifecycleException { + // start Tomcat to get listening port + tomcat.start(); + instanceId = InetAddress.getLocalHost().getHostName() + ":" + serviceId + ":" + getPort(); + + // register into discovery service and start heart beating + register(); + healthService = new HealthService(renewalIntervalInSecs); + + started = true; + + return this; + } + + /** + * @return state of registration to discovery service + */ + public boolean isRegistred() { + return registred; + } + + private void createTomcat() throws LifecycleException { + httpConnector = new Connector(); + httpConnector.setPort(0); + httpConnector.setScheme("http"); + + tomcat = new Tomcat(); + tomcat.setConnector(httpConnector); + + context = tomcat.addContext("", getContextPath()); + addServlet(HealthServlet.class.getSimpleName(), "/application/health", new HealthServlet()); + addServlet(InstanceServlet.class.getSimpleName(), "/application/instance", new InstanceServlet()); + } + + private String getContextPath() { + try { + return new File(System.getProperty("java.io.tmpdir")).getCanonicalPath(); + } catch (IOException e) { + fail(e.getMessage()); + return null; + } + } + + /** + * On begin of initialization is generated from serviceId, hostname and port. + * @return instance if of this client + */ + public String getInstanceId() { + return instanceId; + } + + /** + * Add custom servlet as part of test to simulate a service method + * @param name name of servlet + * @param pattern url to listen + * @param servlet instance of servlet + * @return this instance to next command + */ + public VirtualService addServlet(String name, String pattern, Servlet servlet) { + Tomcat.addServlet(context, name, servlet); + context.addServletMappingDecoded(pattern, name); + + return this; + } + + /** + * Register servlet to echo header value with name headerName + * @param headerName which header value should be in echo + * @return this instance to next command + */ + public VirtualService addGetHeaderServlet(String headerName) { + addServlet("getHeader" + headerName, "/header/" + headerName, new HeaderServlet(headerName)); + + return this; + } + + /** + * Register verify servlet which remember request and its data on url /verify/* to be analyzed then. For this pursose + * is using {@link RequestVerifier} + * @return this instance to next command + */ + public VirtualService addVerifyServlet() { + addServlet(VerifyServlet.class.getSimpleName(), "/verify/*", new VerifyServlet(RequestVerifier.getInstance())); + + return this; + } + + /** + * Method wait for gateways to be this service registred. It will make a check via InstanceServlet. It the response + * is correct (this service will answer) it ends, otherwise it make next checking until that. Whole method has + * timeout to stop if something fails. + * + * The check means make serviceCount calls. There is a preposition that load balancer is based on cyclic queue and + * if it will call multiple times (same to count of instances of same service), it should call all instances. + * + * @param instanceCount Assumed count of instances of the same service at the moment + * @param timeoutSec Timeout in secs to break waiting + */ + public VirtualService waitForGatewayRegistration(int instanceCount, int timeoutSec) { + final long time0 = System.currentTimeMillis(); + boolean slept = false; + for (String gatewayUrl : getGatewayUrls()) { + String url = gatewayUrl + "/application/instance"; + + // count of calls (to make instanceCount times until sleep) + int testCounter = 0; + while (true) { + try { + final ResponseBody responseBody = given().when() + .contentType(MediaType.APPLICATION_JSON_UTF8_VALUE) + .get(url) + .body(); + assertEquals(instanceId, responseBody.print()); + break; + } catch (RuntimeException | AssertionError e) { + testCounter ++; + // less calls than instance counts, continue without waiting + if (testCounter < instanceCount) continue; + + // instance should be called, but didn't, wait for a while and then try call again (instanceCount times) + testCounter = 0; + + if (System.currentTimeMillis() - time0 > timeoutSec * 1000) throw e; + + try { + Thread.sleep(1000); + slept = true; + } catch (InterruptedException ex) { + throw new RuntimeException("Error durring waiting for gateways to go up, slept for " + (System.currentTimeMillis() - time0) / 1000 + "s", ex); + } + } + } + } + + if (slept) { + log.info("Slept for waiting for gateways took {}s", (System.currentTimeMillis() - time0) / 1000); + } + + return this; + } + + /** + * Method will wait until all gateways will unregister this instance. It will make few calls (instanceCountBefore) + * to check if this service will answer. If not it ends immediatelly, otherwise it will wait for a while. + * + * @param instanceCountBefore Count of instances with same serviceId before unregistration + * @param timeoutSec timeout in sec to checking + */ + public VirtualService waitForGatewayUnregistration(int instanceCountBefore, int timeoutSec) { + final long time0 = System.currentTimeMillis(); + boolean slept = false; + for (String gatewayUrl : getGatewayUrls()) { + String url = gatewayUrl + "/application/instance"; + + while (true) { + try { + for (int i = 0; i < instanceCountBefore; i++) { + final ResponseBody responseBody = given().when() + .contentType(MediaType.APPLICATION_JSON_UTF8_VALUE) + .get(url) + .body(); + assertNotEquals(instanceId, responseBody.print()); + } + break; + } catch (RuntimeException | AssertionError e) { + if (System.currentTimeMillis() - time0 > timeoutSec * 1000) throw e; + + try { + Thread.sleep(1000); + slept = true; + } catch (InterruptedException ex) { + throw new RuntimeException("Error durring waiting for gateways to go down, slept for " + (System.currentTimeMillis() - time0) / 1000 + "s", ex); + } + } + } + } + + if (slept) { + log.info("Slept for waiting for gateways took {}s", (System.currentTimeMillis() - time0) / 1000); + } + + return this; + } + + /** + * Unregister service from discovery service and stop tomcat + * @throws LifecycleException Tomcat problem + */ + public void stop() throws LifecycleException { + unregister(); + healthService.stop(); + tomcat.stop(); + tomcat.destroy(); + started = false; + } + + /** + * Stop virtual service - to easy use with try-with-resources + * @throws Exception + */ + @Override + public void close() throws Exception { + if (started) stop(); + } + + /** + * @return port of Tomcat + */ + public int getPort() { + return httpConnector.getLocalPort(); + } + + /** + * @return instance of Tomcat for special configuration etc. + */ + public Tomcat getTomcat() { + return tomcat; + } + + /** + * @return base URL of this service (without slash), ie: http://localhost:65123 + */ + public String getUrl() { + return "http://" + tomcat.getEngine().getDefaultHost() + ":" + getPort(); + } + + private void register() throws UnknownHostException { + addDefaultRouteIfMissing(); + + given().when() + .contentType(MediaType.APPLICATION_JSON_UTF8_VALUE) + .body(new JSONObject() + .put("instance", new JSONObject() + .put("instanceId", instanceId) + .put("hostName", InetAddress.getLocalHost().getHostName()) + .put("vipAddress", serviceId) + .put("app", serviceId) + .put("ipAddr", InetAddress.getLocalHost().getHostAddress()) + .put("status", Status.UP.toString()) + .put("port", new JSONObject() + .put("$", getPort()) + .put("@enabled", "true") + ) + .put("healthCheckUrl", getUrl() + "/application/health") + .put("dataCenterInfo", new JSONObject() + .put("@class", "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo") + .put("name", "MyOwn") + ) + .put("leaseInfo", new JSONObject() + .put("renewalIntervalInSecs", renewalIntervalInSecs) + .put("durationInSecs", renewalIntervalInSecs * 3) + ) + .put("metadata", metadata) + ).toString() + ) + .post(DiscoveryUtils.getDiscoveryUrl() + "/eureka/apps/{appId}", serviceId) + .then().statusCode(SC_NO_CONTENT); + + registred = true; + } + + /** + * Explicitly unregistration, if you need test state after service is down + * @return this instance to next command + */ + public VirtualService unregister() { + if (!registred) return this; + + registred = false; + + given().when() + .delete(DiscoveryUtils.getDiscoveryUrl() + "/eureka/apps/{appId}/{instanceId}", serviceId, instanceId) + .then().statusCode(SC_OK); + + return this; + } + + /** + * To adding metadata of instance. They are usually added before start (send with registration), but method support + * also sending after that, during service is up. + * @param key metadata's key + * @param value metadata's value, empty string has same meaning as unregistry + * @return this instance to next command + */ + public VirtualService addMetadata(String key, String value) { + metadata.put(key, value); + if (registred) { + given().when() + .param(key, value) + .put(DiscoveryUtils.getDiscoveryUrl() + "/eureka/apps/{appId}/{instanceId}/metadata", serviceId, instanceId) + .then().statusCode(SC_OK); + } + + return this; + } + + /** + * To remove metadata. Support also sending during service is up (after registration) + * @param key + */ + public void removeMetadata(String key) { + addMetadata(key, ""); + metadata.remove(key); + } + + /** + * Method to easy set metadata about authentication + * @param authentication authentication of this service (gateway will send right scheme) + * @return this instance to next command + */ + public VirtualService setAuthentication(Authentication authentication) { + if ((authentication == null) || (authentication.getScheme() == null)) { + removeMetadata(AUTHENTICATION_SCHEME); + } else { + addMetadata(AUTHENTICATION_SCHEME, authentication.getScheme().getScheme()); + } + + if ((authentication == null) || StringUtils.isEmpty(authentication.getApplid())) { + removeMetadata(AUTHENTICATION_APPLID); + } else { + addMetadata(AUTHENTICATION_APPLID, authentication.getApplid()); + } + + return this; + } + + /** + * If you need add special routing rule to discovery service, catalog and especially gateway service. If no route is + * added, default one is creating on starting. + * @param gatewayUrl url on gateway side + * @param serviceRelativeUrl url part on this service + * @return this instance to next command + */ + public VirtualService addRoute(String gatewayUrl, String serviceRelativeUrl) { + gatewayUrl = UrlUtils.trimSlashes(gatewayUrl); + + if (gatewayPath == null) gatewayPath = "/" + gatewayUrl; + + String serviceUrl = (serviceRelativeUrl == null ? "" : serviceRelativeUrl); + String key = gatewayUrl.replace("/", "-"); + + addMetadata(String.format("%s.%s.%s", ROUTES, key, ROUTES_GATEWAY_URL), gatewayUrl); + addMetadata(String.format("%s.%s.%s", ROUTES, key, ROUTES_SERVICE_URL), serviceUrl); + + return this; + } + + /** + * Add default routing, not necessary to call, it is part of initialization, you can use it just to be clear that + * there is no other + * @return + */ + public VirtualService addDefaultRoute() { + addRoute("api/v1", "/"); + + return this; + } + + private void addDefaultRouteIfMissing() { + if (!metadata.keySet().stream().filter(x -> x.startsWith(ROUTES + ".")).findAny().isPresent()) { + addDefaultRoute(); + } + } + + /** + * @return URL for each registered gateway + */ + public List getGatewayUrls() { + return DiscoveryUtils.getGatewayUrls().stream().map(x -> x + gatewayPath + "/" + serviceId.toLowerCase()).collect(Collectors.toList()); + } + + /** + * @param headerName name of header, see {@link HeaderServlet} and {@link #addGetHeaderServlet(String)} + * @return list of url to the header servlet, for each registered gateway one + */ + public List getGatewayHeaderUrls(String headerName) { + return getGatewayUrls().stream().map(x -> x + "/header/" + headerName).collect(Collectors.toList()); + } + + /** + * @return url of all gateways to this service and health service + */ + public List getGatewayHealthUrls() { + return getGatewayUrls().stream().map(x -> x + "/application/health").collect(Collectors.toList()); + } + + /** + * @return URL of all gateways to this service and servlet {@link VerifyServlet} + */ + public List getGatewayVerifyUrls() { + return getGatewayUrls().stream().map(x -> x + "/verify").collect(Collectors.toList()); + } + + @AllArgsConstructor + class HeaderServlet extends HttpServlet { + + private final String headerName; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setStatus(HttpStatus.SC_OK); + resp.getWriter().print(req.getHeader(headerName)); + resp.getWriter().close(); + } + + } + + /** + * Service to send heart beat + */ + class HealthService implements Runnable { + + private final int heartbeatInterval; + private boolean up = true; + + HealthService(int heartbeatInterval) { + this.heartbeatInterval = heartbeatInterval; + new Thread(this).start(); + } + + public void stop() { + this.up = false; + } + + public Status getStatus() { + return up ? Status.UP : Status.DOWN; + } + + private void sendHeartBeat() { + given() + .param("value", getStatus().getCode()) + .put(DiscoveryUtils.getDiscoveryUrl() + "/eureka/apps/{appId}/{instanceId}/status", serviceId, instanceId) + .then().statusCode(SC_OK); + } + + @Override + public void run() { + try { + long lastCall = System.currentTimeMillis(); + sendHeartBeat(); + while (up) { + Thread.sleep(100); + if (instanceId == null) continue; + + if (lastCall + 1000 * heartbeatInterval < System.currentTimeMillis()) { + lastCall = System.currentTimeMillis(); + sendHeartBeat(); + } + } + } catch (InterruptedException e) { + // never happened + } + } + + } + + /** + * Serve address /application/health to support health answer. It is helpful for long term tests, because discovery + * service need this heart beat send each 30s. + */ + @NoArgsConstructor + class HealthServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setStatus(HttpStatus.SC_OK); + resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); + final Status status = healthService == null ? Status.UNKNOWN : healthService.getStatus(); + resp.getWriter().print("{\"status\":\"" + status + "\"}"); + resp.getWriter().close(); + } + } + + /** + * Servlet Verify store all request to next analyze, see {@link RequestVerifier} + */ + @AllArgsConstructor + class VerifyServlet extends HttpServlet { + + private RequestVerifier verify; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + verify.add(VirtualService.this, req); + resp.setStatus(HttpStatus.SC_OK); + } + + } + + /** + * Servlet answer on /application/instance instanceId. This is base part of method to verify registration on + * gateways, see {@link #waitForGatewayRegistration(int, int)} and {@link #waitForGatewayUnregistration(int, int)} + */ + class InstanceServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setStatus(HttpStatus.SC_OK); + resp.getWriter().print(VirtualService.this.instanceId); + resp.getWriter().close(); + } + } + +} From 9a00d299c1e083f29624ec6549fa47e78c47c303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Mon, 20 Jan 2020 16:43:18 +0100 Subject: [PATCH 080/122] return type AuthController (Boolean > response code), https://github.com/zowe/api-layer/pull/465#discussion_r366370498 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../ca/mfaas/gateway/controllers/AuthController.java | 10 ++++++++-- .../mfaas/gateway/controllers/AuthControllerTest.java | 9 +++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/AuthController.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/AuthController.java index 17ee0a87f3..3138f1ea79 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/AuthController.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/AuthController.java @@ -16,6 +16,10 @@ import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import static org.apache.http.HttpStatus.SC_OK; +import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE; /** * Controller offer method to control security. It can contains method for user and also method for calling services @@ -29,13 +33,15 @@ public class AuthController { private final AuthenticationService authenticationService; @DeleteMapping(path = "/invalidate/**") - public Boolean invalidateJwtToken(HttpServletRequest request) { + public void invalidateJwtToken(HttpServletRequest request, HttpServletResponse response) { final String path = "/auth/invalidate/"; final String uri = request.getRequestURI(); final int index = uri.indexOf(path); final String jwtToken = (index >= 0) ? uri.substring(index + path.length()) : ""; - return authenticationService.invalidateJwtToken(jwtToken, false); + final boolean invalidated = authenticationService.invalidateJwtToken(jwtToken, false); + + response.setStatus(invalidated ? SC_OK : SC_SERVICE_UNAVAILABLE); } } diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/AuthControllerTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/AuthControllerTest.java index 940bd508e5..7c0af89122 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/AuthControllerTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/AuthControllerTest.java @@ -17,9 +17,10 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import static org.apache.http.HttpStatus.SC_OK; +import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @RunWith(SpringJUnit4ClassRunner.class) @@ -41,12 +42,12 @@ public void setUp() { @Test public void invalidateJwtToken() throws Exception { when(authenticationService.invalidateJwtToken("a/b", false)).thenReturn(Boolean.TRUE); - this.mockMvc.perform(delete("/auth/invalidate/a/b")).andExpect(status().isOk()).andExpect(content().string("true")); + this.mockMvc.perform(delete("/auth/invalidate/a/b")).andExpect(status().is(SC_OK)); when(authenticationService.invalidateJwtToken("abcde", false)).thenReturn(Boolean.TRUE); - this.mockMvc.perform(delete("/auth/invalidate/abcde")).andExpect(status().isOk()).andExpect(content().string("true")); + this.mockMvc.perform(delete("/auth/invalidate/abcde")).andExpect(status().is(SC_OK)); - this.mockMvc.perform(delete("/auth/invalidate/xyz")).andExpect(status().isOk()).andExpect(content().string("false")); + this.mockMvc.perform(delete("/auth/invalidate/xyz")).andExpect(status().is(SC_SERVICE_UNAVAILABLE)); verify(authenticationService, times(1)).invalidateJwtToken("abcde", false); verify(authenticationService, times(1)).invalidateJwtToken("a/b", false); From 761bef6c2b03e3167fa9105aea50c4bcdfb0f5e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Mon, 20 Jan 2020 16:48:51 +0100 Subject: [PATCH 081/122] test for cover method Authentication.isEmpty, https://github.com/zowe/api-layer/pull/465/files/3db9bb555857f5847cf2b61639ed1862fa723cb8#r366754178 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../common/auth/AuthenticationTest.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 apiml-security-common/src/test/java/com/ca/apiml/security/common/auth/AuthenticationTest.java diff --git a/apiml-security-common/src/test/java/com/ca/apiml/security/common/auth/AuthenticationTest.java b/apiml-security-common/src/test/java/com/ca/apiml/security/common/auth/AuthenticationTest.java new file mode 100644 index 0000000000..f240f6933a --- /dev/null +++ b/apiml-security-common/src/test/java/com/ca/apiml/security/common/auth/AuthenticationTest.java @@ -0,0 +1,41 @@ +package com.ca.apiml.security.common.auth;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +@RunWith(JUnit4.class) +public class AuthenticationTest { + + @Test + public void testIsEmpty() { + Authentication a; + + a = new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applid"); + assertFalse(a.isEmpty()); + + a = new Authentication(AuthenticationScheme.ZOSMF, null); + assertFalse(a.isEmpty()); + + a = new Authentication(null, "applid"); + assertFalse(a.isEmpty()); + + a = new Authentication(null, ""); + assertFalse(a.isEmpty()); + + + a = new Authentication(null, null); + assertTrue(a.isEmpty()); + } + +} From 72eeb6a1360b101138db2487c432f61638023a7f Mon Sep 17 00:00:00 2001 From: JirkaAichler Date: Mon, 20 Jan 2020 17:03:48 +0100 Subject: [PATCH 082/122] Review changes Signed-off-by: JirkaAichler --- .../config/AuthConfigurationProperties.java | 2 +- .../AbstractIRRPassTicketException.java | 6 +++ .../IRRPassTicketEvaluationException.java | 4 ++ .../IRRPassTicketGenerationException.java | 4 ++ .../common/service/PassTicketService.java | 19 +++---- .../common/auth/AuthenticationSchemeTest.java | 4 +- .../cache/CompositeKeyGeneratorTest.java | 4 +- .../schema/HttpBasicPassTicketScheme.java | 27 ++++------ .../schema/HttpBasicPassTicketSchemeTest.java | 51 ++++++++++++------- 9 files changed, 71 insertions(+), 50 deletions(-) diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/config/AuthConfigurationProperties.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/config/AuthConfigurationProperties.java index 81a178e638..55831b164e 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/config/AuthConfigurationProperties.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/config/AuthConfigurationProperties.java @@ -67,7 +67,7 @@ public static class CookieProperties { @Data public static class PassTicket { - private Integer timeout = 360; + private Integer timeout = 540; } public AuthConfigurationProperties() { diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/AbstractIRRPassTicketException.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/AbstractIRRPassTicketException.java index e191b8823f..4b5401fa9b 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/AbstractIRRPassTicketException.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/AbstractIRRPassTicketException.java @@ -26,6 +26,12 @@ public abstract class AbstractIRRPassTicketException extends Exception { protected final int racfRc; protected final int racfRsn; + public AbstractIRRPassTicketException(ErrorCode errorCode) { + this.safRc = errorCode.getSafRc(); + this.racfRc = errorCode.getRacfRc(); + this.racfRsn = errorCode.getRacfRsn(); + } + public ErrorCode getErrorCode() { return ErrorCode.getErrorCode(this); } diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicketEvaluationException.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicketEvaluationException.java index ae51cb3a3d..b3b54da202 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicketEvaluationException.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicketEvaluationException.java @@ -20,6 +20,10 @@ public IRRPassTicketEvaluationException(int safRc, int racfRc, int racfRsn) { super(safRc, racfRc, racfRsn); } + public IRRPassTicketEvaluationException(ErrorCode errorCode) { + super(errorCode); + } + @Override public String getMessage() { return getMessage("Error on evaluation of PassTicket:"); diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicketGenerationException.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicketGenerationException.java index 0e71e3ae8d..842bcb37ce 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicketGenerationException.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/IRRPassTicketGenerationException.java @@ -20,6 +20,10 @@ public IRRPassTicketGenerationException(int safRc, int racfRc, int racfRsn) { super(safRc, racfRc, racfRsn); } + public IRRPassTicketGenerationException(ErrorCode errorCode) { + super(errorCode); + } + @Override public String getMessage() { return getMessage("Error on generation of PassTicket:"); diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java index 752b1f4ffd..b8b32624eb 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java @@ -10,6 +10,7 @@ package com.ca.apiml.security.common.service; import com.ca.mfaas.util.ClassOrDefaultProxyUtils; +import com.ca.mfaas.util.ObjectUtil; import lombok.AllArgsConstructor; import lombok.Value; import org.apache.commons.lang.StringUtils; @@ -69,15 +70,12 @@ public static class DefaultPassTicketImpl implements IRRPassTicket { @Override public void evaluate(String userId, String applId, String passTicket) throws IRRPassTicketEvaluationException { - if (userId == null) - throw new IllegalArgumentException("Parameter userId is empty"); - if (applId == null) - throw new IllegalArgumentException("Parameter applId is empty"); - if (passTicket == null) - throw new IllegalArgumentException("Parameter passTicket is empty"); + ObjectUtil.requireNotNull(userId, "Parameter userId is empty"); + ObjectUtil.requireNotNull(applId, "Parameter applId is empty"); + ObjectUtil.requireNotNull(passTicket, "Parameter passTicket is empty"); if (StringUtils.equalsIgnoreCase(UNKNOWN_APPLID, applId)) { - throw new IRRPassTicketEvaluationException(8, 16, 28); + throw new IRRPassTicketEvaluationException(AbstractIRRPassTicketException.ErrorCode.ERR_8_16_28); } if (userId.equals(ZOWE_DUMMY_USERID) && passTicket.startsWith(ZOWE_DUMMY_PASS_TICKET_PREFIX)) { @@ -87,18 +85,18 @@ public void evaluate(String userId, String applId, String passTicket) throws IRR final Set passTickets = userAppToPasstickets.get(new UserApp(userId, applId)); if ((passTickets == null) || !passTickets.contains(passTicket)) { - throw new IRRPassTicketEvaluationException(8, 16, 32); + throw new IRRPassTicketEvaluationException(AbstractIRRPassTicketException.ErrorCode.ERR_8_16_32); } } @Override public String generate(String userId, String applId) throws IRRPassTicketGenerationException { if (StringUtils.equalsIgnoreCase(UNKNOWN_USER, userId)) { - throw new IRRPassTicketGenerationException(8, 8, 16); + throw new IRRPassTicketGenerationException(AbstractIRRPassTicketException.ErrorCode.ERR_8_8_16); } if (StringUtils.equalsIgnoreCase(UNKNOWN_APPLID, applId)) { - throw new IRRPassTicketGenerationException(8, 16, 28); + throw new IRRPassTicketGenerationException(AbstractIRRPassTicketException.ErrorCode.ERR_8_16_28); } if (StringUtils.equalsIgnoreCase(DUMMY_USER, userId)) { @@ -126,7 +124,6 @@ private static class UserApp { private final String applId; } - } } diff --git a/apiml-security-common/src/test/java/com/ca/apiml/security/common/auth/AuthenticationSchemeTest.java b/apiml-security-common/src/test/java/com/ca/apiml/security/common/auth/AuthenticationSchemeTest.java index ba0db615ca..0b8518d376 100644 --- a/apiml-security-common/src/test/java/com/ca/apiml/security/common/auth/AuthenticationSchemeTest.java +++ b/apiml-security-common/src/test/java/com/ca/apiml/security/common/auth/AuthenticationSchemeTest.java @@ -11,6 +11,7 @@ import org.junit.Test; import static org.junit.Assert.*; + public class AuthenticationSchemeTest { @Test @@ -19,7 +20,6 @@ public void testFromScheme() { AuthenticationScheme as2 = AuthenticationScheme.fromScheme(as.getScheme()); assertSame(as, as2); } - assertNull(AuthenticationScheme.fromScheme("absolutly nonsence")); + assertNull(AuthenticationScheme.fromScheme("absolute nonsense")); } - } diff --git a/common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyGeneratorTest.java b/common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyGeneratorTest.java index b57f968438..87faaff1d0 100644 --- a/common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyGeneratorTest.java +++ b/common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyGeneratorTest.java @@ -28,13 +28,13 @@ public void generate() throws NoSuchMethodException { assertEquals("a", g.generate(target, method, "a")); - Object param = new Object[] {"a"}; + Object param = new Object[]{"a"}; Object response = g.generate(target, method, param); assertTrue(response instanceof CompositeKey); assertEquals(param, ((CompositeKey) response).get(0)); assertEquals(new CompositeKey("a", "b"), g.generate(target, method, "a", "b")); - assertEquals(new CompositeKey(new String[] {"a"}, "b"), g.generate(target, method, new String[] {"a"}, "b")); + assertEquals(new CompositeKey(new String[]{"a"}, "b"), g.generate(target, method, new String[]{"a"}, "b")); } } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketScheme.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketScheme.java index 4015951380..b105952a48 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketScheme.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketScheme.java @@ -11,6 +11,7 @@ import com.ca.apiml.security.common.auth.Authentication; import com.ca.apiml.security.common.auth.AuthenticationScheme; +import com.ca.apiml.security.common.config.AuthConfigurationProperties; import com.ca.apiml.security.common.service.IRRPassTicketGenerationException; import com.ca.apiml.security.common.service.PassTicketService; import com.ca.apiml.security.common.token.QueryResponse; @@ -18,9 +19,9 @@ import com.netflix.appinfo.InstanceInfo; import com.netflix.zuul.context.RequestContext; import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.Value; import org.apache.http.HttpHeaders; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.nio.charset.StandardCharsets; @@ -31,16 +32,10 @@ * SAF and generating new authentication header in request. */ @Component +@RequiredArgsConstructor public class HttpBasicPassTicketScheme implements AbstractAuthenticationScheme { - private final PassTicketService passTicketService; - - @Value("${apiml.security.auth.passTicket.timeout:540}") - private Integer timeout; - - public HttpBasicPassTicketScheme(@Autowired PassTicketService passTicketService) { - this.passTicketService = passTicketService; - } + private final AuthConfigurationProperties authConfigurationProperties; @Override public AuthenticationScheme getScheme() { @@ -49,7 +44,7 @@ public AuthenticationScheme getScheme() { @Override public AuthenticationCommand createCommand(Authentication authentication, QueryResponse token) - throws AuthenticationException { + throws AuthenticationException { final long before = System.currentTimeMillis(); final String applId = authentication.getApplid(); @@ -59,18 +54,19 @@ public AuthenticationCommand createCommand(Authentication authentication, QueryR passTicket = passTicketService.generate(userId, applId); } catch (IRRPassTicketGenerationException e) { throw new AuthenticationException( - String.format("Could not generate PassTicket for user ID %s and APPLID %s", userId, applId), e); + String.format("Could not generate PassTicket for user ID %s and APPLID %s", userId, applId), e); } final String encoded = Base64.getEncoder() - .encodeToString((userId + ":" + passTicket).getBytes(StandardCharsets.UTF_8)); + .encodeToString((userId + ":" + passTicket).getBytes(StandardCharsets.UTF_8)); final String value = "Basic " + encoded; - final long expiredAt = Math.min(before + timeout * 1000, token.getExpiration().getTime()); + final long expiredAt = Math.min(before + authConfigurationProperties.getPassTicket().getTimeout() * 1000, + token.getExpiration().getTime()); return new PassTicketCommand(value, expiredAt); } - @lombok.Value + @Value @EqualsAndHashCode(callSuper = false) public static class PassTicketCommand extends AuthenticationCommand { @@ -91,5 +87,4 @@ public boolean isExpired() { } } - } diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java index 4916515077..a88e0e86a2 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java @@ -9,11 +9,11 @@ */ package com.ca.mfaas.gateway.security.service.schema; +import static com.ca.apiml.security.common.service.PassTicketService.DefaultPassTicketImpl.UNKNOWN_USER; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.when; import java.util.Calendar; @@ -21,34 +21,32 @@ import com.ca.apiml.security.common.auth.Authentication; import com.ca.apiml.security.common.auth.AuthenticationScheme; +import com.ca.apiml.security.common.config.AuthConfigurationProperties; import com.ca.apiml.security.common.service.PassTicketService; import com.ca.apiml.security.common.token.QueryResponse; +import com.ca.mfaas.gateway.security.service.AuthenticationException; import com.ca.mfaas.gateway.utils.CleanCurrentRequestContextTest; import com.netflix.zuul.context.RequestContext; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; +import org.junit.rules.ExpectedException; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.test.util.ReflectionTestUtils; -@RunWith(MockitoJUnitRunner.class) public class HttpBasicPassTicketSchemeTest extends CleanCurrentRequestContextTest { - - private final int PASSTICKET_DURATION = 300; - - @Mock - private PassTicketService passTicketService; - - @InjectMocks + private final AuthConfigurationProperties authConfigurationProperties = new AuthConfigurationProperties(); private HttpBasicPassTicketScheme httpBasicPassTicketScheme; + @Rule + public ExpectedException exceptionRule = ExpectedException.none(); + @Before public void init() { - ReflectionTestUtils.setField(httpBasicPassTicketScheme, "timeout", PASSTICKET_DURATION); + PassTicketService passTicketService = new PassTicketService(); + passTicketService.init(); + httpBasicPassTicketScheme = new HttpBasicPassTicketScheme(passTicketService, authConfigurationProperties); } @Test @@ -57,8 +55,6 @@ public void testCreateCommand() throws Exception { Authentication authentication = new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applid"); QueryResponse queryResponse = new QueryResponse("domain", "username", calendar.getTime(), calendar.getTime()); - when(passTicketService.generate("username", "applid")).thenReturn("123456"); - AuthenticationCommand ac = httpBasicPassTicketScheme.createCommand(authentication, queryResponse); assertNotNull(ac); @@ -68,7 +64,8 @@ public void testCreateCommand() throws Exception { RequestContext.testSetCurrentContext(requestContext); ac.apply(null); - assertEquals("Basic dXNlcm5hbWU6MTIzNDU2", requestContext.getZuulRequestHeaders().get("authorization")); + assertEquals("Basic dXNlcm5hbWU6Wm93ZUR1bW15UGFzc1RpY2tldF9hcHBsaWRfdXNlcm5hbWVfMA==", + requestContext.getZuulRequestHeaders().get("authorization")); // JWT token expired one minute ago (command expired also if JWT token expired) calendar.add(Calendar.MINUTE, -1); @@ -87,9 +84,27 @@ public void testCreateCommand() throws Exception { ac = httpBasicPassTicketScheme.createCommand(authentication, queryResponse); calendar = Calendar.getInstance(); - calendar.add(Calendar.SECOND, PASSTICKET_DURATION); + calendar.add(Calendar.SECOND, authConfigurationProperties.getPassTicket().getTimeout()); // checking setup of expired time, JWT expired in future (more than hour), check if set date is similar to passticket timeout (5s) assertEquals(0.0, Math.abs(calendar.getTime().getTime() - (long) ReflectionTestUtils.getField(ac, "expireAt")), 10.0); } + @Test + public void returnsCorrectScheme() { + assertEquals(AuthenticationScheme.HTTP_BASIC_PASSTICKET, httpBasicPassTicketScheme.getScheme()); + } + + @Test + public void getExceptionWhenUserIdNotValid() throws AuthenticationException { + String applId = "applId"; + + Calendar calendar = Calendar.getInstance(); + Authentication authentication = new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, applId); + QueryResponse queryResponse = new QueryResponse("domain", UNKNOWN_USER, calendar.getTime(), calendar.getTime()); + + exceptionRule.expect(AuthenticationException.class); + exceptionRule.expectMessage(String.format("Could not generate PassTicket for user ID %s and APPLID %s", UNKNOWN_USER, applId)); + + httpBasicPassTicketScheme.createCommand(authentication, queryResponse); + } } From 07d326ef6bcfe1afc250fc65df9c0e6b7799750c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Tue, 21 Jan 2020 11:30:57 +0100 Subject: [PATCH 083/122] code coverage improvement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../error/AuthExceptionHandlerTest.java | 8 ++- .../IRRPassTicketEvaluationExceptionTest.java | 35 +++++++++ .../IRRPassTicketGenerationExceptionTest.java | 35 +++++++++ common-service-core/build.gradle | 1 + .../com/ca/mfaas/cache/CompositeKeyTest.java | 22 ++++++ .../ca/mfaas/message/log/ApimlLoggerTest.java | 67 +++++++++++++++++ .../pre/ServiceAuthenticationFilterTest.java | 35 ++++++++- .../service/schema/ByPassSchemeTest.java | 6 +- .../service/schema/ZosmfSchemeTest.java | 72 ++++++++++++++----- .../service/schema/ZoweJwtSchemeTest.java | 28 ++++++++ 10 files changed, 287 insertions(+), 22 deletions(-) create mode 100644 apiml-security-common/src/test/java/com/ca/apiml/security/common/service/IRRPassTicketEvaluationExceptionTest.java create mode 100644 apiml-security-common/src/test/java/com/ca/apiml/security/common/service/IRRPassTicketGenerationExceptionTest.java create mode 100644 common-service-core/src/test/java/com/ca/mfaas/message/log/ApimlLoggerTest.java create mode 100644 gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZoweJwtSchemeTest.java diff --git a/apiml-security-common/src/test/java/com/ca/apiml/security/common/error/AuthExceptionHandlerTest.java b/apiml-security-common/src/test/java/com/ca/apiml/security/common/error/AuthExceptionHandlerTest.java index da15ac70e7..70012078a3 100644 --- a/apiml-security-common/src/test/java/com/ca/apiml/security/common/error/AuthExceptionHandlerTest.java +++ b/apiml-security-common/src/test/java/com/ca/apiml/security/common/error/AuthExceptionHandlerTest.java @@ -155,6 +155,12 @@ public void testAuthenticationFailure_whenExceptionIsAuthenticationException() t verify(objectMapper).writeValue(httpServletResponse.getWriter(), message.mapToView()); } + @Test + public void testInvalidCertificateException() throws ServletException { + authExceptionHandler.handleException(httpServletRequest, httpServletResponse, new InvalidCertificateException("method")); + assertEquals(HttpStatus.FORBIDDEN.value(), httpServletResponse.getStatus()); + } + @Test(expected = ServletException.class) public void testAuthenticationFailure_whenOccurUnexpectedException() throws ServletException { authExceptionHandler.handleException( @@ -163,7 +169,6 @@ public void testAuthenticationFailure_whenOccurUnexpectedException() throws Serv new RuntimeException("unexpectedException")); } - @Configuration static class ContextConfiguration { @Bean @@ -171,4 +176,5 @@ public MessageService messageService() { return new YamlMessageService("/security-service-messages.yml"); } } + } diff --git a/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/IRRPassTicketEvaluationExceptionTest.java b/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/IRRPassTicketEvaluationExceptionTest.java new file mode 100644 index 0000000000..46d0e6ff7f --- /dev/null +++ b/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/IRRPassTicketEvaluationExceptionTest.java @@ -0,0 +1,35 @@ +package com.ca.apiml.security.common.service;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static org.junit.Assert.assertEquals; +@RunWith(JUnit4.class) +public class IRRPassTicketEvaluationExceptionTest { + + @Test + public void testInit() { + IRRPassTicketEvaluationException exception = new IRRPassTicketEvaluationException(8, 12, 20); + assertEquals(8, exception.getSafRc()); + assertEquals(12, exception.getRacfRc()); + assertEquals(20, exception.getRacfRsn()); + assertEquals(AbstractIRRPassTicketException.ErrorCode.ERR_8_12_20, exception.getErrorCode()); + assertEquals("Error on evaluation of PassTicket: Invocation of the Security Server Network Authentication Service Program Call (PC) interface failed with an 'abend in the PC service routine' return code. The symptom record associated with this abend can be found in the logrec data set.", exception.getMessage()); + + IRRPassTicketEvaluationException exception2 = new IRRPassTicketEvaluationException(AbstractIRRPassTicketException.ErrorCode.ERR_8_16_28); + assertEquals(8, exception2.getSafRc()); + assertEquals(16, exception2.getRacfRc()); + assertEquals(28, exception2.getRacfRsn()); + assertEquals(AbstractIRRPassTicketException.ErrorCode.ERR_8_16_28, exception2.getErrorCode()); + } + +} diff --git a/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/IRRPassTicketGenerationExceptionTest.java b/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/IRRPassTicketGenerationExceptionTest.java new file mode 100644 index 0000000000..4b60965f18 --- /dev/null +++ b/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/IRRPassTicketGenerationExceptionTest.java @@ -0,0 +1,35 @@ +package com.ca.apiml.security.common.service;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static org.junit.Assert.assertEquals; +@RunWith(JUnit4.class) +public class IRRPassTicketGenerationExceptionTest { + + @Test + public void testInit() { + IRRPassTicketGenerationException exception = new IRRPassTicketGenerationException(8, 16, 32); + assertEquals(8, exception.getSafRc()); + assertEquals(16, exception.getRacfRc()); + assertEquals(32, exception.getRacfRsn()); + assertEquals(AbstractIRRPassTicketException.ErrorCode.ERR_8_16_32, exception.getErrorCode()); + assertEquals("Error on generation of PassTicket: " + AbstractIRRPassTicketException.ErrorCode.ERR_8_16_32.getMessage(), exception.getMessage()); + + IRRPassTicketGenerationException exception2 = new IRRPassTicketGenerationException(AbstractIRRPassTicketException.ErrorCode.ERR_8_12_8); + assertEquals(8, exception2.getSafRc()); + assertEquals(12, exception2.getRacfRc()); + assertEquals(8, exception2.getRacfRsn()); + assertEquals(AbstractIRRPassTicketException.ErrorCode.ERR_8_12_8, exception2.getErrorCode()); + } + +} diff --git a/common-service-core/build.gradle b/common-service-core/build.gradle index 093b9eff22..91d3cb5129 100644 --- a/common-service-core/build.gradle +++ b/common-service-core/build.gradle @@ -13,6 +13,7 @@ dependencies { compileOnly(libraries.eh_cache) testCompile(libraries.spring_boot_starter_cache) + testCompile(libraries.spring_boot_starter_test) testCompile(libraries.eh_cache) testCompile(libraries.jackson_core) testCompile(libraries.jackson_databind) diff --git a/common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyTest.java b/common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyTest.java index 647d02de4e..72a58ed967 100644 --- a/common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyTest.java +++ b/common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyTest.java @@ -51,6 +51,28 @@ public void testInit() { assertNotEquals(new CompositeKey(new Object[] {"d", "b"}, "c"), ck); } + @Test + public void testEquals() { + CompositeKey c1 = new CompositeKey(); + + assertEquals(c1, c1); + assertNotEquals(c1, null); + assertNotEquals(c1, new CompositeKey() {}); + } + + @Test + public void testHashCode() { + assertEquals( + new CompositeKey("a", "b", new String[] {"c", "d"}).hashCode(), + new CompositeKey("a", "b", new String[] {"c", "d"}).hashCode() + ); + + assertNotEquals( + new CompositeKey("a", "b", new String[] {"c", "d"}).hashCode(), + new CompositeKey("a", "b", new String[] {"c", "E"}).hashCode() + ); + } + @Test public void testToString() { assertEquals("CompositeKey []", new CompositeKey().toString()); diff --git a/common-service-core/src/test/java/com/ca/mfaas/message/log/ApimlLoggerTest.java b/common-service-core/src/test/java/com/ca/mfaas/message/log/ApimlLoggerTest.java new file mode 100644 index 0000000000..1ad7e7b499 --- /dev/null +++ b/common-service-core/src/test/java/com/ca/mfaas/message/log/ApimlLoggerTest.java @@ -0,0 +1,67 @@ +package com.ca.mfaas.message.log;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import com.ca.mfaas.message.core.MessageType; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.slf4j.Logger; +import org.slf4j.Marker; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.*; + +@RunWith(JUnit4.class) +public class ApimlLoggerTest { + + @Test + public void testEmpty() { + ApimlLogger apimlLogger = ApimlLogger.empty(); + Logger logger = (Logger) ReflectionTestUtils.getField(apimlLogger, "logger"); + assertEquals(ApimlLogger.class.getName(), logger.getName()); + assertNull(ReflectionTestUtils.getField(apimlLogger, "messageService")); + + assertNull(apimlLogger.log("someKey")); + } + + @Test + public void testLogLevel() { + ApimlLogger apimlLogger = new ApimlLogger(ApimlLoggerTest.class, null); + + Logger logger = mock(Logger.class); + ReflectionTestUtils.setField(apimlLogger, "logger", logger); + + Marker marker = (Marker) ReflectionTestUtils.getField(apimlLogger, "marker"); + + apimlLogger.log(MessageType.TRACE, "traceLog", new Object[] {"param1"}); + verify(logger, times(1)).trace(marker, "traceLog", new Object[] {"param1"}); + + apimlLogger.log(MessageType.DEBUG, "debugLog", new Object[] {"param2"}); + verify(logger, times(1)).debug(marker, "debugLog", new Object[] {"param2"}); + + apimlLogger.log(MessageType.INFO, "infoLog", new Object[] {"param3"}); + verify(logger, times(1)).info(marker, "infoLog", new Object[] {"param3"}); + + apimlLogger.log(MessageType.WARNING, "warningLog", new Object[] {"param4"}); + verify(logger, times(1)).warn(marker, "warningLog", new Object[] {"param4"}); + + apimlLogger.log(MessageType.ERROR, "errorLog", new Object[] {"param5"}); + verify(logger, times(1)).error(marker, "errorLog", new Object[] {"param5"}); + + verify(logger, times(1)).trace((Marker) any(), anyString(), (Object[]) any()); + verify(logger, times(1)).debug((Marker) any(), anyString(), (Object[]) any()); + verify(logger, times(1)).info((Marker) any(), anyString(), (Object[]) any()); + verify(logger, times(1)).warn((Marker) any(), anyString(), (Object[]) any()); + verify(logger, times(1)).error((Marker) any(), anyString(), (Object[]) any()); + } + +} diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java index efc543f3c0..175a5482ee 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ServiceAuthenticationFilterTest.java @@ -14,18 +14,21 @@ import com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand; import com.ca.mfaas.gateway.utils.CleanCurrentRequestContextTest; import com.netflix.zuul.context.RequestContext; - +import com.netflix.zuul.exception.ZuulException; +import com.netflix.zuul.monitoring.CounterFactory; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.cloud.netflix.zuul.util.ZuulRuntimeException; +import org.springframework.http.HttpStatus; import javax.servlet.http.HttpServletRequest; - import java.util.Optional; +import static org.junit.Assert.*; import static org.mockito.Mockito.*; import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SERVICE_ID_KEY; @@ -49,6 +52,13 @@ public void init() throws Exception { when(serviceAuthenticationService.getAuthenticationCommand(anyString(), any())).thenReturn(command); } + @Test + public void testConfig() { + assertEquals("pre", serviceAuthenticationFilter.filterType()); + assertEquals(9, serviceAuthenticationFilter.filterOrder()); + assertTrue(serviceAuthenticationFilter.shouldFilter()); + } + @Test public void testRun() throws Exception { HttpServletRequest request = mock(HttpServletRequest.class); @@ -67,6 +77,27 @@ public void testRun() throws Exception { when(authenticationService.getJwtTokenFromRequest(any())).thenReturn(Optional.empty()); serviceAuthenticationFilter.run(); verify(serviceAuthenticationService, times(1)).getAuthenticationCommand(anyString(), any()); + + reset(requestContext); + reset(authenticationService); + CounterFactory.initialize(new CounterFactory() { + @Override + public void increment(String name) { + } + }); + when(requestContext.get(SERVICE_ID_KEY)).thenReturn("error"); + when(authenticationService.getJwtTokenFromRequest(any())).thenReturn(Optional.of("token")); + when(serviceAuthenticationService.getAuthenticationCommand(eq("error"), any())) + .thenThrow(new RuntimeException("Potential exception")); + try { + serviceAuthenticationFilter.run(); + fail(); + } catch (ZuulRuntimeException zre) { + assertTrue(zre.getCause() instanceof ZuulException); + ZuulException ze = (ZuulException) zre.getCause(); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR.value(), ze.nStatusCode); + assertEquals(String.valueOf(new RuntimeException("Potential exception")), ze.errorCause); + } } } diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ByPassSchemeTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ByPassSchemeTest.java index 72fc277257..35619d020c 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ByPassSchemeTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ByPassSchemeTest.java @@ -8,16 +8,18 @@ * Copyright Contributors to the Zowe Project. */ +import com.ca.apiml.security.common.auth.AuthenticationScheme; import org.junit.Test; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; + public class ByPassSchemeTest { @Test public void testScheme() { ByPassScheme scheme = new ByPassScheme(); assertTrue(scheme.isDefault()); + assertEquals(AuthenticationScheme.BYPASS, scheme.getScheme()); assertSame(AuthenticationCommand.EMPTY, scheme.createCommand(null, null)); } diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZosmfSchemeTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZosmfSchemeTest.java index 1c6406f528..021ec10506 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZosmfSchemeTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZosmfSchemeTest.java @@ -9,21 +9,6 @@ */ package com.ca.mfaas.gateway.security.service.schema; -import static com.ca.mfaas.gateway.security.service.schema.ZosmfScheme.ZosmfCommand.COOKIE_HEADER; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.Calendar; -import java.util.Optional; - -import javax.servlet.http.HttpServletRequest; - import com.ca.apiml.security.common.auth.Authentication; import com.ca.apiml.security.common.auth.AuthenticationScheme; import com.ca.apiml.security.common.token.QueryResponse; @@ -31,15 +16,24 @@ import com.ca.mfaas.gateway.security.service.AuthenticationService; import com.ca.mfaas.gateway.utils.CleanCurrentRequestContextTest; import com.netflix.zuul.context.RequestContext; - +import io.jsonwebtoken.JwtException; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.util.ReflectionTestUtils; -import io.jsonwebtoken.JwtException; +import javax.servlet.http.HttpServletRequest; +import java.util.Calendar; +import java.util.Date; +import java.util.Optional; + +import static com.ca.mfaas.gateway.security.service.schema.ZosmfScheme.ZosmfCommand.COOKIE_HEADER; +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; @RunWith(MockitoJUnitRunner.class) public class ZosmfSchemeTest extends CleanCurrentRequestContextTest { @@ -106,4 +100,48 @@ public void testCreateCommand() throws Exception { } } + @Test + public void testScheme() { + ZosmfScheme scheme = new ZosmfScheme(authenticationService); + assertEquals(AuthenticationScheme.ZOSMF, scheme.getScheme()); + } + + @Test + public void testExpiration() { + ZosmfScheme scheme = new ZosmfScheme(authenticationService); + + AuthenticationCommand command; + QueryResponse queryResponse = new QueryResponse(); + + // no JWT token + command = scheme.createCommand(null, null); + assertNull(ReflectionTestUtils.getField(command, "expireAt")); + assertFalse(command.isExpired()); + + // token without expiration + command = scheme.createCommand(null, queryResponse); + assertNull(ReflectionTestUtils.getField(command, "expireAt")); + assertFalse(command.isExpired()); + + // token with expiration + queryResponse.setExpiration(new Date(123L)); + command = scheme.createCommand(null, queryResponse); + assertNotNull(ReflectionTestUtils.getField(command, "expireAt")); + assertEquals(123L, ReflectionTestUtils.getField(command, "expireAt")); + assertTrue(command.isExpired()); + + // expired 1 sec ago + Calendar c = Calendar.getInstance(); + c.add(Calendar.SECOND, -1); + queryResponse.setExpiration(c.getTime()); + command = scheme.createCommand(null, queryResponse); + assertTrue(command.isExpired()); + + // expired in 1 secs + c.add(Calendar.SECOND, 2); + queryResponse.setExpiration(c.getTime()); + command = scheme.createCommand(null, queryResponse); + assertFalse(command.isExpired()); + } + } diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZoweJwtSchemeTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZoweJwtSchemeTest.java new file mode 100644 index 0000000000..a453fb0208 --- /dev/null +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZoweJwtSchemeTest.java @@ -0,0 +1,28 @@ +package com.ca.mfaas.gateway.security.service.schema;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import com.ca.apiml.security.common.auth.AuthenticationScheme; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static org.junit.Assert.*; +@RunWith(JUnit4.class) +public class ZoweJwtSchemeTest { + + @Test + public void testScheme() { + ZoweJwtScheme scheme = new ZoweJwtScheme(); + assertFalse(scheme.isDefault()); + assertEquals(AuthenticationScheme.ZOWE_JWT, scheme.getScheme()); + assertSame(AuthenticationCommand.EMPTY, scheme.createCommand(null, null)); + } + +} From e70a283c1219869b647f374443f50a67e9260bc4 Mon Sep 17 00:00:00 2001 From: Andrew Jandacek Date: Tue, 21 Jan 2020 16:04:54 +0100 Subject: [PATCH 084/122] Refactor text up to 'solution' in Gateway & Route locators Signed-off-by: Andrew Jandacek --- docs/registry-communication.md | 170 ++++++++++++++++----------------- 1 file changed, 83 insertions(+), 87 deletions(-) diff --git a/docs/registry-communication.md b/docs/registry-communication.md index 98ed08b6f8..8e741f5275 100644 --- a/docs/registry-communication.md +++ b/docs/registry-communication.md @@ -1,70 +1,63 @@ -# Communication between client, discovery service and gateway +# Communication between Client, Discovery service, and Gateway -This document was created to summarize all knowledge gathered during implementation PassTickets, where like -side-effect delays were shortcuted. +This document is a summary of knowledge gathered during the implementation of PassTickets, and includes how to +short-cut side-effect delays. ## Client and discovery service (server) -On the begin of communication, client should register into discovery server. Client says to server at least: -- serviceId (or applicationId) -- instanceId - - to be unique, it is usually concatenated from serviceId, hostname and port (hostname and port should - be unique itself) - - structure of instanceId usually is "${hostname}:${serviceId}:${port}", as third part could be use also - random number (or any string) - - health URL - - URL where service answers status of client - - discovery client can check this state during the time, but registration is solved via heart beats - - timeout about heartbeat - - client define, how often it will send heartbeat and what is time to unregister - - staying in discovery service is until heartbeat is not delivered in timeout or client unregister itself - - service location information - - information to callback (IP, hostname, port, securePort for HTTPS) - - VipAddress - usually same like serviceId - - information about environment (shortly AWS or own) - - status - - here in registration it replaced first heartbeat - - other information - - you can find other parameters, but their have not impact to this communication - - custom data (outer Eureka's scope) could be stored into metadata - - btw. metadata are just once data, which could be changed after registration (there exists REST method - to update them) - -After registration client should send heartbeat. Until this heartbeat is received, the client is up and it is -possible call it. - -On the end client can unregister from discovery service. This call is optional, because Eureka should solve -failover. Unregister just speed up process of client removing. If this call is missing, discovery should wait -for heartbeat's time out (keep in mind, this timeout is longer than interval to client renew via heartbeat). - -All communication is usually covered with many caches and this is reason why client cannot be used immediately -after registration. In whole system exists many cached places and it takes time to go through all of them. -Usually there is a thread pool and one thread which periodically update caches. All of them are independent by -themself. +To begin communication between the client and the Discovery service, the client should register with the discovery server. Minimum information requirements from the client to communicate to the server include the following: + +- **serviceId (or applicationId)** +- **instanceId** + - Ensure that the instanceId is unique. Typically this is concatenated from `serviceId`, `hostname`, and `port`. Note that `hostname` and `port` should be unique themselves. + - The structure of the instanceId is typically `${hostname}:${serviceId}:${port}`. The third part could also be a random number or string. + - **health URL** + - The URL where the service responds with the status of client + - The discovery client can check this state but active registration occurs via heart beats. + - **timeout about heartbeat** + - The client defines how often a heartbeat is sent and when it is time to unregister. + - The client's active registration with the Discovery service is maintained until a heartbeat is not delivered within the timeout setting or the client unregisters itself. + - **service location information** + - Service location information is used for callback and includes `IP`, `hostname`, `port`, and `securePort` for HTTPS. + - `VipAddress` can be used and is usually the same as the `serviceId`. + - **information about environment (shortly AWS or own)** + - **status** + - In the registration it replaces the first heartbeat + - **other information** (Optional) + - Other parameters can be included, but do not affect communication. + - Customized data outside of the scope of Eureka can be stored in the metadata. + - Note: Metadata are data for one time use, and can changed after registration. However, a REST method can be used to update these metadata. + +After registration, the client must send a heartbeat. Once the Discovery service receives this heartbeat, it is possible to call the client. + +The client can drop communication by unregistering with the Discovery service. This call is optional as Eureka should resolves failover. Unregistering the client simply speeds up the process of client removal. Without the unregistration call, the Discovery service waits for the heartbeat to time out. Note: This timeout is longer than the interval of the renewal of client registration via the heartbest. + +Typically, all communication is covered with caches. As such, a client cannot be used immediately +after registration. Caching occurs in many places for the system as a whole and it takes time to go through all of them. +Typically, there is a thread pool whereby one thread periodically updates caches. All of caches are independent and do not affect other caches. ## Caches -In this paragraph I will describe all caches which I found in improving of time. The main idea was to shortcut -process of registration and unregistration to allows using caches on gateway side (avoid race condition between -different settings in discovery services and gateways). It each description of cache will be also link to solution -how it is solved. +The following section describes all of the caches. The main idea is to shortcut +the process of registration and unregistration to allow the use of caches on the Gateway side. As such, the race condition between +different settings in discovery services and gateways is avoided. Descriptions of cache also include a link to the solution describing how it is solved. ### Discovery service & ResponseCache -On discovery service is implemented ResponseCache. This cache is responsible from minimize call's overhead about -registered instances. If any application call discovery service, at first use the cache or create there a record -for next calls. +`ResponseCache` is implemented on the Discovery service. This cache is responsible for minimizing the call's overhead of +registered instances. If any application calls the Discovery service, initially the cache is used or a record is created for subsequent calls. -This cache as default contains two spaces: read and readWrite. Other application look in read and just in case -record is missing there, it look in readWrite or create it again. Read cache is updated by internal thread which -periodically compare records in both spaces (on references level, null including) and in case of different copy -records from readWrite into read. +The default for this cache contains two spaces: `read` and `readWrite`. Other application look in `read`. If the +record is missing, it looks in `readWrite` or recreates a record. `Read` cache is updated by an internal thread which +periodically compares records in both spaces (on references level, null including). -Two spaces has evidently reason just for NetFlix purpose and was changed with pull -request https://github.com/Netflix/eureka/pull/544. This improvement allow to use configuration to use only -readWrite space, read will be ignored and user look directly to readWrite. This cache could be updated by -discovery service. It also evict records in there on registation and unregistration (after client register -readWrite has evicted records about service, delta and full registry, but read still contains old record). + in case of different copy +records from readWrite into read. + +The two spaces was evidently created for NetFlix purposes and was changed with pull +request https://github.com/Netflix/eureka/pull/544. This improvement allows configuration to use only +`readWrite` space, `read` will be ignored and the user looks directly to `readWrite`. This cache could be updated by the Discovery service. It also produces records on registation and unregistration (after the client registers +readWrite has evicted records about service, delta and full registry, but read still contains old record). This description needs to be refactored to improve clarity. ``` eureka: @@ -74,50 +67,53 @@ eureka: ### Gateway & Discovery client -On gateways site is discovery client which support queries about services and instances on gateway side. It will -cache all information from discovery service and the serve those old cached data. -Data are updated with thread asynchronous. It time by time fetch new registry and then you can use them. For -updating exists two ways: delta, full. - -Full update is using on the very begin. It is necessary to download all information. After that is is using very -rare because performance. Gateway could call full update, but it happens only if data are not correct to fix them. -Probably there is just one possible reason - to long delay between fetching. - -Delta update fetch only changes and project them into local cache. But delta doesn't mean different between -fetching. To save resources delta means just delta in last time interval. Discovery service store changed -instances for a time period (as default 180s). Those changes are stored in the queue which is periodically -cleaned - removed changes older than the limit (next separed thread, asynchronous). When gateway ask for delta -it will return mirror of this queue stored in ResponseCache. Gateway then detect which updates were applied in -the past and which updates are new. For that it uses version (discovery service somehow mark changes with +On the Gateways site there is a discovery client which supports queries about services and instances on the gateway side. It will +cache all information from the discovery service and serve the old cached data. +Data are updated with thread asynchronously. From time to time fetch new registries so you can use them. +Updating can be performed either as delta, or full. + +The full update fetch is the initial fetch as it is necessary to download all required information. After that it is rarely used due to performance. The Gateway could call the full update, but it happens only if data are not correct to fix them. One possible reason could be the long delay between fetching. + +Delta update fetch only changes and projects them into local cache. Delta does not mean different Different what? between +fetching. To save resources delta means just delta delta what? in the last time interval. Discovery service store changed +instances for a time period (as default 180s). This description needs to be refactored for clarity. Those changes are stored in the queue which is periodically +cleaned by removing changes older than the limit (next separed thread, What is a "separed" thread?. asynchronous). When the gateway asks for delta it will return a mirror of this queue stored in `ResponseCache`. The Gateway then detects which updates were applied in the past and which updates are new. Fornew updates, it uses a version (discovery service somehow mark changes with numbers). + + This description needs to be rewritten to make it comprehensible. + + **solution** -This cache was minimized via allowing run asynchronous fetching any time. Used classes for that: -- ApimlDiscoveryClient +This cache was minimized by allowing run asynchronous fetching at any time. The following classes are used: + +- **ApimlDiscoveryClient** - custom implementation of discovery client - via reflection it takes reference to queue responsible for fetching of registry - contains method ```public void fetchRegistry()```, which add new asynchronous command to fetch registry - - DiscoveryClientConfig + - **DiscoveryClientConfig** - configuration bean to construct custom discovery client - - DiscoveryClient also support event, especially CacheRefreshedEvent after fetching - - it is used to notify other bean to evict caches (route locators, ZUUL handle mapping, CacheNotifier) - - ServiceCacheController - - controller to accept information about service changes (ie. new instance, removed instance) - - this controller ask ApimlDiscoveryClient to make fetching (it makes delta and send event about) - - after this call process is asynchronous and cannot be directly checked (only by events) + - `DiscoveryClient` also support event, especially `CacheRefreshedEvent` after fetching + - it is used to notify other bean to evict caches (route locators, ZUUL handle mapping, `CacheNotifier`) + - **ServiceCacheController** + - The controller to accept information about service changes (ie. new instance, removed instance) + - This controller asks `ApimlDiscoveryClient` to fetch (it makes a delta and sends an event) + - After this call, the process is asynchronous and cannot be directly checked (only by events). ### Gateway & Route locators -In gateway exists bean ApimlRouteLocator. This bean is responsible for collecting of client's routes. It means there are -available information about path and services. Those information are required for map URI to service. The most important is -the filter PreDecorationFilter. It call method ```Route getMatchingRoute(String path)``` on locator to translate URI into -information about service. Filter than store information (ie. serviceId) into ZUUL context. +The gateway includes the bean `ApimlRouteLocator`. This bean is responsible for collecting the client's routes. It indicates that information is available about the path and services. This information is required to map the URI to a service. The most important is +the filter `PreDecorationFilter`. It calls the method ```Route getMatchingRoute(String path)``` on the locator to translate the URI into +information about the service. A filter then stores information (ie. `serviceId`) into the ZUUL context. + +In out implementation we use a custom locator, which adds information about static routing. Route locators could be composed of +from many . ...of many what? +Eureka uses `CompositeRouteLocator` which contains `ApimlRouteLocator` and a default. Implementation of static routing +could also be performed by a different locator. In a similar way a super class of `ApimlRouteLocator` uses `ZuulProperties`. This can be also be used +to store a static route. -In out implementation we use custom locator, which add information about static routing. Route locators could be composite -from many. Eureka use CompositeRouteLocator which contains ApimlRouteLocator and a default. Implementation of static routing -could also make as different locator. In similar way super class of ApimlRouteLocator use ZuulProperties, this can be also use -for storing of static route. **This is only for information, could be changed in the future, now it is without any change**. +**Note:** This is only for information, and could be changed in the future. **solution** From 3cea9f13cb53546be71916a5faad93189b7d87d0 Mon Sep 17 00:00:00 2001 From: JirkaAichler Date: Tue, 21 Jan 2020 17:36:48 +0100 Subject: [PATCH 085/122] Fix integration tests Signed-off-by: JirkaAichler --- .../gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java index f91814eb2c..9bbde805af 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java @@ -182,7 +182,7 @@ private void updateRequestByZuulChanges(ExecutionContext context) { if (!newHeaders.isEmpty()) { final RibbonApacheHttpRequest req = (RibbonApacheHttpRequest) context.getRequest(); for (Map.Entry entry : newHeaders.entrySet()) { - req.getContext().getHeaders().add(entry.getKey(), entry.getValue()); + req.getContext().getHeaders().set(entry.getKey(), entry.getValue()); } } } @@ -204,9 +204,8 @@ public void onStartWithServer(ExecutionContext context, ExecutionInfo in } catch (Exception e) { throw new AbortExecutionException(String.valueOf(e), e); } + updateRequestByZuulChanges(context); } - - updateRequestByZuulChanges(context); } @Override From fc21cda3abda1e391266f06f255a84575225158e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Tue, 21 Jan 2020 17:51:58 +0100 Subject: [PATCH 086/122] code coverage improvement, fix getErrorCode in IRR exceptions, mark integration test AuthenticationOnDeploymentTest as local MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../AbstractIRRPassTicketException.java | 8 +- .../AbstractIRRPassTicketExceptionTest.java | 48 ++++++ .../common/service/PassTicketServiceTest.java | 40 +++-- .../mfaas/util/ClassOrDefaultProxyUtils.java | 22 +-- .../ca/mfaas/util/ExceptionMappingError.java | 4 + .../util/ClassOrDefaultProxyUtilsTest.java | 148 ++++++++++++++++++ .../client/api/PassTicketTestController.java | 3 +- .../AuthenticationOnDeploymentTest.java | 4 +- 8 files changed, 247 insertions(+), 30 deletions(-) create mode 100644 apiml-security-common/src/test/java/com/ca/apiml/security/common/service/AbstractIRRPassTicketExceptionTest.java diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/AbstractIRRPassTicketException.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/AbstractIRRPassTicketException.java index 4b5401fa9b..cf47b6de5c 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/AbstractIRRPassTicketException.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/AbstractIRRPassTicketException.java @@ -81,9 +81,11 @@ public enum ErrorCode { public static ErrorCode getErrorCode(AbstractIRRPassTicketException e) { for (final ErrorCode ec : values()) { - if (ec.getSafRc() != null && ec.getSafRc() == e.getSafRc() && - ec.getRacfRc() != null && ec.getRacfRc() == e.getRacfRc() && - ec.getRacfRsn() != null && ec.getRacfRsn() == e.getRacfRsn()) { + if ( + (ec.getSafRc() == null || ec.getSafRc().equals(e.getSafRc())) && + (ec.getRacfRc() == null || ec.getRacfRc().equals(e.getRacfRc())) && + (ec.getRacfRsn() == null || ec.getRacfRsn().equals(e.getRacfRsn())) + ) { return ec; } } diff --git a/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/AbstractIRRPassTicketExceptionTest.java b/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/AbstractIRRPassTicketExceptionTest.java new file mode 100644 index 0000000000..b7f27d1a53 --- /dev/null +++ b/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/AbstractIRRPassTicketExceptionTest.java @@ -0,0 +1,48 @@ +package com.ca.apiml.security.common.service;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.springframework.http.HttpStatus; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +@RunWith(JUnit4.class) +public class AbstractIRRPassTicketExceptionTest { + + @Test + public void testErrorCode() { + TestException te; + + te = new TestException(-1, -1, -1); + assertSame(AbstractIRRPassTicketException.ErrorCode.ERR_UNKNOWN, te.getErrorCode()); + + te = new TestException(8, 16, -1); + assertSame(AbstractIRRPassTicketException.ErrorCode.ERR_8_16_X, te.getErrorCode()); + + te = new TestException(8, 16, 32); + assertSame(AbstractIRRPassTicketException.ErrorCode.ERR_8_16_32, te.getErrorCode()); + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, te.getHttpStatus()); + + te = new TestException(8, 16, 28); + assertSame(AbstractIRRPassTicketException.ErrorCode.ERR_8_16_28, te.getErrorCode()); + assertEquals(HttpStatus.BAD_REQUEST, te.getHttpStatus()); + } + + class TestException extends AbstractIRRPassTicketException { + + public TestException(int safRc, int racfRc, int racfRsn) { + super(safRc, racfRc, racfRsn); + } + + } + +} diff --git a/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java b/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java index a29e9e6e74..7696830fee 100644 --- a/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java +++ b/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java @@ -15,11 +15,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.Order; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.util.ReflectionTestUtils; +import static com.ca.apiml.security.common.service.PassTicketService.DefaultPassTicketImpl.*; import static org.junit.Assert.*; @RunWith(SpringRunner.class) @@ -34,10 +34,15 @@ public class PassTicketServiceTest { private static String evaluated; @Test - @Order(1) - public void testInit() { - assertNotNull(passTicketService); - assertNotNull(ReflectionTestUtils.getField(passTicketService, "irrPassTicket")); + public void testIsUsingSafImplementation() { + IRRPassTicket irrPassTicket = (IRRPassTicket) ReflectionTestUtils.getField(passTicketService, "irrPassTicket"); + ClassOrDefaultProxyUtils.ClassOrDefaultProxyState stateInterface = (ClassOrDefaultProxyUtils.ClassOrDefaultProxyState) irrPassTicket; + assertEquals(stateInterface.isUsingBaseImplementation(), passTicketService.isUsingSafImplementation()); + } + + @Test + public void testInit() throws IRRPassTicketEvaluationException, IRRPassTicketGenerationException { + PassTicketService passTicketService = new PassTicketService(); ReflectionTestUtils.setField(passTicketService, "irrPassTicket", new IRRPassTicket() { @Override public void evaluate(String userId, String applId, String passTicket) { @@ -49,11 +54,7 @@ public String generate(String userId, String applId) { return userId + "-" + applId; } }); - } - @Test - @Order(2) - public void testCalledMethod() throws IRRPassTicketEvaluationException, IRRPassTicketGenerationException { evaluated = null; passTicketService.evaluate("userId", "applId", "passTicket"); assertEquals("userId-applId-passTicket", evaluated); @@ -105,6 +106,23 @@ public void testDefaultPassTicketImpl() throws IRRPassTicketEvaluationException, dpti.evaluate(TEST_USERID, "applId", passTicket1); dpti.evaluate(TEST_USERID, "applId", passTicket2); + try { + dpti.evaluate(TEST_USERID, "applId", "wrongPassTicket"); + fail(); + } catch (IRRPassTicketEvaluationException e) { + assertEquals(AbstractIRRPassTicketException.ErrorCode.ERR_8_16_32, e.getErrorCode()); + } + + try { + dpti.evaluate("anyUser", UNKNOWN_APPLID, "anyPassTicket"); + fail(); + } catch (IRRPassTicketEvaluationException e) { + assertEquals(AbstractIRRPassTicketException.ErrorCode.ERR_8_16_28, e.getErrorCode()); + } + + dpti.evaluate(ZOWE_DUMMY_USERID, "anyApplId", ZOWE_DUMMY_PASS_TICKET_PREFIX); + dpti.evaluate(ZOWE_DUMMY_USERID, "anyApplId", ZOWE_DUMMY_PASS_TICKET_PREFIX + "xyz"); + try { dpti.evaluate("userx", "applId", passTicket1); fail(); @@ -134,8 +152,10 @@ public void testDefaultPassTicketImpl() throws IRRPassTicketEvaluationException, ); } + assertEquals(ZOWE_DUMMY_PASS_TICKET_PREFIX, dpti.generate(DUMMY_USER, "anyApplid")); + try { - dpti.generate("anyUser", PassTicketService.DefaultPassTicketImpl.UNKNOWN_APPLID); + dpti.generate("anyUser", UNKNOWN_APPLID); fail(); } catch (IRRPassTicketGenerationException e) { assertEquals(8, e.getSafRc()); diff --git a/common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java b/common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java index 20588de885..7656ecde8d 100644 --- a/common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java +++ b/common-service-core/src/main/java/com/ca/mfaas/util/ClassOrDefaultProxyUtils.java @@ -9,9 +9,7 @@ */ package com.ca.mfaas.util; -import lombok.AllArgsConstructor; import lombok.Data; -import lombok.Value; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; @@ -150,7 +148,7 @@ private Method findMethod(Class clazz, Method method) { return clazz.getDeclaredMethod(method.getName(), method.getParameterTypes()); } catch (NoSuchMethodException nsme) { if (clazz == Object.class) { - throw new IllegalArgumentException("Cannot construct proxy", nsme); + throw new ExceptionMappingError("Cannot construct proxy", nsme); } return findMethod(clazz.getSuperclass(), method); } @@ -198,7 +196,7 @@ private void initMapping() { final EndPoint newEndPoint = addMapping(implementation, caller, callee); byName.put(ObjectUtil.getMethodIdentifier(caller), newEndPoint); } catch (Exception e) { - throw new IllegalArgumentException("Method " + ObjectUtil.getMethodIdentifier(caller) + " was not found on " + partInterfaceClass); + throw new ExceptionMappingError("Method " + ObjectUtil.getMethodIdentifier(caller) + " was not found on " + implementationClass); } } } @@ -210,7 +208,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl final EndPoint endPoint = mapping.get(method); if (endPoint == null) { - throw new NoSuchMethodException(String.format("Cannot found method %s", method)); + throw new ExceptionMappingError(String.format("Cannot found method %s", method)); } try { @@ -240,8 +238,7 @@ public boolean isUsingBaseImplementation() { * Object define instance of object where should be invoked the method and contains also method instance. It is * prepare before to make invoke in the fastest way. */ - @Value - @AllArgsConstructor + @Data public static final class EndPoint { private final Object target; @@ -286,7 +283,6 @@ public interface ExceptionMapping { * * @param Type of target exception */ - @AllArgsConstructor @Data public static class ByMethodName implements ExceptionMapping { @@ -334,7 +330,7 @@ private Function getMappingFunction(String sourceExceptionClassNam try { eClass = (Class) Class.forName(sourceExceptionClassName); } catch (ClassNotFoundException e) { - log.debug("Exception {} is not available, it will not be mapped into {} : " + e.getLocalizedMessage(), sourceExceptionClassName, targetExceptionClass); + log.debug("Exception {} is not available, it will not be mapped into {} : " + e, sourceExceptionClassName, targetExceptionClass); return null; } @@ -344,15 +340,14 @@ private Function getMappingFunction(String sourceExceptionClassNam for (String methodName : methodNames) { final Method method = findMethod(eClass, methodName); if (method == null) { - log.debug("Cannot find method {} in {} to map exceptions", methodName, sourceExceptionClassName); - return null; + throw new ExceptionMappingError("Cannot find method " + methodName + " in " + sourceExceptionClassName + " to map exceptions"); } argClasses.add(method.getReturnType()); mapFunctions.add(x -> { try { return method.invoke(x); } catch (IllegalAccessException | InvocationTargetException e) { - throw new IllegalArgumentException(e); + throw new ExceptionMappingError("Cannot invoke method " + method, e); } }); } @@ -361,8 +356,7 @@ private Function getMappingFunction(String sourceExceptionClassNam try { return getMappingFunction(targetExceptionClass.getConstructor(argClasses.toArray(new Class[0])), mapFunctions); } catch (NoSuchMethodException e) { - log.debug("Cannot find constructor on {} with {}", sourceExceptionClassName, argClasses); - return null; + throw new ExceptionMappingError("Cannot find constructor on " + sourceExceptionClassName + " with " + argClasses); } } diff --git a/common-service-core/src/main/java/com/ca/mfaas/util/ExceptionMappingError.java b/common-service-core/src/main/java/com/ca/mfaas/util/ExceptionMappingError.java index 3f2a7a7336..ebaac78acd 100644 --- a/common-service-core/src/main/java/com/ca/mfaas/util/ExceptionMappingError.java +++ b/common-service-core/src/main/java/com/ca/mfaas/util/ExceptionMappingError.java @@ -12,6 +12,10 @@ public class ExceptionMappingError extends RuntimeException { private static final long serialVersionUID = 7278565687932741451L; + public ExceptionMappingError(String message) { + super(message); + } + public ExceptionMappingError(String message, Throwable cause) { super(message, cause); } diff --git a/common-service-core/src/test/java/com/ca/mfaas/util/ClassOrDefaultProxyUtilsTest.java b/common-service-core/src/test/java/com/ca/mfaas/util/ClassOrDefaultProxyUtilsTest.java index 5332abd61e..99d2847826 100644 --- a/common-service-core/src/test/java/com/ca/mfaas/util/ClassOrDefaultProxyUtilsTest.java +++ b/common-service-core/src/test/java/com/ca/mfaas/util/ClassOrDefaultProxyUtilsTest.java @@ -12,8 +12,14 @@ import lombok.AllArgsConstructor; import lombok.Getter; import org.junit.Test; +import org.springframework.test.util.ReflectionTestUtils; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.lang.reflect.UndeclaredThrowableException; +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; import static org.junit.Assert.*; @@ -121,6 +127,131 @@ public void testExceptionMappingUnkwnown() { } } + @Test + public void testMissingMethod() { + try { + ClassOrDefaultProxyUtils.createProxy( + TestInterface1.class, + Object.class.getName(), + TestImplementation1B::new + ); + fail(); + } catch (ExceptionMappingError e) { + assertTrue(e.getMessage().contains(" was not found on class java.lang.Object")); + } + } + + class InnerClass implements TestInterface1 { + private String name; + + @Override + public String method1() { + return "Hello world!"; + } + + @Override + public String method2(String x, int y) { + return x + y; + } + + @Override + public void method3() { + } + } + + @Test + public void testSyntentic() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + new InnerClass().name = "toCompilarMakeASyntheticMethod"; + + TestInterface1Super ti = ClassOrDefaultProxyUtils.createProxy(TestInterface1Super.class, "unknownClassName", InnerClass::new); + Optional jacocoMethod = Arrays.stream(InnerClass.class.getDeclaredMethods()).filter(x -> x.isSynthetic()).filter(x -> x.getParameterTypes().length == 0).findFirst(); + assertTrue(jacocoMethod.isPresent()); + + Method method = jacocoMethod.get(); + method.setAccessible(true); + method.invoke(ti); + } + + @Test + public void testImplementationWithExtends() { + TestInterface2 ti = ClassOrDefaultProxyUtils.createProxy(TestInterface2.class, "unknownClassName", Extends::new); + assertEquals("method1Response", ti.method1()); + } + + @Test + public void testWrongMapping() { + TestInterface1 ti = ClassOrDefaultProxyUtils.createProxy(TestInterface1.class, "unknownClassName", TestImplementation1B::new); + ((Map) ReflectionTestUtils.getField(ReflectionTestUtils.getField(ti, "h"), "mapping")).clear(); + try { + ti.method1(); + fail(); + } catch (ExceptionMappingError e) { + assertTrue(e.getMessage().startsWith("Cannot found method ")); + } + } + + @Test + public void testExceptionMappingSourceMethod() { + try { + new ClassOrDefaultProxyUtils.ByMethodName<>("java.lang.NullPointerException", NullPointerException.class, "getOurStackTraceX"); + fail(); + } catch (ExceptionMappingError e) { + assertEquals("Cannot find method getOurStackTraceX in java.lang.NullPointerException to map exceptions", e.getMessage()); + } + + try { + new ClassOrDefaultProxyUtils.ByMethodName<>("java.lang.NullPointerException", NullPointerException.class, "getOurStackTrace").apply(new NullPointerException("x")); + fail(); + } catch (ExceptionMappingError e) { + assertEquals("Cannot find constructor on java.lang.NullPointerException with [class [Ljava.lang.StackTraceElement;]", e.getMessage()); + } + } + + @Test + public void testCannotConstructException() throws NullPointerExceptionPrivate, NullPointerExceptionException { + try { + new ClassOrDefaultProxyUtils.ByMethodName<>("java.lang.NullPointerException", NullPointerExceptionPrivate.class, "getMessage").apply(new NullPointerException("x")); + fail(); + } catch (ExceptionMappingError e) { + assertEquals("Cannot find constructor on java.lang.NullPointerException with [class java.lang.String]", e.getMessage()); + } + + try { + new ClassOrDefaultProxyUtils.ByMethodName<>("com.ca.mfaas.util.ClassOrDefaultProxyUtilsTest$NullPointerExceptionPrivate", NullPointerException.class, "getMsg").apply(new NullPointerExceptionPrivate("x")); + } catch (ExceptionMappingError e) { + assertTrue(e.getMessage().contains("Cannot invoke method private")); + } + + try { + new ClassOrDefaultProxyUtils.ByMethodName<>("java.lang.NullPointerException", NullPointerExceptionException.class, "getMessage").apply(new NullPointerException("x")); + fail(); + } catch (ExceptionMappingError e) { + assertTrue(e.getMessage().startsWith("Cannot construct exception")); + } + } + + private static class NullPointerExceptionPrivate extends Exception { + + private NullPointerExceptionPrivate(String arg) {} + + private String getMsg() { + return "x"; + } + + } + + public static class NullPointerExceptionException extends Exception { + + public NullPointerExceptionException(String arg) { + throw new RuntimeException("an error"); + } + + private String getMsg() { + return "x"; + } + + } + public interface TestInterface1Super { public String method1(); @@ -143,6 +274,23 @@ public interface TestInterface2 { } + public class ExtendsSuper { + + public String method1() { + return "method1Response"; + } + + } + + public class Extends extends ExtendsSuper implements TestInterface2 { + + @Override + public Class getImplementationClass() { + return this.getClass(); + } + + } + public static class TestImplementation1A { public String method1() { diff --git a/discoverable-client/src/main/java/com/ca/mfaas/client/api/PassTicketTestController.java b/discoverable-client/src/main/java/com/ca/mfaas/client/api/PassTicketTestController.java index 3553d5c9bf..a1919213be 100644 --- a/discoverable-client/src/main/java/com/ca/mfaas/client/api/PassTicketTestController.java +++ b/discoverable-client/src/main/java/com/ca/mfaas/client/api/PassTicketTestController.java @@ -45,7 +45,8 @@ public PassTicketTestController(PassTicketService passTicketService) { @ApiOperation(value = "Validate that the PassTicket in Authorization header is valid", tags = { "Test Operations" }) public void passticketTest(@RequestHeader("authorization") String authorization, @RequestParam(value = "applId", defaultValue = "", required = false) String applId) - throws IRRPassTicketEvaluationException { + throws IRRPassTicketEvaluationException + { if (authorization != null && authorization.toLowerCase().startsWith("basic")) { String base64Credentials = authorization.substring("Basic".length()).trim(); String credentials = new String(Base64.getDecoder().decode(base64Credentials), StandardCharsets.UTF_8); diff --git a/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/AuthenticationOnDeploymentTest.java b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/AuthenticationOnDeploymentTest.java index fc6d667a92..29e3c5e09a 100644 --- a/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/AuthenticationOnDeploymentTest.java +++ b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/AuthenticationOnDeploymentTest.java @@ -12,7 +12,7 @@ import com.ca.apiml.security.common.auth.Authentication; import com.ca.apiml.security.common.auth.AuthenticationScheme; import com.ca.apiml.security.common.service.PassTicketService; -import com.ca.mfaas.util.categories.MainframeDependentTests; +import com.ca.mfaas.util.categories.AdditionalLocalTest; import com.ca.mfaas.util.service.RequestVerifier; import com.ca.mfaas.util.service.VirtualService; import io.restassured.RestAssured; @@ -36,7 +36,7 @@ * This test requires to allow endpoint routes on gateway (ie profile dev) */ @RunWith(JUnit4.class) -@Category(MainframeDependentTests.class) +@Category(AdditionalLocalTest.class) public class AuthenticationOnDeploymentTest { private static final int TIMEOUT = 10; From 657662ee00c2ef7affec133f1f5704e4541d4a1b Mon Sep 17 00:00:00 2001 From: Andrew Jandacek Date: Wed, 22 Jan 2020 09:42:17 +0100 Subject: [PATCH 087/122] fix formatting Signed-off-by: Andrew Jandacek --- docs/registry-communication.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/registry-communication.md b/docs/registry-communication.md index 8e741f5275..59e82d74fb 100644 --- a/docs/registry-communication.md +++ b/docs/registry-communication.md @@ -142,11 +142,11 @@ Ribbon can throw many exception in this time, and it is not sure, that it retry **solution** -Now we use as load balancer implementation ApimlZoneAwareLoadBalancer (it extends original ZoneAwareLoadBalancer). This +Now we use as load balancer implementation `ApimlZoneAwareLoadBalancer` (it extends original ZoneAwareLoadBalancer). This implementation only add method ```public void serverChanged()``` which call super class to reload information about servers, it means about instances and their addresses. -This is call from ServiceCacheEvictor to be sure, that before custom EhCaches are evicted and load balancer get right +This is call from `ServiceCacheEvictor` to be sure, that before custom EhCaches are evicted and load balancer get right information from ZUUL. ### Service cache - our custom EhCache @@ -155,12 +155,12 @@ For own purpose was added EhCache, which can collect many information about proc state of EhCache with discovery client. If not, it is possible to use old values (ie. before registering new service's instance with different data than old one). It can make many problems in logic (based on race condition). -It was reason to add CacheServiceController. This controller is called from discovery service (exactly from -EurekaInstanceRegisteredListener by event EurekaInstanceRegisteredEvent). For cleaning caches gateway uses interface -ServiceCacheEvict. It means each bean can be called about any changes in registry and evict EhCache (or different cache). +It was reason to add `CacheServiceController`. This controller is called from discovery service (exactly from +`EurekaInstanceRegisteredListener` by event `EurekaInstanceRegisteredEvent`). For cleaning caches gateway uses interface +`ServiceCacheEvict`. It means each bean can be called about any changes in registry and evict EhCache (or different cache). -Controller evict all custom caches via interface ServiceCacheEvict and as ApimlDiscoveryClient to fetch new registry. After -than other beans are notified (see CacheRefreshedEvent from discovery client). +Controller evict all custom caches via interface `ServiceCacheEvict` and as `ApimlDiscoveryClient` to fetch new registry. After +than other beans are notified (see `CacheRefreshedEvent` from discovery client). This mechanism is working, but not strictly right. There is one case: @@ -169,12 +169,12 @@ This mechanism is working, but not strictly right. There is one case: 3. new request accept and make a cache (again with old state) - **this is wrong** 4. fetching of registry is done, evict all Eureka caches -For this reason there was added new bean CacheEvictor. +For this reason there was added new bean `CacheEvictor`. #### CacheEvictor -This bean collect all calls from CacheServiceController and is waiting for registry fetching. On this event it will clean all -custom caches (via interface ServiceCacheEvict). On the end it means that custom caches are evicted twice (before Eureka parts +This bean collect all calls from `CacheServiceController` and is waiting for registry fetching. On this event it will clean all +custom caches (via interface `ServiceCacheEvict`). On the end it means that custom caches are evicted twice (before Eureka parts and after). It fully supported right state. ## Other improvements @@ -183,10 +183,10 @@ Implementation of this improvement wasn't just about caches, but discovery servi ### Event from InstanceRegistry -In Discovery service exist bean InstanceRegistry. This bean is call for register, renew and unregister of service. +In Discovery service exist bean `InstanceRegistry`. This bean is call for register, renew and unregister of service. Unfortunately, this bean contains also one problem. It notified about newly registered instances before it register it, in similar way about unregister (cancellation) and renew. It doesnt matter about renew, but other makes problem for us. We -can clean caches before update in InstanceRegistry happened. On this topic exists issue: +can clean caches before update in `InstanceRegistry` happened. On this topic exists issue: ``` #2659 Race condition with registration events in Eureka server https://github.com/spring-cloud/spring-cloud-netflix/issues/2659 @@ -199,10 +199,10 @@ Eureka will be fixed. ## Using caches and their evicting If you use anywhere custom cache, implement interface ServiceCacheEvict to evict. It offer to methods: -- public void evictCacheService(String serviceId) +- `public void evictCacheService(String serviceId)` - to evict only part of caches for service with serviceId - if there is no way how to do it, you can evict all records -- public void evictCacheAllService() +- public void `evictCacheAllService()` - to evict all records in the caches, which can has a relationship with any service - this method will be call very rare, only in case that, there is impossible to get serviceId (ie. wrong format of instanceId) From 81f451c0fef1ebc006d9dbb2ac3175957c0ea219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Wed, 22 Jan 2020 11:36:19 +0100 Subject: [PATCH 088/122] code coverage improvement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../common/auth/AuthenticationSchemeTest.java | 2 ++ .../common/service/PassTicketServiceTest.java | 11 +++++++ .../ca/mfaas/discovery/GatewayNotifier.java | 2 +- .../mfaas/discovery/GatewayNotifierTest.java | 31 ++++++++++++++++--- .../gateway/config/DiscoveryClientConfig.java | 2 +- .../gateway/controllers/AuthController.java | 4 +-- .../controllers/CacheServiceController.java | 2 +- .../service/AuthenticationService.java | 2 +- .../schema/AuthenticationSchemeFactory.java | 2 +- .../cache/ServiceCacheEvictorTest.java | 4 +++ .../controllers/AuthControllerTest.java | 6 ++-- .../CacheServiceControllerTest.java | 4 +-- .../service/AuthenticationServiceTest.java | 4 +-- .../ServiceAuthenticationServiceImplTest.java | 11 ++++--- .../AuthenticationSchemeFactoryTest.java | 21 +++++++++++++ 15 files changed, 85 insertions(+), 23 deletions(-) diff --git a/apiml-security-common/src/test/java/com/ca/apiml/security/common/auth/AuthenticationSchemeTest.java b/apiml-security-common/src/test/java/com/ca/apiml/security/common/auth/AuthenticationSchemeTest.java index 0b8518d376..aaf610272e 100644 --- a/apiml-security-common/src/test/java/com/ca/apiml/security/common/auth/AuthenticationSchemeTest.java +++ b/apiml-security-common/src/test/java/com/ca/apiml/security/common/auth/AuthenticationSchemeTest.java @@ -21,5 +21,7 @@ public void testFromScheme() { assertSame(as, as2); } assertNull(AuthenticationScheme.fromScheme("absolute nonsense")); + assertEquals("bypass", AuthenticationScheme.fromScheme("bypass").toString()); } + } diff --git a/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java b/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java index 7696830fee..98712a5494 100644 --- a/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java +++ b/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java @@ -123,6 +123,17 @@ public void testDefaultPassTicketImpl() throws IRRPassTicketEvaluationException, dpti.evaluate(ZOWE_DUMMY_USERID, "anyApplId", ZOWE_DUMMY_PASS_TICKET_PREFIX); dpti.evaluate(ZOWE_DUMMY_USERID, "anyApplId", ZOWE_DUMMY_PASS_TICKET_PREFIX + "xyz"); + try { + dpti.evaluate("unknownUser", "anyApplId", ZOWE_DUMMY_PASS_TICKET_PREFIX); + fail(); + } catch (IRRPassTicketEvaluationException e) { + } + try { + dpti.evaluate(ZOWE_DUMMY_USERID, "anyApplId", "wrongPassticket"); + fail(); + } catch (IRRPassTicketEvaluationException e) { + } + try { dpti.evaluate("userx", "applId", passTicket1); fail(); diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java index 31630bc6db..7faea6a9cb 100644 --- a/discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java @@ -56,7 +56,7 @@ public void serviceUpdated(String serviceId) { for (final InstanceInfo instanceInfo : gatewayInstances) { final StringBuilder url = new StringBuilder(); - url.append(EurekaUtils.getUrl(instanceInfo)).append("/cache/services"); + url.append(EurekaUtils.getUrl(instanceInfo)).append("/api/v1/gateway/cache/services"); if (serviceId != null) url.append('/').append(serviceId); restTemplate.delete(url.toString()); diff --git a/discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java b/discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java index 206c5a6b7b..b08f60f19f 100644 --- a/discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java +++ b/discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java @@ -8,7 +8,10 @@ * Copyright Contributors to the Zowe Project. */ +import com.ca.mfaas.message.core.Message; import com.ca.mfaas.message.core.MessageService; +import com.ca.mfaas.message.core.MessageType; +import com.ca.mfaas.message.template.MessageTemplate; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.shared.Application; import com.netflix.eureka.EurekaServerContext; @@ -62,14 +65,34 @@ public void testServiceUpdated() { when(registry.getApplication("GATEWAY")).thenReturn(application); gatewayNotifier.serviceUpdated("testService"); - verify(restTemplate, times(1)).delete("https://hostname1:1433/cache/services/testService"); - verify(restTemplate, times(1)).delete("http://hostname2:1000/cache/services/testService"); + verify(restTemplate, times(1)).delete("https://hostname1:1433/api/v1/gateway/cache/services/testService"); + verify(restTemplate, times(1)).delete("http://hostname2:1000/api/v1/gateway/cache/services/testService"); gatewayNotifier.serviceUpdated(null); - verify(restTemplate, times(1)).delete("https://hostname1:1433/cache/services"); - verify(restTemplate, times(1)).delete("http://hostname2:1000/cache/services"); + verify(restTemplate, times(1)).delete("https://hostname1:1433/api/v1/gateway/cache/services"); + verify(restTemplate, times(1)).delete("http://hostname2:1000/api/v1/gateway/cache/services"); verify(restTemplate, times(4)).delete(anyString()); } + @Test + public void testMissingGateway() { + final String messageKey = "apiml.discovery.errorNotifyingGateway"; + final MessageTemplate mt = new MessageTemplate(); + mt.setKey(messageKey); + mt.setText("message"); + mt.setType(MessageType.INFO); + final Message m = Message.of(messageKey, mt, new Object[0]); + + RestTemplate restTemplate = mock(RestTemplate.class); + MessageService messageService = mock(MessageService.class); + GatewayNotifier gatewayNotifier = new GatewayNotifier(restTemplate, messageService); + when(registry.getApplication(anyString())).thenReturn(null); + when(messageService.createMessage(messageKey)).thenReturn(m); + + gatewayNotifier.serviceUpdated("serviceX"); + + verify(messageService, times(1)).createMessage(messageKey); + } + } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/config/DiscoveryClientConfig.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/config/DiscoveryClientConfig.java index 9fdf18735f..83238d9285 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/config/DiscoveryClientConfig.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/config/DiscoveryClientConfig.java @@ -70,7 +70,7 @@ public ApimlDiscoveryClient eurekaClient(ApplicationInfoManager manager, discoveryClientClient.registerEventListener(event -> { if (event instanceof CacheRefreshedEvent) { - refreshableRouteLocators.forEach(x -> x.refresh()); + refreshableRouteLocators.forEach(RefreshableRouteLocator::refresh); zuulHandlerMapping.setDirty(true); } }); diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/AuthController.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/AuthController.java index 3138f1ea79..0fa2e0dbe1 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/AuthController.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/AuthController.java @@ -27,7 +27,7 @@ */ @AllArgsConstructor @RestController -@RequestMapping("/auth") +@RequestMapping("/api/v1/gateway/auth") public class AuthController { private final AuthenticationService authenticationService; @@ -38,7 +38,7 @@ public void invalidateJwtToken(HttpServletRequest request, HttpServletResponse r final String uri = request.getRequestURI(); final int index = uri.indexOf(path); - final String jwtToken = (index >= 0) ? uri.substring(index + path.length()) : ""; + final String jwtToken = uri.substring(index + path.length()); final boolean invalidated = authenticationService.invalidateJwtToken(jwtToken, false); response.setStatus(invalidated ? SC_OK : SC_SERVICE_UNAVAILABLE); diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/CacheServiceController.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/CacheServiceController.java index 6747fcb33a..f50eb04cdc 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/CacheServiceController.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/CacheServiceController.java @@ -26,7 +26,7 @@ */ @AllArgsConstructor @RestController -@RequestMapping("/cache/services") +@RequestMapping("/api/v1/gateway/cache/services") public class CacheServiceController { private final List toEvict; diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java index cec49a346e..e01f3a38e1 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java @@ -116,7 +116,7 @@ public Boolean invalidateJwtToken(String jwtToken, boolean distribute) { for (final InstanceInfo instanceInfo : application.getInstances()) { if (StringUtils.equals(myInstanceId, instanceInfo.getInstanceId())) continue; - final String url = EurekaUtils.getUrl(instanceInfo) + "/auth/invalidate/{}"; + final String url = EurekaUtils.getUrl(instanceInfo) + "/api/v1/gateway/auth/invalidate/{}"; restTemplate.delete(url, jwtToken); } } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactory.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactory.java index 322840e294..285be386bc 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactory.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactory.java @@ -49,7 +49,7 @@ public AuthenticationSchemeFactory(@Autowired AuthenticationService authenticati if (prev != null) { throw new IllegalArgumentException("Multiple beans for scheme " + aas.getScheme() + - " : " + prev.getScheme() + " x " + aas.getScheme()); + " : " + prev.getClass() + " x " + aas.getClass()); } if (aas.isDefault()) { diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/cache/ServiceCacheEvictorTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/cache/ServiceCacheEvictorTest.java index be1aa8c0f9..b664a495b4 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/cache/ServiceCacheEvictorTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/cache/ServiceCacheEvictorTest.java @@ -44,6 +44,9 @@ public void setUp() { @Test public void testService() { + serviceCacheEvictor.onEvent(mock(CacheRefreshedEvent.class)); + verify(apimlZoneAwareLoadBalancer, never()).serverChanged(); + serviceCacheEvictor.evictCacheService("service1"); serviceCacheEvictor.evictCacheService("service1"); serviceCacheEvictor.evictCacheService("service2"); @@ -56,6 +59,7 @@ public void testService() { serviceCacheEvictor.evictCacheService("service3"); serviceCacheEvictor.evictCacheAllService(); + serviceCacheEvictor.evictCacheService("service4"); serviceCacheEvictor.onEvent(mock(CacheRefreshedEvent.class)); serviceCacheEvicts.forEach(x -> { verify(x, never()).evictCacheService("service3"); diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/AuthControllerTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/AuthControllerTest.java index 7c0af89122..ec06a02dee 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/AuthControllerTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/AuthControllerTest.java @@ -42,12 +42,12 @@ public void setUp() { @Test public void invalidateJwtToken() throws Exception { when(authenticationService.invalidateJwtToken("a/b", false)).thenReturn(Boolean.TRUE); - this.mockMvc.perform(delete("/auth/invalidate/a/b")).andExpect(status().is(SC_OK)); + this.mockMvc.perform(delete("/api/v1/gateway/auth/invalidate/a/b")).andExpect(status().is(SC_OK)); when(authenticationService.invalidateJwtToken("abcde", false)).thenReturn(Boolean.TRUE); - this.mockMvc.perform(delete("/auth/invalidate/abcde")).andExpect(status().is(SC_OK)); + this.mockMvc.perform(delete("/api/v1/gateway/auth/invalidate/abcde")).andExpect(status().is(SC_OK)); - this.mockMvc.perform(delete("/auth/invalidate/xyz")).andExpect(status().is(SC_SERVICE_UNAVAILABLE)); + this.mockMvc.perform(delete("/api/v1/gateway/auth/invalidate/xyz")).andExpect(status().is(SC_SERVICE_UNAVAILABLE)); verify(authenticationService, times(1)).invalidateJwtToken("abcde", false); verify(authenticationService, times(1)).invalidateJwtToken("a/b", false); diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/CacheServiceControllerTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/CacheServiceControllerTest.java index 329588cf09..04bd260e6c 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/CacheServiceControllerTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/CacheServiceControllerTest.java @@ -52,7 +52,7 @@ public void testEvictAll() throws Exception { verify(service2, never()).evictCacheAllService(); verify(discoveryClient, never()).fetchRegistry(); - this.mockMvc.perform(delete("/cache/services")).andExpect(status().isOk()); + this.mockMvc.perform(delete("/api/v1/gateway/cache/services")).andExpect(status().isOk()); verify(service1, times(1)).evictCacheAllService(); verify(service2, times(1)).evictCacheAllService(); @@ -65,7 +65,7 @@ public void testEvict() throws Exception { verify(service2, never()).evictCacheService(any()); verify(discoveryClient, never()).fetchRegistry(); - this.mockMvc.perform(delete("/cache/services/service01")).andExpect(status().isOk()); + this.mockMvc.perform(delete("/api/v1/gateway/cache/services/service01")).andExpect(status().isOk()); verify(service1, times(1)).evictCacheService("service01"); verify(service2, times(1)).evictCacheService("service01"); diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/AuthenticationServiceTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/AuthenticationServiceTest.java index cb4af675c2..2fd2ee7c1c 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/AuthenticationServiceTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/AuthenticationServiceTest.java @@ -257,8 +257,8 @@ public void invalidateToken() { tokenAuthentication = authService.validateJwtToken(jwt1); assertFalse(tokenAuthentication.isAuthenticated()); verify(restTemplate, times(2)).delete(anyString(), (Object[]) any()); - verify(restTemplate).delete("https://hostname1:10433/auth/invalidate/{}", jwt1); - verify(restTemplate).delete("http://hostname2:10001/auth/invalidate/{}", jwt1); + verify(restTemplate).delete("https://hostname1:10433/api/v1/gateway/auth/invalidate/{}", jwt1); + verify(restTemplate).delete("http://hostname2:10001/api/v1/gateway/auth/invalidate/{}", jwt1); } @Test diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java index 6ae4841b1d..ad1c7777e7 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java @@ -12,11 +12,7 @@ import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_APPLID; import static com.ca.mfaas.constants.EurekaMetadataDefinition.AUTHENTICATION_SCHEME; import static com.ca.mfaas.gateway.security.service.ServiceAuthenticationServiceImpl.AUTHENTICATION_COMMAND_KEY; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; @@ -248,6 +244,9 @@ public void testGetAuthenticationCommandByServiceId() throws Exception { reset(discoveryClient); when(discoveryClient.getInstancesById("svr03")).thenReturn(Collections.singletonList(ii5)); assertSame(AuthenticationCommand.EMPTY, sas.getAuthenticationCommand("svr03", "jwt03")); + + when(discoveryClient.getInstancesById("svr04")).thenReturn(Collections.emptyList()); + assertSame(AuthenticationCommand.EMPTY, sas.getAuthenticationCommand("svr04", "jwt03")); } @Test @@ -281,6 +280,7 @@ public void testGetAuthenticationCommandByServiceIdCache() throws Exception { @Test public void testUniversalAuthenticationCommand() throws Exception { ServiceAuthenticationServiceImpl.UniversalAuthenticationCommand uac = serviceAuthenticationServiceImpl.new UniversalAuthenticationCommand(); + assertFalse(uac.isExpired()); try { uac.apply(null); @@ -308,6 +308,7 @@ public void testUniversalAuthenticationCommand() throws Exception { @Test public void testLoadBalancerAuthenticationCommand() { ServiceAuthenticationServiceImpl.LoadBalancerAuthenticationCommand lbac = serviceAuthenticationServiceImpl.new LoadBalancerAuthenticationCommand(); + assertFalse(lbac.isExpired()); RequestContext requestContext = new RequestContext(); RequestContext.testSetCurrentContext(requestContext); diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java index cab4ff1e99..3201eb50df 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java @@ -102,6 +102,21 @@ public void testInit() { assertTrue(e.getMessage().contains(AuthenticationScheme.BYPASS.getScheme())); assertTrue(e.getMessage().contains(AuthenticationScheme.HTTP_BASIC_PASSTICKET.getScheme())); } + + // multiple same scheme + try { + new AuthenticationSchemeFactory( + mock(AuthenticationService.class), + Arrays.asList( + createScheme(AuthenticationScheme.BYPASS, true), + createScheme(AuthenticationScheme.BYPASS, false) + ) + ); + fail(); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Multiple beans for scheme")); + assertTrue(e.getMessage().contains("AuthenticationSchemeFactoryTest$1")); + } } @Test @@ -153,7 +168,13 @@ public void testGetAuthenticationCommand() throws Exception { verify(passTicket, times(1)).createCommand(authentication, null); authentication.setScheme(null); asf.getAuthenticationCommand(authentication); + + verify(byPass, times(2)).createCommand(authentication, null); + verify(passTicket, times(1)).createCommand(authentication, null); + asf.getAuthenticationCommand(null); + verify(byPass, times(2)).createCommand(authentication, null); + verify(byPass, times(1)).createCommand(null, null); verify(passTicket, times(1)).createCommand(authentication, null); RequestContext.testSetCurrentContext(requestContext); From fab62913f65da083eb102c6615e44ecd0dc31a3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Wed, 22 Jan 2020 12:55:32 +0100 Subject: [PATCH 089/122] attempt to fix ClassOrDefaultProxyUtilsTest on OpenJDK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../java/com/ca/mfaas/util/ClassOrDefaultProxyUtilsTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common-service-core/src/test/java/com/ca/mfaas/util/ClassOrDefaultProxyUtilsTest.java b/common-service-core/src/test/java/com/ca/mfaas/util/ClassOrDefaultProxyUtilsTest.java index 99d2847826..839a6de002 100644 --- a/common-service-core/src/test/java/com/ca/mfaas/util/ClassOrDefaultProxyUtilsTest.java +++ b/common-service-core/src/test/java/com/ca/mfaas/util/ClassOrDefaultProxyUtilsTest.java @@ -200,10 +200,10 @@ public void testExceptionMappingSourceMethod() { } try { - new ClassOrDefaultProxyUtils.ByMethodName<>("java.lang.NullPointerException", NullPointerException.class, "getOurStackTrace").apply(new NullPointerException("x")); + new ClassOrDefaultProxyUtils.ByMethodName<>("java.lang.NullPointerException", NullPointerException.class, "getCause").apply(new NullPointerException("x")); fail(); } catch (ExceptionMappingError e) { - assertEquals("Cannot find constructor on java.lang.NullPointerException with [class [Ljava.lang.StackTraceElement;]", e.getMessage()); + assertEquals("Cannot find constructor on java.lang.NullPointerException with [class java.lang.Throwable]", e.getMessage()); } } From 94056118a89077be29438af3ce8df5822cb59b89 Mon Sep 17 00:00:00 2001 From: JirkaAichler Date: Wed, 22 Jan 2020 13:25:03 +0100 Subject: [PATCH 090/122] Fix swagger doc Signed-off-by: JirkaAichler --- .../src/main/resources/gateway-api-doc.json | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/gateway-service/src/main/resources/gateway-api-doc.json b/gateway-service/src/main/resources/gateway-api-doc.json index 0d7e0b9f96..69329ea77f 100644 --- a/gateway-service/src/main/resources/gateway-api-doc.json +++ b/gateway-service/src/main/resources/gateway-api-doc.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "description": "REST API for the API Gateway service, which is a component of the API\nMediation Layer. Use this API to perform tasks such as logging in with the\nmainframe credentials and checking authorization to mainframe resources.", - "version": "1.8.0", + "version": "1.3.0", "title": "API Gateway" }, "tags": [ @@ -58,6 +58,9 @@ }, "405": { "description": "Method Not Allowed" + }, + "500": { + "description": "Internal error" } } } @@ -97,6 +100,9 @@ }, "405": { "description": "Method Not Allowed" + }, + "500": { + "description": "Internal error" } } } @@ -107,7 +113,7 @@ "Security" ], "summary": "Generate a passticket for the user associated with a token.", - "description": "Use the `/ticket` API to request a passticket for the user associated with a token.\n\n**HTTP Headers:**\n\nThe ticket request requires the token in one of the following formats: \n * Cookie named `apimlAuthenticationToken`.\n * Bearer authentication\n \n*Header example:* Authorization: Bearer *token*\n\n**Request payload:**\n\nThe request takes one parameter, the name of the application for which the passticket should be generated. This parameter must be supplied.\n\n**Response Payload:**\n\nThe response is a JSON object, which contains information associated with the ticket.\n", + "description": "Use the `/ticket` API to request a passticket for the user associated with a token.\n\nThis endpoint is protect by a client certificate.\n\n**HTTP Headers:**\n\nThe ticket request requires the token in one of the following formats: \n * Cookie named `apimlAuthenticationToken`.\n * Bearer authentication\n \n*Header example:* Authorization: Bearer *token*\n\n**Request payload:**\n\nThe request takes one parameter, the name of the application for which the passticket should be generated. This parameter must be supplied.\n\n**Response Payload:**\n\nThe response is a JSON object, which contains information associated with the ticket.\n", "operationId": "GenerateTicketUsingPOST", "requestBody": { "content": { @@ -138,14 +144,23 @@ } } }, + "400": { + "description": "Incorrect applicationName parameter. The parameter is not provided, is invalid or not defined to security." + }, "401": { - "description": "Unauthorized" + "description": "Zowe token is not provided, is invalid or is expired." + }, + "403": { + "description": "A client certificate is not provided or is expired." }, "404": { "description": "Not Found" }, "405": { "description": "Method Not Allowed" + }, + "500": { + "description": "The external security manager failed to generate a PassTicket for the user and application specified." } } } @@ -236,7 +251,7 @@ "applicationName" ], "example": { - "applicationName": "MYAPP" + "applicationName": "ZOWEAPPL" } }, "TicketResponse": { @@ -263,7 +278,7 @@ "example": { "token": "eyJhbGciOiJSUzI1N", "userId": "John", - "applicationName": "MYAPP", + "applicationName": "ZOWEAPPL", "ticket": "LZTKEEDQ" } } From e57a8690052987a432279eef8c8c7e24d1df7eb8 Mon Sep 17 00:00:00 2001 From: JirkaAichler Date: Wed, 22 Jan 2020 14:20:32 +0100 Subject: [PATCH 091/122] Fix Sonar issue Add test Signed-off-by: JirkaAichler --- .../gateway/config/DiscoveryClientConfig.java | 23 +++++++------------ .../ticket/SuccessfulTicketHandlerTest.java | 13 +++++++++++ .../src/test/resources/gateway-messages.yml | 7 ++++++ 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/config/DiscoveryClientConfig.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/config/DiscoveryClientConfig.java index 83238d9285..eff85e5e0c 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/config/DiscoveryClientConfig.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/config/DiscoveryClientConfig.java @@ -16,6 +16,7 @@ import com.netflix.discovery.AbstractDiscoveryClientOptionalArgs; import com.netflix.discovery.CacheRefreshedEvent; import com.netflix.discovery.EurekaClientConfig; +import lombok.RequiredArgsConstructor; import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.context.config.annotation.RefreshScope; @@ -32,24 +33,17 @@ * This configuration override bean EurekaClient with custom ApimlDiscoveryClient. This bean offer additional method * fetchRegistry. User can call this method to asynchronously fetch new data from discovery service. There is no time * to fetching. - * + *

* Configuration also add listeners to call other beans waiting for fetch new registry. It speed up distribution of * changes in whole gateway. */ @Configuration +@RequiredArgsConstructor public class DiscoveryClientConfig { - - @Autowired - private ApplicationContext context; - - @Autowired - private AbstractDiscoveryClientOptionalArgs optionalArgs; - - @Autowired - private List refreshableRouteLocators; - - @Autowired - private ZuulHandlerMapping zuulHandlerMapping; + private final ApplicationContext context; + private final AbstractDiscoveryClientOptionalArgs optionalArgs; + private final List refreshableRouteLocators; + private final ZuulHandlerMapping zuulHandlerMapping; @Bean(destroyMethod = "shutdown") @RefreshScope @@ -61,8 +55,7 @@ public ApimlDiscoveryClient eurekaClient(ApplicationInfoManager manager, ApplicationInfoManager appManager; if (AopUtils.isAopProxy(manager)) { appManager = ProxyUtils.getTargetObject(manager); - } - else { + } else { appManager = manager; } final ApimlDiscoveryClient discoveryClientClient = new ApimlDiscoveryClient(appManager, config, this.optionalArgs, this.context); diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandlerTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandlerTest.java index d16dc0df0f..40db722d56 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandlerTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/ticket/SuccessfulTicketHandlerTest.java @@ -25,6 +25,7 @@ import java.io.UnsupportedEncodingException; +import static com.ca.apiml.security.common.service.PassTicketService.DefaultPassTicketImpl.UNKNOWN_APPLID; import static com.ca.apiml.security.common.service.PassTicketService.DefaultPassTicketImpl.ZOWE_DUMMY_PASS_TICKET_PREFIX; import static org.junit.Assert.*; @@ -79,4 +80,16 @@ public void shouldFailWhenNoApplicationName() throws UnsupportedEncodingExceptio assertTrue(httpServletResponse.getContentAsString().contains("ZWEAG140E")); assertTrue(httpServletResponse.isCommitted()); } + + @Test + public void shouldFailWhenGenerationFails() throws JsonProcessingException, UnsupportedEncodingException { + httpServletRequest.setContent(mapper.writeValueAsBytes(new TicketRequest(UNKNOWN_APPLID))); + + successfulTicketHandlerHandler.onAuthenticationSuccess(httpServletRequest, httpServletResponse, tokenAuthentication); + + assertEquals(MediaType.APPLICATION_JSON_UTF8_VALUE, httpServletResponse.getContentType()); + assertEquals(HttpStatus.BAD_REQUEST.value(), httpServletResponse.getStatus()); + assertTrue(httpServletResponse.getContentAsString().contains("ZWEAG141E")); + assertTrue(httpServletResponse.isCommitted()); + } } diff --git a/gateway-service/src/test/resources/gateway-messages.yml b/gateway-service/src/test/resources/gateway-messages.yml index 304068aba1..b3a33db0d6 100644 --- a/gateway-service/src/test/resources/gateway-messages.yml +++ b/gateway-service/src/test/resources/gateway-messages.yml @@ -154,3 +154,10 @@ messages: text: "The 'applicationName' parameter name is missing." reason: "The application name is not provided." action: "Provide the 'applicationName' parameter." + + - key: apiml.security.ticket.generateFailed + number: ZWEAG141 + type: ERROR + text: "The generation of the PassTicket failed. Reason: %s" + reason: "An error occurred in the SAF Auth Service. Review the reason in the error message." + action: "Supply a valid user and application name, and check that corresponding permissions have been set up." From 2afee1ef298ba1b8ab92788c2ca60cd6e16c3a51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Wed, 22 Jan 2020 15:34:45 +0100 Subject: [PATCH 092/122] fix caches on loadbalancer and its code coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../CompositeKeyGeneratorWithoutLast.java | 26 ++ .../CompositeKeyGeneratorWithoutLastTest.java | 40 +++ .../ca/mfaas/gateway/config/CacheConfig.java | 7 + .../gateway/ribbon/GatewayRibbonConfig.java | 11 +- .../GatewayRibbonLoadBalancingHttpClient.java | 216 +------------ ...ewayRibbonLoadBalancingHttpClientImpl.java | 268 ++++++++++++++++ ...RibbonLoadBalancingHttpClientImplTest.java | 291 ++++++++++++++++++ 7 files changed, 644 insertions(+), 215 deletions(-) create mode 100644 common-service-core/src/main/java/com/ca/mfaas/cache/CompositeKeyGeneratorWithoutLast.java create mode 100644 common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyGeneratorWithoutLastTest.java create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClientImpl.java create mode 100644 gateway-service/src/test/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClientImplTest.java diff --git a/common-service-core/src/main/java/com/ca/mfaas/cache/CompositeKeyGeneratorWithoutLast.java b/common-service-core/src/main/java/com/ca/mfaas/cache/CompositeKeyGeneratorWithoutLast.java new file mode 100644 index 0000000000..e1798904ef --- /dev/null +++ b/common-service-core/src/main/java/com/ca/mfaas/cache/CompositeKeyGeneratorWithoutLast.java @@ -0,0 +1,26 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.cache; + +import java.lang.reflect.Method; +import java.util.Arrays; + +/** + * This key generator is for put method. As default it assumes that the last arguments contains value to put. + */ +public class CompositeKeyGeneratorWithoutLast extends CompositeKeyGenerator { + + @Override + public Object generate(Object target, Method method, Object... params) { + if (params.length == 0) throw new IllegalArgumentException("At least one argument with value is required"); + return super.generate(target, method, Arrays.copyOf(params, params.length - 1)); + } + +} diff --git a/common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyGeneratorWithoutLastTest.java b/common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyGeneratorWithoutLastTest.java new file mode 100644 index 0000000000..131af874ad --- /dev/null +++ b/common-service-core/src/test/java/com/ca/mfaas/cache/CompositeKeyGeneratorWithoutLastTest.java @@ -0,0 +1,40 @@ +package com.ca.mfaas.cache;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static org.junit.Assert.assertEquals; +@RunWith(JUnit4.class) +public class CompositeKeyGeneratorWithoutLastTest { + + @Test + public void testGenerate() { + CompositeKeyGeneratorWithoutLast kg = new CompositeKeyGeneratorWithoutLast(); + + assertEquals( + new CompositeKey("a", "b"), + kg.generate(null, null, new Object[] {"a", "b", "c"}) + ); + + assertEquals( + new CompositeKey(), + kg.generate(null, null, new Object[] {"a"}) + ); + + try { + kg.generate(null, null); + } catch (IllegalArgumentException e) { + assertEquals("At least one argument with value is required", e.getMessage()); + } + } + +} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/config/CacheConfig.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/config/CacheConfig.java index fade04acbf..97747e6ce8 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/config/CacheConfig.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/config/CacheConfig.java @@ -10,6 +10,7 @@ package com.ca.mfaas.gateway.config; import com.ca.mfaas.cache.CompositeKeyGenerator; +import com.ca.mfaas.cache.CompositeKeyGeneratorWithoutLast; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.ehcache.EhCacheCacheManager; @@ -27,6 +28,7 @@ public class CacheConfig { public static final String COMPOSITE_KEY_GENERATOR = "compositeKeyGenerator"; + public static final String COMPOSITE_KEY_GENERATOR_WITHOUT_LAST = "compositeKeyGeneratorWithoutLast"; @Bean public CacheManager cacheManager() { @@ -46,4 +48,9 @@ public KeyGenerator getCompositeKeyGenerator() { return new CompositeKeyGenerator(); } + @Bean(CacheConfig.COMPOSITE_KEY_GENERATOR_WITHOUT_LAST) + public KeyGenerator getCompositeKeyGeneratorWithoutLast() { + return new CompositeKeyGeneratorWithoutLast(); + } + } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonConfig.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonConfig.java index cd424235d8..d857d43aba 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonConfig.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonConfig.java @@ -15,10 +15,11 @@ import com.netflix.loadbalancer.*; import org.apache.http.impl.client.CloseableHttpClient; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.CacheManager; import org.springframework.cloud.netflix.ribbon.PropertiesFactory; import org.springframework.cloud.netflix.ribbon.RibbonClientName; import org.springframework.cloud.netflix.ribbon.ServerIntrospector; -import org.springframework.cloud.netflix.ribbon.apache.RibbonLoadBalancingHttpClient; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -33,13 +34,15 @@ public class GatewayRibbonConfig { @Bean @Autowired - public RibbonLoadBalancingHttpClient ribbonLoadBalancingHttpClient( + public GatewayRibbonLoadBalancingHttpClient ribbonLoadBalancingHttpClient( CloseableHttpClient secureHttpClient, IClientConfig config, ServerIntrospector serverIntrospector, - EurekaClient discoveryClient + EurekaClient discoveryClient, + CacheManager cacheManager, + ApplicationContext applicationContext ) { - return new GatewayRibbonLoadBalancingHttpClient(secureHttpClient, config, serverIntrospector, discoveryClient); + return new GatewayRibbonLoadBalancingHttpClientImpl(secureHttpClient, config, serverIntrospector, discoveryClient, cacheManager, applicationContext); } @Bean diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java index 9bbde805af..176ddb1ae9 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClient.java @@ -9,220 +9,14 @@ */ package com.ca.mfaas.gateway.ribbon; -import com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand; +import com.ca.mfaas.gateway.security.service.ServiceCacheEvict; import com.netflix.appinfo.InstanceInfo; -import com.netflix.client.config.CommonClientConfigKey; -import com.netflix.client.config.IClientConfig; -import com.netflix.discovery.EurekaClient; -import com.netflix.discovery.shared.Application; -import com.netflix.loadbalancer.Server; -import com.netflix.loadbalancer.reactive.ExecutionContext; -import com.netflix.loadbalancer.reactive.ExecutionInfo; -import com.netflix.loadbalancer.reactive.ExecutionListener; -import com.netflix.loadbalancer.reactive.LoadBalancerCommand; -import com.netflix.niws.loadbalancer.DiscoveryEnabledServer; -import com.netflix.zuul.context.RequestContext; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang.StringUtils; -import org.apache.http.HttpResponse; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.impl.client.CloseableHttpClient; -import org.springframework.cache.annotation.CachePut; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.cloud.netflix.ribbon.ServerIntrospector; -import org.springframework.cloud.netflix.ribbon.apache.RibbonApacheHttpRequest; -import org.springframework.cloud.netflix.ribbon.apache.RibbonApacheHttpResponse; -import org.springframework.cloud.netflix.ribbon.apache.RibbonLoadBalancingHttpClient; -import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.cloud.client.loadbalancer.ServiceInstanceChooser; -import java.net.URI; -import java.util.Collections; -import java.util.Map; +public interface GatewayRibbonLoadBalancingHttpClient extends ServiceInstanceChooser, ServiceCacheEvict { -import static com.ca.mfaas.gateway.security.service.ServiceAuthenticationServiceImpl.AUTHENTICATION_COMMAND_KEY; -import static org.springframework.cloud.netflix.ribbon.RibbonUtils.updateToSecureConnectionIfNeeded; + public InstanceInfo putInstanceInfo(String serviceId, String instanceId, InstanceInfo instanceInfo); -@Slf4j -public class GatewayRibbonLoadBalancingHttpClient extends RibbonLoadBalancingHttpClient { + public InstanceInfo getInstanceInfo(String serviceId, String instanceId); - private static final String HTTPS = "https"; - private static final String HTTP = "http"; - - private final EurekaClient discoveryClient; - - /** - * Ribbon load balancer - * - * @param secureHttpClient custom http client for our certificates - * @param config configuration details - * @param serverIntrospector introspector - */ - public GatewayRibbonLoadBalancingHttpClient(CloseableHttpClient secureHttpClient, IClientConfig config, ServerIntrospector serverIntrospector, EurekaClient discoveryClient) { - super(secureHttpClient, config, serverIntrospector); - this.discoveryClient = discoveryClient; - } - - @Override - public URI reconstructURIWithServer(Server server, URI original) { - URI uriToSend; - URI updatedURI = updateToSecureConnectionIfNeeded(original, this.config, this.serverIntrospector, - server); - final URI uriWithServer = super.reconstructURIWithServer(server, updatedURI); - - // if instance is not secure, override with http: - DiscoveryEnabledServer discoveryEnabledServer = (DiscoveryEnabledServer) server; - final InstanceInfo instanceInfo = discoveryEnabledServer.getInstanceInfo(); - if (instanceInfo.isPortEnabled(InstanceInfo.PortType.UNSECURE)) { - log.debug("Resetting scheme to HTTP based on instance info of instance: " + instanceInfo.getId()); - UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUri(uriWithServer).scheme(HTTP); - uriToSend = uriComponentsBuilder.build(true).toUri(); - } else { - uriToSend = uriWithServer; - if (uriWithServer.getScheme().equalsIgnoreCase("http")) { - uriToSend = UriComponentsBuilder.fromUri(uriWithServer).scheme(HTTPS).build(true).toUri(); - } - } - return uriToSend; - } - - - @Override - public RibbonApacheHttpResponse execute(RibbonApacheHttpRequest request, IClientConfig configOverride) throws Exception { - RibbonApacheHttpRequest sendRequest = null; - configOverride.set(CommonClientConfigKey.IsSecure, HTTPS.equals(request.getURI().getScheme())); - final RequestConfig.Builder builder = RequestConfig.custom(); - builder.setConnectTimeout(configOverride.get( - CommonClientConfigKey.ConnectTimeout, this.connectTimeout)); - builder.setSocketTimeout(configOverride.get( - CommonClientConfigKey.ReadTimeout, this.readTimeout)); - builder.setRedirectsEnabled(configOverride.get( - CommonClientConfigKey.FollowRedirects, this.followRedirects)); - builder.setContentCompressionEnabled(false); - - final RequestConfig requestConfig = builder.build(); - if (HTTPS.equals(request.getURI().getScheme())) { - final URI secureUri = UriComponentsBuilder.fromUri(request.getUri()) - .scheme(HTTPS).build().toUri(); - sendRequest = request.withNewUri(secureUri); - } - if (sendRequest == null) { - sendRequest = request; - } - final HttpUriRequest httpUriRequest = sendRequest.toRequest(requestConfig); - final HttpResponse httpResponse = this.delegate.execute(httpUriRequest); - return new RibbonApacheHttpResponse(httpResponse, httpUriRequest.getURI()); - } - - /** - * This methods write specific InstanceInfo into cache - * - * @param serviceId serviceId of instance - * @param instanceId ID of instance - * @param instanceInfo instanceInfo to store - * @return cached instanceInfo object - */ - @CachePut(value = "instanceInfoByInstanceId", key = "{#serviceId, #instanceId}") - public InstanceInfo putInstanceInfo(String serviceId, String instanceId, InstanceInfo instanceInfo) { - return instanceInfo; - } - - /** - * Get the InstanceInfo by id. For searching is used serviceId. It method found another instances it will - * cache them for next using - * - * @param serviceId service to call - * @param instanceId selected instance of service - * @return instance with matching service and instanceId - */ - @Cacheable("instanceInfoByInstanceId") - public InstanceInfo getInstanceInfo(String serviceId, String instanceId) { - InstanceInfo output = null; - - Application application = discoveryClient.getApplication(serviceId); - if (application == null) return null; - - for (final InstanceInfo instanceInfo : application.getInstances()) { - if (StringUtils.equals(instanceId, instanceInfo.getInstanceId())) { - // found instance, store it for output - output = instanceInfo; - } - - /* - * Getting all instance is pretty heavy, cache them, therefor it is very probably, nex using of service - * will use different instance and need to find it too. - */ - putInstanceInfo(serviceId, instanceInfo.getInstanceId(), instanceInfo); - } - return output; - } - - @Override - protected void customizeLoadBalancerCommandBuilder(RibbonApacheHttpRequest request, IClientConfig config, LoadBalancerCommand.Builder builder) { - super.customizeLoadBalancerCommandBuilder(request, config, builder); - - /* - * add into builder listener to work with request immediately when instance if selected - * it is helpful for selecting {@com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand} in - * case there is multiple instances with same serviceId which have different authentication. Therefor it - * is necessary wait for selection of instance to apply right authentication command. - */ - builder.withListeners(Collections.singletonList(new ExecutionListener() { - - /** - * This method update current request by added values on sending in load balancer. It is used at least - * for service authentication. - * - * Now it updates only headers, but could be extended for another values in future. - * - * @param context - */ - private void updateRequestByZuulChanges(ExecutionContext context) { - final Map newHeaders = RequestContext.getCurrentContext().getZuulRequestHeaders(); - if (!newHeaders.isEmpty()) { - final RibbonApacheHttpRequest req = (RibbonApacheHttpRequest) context.getRequest(); - for (Map.Entry entry : newHeaders.entrySet()) { - req.getContext().getHeaders().set(entry.getKey(), entry.getValue()); - } - } - } - - @Override - public void onExecutionStart(ExecutionContext context) { - // dont needed yet - } - - @Override - public void onStartWithServer(ExecutionContext context, ExecutionInfo info) { - final AuthenticationCommand cmd = (AuthenticationCommand) RequestContext.getCurrentContext().get(AUTHENTICATION_COMMAND_KEY); - if (cmd != null) { - // in context is a command, it means update of authentication is waiting for select an instance - final Server.MetaInfo metaInfo = info.getServer().getMetaInfo(); - final InstanceInfo instanceInfo = getInstanceInfo(metaInfo.getServiceIdForDiscovery(), metaInfo.getInstanceId()); - try { - cmd.apply(instanceInfo); - } catch (Exception e) { - throw new AbortExecutionException(String.valueOf(e), e); - } - updateRequestByZuulChanges(context); - } - } - - @Override - public void onExceptionWithServer(ExecutionContext context, Throwable exception, ExecutionInfo info) { - // dont needed yet - } - - @Override - public void onExecutionSuccess(ExecutionContext context, RibbonApacheHttpResponse response, ExecutionInfo info) { - // dont needed yet - } - - @Override - public void onExecutionFailed(ExecutionContext context, Throwable finalException, ExecutionInfo info) { - // dont needed yet - } - })); - builder.withExecutionContext(new ExecutionContext(request, config, config, null)); - } } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClientImpl.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClientImpl.java new file mode 100644 index 0000000000..f60e8071f3 --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClientImpl.java @@ -0,0 +1,268 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.ribbon; + +import com.ca.mfaas.gateway.config.CacheConfig; +import com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand; +import com.ca.mfaas.util.CacheUtils; +import com.netflix.appinfo.InstanceInfo; +import com.netflix.client.config.CommonClientConfigKey; +import com.netflix.client.config.IClientConfig; +import com.netflix.discovery.EurekaClient; +import com.netflix.discovery.shared.Application; +import com.netflix.loadbalancer.Server; +import com.netflix.loadbalancer.reactive.ExecutionContext; +import com.netflix.loadbalancer.reactive.ExecutionInfo; +import com.netflix.loadbalancer.reactive.ExecutionListener; +import com.netflix.loadbalancer.reactive.LoadBalancerCommand; +import com.netflix.niws.loadbalancer.DiscoveryEnabledServer; +import com.netflix.zuul.context.RequestContext; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; +import org.apache.http.HttpResponse; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.impl.client.CloseableHttpClient; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cloud.netflix.ribbon.ServerIntrospector; +import org.springframework.cloud.netflix.ribbon.apache.RibbonApacheHttpRequest; +import org.springframework.cloud.netflix.ribbon.apache.RibbonApacheHttpResponse; +import org.springframework.cloud.netflix.ribbon.apache.RibbonLoadBalancingHttpClient; +import org.springframework.context.ApplicationContext; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.annotation.PostConstruct; +import java.net.URI; +import java.util.Collections; +import java.util.Map; + +import static com.ca.mfaas.gateway.security.service.ServiceAuthenticationServiceImpl.AUTHENTICATION_COMMAND_KEY; +import static org.springframework.cloud.netflix.ribbon.RibbonUtils.updateToSecureConnectionIfNeeded; + +@Slf4j +public class GatewayRibbonLoadBalancingHttpClientImpl extends RibbonLoadBalancingHttpClient implements GatewayRibbonLoadBalancingHttpClient { + + private static final String HTTPS = "https"; + private static final String HTTP = "http"; + + private final EurekaClient discoveryClient; + private final CacheManager cacheManager; + private final ApplicationContext applicationContext; + /** + * Instance to use AOP on method invocation - required for internal call to cache + */ + private GatewayRibbonLoadBalancingHttpClient me; + + private static final String INSTANCE_INFO_BY_INSTANCE_ID = "instanceInfoByInstanceId"; + + /** + * Ribbon load balancer + * + * @param secureHttpClient custom http client for our certificates + * @param config configuration details + * @param serverIntrospector introspector + */ + public GatewayRibbonLoadBalancingHttpClientImpl( + CloseableHttpClient secureHttpClient, + IClientConfig config, + ServerIntrospector serverIntrospector, + EurekaClient discoveryClient, + CacheManager cacheManager, + ApplicationContext applicationContext + ) { + super(secureHttpClient, config, serverIntrospector); + this.discoveryClient = discoveryClient; + this.cacheManager = cacheManager; + this.applicationContext = applicationContext; + } + + @PostConstruct + public void afterPropertiesSet() { + me = applicationContext.getBean(GatewayRibbonLoadBalancingHttpClient.class); + } + + @Override + public URI reconstructURIWithServer(Server server, URI original) { + URI uriToSend; + URI updatedURI = updateToSecureConnectionIfNeeded(original, this.config, this.serverIntrospector, + server); + final URI uriWithServer = super.reconstructURIWithServer(server, updatedURI); + + // if instance is not secure, override with http: + DiscoveryEnabledServer discoveryEnabledServer = (DiscoveryEnabledServer) server; + final InstanceInfo instanceInfo = discoveryEnabledServer.getInstanceInfo(); + if (instanceInfo.isPortEnabled(InstanceInfo.PortType.UNSECURE)) { + log.debug("Resetting scheme to HTTP based on instance info of instance: " + instanceInfo.getId()); + UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUri(uriWithServer).scheme(HTTP); + uriToSend = uriComponentsBuilder.build(true).toUri(); + } else { + uriToSend = uriWithServer; + if (uriWithServer.getScheme().equalsIgnoreCase("http")) { + uriToSend = UriComponentsBuilder.fromUri(uriWithServer).scheme(HTTPS).build(true).toUri(); + } + } + return uriToSend; + } + + + @Override + public RibbonApacheHttpResponse execute(RibbonApacheHttpRequest request, IClientConfig configOverride) throws Exception { + RibbonApacheHttpRequest sendRequest = null; + configOverride.set(CommonClientConfigKey.IsSecure, HTTPS.equals(request.getURI().getScheme())); + final RequestConfig.Builder builder = RequestConfig.custom(); + builder.setConnectTimeout(configOverride.get( + CommonClientConfigKey.ConnectTimeout, this.connectTimeout)); + builder.setSocketTimeout(configOverride.get( + CommonClientConfigKey.ReadTimeout, this.readTimeout)); + builder.setRedirectsEnabled(configOverride.get( + CommonClientConfigKey.FollowRedirects, this.followRedirects)); + builder.setContentCompressionEnabled(false); + + final RequestConfig requestConfig = builder.build(); + if (HTTPS.equals(request.getURI().getScheme())) { + final URI secureUri = UriComponentsBuilder.fromUri(request.getUri()) + .scheme(HTTPS).build().toUri(); + sendRequest = request.withNewUri(secureUri); + } + if (sendRequest == null) { + sendRequest = request; + } + final HttpUriRequest httpUriRequest = sendRequest.toRequest(requestConfig); + final HttpResponse httpResponse = this.delegate.execute(httpUriRequest); + return new RibbonApacheHttpResponse(httpResponse, httpUriRequest.getURI()); + } + + @Override + public void evictCacheService(String serviceId) { + CacheUtils.evictSubset(cacheManager, INSTANCE_INFO_BY_INSTANCE_ID, x -> StringUtils.equalsIgnoreCase((String) x.get(0), serviceId)); + } + + @Override + @CacheEvict(value = INSTANCE_INFO_BY_INSTANCE_ID, allEntries = true) + public void evictCacheAllService() { + // evict all records + } + + /** + * This methods write specific InstanceInfo into cache + * + * @param serviceId serviceId of instance + * @param instanceId ID of instance + * @param instanceInfo instanceInfo to store + * @return cached instanceInfo object + */ + @CachePut(value = INSTANCE_INFO_BY_INSTANCE_ID, keyGenerator = CacheConfig.COMPOSITE_KEY_GENERATOR_WITHOUT_LAST) + public InstanceInfo putInstanceInfo(String serviceId, String instanceId, InstanceInfo instanceInfo) { + return instanceInfo; + } + + /** + * Get the InstanceInfo by id. For searching is used serviceId. It method found another instances it will + * cache them for next using + * + * @param serviceId service to call + * @param instanceId selected instance of service + * @return instance with matching service and instanceId + */ + @Cacheable(value = INSTANCE_INFO_BY_INSTANCE_ID, keyGenerator = CacheConfig.COMPOSITE_KEY_GENERATOR) + public InstanceInfo getInstanceInfo(String serviceId, String instanceId) { + InstanceInfo output = null; + + Application application = discoveryClient.getApplication(serviceId); + if (application == null) return null; + + for (final InstanceInfo instanceInfo : application.getInstances()) { + if (StringUtils.equals(instanceId, instanceInfo.getInstanceId())) { + // found instance, store it for output + output = instanceInfo; + } + + /* + * Getting all instance is pretty heavy, cache them, therefor it is very probably, nex using of service + * will use different instance and need to find it too. + */ + me.putInstanceInfo(serviceId, instanceInfo.getInstanceId(), instanceInfo); + } + return output; + } + + @Override + protected void customizeLoadBalancerCommandBuilder(RibbonApacheHttpRequest request, IClientConfig config, LoadBalancerCommand.Builder builder) { + super.customizeLoadBalancerCommandBuilder(request, config, builder); + + /* + * add into builder listener to work with request immediately when instance if selected + * it is helpful for selecting {@com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand} in + * case there is multiple instances with same serviceId which have different authentication. Therefor it + * is necessary wait for selection of instance to apply right authentication command. + */ + builder.withListeners(Collections.singletonList(new ExecutionListener() { + + /** + * This method update current request by added values on sending in load balancer. It is used at least + * for service authentication. + * + * Now it updates only headers, but could be extended for another values in future. + * + * @param context + */ + private void updateRequestByZuulChanges(ExecutionContext context) { + final Map newHeaders = RequestContext.getCurrentContext().getZuulRequestHeaders(); + if (!newHeaders.isEmpty()) { + final RibbonApacheHttpRequest req = (RibbonApacheHttpRequest) context.getRequest(); + for (Map.Entry entry : newHeaders.entrySet()) { + req.getContext().getHeaders().set(entry.getKey(), entry.getValue()); + } + } + } + + @Override + public void onExecutionStart(ExecutionContext context) { + // dont needed yet + } + + @Override + public void onStartWithServer(ExecutionContext context, ExecutionInfo info) { + final AuthenticationCommand cmd = (AuthenticationCommand) RequestContext.getCurrentContext().get(AUTHENTICATION_COMMAND_KEY); + if (cmd != null) { + // in context is a command, it means update of authentication is waiting for select an instance + final Server.MetaInfo metaInfo = info.getServer().getMetaInfo(); + final InstanceInfo instanceInfo = me.getInstanceInfo(metaInfo.getServiceIdForDiscovery(), metaInfo.getInstanceId()); + try { + cmd.apply(instanceInfo); + } catch (Exception e) { + throw new AbortExecutionException(String.valueOf(e), e); + } + updateRequestByZuulChanges(context); + } + } + + @Override + public void onExceptionWithServer(ExecutionContext context, Throwable exception, ExecutionInfo info) { + // dont needed yet + } + + @Override + public void onExecutionSuccess(ExecutionContext context, RibbonApacheHttpResponse response, ExecutionInfo info) { + // dont needed yet + } + + @Override + public void onExecutionFailed(ExecutionContext context, Throwable finalException, ExecutionInfo info) { + // dont needed yet + } + })); + builder.withExecutionContext(new ExecutionContext(request, config, config, null)); + } + +} diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClientImplTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClientImplTest.java new file mode 100644 index 0000000000..de98d62629 --- /dev/null +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClientImplTest.java @@ -0,0 +1,291 @@ +package com.ca.mfaas.gateway.ribbon;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import com.ca.mfaas.gateway.config.CacheConfig; +import com.ca.mfaas.gateway.security.service.AuthenticationException; +import com.ca.mfaas.gateway.security.service.schema.AuthenticationCommand; +import com.netflix.appinfo.InstanceInfo; +import com.netflix.client.config.IClientConfig; +import com.netflix.discovery.EurekaClient; +import com.netflix.discovery.shared.Application; +import com.netflix.loadbalancer.Server; +import com.netflix.loadbalancer.reactive.ExecutionContext; +import com.netflix.loadbalancer.reactive.ExecutionInfo; +import com.netflix.loadbalancer.reactive.ExecutionListener; +import com.netflix.loadbalancer.reactive.LoadBalancerCommand; +import com.netflix.zuul.context.RequestContext; +import lombok.Getter; +import org.apache.http.impl.client.CloseableHttpClient; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.CacheManager; +import org.springframework.cloud.netflix.ribbon.ServerIntrospector; +import org.springframework.cloud.netflix.ribbon.apache.RibbonApacheHttpRequest; +import org.springframework.cloud.netflix.ribbon.apache.RibbonApacheHttpResponse; +import org.springframework.cloud.netflix.ribbon.support.RibbonCommandContext; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import java.util.Arrays; +import java.util.List; + +import static com.ca.mfaas.gateway.security.service.ServiceAuthenticationServiceImpl.AUTHENTICATION_COMMAND_KEY; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = { + GatewayRibbonLoadBalancingHttpClientImplTest.Context.class, + CacheConfig.class +}) +public class GatewayRibbonLoadBalancingHttpClientImplTest { + + @Autowired + private GatewayRibbonLoadBalancingHttpClientTest bean; + + @Autowired + private EurekaClient discoveryClient; + + @Autowired + private IClientConfig config; + + private InstanceInfo createInstanceInfo(String instanceId) { + InstanceInfo out = mock(InstanceInfo.class); + when(out.getInstanceId()).thenReturn(instanceId); + return out; + } + + private Application createApplication(InstanceInfo...instanceInfos) { + Application out = mock(Application.class); + when(out.getInstances()).thenReturn(Arrays.asList(instanceInfos)); + return out; + } + + @Test + public void testCache() { + InstanceInfo ii = mock(InstanceInfo.class); + bean.putInstanceInfo("service1", "host:service1:123", ii); + + verify(discoveryClient, never()).getApplications(any()); + assertSame(ii, bean.getInstanceInfo("service1", "host:service1:123")); + + InstanceInfo ii1 = createInstanceInfo("host:service2:1"); + InstanceInfo ii2 = createInstanceInfo("host:service2:2"); + Application application = createApplication(ii1, ii2); + when(discoveryClient.getApplication("service2")).thenReturn(application); + assertSame(ii1, bean.getInstanceInfo("service2", "host:service2:1")); + assertSame(ii2, bean.getInstanceInfo("service2", "host:service2:2")); + verify(discoveryClient, times(1)).getApplication("service2"); + + bean.evictCacheService("service2"); + + assertSame(ii, bean.getInstanceInfo("service1", "host:service1:123")); + verify(discoveryClient, times(1)).getApplication("service2"); + + assertSame(ii2, bean.getInstanceInfo("service2", "host:service2:2")); + verify(discoveryClient, times(2)).getApplication("service2"); + } + + private ExecutionListener getListener() { + BuilderHolder builderHolder = new BuilderHolder(); + bean.customizeLoadBalancerCommandBuilder(mock(RibbonApacheHttpRequest.class), config, builderHolder.getBuilder()); + return builderHolder.getListener(); + } + + @Test + public void testOnStartWithServer() { + // base configuration about ZUUL + ExecutionListener listener = getListener(); + RequestContext requestContext = new RequestContext(); + RequestContext.testSetCurrentContext(requestContext); + ExecutionContext context = mock(ExecutionContext.class); + ExecutionInfo info = mock(ExecutionInfo.class); + + // test without command + listener.onStartWithServer(context, info); + verify(info, never()).getServer(); + + // configuration for deeply test with commands + Server server = mock(Server.class); + Server.MetaInfo metaInfo = mock(Server.MetaInfo.class); + when(metaInfo.getServiceIdForDiscovery()).thenReturn("service3"); + when(metaInfo.getInstanceId()).thenReturn("host:service3:1"); + when(server.getMetaInfo()).thenReturn(metaInfo); + when(info.getServer()).thenReturn(server); + InstanceInfo ii1 = createInstanceInfo("host:service3:1"); + InstanceInfo ii2 = createInstanceInfo("host:service3:2"); + Application application = createApplication(ii1, ii2); + when(discoveryClient.getApplication("service3")).thenReturn(application); + RibbonApacheHttpRequest request = mock(RibbonApacheHttpRequest.class); + RibbonCommandContext ribbonCommandContext = mock(RibbonCommandContext.class); + MultiValueMap headers = new LinkedMultiValueMap<>(); + when(ribbonCommandContext.getHeaders()).thenReturn(headers); + when(request.getContext()).thenReturn(ribbonCommandContext); + when(context.getRequest()).thenReturn(request); + + // test command without any action + requestContext.put(AUTHENTICATION_COMMAND_KEY, AuthenticationCommand.EMPTY); + listener.onStartWithServer(context, info); + assertTrue(headers.isEmpty()); + + // test command which throw exception + requestContext.put(AUTHENTICATION_COMMAND_KEY, new TestAuthenticationCommand2()); + try { + listener.onStartWithServer(context, info); + fail(); + } catch (ExecutionListener.AbortExecutionException e) { + assertTrue(e.getCause() instanceof AuthenticationException); + assertEquals("Test exception", ((AuthenticationException) e.getCause()).getMessage()); + } + + // test command with header set + requestContext.put(AUTHENTICATION_COMMAND_KEY, new TestAuthenticationCommand()); + listener.onStartWithServer(context, info); + assertEquals(2, headers.size()); + assertEquals(1, headers.get("testkey").size()); + assertEquals("testValue", headers.get("testkey").get(0)); + assertEquals(1, headers.get("testinstanceid").size()); + assertEquals("host:service3:1", headers.get("testinstanceid").get(0)); + verify(discoveryClient, times(1)).getApplication("service3"); + bean.getInstanceInfo("service3", "host:service3:1"); + bean.getInstanceInfo("service3", "host:service3:2"); + verify(discoveryClient, times(1)).getApplication("service3"); + } + + class TestAuthenticationCommand extends AuthenticationCommand { + + @Override + public void apply(InstanceInfo instanceInfo) throws AuthenticationException { + RequestContext.getCurrentContext().addZuulRequestHeader("testKey", "testValue"); + RequestContext.getCurrentContext().addZuulRequestHeader("testInstanceId", instanceInfo.getInstanceId()); + } + + @Override + public boolean isExpired() { + return false; + } + + } + + class TestAuthenticationCommand2 extends AuthenticationCommand { + + @Override + public void apply(InstanceInfo instanceInfo) throws AuthenticationException { + throw new AuthenticationException("Test exception", null); + } + + @Override + public boolean isExpired() { + return false; + } + + } + + @Getter + class BuilderHolder { + + private ExecutionListener listener; + + public LoadBalancerCommand.Builder getBuilder() { + LoadBalancerCommand.Builder builder = mock(LoadBalancerCommand.Builder.class); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + List> listeners = invocation.getArgument(0); + assertNotNull(listeners); + assertEquals(1, listeners.size()); + listener = listeners.get(0); + return null; + } + }).when(builder).withListeners(any()); + return builder; + } + + } + + @Configuration + public static class Context { + + @Bean + public EurekaClient getDiscoveryClient() { + return mock(EurekaClient.class); + } + + @Bean + public CloseableHttpClient getSecureHttpClient() { + return mock(CloseableHttpClient.class); + } + + @Bean + public IClientConfig getConfig() { + IClientConfig out = mock(IClientConfig.class); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return invocation.getArgument(1); + } + }).when(out).get(any(), any()); + return out; + } + + @Bean + public ServerIntrospector getServerIntrospector() { + return mock(ServerIntrospector.class); + } + + @Bean + public GatewayRibbonLoadBalancingHttpClient ribbonLoadBalancingHttpClient( + CloseableHttpClient secureHttpClient, + IClientConfig config, + ServerIntrospector serverIntrospector, + EurekaClient discoveryClient, + CacheManager cacheManager, + ApplicationContext applicationContext + ) { + return new GatewayRibbonLoadBalancingHttpClientImplTestBean(secureHttpClient, config, serverIntrospector, discoveryClient, cacheManager, applicationContext); + } + + } + + public interface GatewayRibbonLoadBalancingHttpClientTest extends GatewayRibbonLoadBalancingHttpClient { + + public void customizeLoadBalancerCommandBuilder(RibbonApacheHttpRequest request, IClientConfig config, LoadBalancerCommand.Builder builder); + + } + + public static class GatewayRibbonLoadBalancingHttpClientImplTestBean extends GatewayRibbonLoadBalancingHttpClientImpl implements GatewayRibbonLoadBalancingHttpClientTest { + + /** + * Ribbon load balancer + * + * @param secureHttpClient custom http client for our certificates + * @param config configuration details + * @param serverIntrospector introspector + * @param discoveryClient + */ + public GatewayRibbonLoadBalancingHttpClientImplTestBean(CloseableHttpClient secureHttpClient, IClientConfig config, ServerIntrospector serverIntrospector, EurekaClient discoveryClient, CacheManager cacheManager, ApplicationContext applicationContext) { + super(secureHttpClient, config, serverIntrospector, discoveryClient, cacheManager, applicationContext); + } + + @Override + public void customizeLoadBalancerCommandBuilder(RibbonApacheHttpRequest request, IClientConfig config, LoadBalancerCommand.Builder builder) { + super.customizeLoadBalancerCommandBuilder(request, config, builder); + } + } + +} From 89a62858ece9869aaad1a3c9f5edf7ac1305b1f5 Mon Sep 17 00:00:00 2001 From: JirkaAichler Date: Wed, 22 Jan 2020 17:32:20 +0100 Subject: [PATCH 093/122] Fix sonar smells Signed-off-by: JirkaAichler --- .../discovery/staticdef/StaticRegistrationResult.java | 6 +++++- .../staticdef/StaticServicesRegistrationService.java | 4 ++-- .../staticdef/StaticServicesRegistrationServiceTest.java | 6 +++--- .../com/ca/mfaas/gateway/ribbon/GatewayRibbonConfig.java | 9 ++++++--- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/StaticRegistrationResult.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/StaticRegistrationResult.java index a643c3f34c..a9aff9e3b0 100644 --- a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/StaticRegistrationResult.java +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/StaticRegistrationResult.java @@ -14,10 +14,14 @@ import java.util.*; +/** + * Result of registration of static services + * Contains registered services, additional metadata, errors ... + */ @Data public class StaticRegistrationResult { private final List errors = new LinkedList<>(); private final List instances = new LinkedList<>(); private final Map additionalServiceMetadata = new HashMap<>(); - private final List registredServices = new LinkedList<>(); + private final List registeredServices = new LinkedList<>(); } diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/StaticServicesRegistrationService.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/StaticServicesRegistrationService.java index c612247e00..4494793967 100644 --- a/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/StaticServicesRegistrationService.java +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/staticdef/StaticServicesRegistrationService.java @@ -92,7 +92,7 @@ public synchronized StaticRegistrationResult reloadServices() { PeerAwareInstanceRegistry registry = getRegistry(); for (InstanceInfo info: oldStaticInstances) { - if (!result.getRegistredServices().contains(info.getInstanceId())) { + if (!result.getRegisteredServices().contains(info.getInstanceId())) { log.info("Instance {} is not defined in the new static API definitions. It will be removed", info.getInstanceId()); registry.cancel(info.getAppName(), info.getId(), false); } @@ -114,7 +114,7 @@ StaticRegistrationResult registerServices(String staticApiDefinitionsDirectories // register static services for (InstanceInfo instanceInfo : result.getInstances()) { - result.getRegistredServices().add(instanceInfo.getInstanceId()); + result.getRegisteredServices().add(instanceInfo.getInstanceId()); staticInstances.add(instanceInfo); registry.register(instanceInfo, false); } diff --git a/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/StaticServicesRegistrationServiceTest.java b/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/StaticServicesRegistrationServiceTest.java index c5c3417419..f507c7ff0e 100644 --- a/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/StaticServicesRegistrationServiceTest.java +++ b/discovery-service/src/test/java/com/ca/mfaas/discovery/staticdef/StaticServicesRegistrationServiceTest.java @@ -122,7 +122,7 @@ public void testReloadServicesWithUnregisteringService() { registrationService.reloadServices(); StaticRegistrationResult result = registrationService.reloadServices(); - assertThat(result.getRegistredServices().contains(service), is(false)); + assertThat(result.getRegisteredServices().contains(service), is(false)); verify(serviceDefinitionProcessor, times(2)).findStaticServicesData(null); verify(mockRegistry, times(1)).cancel(instance.getAppName(), instance.getId(), false); } @@ -142,8 +142,8 @@ public void testReloadServicesWithAddingNewService() { registrationService.reloadServices(); StaticRegistrationResult result = registrationService.reloadServices(); - assertThat(result.getRegistredServices().contains(serviceA), is(true)); - assertThat(result.getRegistredServices().contains(serviceB), is(true)); + assertThat(result.getRegisteredServices().contains(serviceA), is(true)); + assertThat(result.getRegisteredServices().contains(serviceB), is(true)); verify(serviceDefinitionProcessor, times(2)).findStaticServicesData(null); verify(mockRegistry, times(0)).cancel(any(String.class), any(String.class), eq(false)); } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonConfig.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonConfig.java index d857d43aba..b1a7da9d51 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonConfig.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonConfig.java @@ -13,6 +13,7 @@ import com.netflix.client.config.IClientConfig; import com.netflix.discovery.EurekaClient; import com.netflix.loadbalancer.*; +import lombok.RequiredArgsConstructor; import org.apache.http.impl.client.CloseableHttpClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.CacheManager; @@ -23,15 +24,17 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +/** + * Configuration of client side load balancing with Ribbon + */ @Configuration +@RequiredArgsConstructor public class GatewayRibbonConfig { + private final PropertiesFactory propertiesFactory; @RibbonClientName private String ribbonClientName = "client"; - @Autowired - private PropertiesFactory propertiesFactory; - @Bean @Autowired public GatewayRibbonLoadBalancingHttpClient ribbonLoadBalancingHttpClient( From 60983b8ba631209c63c36b37143e84479b7eaa4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Thu, 23 Jan 2020 14:35:18 +0100 Subject: [PATCH 094/122] revert url of Auth and CacheService controllers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../main/java/com/ca/mfaas/discovery/GatewayNotifier.java | 2 +- .../java/com/ca/mfaas/discovery/GatewayNotifierTest.java | 8 ++++---- .../java/com/ca/mfaas/gateway/GatewayApplication.java | 4 +++- .../com/ca/mfaas/gateway/controllers/AuthController.java | 2 +- .../mfaas/gateway/controllers/CacheServiceController.java | 2 +- .../com/ca/mfaas/gateway/ribbon/GatewayRibbonConfig.java | 6 +++++- .../gateway/security/service/AuthenticationService.java | 2 +- .../ca/mfaas/gateway/controllers/AuthControllerTest.java | 6 +++--- .../gateway/controllers/CacheServiceControllerTest.java | 4 ++-- .../security/service/AuthenticationServiceTest.java | 4 ++-- .../gatewayservice/AuthenticationOnDeploymentTest.java | 2 -- 11 files changed, 23 insertions(+), 19 deletions(-) diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java index 7faea6a9cb..31630bc6db 100644 --- a/discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/GatewayNotifier.java @@ -56,7 +56,7 @@ public void serviceUpdated(String serviceId) { for (final InstanceInfo instanceInfo : gatewayInstances) { final StringBuilder url = new StringBuilder(); - url.append(EurekaUtils.getUrl(instanceInfo)).append("/api/v1/gateway/cache/services"); + url.append(EurekaUtils.getUrl(instanceInfo)).append("/cache/services"); if (serviceId != null) url.append('/').append(serviceId); restTemplate.delete(url.toString()); diff --git a/discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java b/discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java index b08f60f19f..9e9469ba8d 100644 --- a/discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java +++ b/discovery-service/src/test/java/com/ca/mfaas/discovery/GatewayNotifierTest.java @@ -65,12 +65,12 @@ public void testServiceUpdated() { when(registry.getApplication("GATEWAY")).thenReturn(application); gatewayNotifier.serviceUpdated("testService"); - verify(restTemplate, times(1)).delete("https://hostname1:1433/api/v1/gateway/cache/services/testService"); - verify(restTemplate, times(1)).delete("http://hostname2:1000/api/v1/gateway/cache/services/testService"); + verify(restTemplate, times(1)).delete("https://hostname1:1433/cache/services/testService"); + verify(restTemplate, times(1)).delete("http://hostname2:1000/cache/services/testService"); gatewayNotifier.serviceUpdated(null); - verify(restTemplate, times(1)).delete("https://hostname1:1433/api/v1/gateway/cache/services"); - verify(restTemplate, times(1)).delete("http://hostname2:1000/api/v1/gateway/cache/services"); + verify(restTemplate, times(1)).delete("https://hostname1:1433/cache/services"); + verify(restTemplate, times(1)).delete("http://hostname2:1000/cache/services"); verify(restTemplate, times(4)).delete(anyString()); } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/GatewayApplication.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/GatewayApplication.java index 0f4e34a047..6e0739e71b 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/GatewayApplication.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/GatewayApplication.java @@ -40,7 +40,9 @@ "com.ca.apiml.security.common" }, excludeFilters = { - @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*RibbonConfig")}) + @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*RibbonConfig") + } +) @RibbonClients(defaultConfiguration = GatewayRibbonConfig.class) @EnableEurekaClient @EnableWebSocket diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/AuthController.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/AuthController.java index 0fa2e0dbe1..20515193cc 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/AuthController.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/AuthController.java @@ -27,7 +27,7 @@ */ @AllArgsConstructor @RestController -@RequestMapping("/api/v1/gateway/auth") +@RequestMapping("/auth") public class AuthController { private final AuthenticationService authenticationService; diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/CacheServiceController.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/CacheServiceController.java index f50eb04cdc..6747fcb33a 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/CacheServiceController.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/CacheServiceController.java @@ -26,7 +26,7 @@ */ @AllArgsConstructor @RestController -@RequestMapping("/api/v1/gateway/cache/services") +@RequestMapping("/cache/services") public class CacheServiceController { private final List toEvict; diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonConfig.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonConfig.java index b1a7da9d51..1416b8c105 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonConfig.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonConfig.java @@ -20,9 +20,11 @@ import org.springframework.cloud.netflix.ribbon.PropertiesFactory; import org.springframework.cloud.netflix.ribbon.RibbonClientName; import org.springframework.cloud.netflix.ribbon.ServerIntrospector; +import org.springframework.cloud.netflix.ribbon.apache.RibbonLoadBalancingHttpClient; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; /** * Configuration of client side load balancing with Ribbon @@ -36,8 +38,9 @@ public class GatewayRibbonConfig { private String ribbonClientName = "client"; @Bean + @Primary @Autowired - public GatewayRibbonLoadBalancingHttpClient ribbonLoadBalancingHttpClient( + public RibbonLoadBalancingHttpClient ribbonLoadBalancingHttpClient( CloseableHttpClient secureHttpClient, IClientConfig config, ServerIntrospector serverIntrospector, @@ -49,6 +52,7 @@ public GatewayRibbonLoadBalancingHttpClient ribbonLoadBalancingHttpClient( } @Bean + @Primary @Autowired public ILoadBalancer ribbonLoadBalancer(IClientConfig config, ServerList serverList, ServerListFilter serverListFilter, diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java index e01f3a38e1..cec49a346e 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java @@ -116,7 +116,7 @@ public Boolean invalidateJwtToken(String jwtToken, boolean distribute) { for (final InstanceInfo instanceInfo : application.getInstances()) { if (StringUtils.equals(myInstanceId, instanceInfo.getInstanceId())) continue; - final String url = EurekaUtils.getUrl(instanceInfo) + "/api/v1/gateway/auth/invalidate/{}"; + final String url = EurekaUtils.getUrl(instanceInfo) + "/auth/invalidate/{}"; restTemplate.delete(url, jwtToken); } } diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/AuthControllerTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/AuthControllerTest.java index ec06a02dee..7c0af89122 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/AuthControllerTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/AuthControllerTest.java @@ -42,12 +42,12 @@ public void setUp() { @Test public void invalidateJwtToken() throws Exception { when(authenticationService.invalidateJwtToken("a/b", false)).thenReturn(Boolean.TRUE); - this.mockMvc.perform(delete("/api/v1/gateway/auth/invalidate/a/b")).andExpect(status().is(SC_OK)); + this.mockMvc.perform(delete("/auth/invalidate/a/b")).andExpect(status().is(SC_OK)); when(authenticationService.invalidateJwtToken("abcde", false)).thenReturn(Boolean.TRUE); - this.mockMvc.perform(delete("/api/v1/gateway/auth/invalidate/abcde")).andExpect(status().is(SC_OK)); + this.mockMvc.perform(delete("/auth/invalidate/abcde")).andExpect(status().is(SC_OK)); - this.mockMvc.perform(delete("/api/v1/gateway/auth/invalidate/xyz")).andExpect(status().is(SC_SERVICE_UNAVAILABLE)); + this.mockMvc.perform(delete("/auth/invalidate/xyz")).andExpect(status().is(SC_SERVICE_UNAVAILABLE)); verify(authenticationService, times(1)).invalidateJwtToken("abcde", false); verify(authenticationService, times(1)).invalidateJwtToken("a/b", false); diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/CacheServiceControllerTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/CacheServiceControllerTest.java index 04bd260e6c..329588cf09 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/CacheServiceControllerTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/controllers/CacheServiceControllerTest.java @@ -52,7 +52,7 @@ public void testEvictAll() throws Exception { verify(service2, never()).evictCacheAllService(); verify(discoveryClient, never()).fetchRegistry(); - this.mockMvc.perform(delete("/api/v1/gateway/cache/services")).andExpect(status().isOk()); + this.mockMvc.perform(delete("/cache/services")).andExpect(status().isOk()); verify(service1, times(1)).evictCacheAllService(); verify(service2, times(1)).evictCacheAllService(); @@ -65,7 +65,7 @@ public void testEvict() throws Exception { verify(service2, never()).evictCacheService(any()); verify(discoveryClient, never()).fetchRegistry(); - this.mockMvc.perform(delete("/api/v1/gateway/cache/services/service01")).andExpect(status().isOk()); + this.mockMvc.perform(delete("/cache/services/service01")).andExpect(status().isOk()); verify(service1, times(1)).evictCacheService("service01"); verify(service2, times(1)).evictCacheService("service01"); diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/AuthenticationServiceTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/AuthenticationServiceTest.java index 2fd2ee7c1c..cb4af675c2 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/AuthenticationServiceTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/AuthenticationServiceTest.java @@ -257,8 +257,8 @@ public void invalidateToken() { tokenAuthentication = authService.validateJwtToken(jwt1); assertFalse(tokenAuthentication.isAuthenticated()); verify(restTemplate, times(2)).delete(anyString(), (Object[]) any()); - verify(restTemplate).delete("https://hostname1:10433/api/v1/gateway/auth/invalidate/{}", jwt1); - verify(restTemplate).delete("http://hostname2:10001/api/v1/gateway/auth/invalidate/{}", jwt1); + verify(restTemplate).delete("https://hostname1:10433/auth/invalidate/{}", jwt1); + verify(restTemplate).delete("http://hostname2:10001/auth/invalidate/{}", jwt1); } @Test diff --git a/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/AuthenticationOnDeploymentTest.java b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/AuthenticationOnDeploymentTest.java index 29e3c5e09a..51311e6aa7 100644 --- a/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/AuthenticationOnDeploymentTest.java +++ b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/AuthenticationOnDeploymentTest.java @@ -61,7 +61,6 @@ public void testMultipleAuthenticationSchemes() throws Exception { ) { // start first instance - without passTickets service1 - .addGetHeaderServlet(HttpHeaders.AUTHORIZATION) .addVerifyServlet() .start() .waitForGatewayRegistration(1, TIMEOUT); @@ -82,7 +81,6 @@ public void testMultipleAuthenticationSchemes() throws Exception { // start second service (with passTicket authorization) service2 - .addGetHeaderServlet(HttpHeaders.AUTHORIZATION) .addVerifyServlet() .setAuthentication(new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "TESTAPPL")) .start() From 33c359a660e7aa23cfe815a4900a32b3bd7fb0c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Thu, 23 Jan 2020 15:54:51 +0100 Subject: [PATCH 095/122] fixes in document registry-communication.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- docs/registry-communication.md | 102 ++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 45 deletions(-) diff --git a/docs/registry-communication.md b/docs/registry-communication.md index 59e82d74fb..dc1138f7c0 100644 --- a/docs/registry-communication.md +++ b/docs/registry-communication.md @@ -1,7 +1,7 @@ # Communication between Client, Discovery service, and Gateway This document is a summary of knowledge gathered during the implementation of PassTickets, and includes how to -short-cut side-effect delays. +short-cut delays in registration process (implemented as side-effect). ## Client and discovery service (server) @@ -26,11 +26,13 @@ To begin communication between the client and the Discovery service, the client - **other information** (Optional) - Other parameters can be included, but do not affect communication. - Customized data outside of the scope of Eureka can be stored in the metadata. - - Note: Metadata are data for one time use, and can changed after registration. However, a REST method can be used to update these metadata. + - Note: Metadata aren't data for one time use, and can changed after registration. However, a REST method can be used to update these metadata. Other data cannot be changed after registration -After registration, the client must send a heartbeat. Once the Discovery service receives this heartbeat, it is possible to call the client. +After registration, the client sends a heartbeat. Heart beat renews (extends) registration on discovery server. Missing heart beat longer than timeout will unregister service automatically. -The client can drop communication by unregistering with the Discovery service. This call is optional as Eureka should resolves failover. Unregistering the client simply speeds up the process of client removal. Without the unregistration call, the Discovery service waits for the heartbeat to time out. Note: This timeout is longer than the interval of the renewal of client registration via the heartbest. +The client can drop communication by unregistering with the Discovery service. Unregistering the client simply speeds up the process of client removal. +Without the unregistration call, the Discovery service waits for the heartbeat to time out. +Note: This timeout is longer than the interval of the renewal of client registration via the heartbest. Typically, all communication is covered with caches. As such, a client cannot be used immediately after registration. Caching occurs in many places for the system as a whole and it takes time to go through all of them. @@ -51,13 +53,16 @@ The default for this cache contains two spaces: `read` and `readWrite`. Other ap record is missing, it looks in `readWrite` or recreates a record. `Read` cache is updated by an internal thread which periodically compares records in both spaces (on references level, null including). - in case of different copy -records from readWrite into read. + If values are different thread copies records from `readWrite` into `read` space. The two spaces was evidently created for NetFlix purposes and was changed with pull request https://github.com/Netflix/eureka/pull/544. This improvement allows configuration to use only -`readWrite` space, `read` will be ignored and the user looks directly to `readWrite`. This cache could be updated by the Discovery service. It also produces records on registation and unregistration (after the client registers -readWrite has evicted records about service, delta and full registry, but read still contains old record). This description needs to be refactored to improve clarity. +`readWrite` space, `read` will be ignored and the user looks directly to `readWrite`. + +In default setting (read cache is on), Discovery service evicts on registration and unregistration records about service, delta and full registry only in `readWrite` space. +`Read` still contains old record until refresh thread will be done). +Disable `read` cache allows to read directly from `readWrite`. It remove delay here. + This description needs to be refactored to improve clarity. ``` eureka: @@ -72,15 +77,20 @@ cache all information from the discovery service and serve the old cached data. Data are updated with thread asynchronously. From time to time fetch new registries so you can use them. Updating can be performed either as delta, or full. -The full update fetch is the initial fetch as it is necessary to download all required information. After that it is rarely used due to performance. The Gateway could call the full update, but it happens only if data are not correct to fix them. One possible reason could be the long delay between fetching. - -Delta update fetch only changes and projects them into local cache. Delta does not mean different Different what? between -fetching. To save resources delta means just delta delta what? in the last time interval. Discovery service store changed -instances for a time period (as default 180s). This description needs to be refactored for clarity. Those changes are stored in the queue which is periodically -cleaned by removing changes older than the limit (next separed thread, What is a "separed" thread?. asynchronous). When the gateway asks for delta it will return a mirror of this queue stored in `ResponseCache`. The Gateway then detects which updates were applied in the past and which updates are new. Fornew updates, it uses a version (discovery service somehow mark changes with -numbers). +The full update fetch is the initial fetch as it is necessary to download all required information. After that it is rarely used due to performance. +The Gateway could call the full update, but it happens only if data are not correct to fix them. One possible reason could be the long delay between fetching. +Delta fetching load just part of registry - the last changes. Delta is not related to specific Gateway, it doesn't return differences since last call. +Discovery service collects changes in registry (client registration and +cancellation) and store them into queue. This queue is served to Gateways. They detect what is new and update its own registry copy (one gateway can get the same information many times). +The queue on discovery service contains information about changes for a period (as default it is 180s). Queue is periodically cleaned by separated thread (other asynchronous task). +Cleaning remove all information about changes older then configuration. +For easy detection of new updates by gateway there is a mechanism to store version to each update (incrementing number). + Different what? + delta what? + This description needs to be refactored for clarity. + What is a "separed" thread?. This description needs to be rewritten to make it comprehensible. @@ -90,7 +100,7 @@ This cache was minimized by allowing run asynchronous fetching at any time. The - **ApimlDiscoveryClient** - custom implementation of discovery client - - via reflection it takes reference to queue responsible for fetching of registry + - via reflection it takes reference to thread pool responsible for fetching of registry - contains method ```public void fetchRegistry()```, which add new asynchronous command to fetch registry - **DiscoveryClientConfig** - configuration bean to construct custom discovery client @@ -103,28 +113,28 @@ This cache was minimized by allowing run asynchronous fetching at any time. The ### Gateway & Route locators -The gateway includes the bean `ApimlRouteLocator`. This bean is responsible for collecting the client's routes. It indicates that information is available about the path and services. This information is required to map the URI to a service. The most important is +The gateway includes the bean `ApimlRouteLocator`. This bean is responsible for collecting the client's routes. It indicates that information +is available about the path and services. This information is required to map the URI to a service. The most important is the filter `PreDecorationFilter`. It calls the method ```Route getMatchingRoute(String path)``` on the locator to translate the URI into -information about the service. A filter then stores information (ie. `serviceId`) into the ZUUL context. +information about the service. A filter then stores information about (ie. `serviceId`) into the ZUUL context. -In out implementation we use a custom locator, which adds information about static routing. Route locators could be composed of -from many . ...of many what? -Eureka uses `CompositeRouteLocator` which contains `ApimlRouteLocator` and a default. Implementation of static routing -could also be performed by a different locator. In a similar way a super class of `ApimlRouteLocator` uses `ZuulProperties`. This can be also be used -to store a static route. +In our implementation we use a custom locator, which adds information about static routing. There is possible to have multiple locators. All of them +could be collected by `CompositeRouteLocator`. Now `CompositeRouteLocator` contains `ApimlRouteLocator` and a default implementation. Implementation of static routing +could also be performed by a different locator (it is not necessary to override locator based on `DiscoveryClient`). In a similar way a super class of +`ApimlRouteLocator` uses `ZuulProperties`. This can be also be used to store a static route. -**Note:** This is only for information, and could be changed in the future. +**Note:** To replace `ApimlRouteLocator` with multiple locators is only for information, and it could be changed in the future. **solution** -Anyway this bean should evicted. It is realized via event from fetching registry (implemented in DiscoveryClientConfig) and +Anyway this bean should be evicted. It is realized via event from fetching registry (implemented in DiscoveryClientConfig) and call on each locator method refresh(). This method call discoveryClient and then construct location mapping again. Now after fetching new version of registry is constructed well, with new services. ### Gateway & ZuulHandlerMapping -This bean serve method to detect endpoint and return by it handler. Handlers are created on the begin and then just looked up -by URI. In there is mechanism of dirty data. It means, that it create handlers and they are available (dont use locators) +This bean serve method to detect endpoint and return right handler. Handlers are created on the begin and then just looked up +by URI. In there is mechanism of dirty data. It means, that it create handlers and they are available (don't use locators) until they are mark as dirty. Then next call refresh all handlers by data from locators. **solution** @@ -133,20 +143,20 @@ In DiscoveryClientConfig is implemented listener of fetched registry. It will ma ### Ribbon load balancer -On the end of ZUUL is load balancer. For that we use Ribbon (before implementation implementation was ZoneAwareLoadBalancer). -Ribbon has also own cache it is use to have information about instances. Shortly, ZUUL give to Ribbon request and it should +On the end of ZUUL is load balancer. For that we use Ribbon (before speed up implementation it was `ZoneAwareLoadBalancer`). +Ribbon has also own cache. It is used to have information about instances. Shortly, ZUUL give to Ribbon request and it should send to an instance. ZUUL contains information about servers (serviceId -> 1-N instances) and information about state of load -balancing (depends on selected mechanism way to select next instance). If this cache is not evicted, Ribbon can try send +balancing (depends on selected mechanism - a way to select next instance). If this cache is not evicted, Ribbon can try send request to server which was removed, don't know any server to send or just overload an instance, because don't know about other. Ribbon can throw many exception in this time, and it is not sure, that it retry sending in right way. **solution** -Now we use as load balancer implementation `ApimlZoneAwareLoadBalancer` (it extends original ZoneAwareLoadBalancer). This +Now we use as load balancer implementation `ApimlZoneAwareLoadBalancer` (it extends original `ZoneAwareLoadBalancer`). This implementation only add method ```public void serverChanged()``` which call super class to reload information about servers, it means about instances and their addresses. -This is call from `ServiceCacheEvictor` to be sure, that before custom EhCaches are evicted and load balancer get right +Method serverChanged is called from `ServiceCacheEvictor` to be sure, that before custom EhCaches are evicted and load balancer get right information from ZUUL. ### Service cache - our custom EhCache @@ -164,16 +174,16 @@ than other beans are notified (see `CacheRefreshedEvent` from discovery client). This mechanism is working, but not strictly right. There is one case: -1. instance changes in discovery client -2. gateway are notified, clean custome caches and ask for new registry fetching -3. new request accept and make a cache (again with old state) - **this is wrong** +1. instance changed in discovery client +2. gateway are notified, clean custom caches and ask for new registry fetching +3. ZUUL accepts new request and make a cache (again with old state) - **this is wrong** 4. fetching of registry is done, evict all Eureka caches For this reason there was added new bean `CacheEvictor`. #### CacheEvictor -This bean collect all calls from `CacheServiceController` and is waiting for registry fetching. On this event it will clean all +This bean collects all calls from `CacheServiceController` and it is waiting for registry fetching. On this event it will clean all custom caches (via interface `ServiceCacheEvict`). On the end it means that custom caches are evicted twice (before Eureka parts and after). It fully supported right state. @@ -183,37 +193,39 @@ Implementation of this improvement wasn't just about caches, but discovery servi ### Event from InstanceRegistry -In Discovery service exist bean `InstanceRegistry`. This bean is call for register, renew and unregister of service. +In Discovery service bean `InstanceRegistry` exists. This bean is called for register, renew and unregister of service (client). Unfortunately, this bean contains also one problem. It notified about newly registered instances before it register it, in -similar way about unregister (cancellation) and renew. It doesnt matter about renew, but other makes problem for us. We +similar way about unregister (cancellation) and renew. It doesn't matter about renew (it is not a change), but other makes problem for us. We can clean caches before update in `InstanceRegistry` happened. On this topic exists issue: + ``` #2659 Race condition with registration events in Eureka server https://github.com/spring-cloud/spring-cloud-netflix/issues/2659 ``` -This issue takes long time and it is not good wait for implementation, for this reason was implemented ApimlInstanceRegistry. +This issue takes long time and it is not good wait for implementation, for this reason was implemented `ApimlInstanceRegistry`. This bean replace implementation and make notification in right order. It is via java reflection and it will be removed when Eureka will be fixed. ## Using caches and their evicting -If you use anywhere custom cache, implement interface ServiceCacheEvict to evict. It offer to methods: +If you use anywhere custom cache, implement interface ServiceCacheEvict to evict. It offers two methods: - `public void evictCacheService(String serviceId)` - - to evict only part of caches for service with serviceId + - to evict only part of caches related to one service (multiple instances with same serviceId) - if there is no way how to do it, you can evict all records -- public void `evictCacheAllService()` - - to evict all records in the caches, which can has a relationship with any service - - this method will be call very rare, only in case that, there is impossible to get serviceId (ie. wrong format of instanceId) +- `public void evictCacheAllService()` + - to evict all records in the caches, which can have a relationship with any service + - this method will be call very rare, only in case that there is impossible to get serviceId (ie. wrong format of instanceId) ## Order to clean caches From Instance registry is information distributed in this order: + ``` Discovery service > ResponseCache in discovery service > Discovery client in gateway > Route locators in gateway > ZUUL handler mapping ``` -After those chain is our EhCache (because this is first time, which could cache new data) +After this chain is our EhCache (because this is first time, which could cache new data) From user point of view after ZUUL handler mapping exists Ribbon load balancer cache From 908b8fe1a0a3e1783ab9b34e9bd473052f269bc9 Mon Sep 17 00:00:00 2001 From: JirkaAichler Date: Thu, 23 Jan 2020 16:17:58 +0100 Subject: [PATCH 096/122] Fix Sonar issues Signed-off-by: JirkaAichler --- .../utils/CleanCurrentRequestContextTest.java | 2 +- .../utils/CurrentRequestContextTest.java | 2 +- .../AuthenticationOnDeploymentTest.java | 2 +- .../ca/mfaas/util/service/VirtualService.java | 153 +++++++++--------- 4 files changed, 80 insertions(+), 79 deletions(-) diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/utils/CleanCurrentRequestContextTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/utils/CleanCurrentRequestContextTest.java index de03c48518..facac9acca 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/utils/CleanCurrentRequestContextTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/utils/CleanCurrentRequestContextTest.java @@ -16,7 +16,7 @@ * Makes sure that only one test class derived from this class is using * RequestContext.getCurrentContext at a time. */ -public class CleanCurrentRequestContextTest extends CurrentRequestContextTest { +public abstract class CleanCurrentRequestContextTest extends CurrentRequestContextTest { @Before public void setup() { this.lockAndClearRequestContext(); diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/utils/CurrentRequestContextTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/utils/CurrentRequestContextTest.java index 4a66187511..b3a4b7e57f 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/utils/CurrentRequestContextTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/utils/CurrentRequestContextTest.java @@ -19,7 +19,7 @@ * Provides functions to lock, clear, and unlock CurrentRequestContext in a test * so there are no race conditions in parallel test execution. */ -public class CurrentRequestContextTest { +public abstract class CurrentRequestContextTest { private final static ReentrantLock currentRequestContext = new ReentrantLock(); protected RequestContext ctx; diff --git a/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/AuthenticationOnDeploymentTest.java b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/AuthenticationOnDeploymentTest.java index 51311e6aa7..495bae587f 100644 --- a/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/AuthenticationOnDeploymentTest.java +++ b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/AuthenticationOnDeploymentTest.java @@ -115,7 +115,7 @@ public void testMultipleAuthenticationSchemes() throws Exception { // stop first service without authentication service1 .unregister() - .waitForGatewayUnregistration(2, TIMEOUT) + .waitForGatewayUnregistering(2, TIMEOUT) .stop(); // check second service, all called second one with passTicket, same url like service1 (removed) diff --git a/integration-tests/src/test/java/com/ca/mfaas/util/service/VirtualService.java b/integration-tests/src/test/java/com/ca/mfaas/util/service/VirtualService.java index 77d441a0b9..468551df49 100644 --- a/integration-tests/src/test/java/com/ca/mfaas/util/service/VirtualService.java +++ b/integration-tests/src/test/java/com/ca/mfaas/util/service/VirtualService.java @@ -26,7 +26,6 @@ import org.springframework.http.MediaType; import javax.servlet.Servlet; -import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -37,28 +36,30 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static com.ca.mfaas.constants.EurekaMetadataDefinition.*; import static io.restassured.RestAssured.given; import static org.apache.http.HttpStatus.SC_NO_CONTENT; import static org.apache.http.HttpStatus.SC_OK; +import static org.awaitility.Awaitility.await; import static org.junit.Assert.*; /** * This class simulate a service. You can create new instance dynamically in the test. It will register into discovery - * service and then you can call methods on it. This service also support heartBean, health check and unregistration. - * - * It is recommended to use try-with-resource to be sure, service will be unregistred on the end, ie.: - * + * service and then you can call methods on it. This service also support heartBean, health check and unregistering. + *

+ * It is recommended to use try-with-resource to be sure, service will be unregistered on the end, ie.: + *

* try (final VirtualService service = new VirtualService("testService")) { * service * // add same servlet and setting of service * .start() - * // you should wait until service is registred, usually registration is faster, but for sure - registration + * // you should wait until service is registered, usually registration is faster, but for sure - registration * // contains many asynchronous steps * .waitForGatewayRegistration(1, TIMEOUT); - * // use serice + * // use service * } * * If you want to unregister service during the test, you can do that like this: @@ -66,22 +67,22 @@ * service1 * .unregister() * // similar to registration, unregister contains same asynchronous steps - * .waitForGatewayUnregistration(1, TIMEOUT) + * .waitForGatewayUnregistering(1, TIMEOUT) * .stop(); * * VirtualService allow to you add custom servlets for checking, but few are implemented yet: * - HeaderServlet * - register with addGetHeaderServlet({header}) * - it will response content of header with name {header} on /header/{header} - * - VerifyServlet + * - VerifyServlet * - register with addVerifyServlet * - it allows to store all request on path /verify/* and then make asserts on them * - see also method getGatewayVerifyUrls - * - InstanceServlet + * - InstanceServlet * - automatically created * - return instanceId in the body at /application/instance - * - it is used for checking of gateways (see waitForGatewayRegistration and waitForGatewayUnregistration) - * - HealthServlet + * - it is used for checking of gateways (see waitForGatewayRegistration and waitForGatewayUnregistering) + * - HealthServlet * - automatically created * - to check state of service from discovery service (see /application/health) */ @@ -91,28 +92,29 @@ public class VirtualService implements AutoCloseable { private final String serviceId; private String instanceId; - private boolean registred, started; + private boolean registered, started; private Tomcat tomcat; private Context context; private Connector httpConnector; private VirtualService.HealthService healthService; - private int renewalIntervalInSecs = 10; + private final int renewalIntervalInSecs = 10; - private Map metadata = new HashMap(); + private final Map metadata = new HashMap<>(); private String gatewayPath; - public VirtualService(String serviceId) throws LifecycleException { + public VirtualService(String serviceId) { this.serviceId = serviceId; createTomcat(); } /** * To start tomcat and register service + * * @return this instance to next command - * @throws IOException problem with socket + * @throws IOException problem with socket * @throws LifecycleException Tomcat exception */ public VirtualService start() throws IOException, LifecycleException { @@ -132,11 +134,11 @@ public VirtualService start() throws IOException, LifecycleException { /** * @return state of registration to discovery service */ - public boolean isRegistred() { - return registred; + public boolean isRegistered() { + return registered; } - private void createTomcat() throws LifecycleException { + private void createTomcat() { httpConnector = new Connector(); httpConnector.setPort(0); httpConnector.setScheme("http"); @@ -160,6 +162,7 @@ private String getContextPath() { /** * On begin of initialization is generated from serviceId, hostname and port. + * * @return instance if of this client */ public String getInstanceId() { @@ -168,7 +171,8 @@ public String getInstanceId() { /** * Add custom servlet as part of test to simulate a service method - * @param name name of servlet + * + * @param name name of servlet * @param pattern url to listen * @param servlet instance of servlet * @return this instance to next command @@ -182,6 +186,7 @@ public VirtualService addServlet(String name, String pattern, Servlet servlet) { /** * Register servlet to echo header value with name headerName + * * @param headerName which header value should be in echo * @return this instance to next command */ @@ -192,8 +197,9 @@ public VirtualService addGetHeaderServlet(String headerName) { } /** - * Register verify servlet which remember request and its data on url /verify/* to be analyzed then. For this pursose + * Register verify servlet which remember request and its data on url /verify/* to be analyzed then. For this purpose * is using {@link RequestVerifier} + * * @return this instance to next command */ public VirtualService addVerifyServlet() { @@ -203,15 +209,15 @@ public VirtualService addVerifyServlet() { } /** - * Method wait for gateways to be this service registred. It will make a check via InstanceServlet. It the response + * Method wait for gateways to be this service registered. It will make a check via InstanceServlet. It the response * is correct (this service will answer) it ends, otherwise it make next checking until that. Whole method has * timeout to stop if something fails. - * + *

* The check means make serviceCount calls. There is a preposition that load balancer is based on cyclic queue and * if it will call multiple times (same to count of instances of same service), it should call all instances. * * @param instanceCount Assumed count of instances of the same service at the moment - * @param timeoutSec Timeout in secs to break waiting + * @param timeoutSec Timeout in secs to break waiting */ public VirtualService waitForGatewayRegistration(int instanceCount, int timeoutSec) { final long time0 = System.currentTimeMillis(); @@ -230,7 +236,7 @@ public VirtualService waitForGatewayRegistration(int instanceCount, int timeoutS assertEquals(instanceId, responseBody.print()); break; } catch (RuntimeException | AssertionError e) { - testCounter ++; + testCounter++; // less calls than instance counts, continue without waiting if (testCounter < instanceCount) continue; @@ -239,12 +245,8 @@ public VirtualService waitForGatewayRegistration(int instanceCount, int timeoutS if (System.currentTimeMillis() - time0 > timeoutSec * 1000) throw e; - try { - Thread.sleep(1000); - slept = true; - } catch (InterruptedException ex) { - throw new RuntimeException("Error durring waiting for gateways to go up, slept for " + (System.currentTimeMillis() - time0) / 1000 + "s", ex); - } + await().timeout(1, TimeUnit.SECONDS); + slept = true; } } } @@ -258,12 +260,12 @@ public VirtualService waitForGatewayRegistration(int instanceCount, int timeoutS /** * Method will wait until all gateways will unregister this instance. It will make few calls (instanceCountBefore) - * to check if this service will answer. If not it ends immediatelly, otherwise it will wait for a while. + * to check if this service will answer. If not it ends immediately, otherwise it will wait for a while. * - * @param instanceCountBefore Count of instances with same serviceId before unregistration - * @param timeoutSec timeout in sec to checking + * @param instanceCountBefore Count of instances with same serviceId before unregistering + * @param timeoutSec timeout in sec to checking */ - public VirtualService waitForGatewayUnregistration(int instanceCountBefore, int timeoutSec) { + public VirtualService waitForGatewayUnregistering(int instanceCountBefore, int timeoutSec) { final long time0 = System.currentTimeMillis(); boolean slept = false; for (String gatewayUrl : getGatewayUrls()) { @@ -282,12 +284,8 @@ public VirtualService waitForGatewayUnregistration(int instanceCountBefore, int } catch (RuntimeException | AssertionError e) { if (System.currentTimeMillis() - time0 > timeoutSec * 1000) throw e; - try { - Thread.sleep(1000); - slept = true; - } catch (InterruptedException ex) { - throw new RuntimeException("Error durring waiting for gateways to go down, slept for " + (System.currentTimeMillis() - time0) / 1000 + "s", ex); - } + await().timeout(1, TimeUnit.SECONDS); + slept = true; } } } @@ -301,6 +299,7 @@ public VirtualService waitForGatewayUnregistration(int instanceCountBefore, int /** * Unregister service from discovery service and stop tomcat + * * @throws LifecycleException Tomcat problem */ public void stop() throws LifecycleException { @@ -313,7 +312,8 @@ public void stop() throws LifecycleException { /** * Stop virtual service - to easy use with try-with-resources - * @throws Exception + * + * @throws Exception when any issue */ @Override public void close() throws Exception { @@ -360,7 +360,7 @@ private void register() throws UnknownHostException { ) .put("healthCheckUrl", getUrl() + "/application/health") .put("dataCenterInfo", new JSONObject() - .put("@class", "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo") + .put("@class", "com.netflix.appinfo.") .put("name", "MyOwn") ) .put("leaseInfo", new JSONObject() @@ -373,17 +373,18 @@ private void register() throws UnknownHostException { .post(DiscoveryUtils.getDiscoveryUrl() + "/eureka/apps/{appId}", serviceId) .then().statusCode(SC_NO_CONTENT); - registred = true; + registered = true; } /** - * Explicitly unregistration, if you need test state after service is down + * Explicitly unregistering, if you need test state after service is down + * * @return this instance to next command */ public VirtualService unregister() { - if (!registred) return this; + if (!registered) return this; - registred = false; + registered = false; given().when() .delete(DiscoveryUtils.getDiscoveryUrl() + "/eureka/apps/{appId}/{instanceId}", serviceId, instanceId) @@ -395,13 +396,14 @@ public VirtualService unregister() { /** * To adding metadata of instance. They are usually added before start (send with registration), but method support * also sending after that, during service is up. - * @param key metadata's key - * @param value metadata's value, empty string has same meaning as unregistry + * + * @param key metadata's key + * @param value metadata's value, empty string will unregister the service * @return this instance to next command */ public VirtualService addMetadata(String key, String value) { metadata.put(key, value); - if (registred) { + if (registered) { given().when() .param(key, value) .put(DiscoveryUtils.getDiscoveryUrl() + "/eureka/apps/{appId}/{instanceId}/metadata", serviceId, instanceId) @@ -413,7 +415,8 @@ public VirtualService addMetadata(String key, String value) { /** * To remove metadata. Support also sending during service is up (after registration) - * @param key + * + * @param key of metadata which should be removed */ public void removeMetadata(String key) { addMetadata(key, ""); @@ -422,6 +425,7 @@ public void removeMetadata(String key) { /** * Method to easy set metadata about authentication + * * @param authentication authentication of this service (gateway will send right scheme) * @return this instance to next command */ @@ -444,7 +448,8 @@ public VirtualService setAuthentication(Authentication authentication) { /** * If you need add special routing rule to discovery service, catalog and especially gateway service. If no route is * added, default one is creating on starting. - * @param gatewayUrl url on gateway side + * + * @param gatewayUrl url on gateway side * @param serviceRelativeUrl url part on this service * @return this instance to next command */ @@ -465,7 +470,8 @@ public VirtualService addRoute(String gatewayUrl, String serviceRelativeUrl) { /** * Add default routing, not necessary to call, it is part of initialization, you can use it just to be clear that * there is no other - * @return + * + * @return the service */ public VirtualService addDefaultRoute() { addRoute("api/v1", "/"); @@ -474,7 +480,7 @@ public VirtualService addDefaultRoute() { } private void addDefaultRouteIfMissing() { - if (!metadata.keySet().stream().filter(x -> x.startsWith(ROUTES + ".")).findAny().isPresent()) { + if (metadata.keySet().stream().noneMatch(x -> x.startsWith(ROUTES + "."))) { addDefaultRoute(); } } @@ -509,12 +515,12 @@ public List getGatewayVerifyUrls() { } @AllArgsConstructor - class HeaderServlet extends HttpServlet { + static class HeaderServlet extends HttpServlet { private final String headerName; @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setStatus(HttpStatus.SC_OK); resp.getWriter().print(req.getHeader(headerName)); resp.getWriter().close(); @@ -552,23 +558,18 @@ private void sendHeartBeat() { @Override public void run() { - try { - long lastCall = System.currentTimeMillis(); - sendHeartBeat(); - while (up) { - Thread.sleep(100); - if (instanceId == null) continue; - - if (lastCall + 1000 * heartbeatInterval < System.currentTimeMillis()) { - lastCall = System.currentTimeMillis(); - sendHeartBeat(); - } + long lastCall = System.currentTimeMillis(); + sendHeartBeat(); + while (up) { + await().timeout(100, TimeUnit.MILLISECONDS); + if (instanceId == null) continue; + + if (lastCall + 1000 * heartbeatInterval < System.currentTimeMillis()) { + lastCall = System.currentTimeMillis(); + sendHeartBeat(); } - } catch (InterruptedException e) { - // never happened } } - } /** @@ -579,10 +580,10 @@ public void run() { class HealthServlet extends HttpServlet { @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setStatus(HttpStatus.SC_OK); resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); - final Status status = healthService == null ? Status.UNKNOWN : healthService.getStatus(); + final Status status = healthService == null ? Status.UNKNOWN : healthService.getStatus(); resp.getWriter().print("{\"status\":\"" + status + "\"}"); resp.getWriter().close(); } @@ -597,7 +598,7 @@ class VerifyServlet extends HttpServlet { private RequestVerifier verify; @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { verify.add(VirtualService.this, req); resp.setStatus(HttpStatus.SC_OK); } @@ -606,12 +607,12 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se /** * Servlet answer on /application/instance instanceId. This is base part of method to verify registration on - * gateways, see {@link #waitForGatewayRegistration(int, int)} and {@link #waitForGatewayUnregistration(int, int)} + * gateways, see {@link #waitForGatewayRegistration(int, int)} and {@link #waitForGatewayUnregistering(int, int)} */ class InstanceServlet extends HttpServlet { @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setStatus(HttpStatus.SC_OK); resp.getWriter().print(VirtualService.this.instanceId); resp.getWriter().close(); From b0f29d80b340bbb06ab4014f43d2bd47d81782be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Thu, 23 Jan 2020 17:00:30 +0100 Subject: [PATCH 097/122] improvement of exception handling in ApimlInstanceRegistry to don't cover original RuntimeExceptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../mfaas/discovery/ApimlInstanceRegistry.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/ApimlInstanceRegistry.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/ApimlInstanceRegistry.java index ddd704c9e0..0e902f76b7 100644 --- a/discovery-service/src/main/java/com/ca/mfaas/discovery/ApimlInstanceRegistry.java +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/ApimlInstanceRegistry.java @@ -22,6 +22,7 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; +import java.lang.invoke.WrongMethodTypeException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -126,6 +127,10 @@ private void init() { protected int resolveInstanceLeaseDurationRewritten(final InstanceInfo info) { try { return (int) handlerResolveInstanceLeaseDurationMethod.invokeWithArguments(this, info); + } catch (ClassCastException | WrongMethodTypeException e) { + throw new IllegalArgumentException(EXCEPTION_MESSAGE, e); + } catch (RuntimeException re) { + throw re; } catch (Throwable t) { throw new IllegalArgumentException(EXCEPTION_MESSAGE, t); } @@ -136,6 +141,10 @@ public void register(InstanceInfo info, int leaseDuration, boolean isReplication try { register3ArgsMethodHandle.invokeWithArguments(this, info, leaseDuration, isReplication); handleRegistrationMethod.invokeWithArguments(this, info, leaseDuration, isReplication); + } catch (ClassCastException | WrongMethodTypeException e) { + throw new IllegalArgumentException(EXCEPTION_MESSAGE, e); + } catch (RuntimeException re) { + throw re; } catch (Throwable t) { throw new IllegalArgumentException(EXCEPTION_MESSAGE, t); } @@ -146,6 +155,10 @@ public void register(final InstanceInfo info, final boolean isReplication) { try { register2ArgsMethodHandle.invokeWithArguments(this, info, isReplication); handleRegistrationMethod.invokeWithArguments(this, info, resolveInstanceLeaseDurationRewritten(info), isReplication); + } catch (ClassCastException | WrongMethodTypeException e) { + throw new IllegalArgumentException(EXCEPTION_MESSAGE, e); + } catch (RuntimeException re) { + throw re; } catch (Throwable t) { throw new IllegalArgumentException(EXCEPTION_MESSAGE, t); } @@ -157,6 +170,10 @@ public boolean cancel(String appName, String serverId, boolean isReplication) { final boolean out = (boolean) cancelMethodHandle.invokeWithArguments(this, appName, serverId, isReplication); handleCancelationMethod.invokeWithArguments(this, appName, serverId, isReplication); return out; + } catch (ClassCastException | WrongMethodTypeException e) { + throw new IllegalArgumentException(EXCEPTION_MESSAGE, e); + } catch (RuntimeException re) { + throw re; } catch (Throwable t) { throw new IllegalArgumentException(EXCEPTION_MESSAGE, t); } From 30a3a8e1a83a922bdbca89de2293a757f05c6298 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Thu, 23 Jan 2020 13:21:43 -0600 Subject: [PATCH 098/122] Review usage of hardcoded class name Signed-off-by: Petr Plavjanik --- .../com/ca/mfaas/gateway/discovery/ApimlDiscoveryClient.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/discovery/ApimlDiscoveryClient.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/discovery/ApimlDiscoveryClient.java index 43e50d6abe..5b06f426df 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/discovery/ApimlDiscoveryClient.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/discovery/ApimlDiscoveryClient.java @@ -50,10 +50,9 @@ public void init() { Field schedulerField = DiscoveryClient.class.getDeclaredField("scheduler"); schedulerField.setAccessible(true); scheduler = (ScheduledExecutorService) schedulerField.get(this); - // find class with process to fetch from discovery server and construct instance for call to fetch Optional> cacheRefreshClass = Arrays.stream(DiscoveryClient.class.getDeclaredClasses()) - .filter(x -> "CacheRefreshThread".equals(x.getSimpleName())).findFirst(); + .filter(x -> "CacheRefreshThread".equals(x.getSimpleName())).findFirst(); // NOSONAR: This class cannot be imported for usage with instanceof if (!cacheRefreshClass.isPresent()) throw new NoSuchMethodException(); Constructor cacheRefreshConstructor = cacheRefreshClass.get().getDeclaredConstructor(DiscoveryClient.class); cacheRefreshConstructor.setAccessible(true); From 7aacb2fc46404a0ed567a678d88c1beb3dd5a253 Mon Sep 17 00:00:00 2001 From: Petr Plavjanik Date: Thu, 23 Jan 2020 13:29:02 -0600 Subject: [PATCH 099/122] Add common messages to DC Signed-off-by: Petr Plavjanik --- .../ca/mfaas/client/configuration/ApplicationConfiguration.java | 1 + 1 file changed, 1 insertion(+) diff --git a/discoverable-client/src/main/java/com/ca/mfaas/client/configuration/ApplicationConfiguration.java b/discoverable-client/src/main/java/com/ca/mfaas/client/configuration/ApplicationConfiguration.java index d930023246..0b7ed09ca1 100644 --- a/discoverable-client/src/main/java/com/ca/mfaas/client/configuration/ApplicationConfiguration.java +++ b/discoverable-client/src/main/java/com/ca/mfaas/client/configuration/ApplicationConfiguration.java @@ -19,6 +19,7 @@ public class ApplicationConfiguration { @Bean public MessageService messageService() { MessageService messageService = YamlMessageServiceInstance.getInstance(); + messageService.loadMessages("/common-log-messages.yml"); messageService.loadMessages("/api-messages.yml"); messageService.loadMessages("/log-messages.yml"); return messageService; From 4324721ea724b7fd6d0a1dbc8403a9345e2506a5 Mon Sep 17 00:00:00 2001 From: JirkaAichler Date: Fri, 24 Jan 2020 10:51:38 +0100 Subject: [PATCH 100/122] Add authentication scheme for zosmf template Signed-off-by: JirkaAichler --- config/local/api-defs/zosmf-sample.yml_ | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/local/api-defs/zosmf-sample.yml_ b/config/local/api-defs/zosmf-sample.yml_ index 79b7b76a44..d1953b23ec 100644 --- a/config/local/api-defs/zosmf-sample.yml_ +++ b/config/local/api-defs/zosmf-sample.yml_ @@ -20,6 +20,8 @@ services: catalogUiTileId: zosmf # ID of the API Catalog UI tile for z/OSMF services instanceBaseUrls: # list of base URLs for each instance - # scheme://hostname:port/contextPath + authentication: + scheme: zosmf # This service expects z/OSMF LTPA token homePageRelativeUrl: # Home page of the z/OSMF service routes: - gatewayUrl: api # [api/ui/ws]/v{majorVersion} From e41c53c6e6936352b447ff73af744132a915c34a Mon Sep 17 00:00:00 2001 From: JirkaAichler Date: Fri, 24 Jan 2020 10:58:25 +0100 Subject: [PATCH 101/122] Fix Sonar issue Signed-off-by: JirkaAichler --- .../com/ca/mfaas/gateway/controllers/AuthController.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/AuthController.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/AuthController.java index 20515193cc..8bb41ee8df 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/AuthController.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/controllers/AuthController.java @@ -34,11 +34,11 @@ public class AuthController { @DeleteMapping(path = "/invalidate/**") public void invalidateJwtToken(HttpServletRequest request, HttpServletResponse response) { - final String path = "/auth/invalidate/"; + final String endpoint = "/auth/invalidate/"; final String uri = request.getRequestURI(); - final int index = uri.indexOf(path); + final int index = uri.indexOf(endpoint); - final String jwtToken = uri.substring(index + path.length()); + final String jwtToken = uri.substring(index + endpoint.length()); final boolean invalidated = authenticationService.invalidateJwtToken(jwtToken, false); response.setStatus(invalidated ? SC_OK : SC_SERVICE_UNAVAILABLE); From 0a6ba3d7b381b13e71a0233ad15e54e41c0240a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Fri, 24 Jan 2020 12:17:18 +0100 Subject: [PATCH 102/122] fix VirtualService (test AuthenticationOnDeploymentTest), mistake on previous commit to fix Sonar issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../src/test/java/com/ca/mfaas/util/service/VirtualService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-tests/src/test/java/com/ca/mfaas/util/service/VirtualService.java b/integration-tests/src/test/java/com/ca/mfaas/util/service/VirtualService.java index 468551df49..ebc5914c68 100644 --- a/integration-tests/src/test/java/com/ca/mfaas/util/service/VirtualService.java +++ b/integration-tests/src/test/java/com/ca/mfaas/util/service/VirtualService.java @@ -360,7 +360,7 @@ private void register() throws UnknownHostException { ) .put("healthCheckUrl", getUrl() + "/application/health") .put("dataCenterInfo", new JSONObject() - .put("@class", "com.netflix.appinfo.") + .put("@class", "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo") .put("name", "MyOwn") ) .put("leaseInfo", new JSONObject() From eec3363ed5d6212ca439649b9959344873b1863f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Wed, 29 Jan 2020 08:56:14 +0100 Subject: [PATCH 103/122] implementation of using JWT token from z/OSMF (it supports two modes: Zowe token with LTPA, z/OSMF JWT token, supports multiple endpoints of z/OSMF: /zosmf/info and /zosmf/services/authenticate) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../security/common/token/QueryResponse.java | 19 ++ .../common/token/TokenAuthentication.java | 10 + .../common/token/QueryResponseTest.java | 15 +- .../common/token/TokenAuthenticationTest.java | 28 ++ .../EurekaInstanceRegisteredListener.java | 2 +- .../metadata/MetadataTranslationService.java | 14 +- .../EurekaInstanceRegisteredListenerTest.java | 15 +- .../MetadataTranslationServiceTest.java | 54 +++- .../gateway/filters/pre/ZosmfFilter.java | 81 ----- .../gateway/routing/ApimlRoutingConfig.java | 7 - .../zosmf/ZosmfAuthenticationProvider.java | 163 ++-------- .../service/AuthenticationService.java | 172 +++++++++-- .../security/service/ZosmfService.java | 84 ++++++ .../security/service/schema/ZosmfScheme.java | 28 +- .../service/zosmf/AbstractZosmfService.java | 165 ++++++++++ .../service/zosmf/ZosmfServiceFacade.java | 146 +++++++++ .../service/zosmf/ZosmfServiceV1.java | 92 ++++++ .../service/zosmf/ZosmfServiceV2.java | 115 +++++++ .../src/main/resources/ehcache.xml | 2 + .../gateway/filters/pre/ZosmfFilterTest.java | 118 -------- .../ZosmfAuthenticationProviderTest.java | 159 ++++++---- .../query/SuccessfulQueryHandlerTest.java | 11 +- .../service/AuthenticationServiceTest.java | 252 ++++++++++++++-- .../ServiceAuthenticationServiceImplTest.java | 6 +- .../AuthenticationSchemeFactoryTest.java | 2 +- .../schema/HttpBasicPassTicketSchemeTest.java | 10 +- .../service/schema/ZosmfSchemeTest.java | 30 +- .../service/zosmf/ZosmfServiceFacadeTest.java | 230 ++++++++++++++ .../service/zosmf/ZosmfServiceV1Test.java | 209 +++++++++++++ .../service/zosmf/ZosmfServiceV2Test.java | 285 ++++++++++++++++++ .../service/GatewaySecurityServiceTest.java | 2 +- .../token/GatewayTokenProviderTest.java | 2 +- 32 files changed, 2040 insertions(+), 488 deletions(-) create mode 100644 apiml-security-common/src/test/java/com/ca/apiml/security/common/token/TokenAuthenticationTest.java delete mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ZosmfFilter.java create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ZosmfService.java create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/AbstractZosmfService.java create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceFacade.java create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV1.java create mode 100644 gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV2.java delete mode 100644 gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ZosmfFilterTest.java create mode 100644 gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceFacadeTest.java create mode 100644 gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV1Test.java create mode 100644 gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV2Test.java diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/token/QueryResponse.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/token/QueryResponse.java index 3bce8aaaf0..2cd676f14c 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/token/QueryResponse.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/token/QueryResponse.java @@ -13,6 +13,7 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.apache.commons.lang.StringUtils; import java.util.Date; @@ -28,10 +29,28 @@ public class QueryResponse implements EntryExpiration { private String userId; private Date creation; private Date expiration; + private Source source; @Override public boolean isExpired() { return expiration.before(new Date()); } + public enum Source { + + ZOWE, + ZOSMF + + ; + + public static Source valueByIssuer(String issuer) { + if (StringUtils.equalsIgnoreCase(issuer, "zOSMF")) { + return ZOSMF; + } + + return ZOWE; + } + + } + } diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/token/TokenAuthentication.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/token/TokenAuthentication.java index 2899765623..dda043b27d 100644 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/token/TokenAuthentication.java +++ b/apiml-security-common/src/main/java/com/ca/apiml/security/common/token/TokenAuthentication.java @@ -20,6 +20,9 @@ */ @EqualsAndHashCode(callSuper = false) public class TokenAuthentication extends AbstractAuthenticationToken { + + private static final long serialVersionUID = 9187160928171618141L; + private final String username; private final String token; @@ -50,4 +53,11 @@ public String getCredentials() { public String getPrincipal() { return username; } + + public static TokenAuthentication createAuthenticated(String username, String token) { + final TokenAuthentication out = new TokenAuthentication(username, token); + out.setAuthenticated(true); + return out; + } + } diff --git a/apiml-security-common/src/test/java/com/ca/apiml/security/common/token/QueryResponseTest.java b/apiml-security-common/src/test/java/com/ca/apiml/security/common/token/QueryResponseTest.java index 02ae7ee38e..df7b58b776 100644 --- a/apiml-security-common/src/test/java/com/ca/apiml/security/common/token/QueryResponseTest.java +++ b/apiml-security-common/src/test/java/com/ca/apiml/security/common/token/QueryResponseTest.java @@ -13,8 +13,7 @@ import java.util.Calendar; import java.util.Date; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; public class QueryResponseTest { @@ -27,8 +26,16 @@ public void testIsExpired() { c.add(Calendar.MINUTE, 2); final Date after = c.getTime(); - assertTrue(new QueryResponse("domain", "user", now, before).isExpired()); - assertFalse(new QueryResponse("domain", "user", now, after).isExpired()); + assertTrue(new QueryResponse("domain", "user", now, before, QueryResponse.Source.ZOWE).isExpired()); + assertFalse(new QueryResponse("domain", "user", now, after, QueryResponse.Source.ZOWE).isExpired()); + } + + @Test + public void testSource() { + assertEquals(QueryResponse.Source.ZOSMF, QueryResponse.Source.valueByIssuer("zosmf")); + assertEquals(QueryResponse.Source.ZOSMF, QueryResponse.Source.valueByIssuer("zOSMF")); + assertEquals(QueryResponse.Source.ZOWE, QueryResponse.Source.valueByIssuer("zosmfX")); + assertEquals(QueryResponse.Source.ZOWE, QueryResponse.Source.valueByIssuer(null)); } } diff --git a/apiml-security-common/src/test/java/com/ca/apiml/security/common/token/TokenAuthenticationTest.java b/apiml-security-common/src/test/java/com/ca/apiml/security/common/token/TokenAuthenticationTest.java new file mode 100644 index 0000000000..782fd1e09a --- /dev/null +++ b/apiml-security-common/src/test/java/com/ca/apiml/security/common/token/TokenAuthenticationTest.java @@ -0,0 +1,28 @@ +package com.ca.apiml.security.common.token;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +@RunWith(JUnit4.class) +public class TokenAuthenticationTest { + + @Test + public void testCreateAuthenticated() { + TokenAuthentication ta = TokenAuthentication.createAuthenticated("user", "token"); + assertEquals("user", ta.getPrincipal()); + assertEquals("token", ta.getCredentials()); + assertTrue(ta.isAuthenticated()); + } + +} diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/EurekaInstanceRegisteredListener.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/EurekaInstanceRegisteredListener.java index 15fea64ab5..acc8736658 100644 --- a/discovery-service/src/main/java/com/ca/mfaas/discovery/EurekaInstanceRegisteredListener.java +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/EurekaInstanceRegisteredListener.java @@ -40,7 +40,7 @@ public void listen(EurekaInstanceRegisteredEvent event) { final Map metadata = instanceInfo.getMetadata(); final String serviceId = EurekaUtils.getServiceIdFromInstanceId(instanceInfo.getInstanceId()); - metadataTranslationService.translateMetadata(metadata); + metadataTranslationService.translateMetadata(serviceId, metadata); metadataDefaultsService.updateMetadata(serviceId, metadata); // ie. new instance can have different authentication (than other one), this is reason to evict caches on gateway gatewayNotifier.serviceUpdated(serviceId); diff --git a/discovery-service/src/main/java/com/ca/mfaas/discovery/metadata/MetadataTranslationService.java b/discovery-service/src/main/java/com/ca/mfaas/discovery/metadata/MetadataTranslationService.java index 39e23391a5..ffd019e64d 100644 --- a/discovery-service/src/main/java/com/ca/mfaas/discovery/metadata/MetadataTranslationService.java +++ b/discovery-service/src/main/java/com/ca/mfaas/discovery/metadata/MetadataTranslationService.java @@ -9,6 +9,8 @@ */ package com.ca.mfaas.discovery.metadata; +import com.ca.apiml.security.common.auth.AuthenticationScheme; +import org.apache.commons.lang.StringUtils; import org.springframework.stereotype.Service; import java.util.Map; @@ -27,12 +29,14 @@ public class MetadataTranslationService { * * @param metadata to be translated */ - public void translateMetadata(Map metadata) { + public void translateMetadata(String serviceId, Map metadata) { // Version check String version = metadata.get(VERSION); if (version == null) { translateV1toV2(metadata); } + + updateZosmfAuthentication(serviceId, metadata); } private void translateV1toV2(Map metadata) { @@ -86,4 +90,12 @@ private void translateParameter(String oldParameter, String newParameter, Map metadata) { + if (!StringUtils.containsIgnoreCase(serviceId, "zosmf")) return; + if (metadata.containsKey(AUTHENTICATION_SCHEME)) return; + + metadata.put(AUTHENTICATION_SCHEME, AuthenticationScheme.ZOSMF.getScheme()); + } + } diff --git a/discovery-service/src/test/java/com/ca/mfaas/discovery/EurekaInstanceRegisteredListenerTest.java b/discovery-service/src/test/java/com/ca/mfaas/discovery/EurekaInstanceRegisteredListenerTest.java index 0fab46e894..fdd3550b0b 100644 --- a/discovery-service/src/test/java/com/ca/mfaas/discovery/EurekaInstanceRegisteredListenerTest.java +++ b/discovery-service/src/test/java/com/ca/mfaas/discovery/EurekaInstanceRegisteredListenerTest.java @@ -13,22 +13,29 @@ import com.ca.mfaas.discovery.metadata.MetadataTranslationService; import com.netflix.appinfo.InstanceInfo; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; import org.mockito.Mockito; import org.springframework.cloud.netflix.eureka.server.event.EurekaInstanceRegisteredEvent; +import java.util.HashMap; +import java.util.Map; + import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; +@RunWith(JUnit4.class) public class EurekaInstanceRegisteredListenerTest { @Test public void getServiceId() { MetadataTranslationService metadataTranslationService = Mockito.mock(MetadataTranslationService.class); MetadataDefaultsService metadataDefaultsService = Mockito.mock(MetadataDefaultsService.class); + GatewayNotifier gatewayNotifier = mock(GatewayNotifier.class); - EurekaInstanceRegisteredListener eirl = new EurekaInstanceRegisteredListener(metadataTranslationService, metadataDefaultsService, mock(GatewayNotifier.class)); + EurekaInstanceRegisteredListener eirl = new EurekaInstanceRegisteredListener(metadataTranslationService, metadataDefaultsService, gatewayNotifier); doAnswer( x -> { @@ -37,6 +44,8 @@ public void getServiceId() { } ).when(metadataDefaultsService).updateMetadata(anyString(), any()); + final Map metadata = new HashMap<>(); + InstanceInfo instanceInfo = mock(InstanceInfo.class); when(instanceInfo.getInstanceId()).thenReturn("1:serviceName:2"); EurekaInstanceRegisteredEvent event = mock(EurekaInstanceRegisteredEvent.class); @@ -44,7 +53,9 @@ public void getServiceId() { eirl.listen(event); - verify(metadataDefaultsService, times(1)).updateMetadata(anyString(), any()); + verify(metadataTranslationService, times(1)).translateMetadata("serviceName", metadata); + verify(metadataDefaultsService, times(1)).updateMetadata("serviceName", metadata); + verify(gatewayNotifier, times(1)).serviceUpdated("serviceName"); } private EurekaInstanceRegisteredEvent createEvent(String instanceId) { diff --git a/discovery-service/src/test/java/com/ca/mfaas/discovery/metadata/MetadataTranslationServiceTest.java b/discovery-service/src/test/java/com/ca/mfaas/discovery/metadata/MetadataTranslationServiceTest.java index 6a86d0ad14..daa6352ed6 100644 --- a/discovery-service/src/test/java/com/ca/mfaas/discovery/metadata/MetadataTranslationServiceTest.java +++ b/discovery-service/src/test/java/com/ca/mfaas/discovery/metadata/MetadataTranslationServiceTest.java @@ -9,6 +9,7 @@ */ package com.ca.mfaas.discovery.metadata; +import com.ca.apiml.security.common.auth.AuthenticationScheme; import org.junit.Test; import java.util.HashMap; @@ -17,7 +18,8 @@ import static com.ca.mfaas.constants.EurekaMetadataDefinition.*; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.collection.IsMapContaining.hasEntry; -import static org.junit.Assert.assertThat; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; public class MetadataTranslationServiceTest { @@ -28,7 +30,7 @@ public void translateMetadata() { Map metadata = generateMetadataV1(); MetadataTranslationService metadataTranslationService = new MetadataTranslationService(); - metadataTranslationService.translateMetadata(metadata); + metadataTranslationService.translateMetadata("serviceV1", metadata); assertThat(metadata, hasEntry(ROUTES + ".ui__v1." + ROUTES_SERVICE_URL, "/" + INSTANCE_NAME)); assertThat(metadata, hasEntry(ROUTES + ".ui__v1." + ROUTES_GATEWAY_URL, "ui/v1")); @@ -90,4 +92,52 @@ private Map generateMetadataV1() { metadata.put(ENABLE_APIDOC_V1, "true"); return metadata; } + + @Test + public void testSetZosmfAuthentication() { + MetadataTranslationService metadataTranslationService = new MetadataTranslationService(); + Map metadata = new HashMap<>(); + + metadataTranslationService.updateZosmfAuthentication("service", metadata); + assertTrue(metadata.isEmpty()); + + metadataTranslationService.updateZosmfAuthentication("serviceZosmfX", metadata); + assertFalse(metadata.isEmpty()); + assertEquals(1, metadata.size()); + assertEquals(AuthenticationScheme.ZOSMF.getScheme(), metadata.get(AUTHENTICATION_SCHEME)); + + metadata.put(AUTHENTICATION_SCHEME, AuthenticationScheme.BYPASS.getScheme()); + metadataTranslationService.updateZosmfAuthentication("serviceZosmfX", metadata); + assertEquals(AuthenticationScheme.BYPASS.getScheme(), metadata.get(AUTHENTICATION_SCHEME)); + assertEquals(1, metadata.size()); + + metadata.put(AUTHENTICATION_SCHEME, AuthenticationScheme.HTTP_BASIC_PASSTICKET.getScheme()); + metadata.put(AUTHENTICATION_APPLID, "applid"); + metadataTranslationService.updateZosmfAuthentication("serviceZosmfX", metadata); + assertEquals(AuthenticationScheme.HTTP_BASIC_PASSTICKET.getScheme(), metadata.get(AUTHENTICATION_SCHEME)); + assertEquals("applid", metadata.get(AUTHENTICATION_APPLID)); + assertEquals(2, metadata.size()); + } + + @Test + public void testDependentMethods() { + MetadataTranslationService metadataTranslationService = spy(new MetadataTranslationService()); + Map metadata = new HashMap<>(); + + metadata.put(CATALOG_ID_V1, "x"); + metadataTranslationService.translateMetadata("XzosmfX", metadata); + verify(metadataTranslationService, times(1)).updateZosmfAuthentication("XzosmfX", metadata); + assertEquals(AuthenticationScheme.ZOSMF.getScheme(), metadata.get(AUTHENTICATION_SCHEME)); + assertEquals("x", metadata.get(CATALOG_ID)); + + metadata = new HashMap<>(); + metadata.put(CATALOG_ID_V1, "x"); + metadata.put(VERSION, "1"); + metadataTranslationService.translateMetadata("XzosmfX", metadata); + verify(metadataTranslationService, times(1)).updateZosmfAuthentication("XzosmfX", metadata); + assertEquals(AuthenticationScheme.ZOSMF.getScheme(), metadata.get(AUTHENTICATION_SCHEME)); + assertEquals("x", metadata.get(CATALOG_ID_V1)); + assertEquals("1", metadata.get(VERSION)); + } + } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ZosmfFilter.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ZosmfFilter.java deleted file mode 100644 index c17642d608..0000000000 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/filters/pre/ZosmfFilter.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ -package com.ca.mfaas.gateway.filters.pre; - -import com.ca.mfaas.gateway.security.service.AuthenticationService; -import com.netflix.zuul.ZuulFilter; -import com.netflix.zuul.context.RequestContext; -import org.apache.commons.lang.StringUtils; -import org.springframework.beans.factory.annotation.Autowired; - -import java.util.Optional; - -import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_DECORATION_FILTER_ORDER; -import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE; -import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SERVICE_ID_KEY; - -/** - * Extract LTPA token from JWT token and set it as a cookie when accessing z/OSMF - * - * @deprecated TODO: Remove after zowe-install-packaging is updated - */ -@Deprecated -public class ZosmfFilter extends ZuulFilter { - private static final String ZOSMF = "zosmf"; - - private static final String COOKIE_HEADER = "cookie"; - - private final AuthenticationService authenticationService; - - @Autowired - public ZosmfFilter(AuthenticationService tokenService) { - this.authenticationService = tokenService; - } - - @Override - public boolean shouldFilter() { - RequestContext context = RequestContext.getCurrentContext(); - - String serviceId = (String) context.get(SERVICE_ID_KEY); - return StringUtils.containsIgnoreCase(serviceId, ZOSMF); - } - - @Override - public String filterType() { - return PRE_TYPE; - } - - @Override - public int filterOrder() { - return PRE_DECORATION_FILTER_ORDER + 3; - } - - @Override - public Object run() { - RequestContext context = RequestContext.getCurrentContext(); - - Optional jwtToken = authenticationService.getJwtTokenFromRequest(context.getRequest()); - jwtToken.ifPresent(token -> { - String ltpaToken = authenticationService.getLtpaTokenFromJwtToken(token); - - String cookie = context.getZuulRequestHeaders().get(COOKIE_HEADER); - if (cookie != null) { - cookie += "; " + ltpaToken; - } else { - cookie = ltpaToken; - } - - context.addZuulRequestHeader(COOKIE_HEADER, cookie); - }); - - - return null; - } -} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/routing/ApimlRoutingConfig.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/routing/ApimlRoutingConfig.java index 178ea9ce30..c479ba8ce3 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/routing/ApimlRoutingConfig.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/routing/ApimlRoutingConfig.java @@ -15,8 +15,6 @@ import com.ca.mfaas.gateway.filters.pre.LocationFilter; import com.ca.mfaas.gateway.filters.pre.ServiceAuthenticationFilter; import com.ca.mfaas.gateway.filters.pre.SlashFilter; -import com.ca.mfaas.gateway.filters.pre.ZosmfFilter; -import com.ca.mfaas.gateway.security.service.AuthenticationService; import com.ca.mfaas.gateway.ws.WebSocketProxyServerHandler; import com.ca.mfaas.product.gateway.GatewayConfigProperties; import com.ca.mfaas.product.routing.RoutedServicesUser; @@ -44,11 +42,6 @@ public SlashFilter slashFilter() { return new SlashFilter(); } - @Bean - public ZosmfFilter zosmfFilter(AuthenticationService authenticationService) { - return new ZosmfFilter(authenticationService); - } - @Bean public ServiceAuthenticationFilter serviceAuthenticationFilter() { return new ServiceAuthenticationFilter(); diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/login/zosmf/ZosmfAuthenticationProvider.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/login/zosmf/ZosmfAuthenticationProvider.java index 523b9f1bec..a82b709df6 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/login/zosmf/ZosmfAuthenticationProvider.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/login/zosmf/ZosmfAuthenticationProvider.java @@ -9,64 +9,39 @@ */ package com.ca.mfaas.gateway.security.login.zosmf; -import com.ca.apiml.security.common.config.AuthConfigurationProperties; -import com.ca.apiml.security.common.error.ServiceNotAccessibleException; -import com.ca.apiml.security.common.token.TokenAuthentication; import com.ca.mfaas.gateway.security.service.AuthenticationService; +import com.ca.mfaas.gateway.security.service.ZosmfService; import com.ca.mfaas.message.log.ApimlLogger; import com.ca.mfaas.product.logging.annotations.InjectApimlLogger; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import org.springframework.cloud.client.discovery.DiscoveryClient; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; -import org.springframework.web.client.ResourceAccessException; -import org.springframework.web.client.RestClientException; -import org.springframework.web.client.RestTemplate; -import java.io.IOException; -import java.util.Base64; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.function.Supplier; +import static com.ca.mfaas.gateway.security.service.ZosmfService.TokenType.JWT; +import static com.ca.mfaas.gateway.security.service.ZosmfService.TokenType.LTPA; /** * Authentication provider that verifies credentials against z/OSMF service */ @Component public class ZosmfAuthenticationProvider implements AuthenticationProvider { + @InjectApimlLogger private ApimlLogger apimlLog = ApimlLogger.empty(); - private static final String ZOSMF_END_POINT = "zosmf/info"; - private static final String ZOSMF_CSRF_HEADER = "X-CSRF-ZOSMF-HEADER"; - private static final String ZOSMF_DOMAIN = "zosmf_saf_realm"; - - private final AuthConfigurationProperties authConfigurationProperties; private final AuthenticationService authenticationService; - private final DiscoveryClient discovery; - private final ObjectMapper securityObjectMapper; - private final RestTemplate restTemplate; + private final ZosmfService zosmfService; - public ZosmfAuthenticationProvider(AuthConfigurationProperties authConfigurationProperties, - AuthenticationService authenticationService, - DiscoveryClient discovery, - ObjectMapper securityObjectMapper, - RestTemplate restTemplate) { - this.authConfigurationProperties = authConfigurationProperties; - this.discovery = discovery; + public ZosmfAuthenticationProvider( + AuthenticationService authenticationService, + ZosmfService zosmfService, + ObjectMapper securityObjectMapper + ) { this.authenticationService = authenticationService; - this.securityObjectMapper = securityObjectMapper; - this.restTemplate = restTemplate; + this.zosmfService = zosmfService; } /** @@ -77,116 +52,30 @@ public ZosmfAuthenticationProvider(AuthConfigurationProperties authConfiguration */ @Override public Authentication authenticate(Authentication authentication) { - String zosmf = authConfigurationProperties.validatedZosmfServiceId(); - String uri = getURI(zosmf); - - String user = authentication.getPrincipal().toString(); - String password = authentication.getCredentials().toString(); - - String credentials = user + ":" + password; - String authorization = "Basic " + Base64.getEncoder().encodeToString(credentials.getBytes()); - - HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.AUTHORIZATION, authorization); - headers.add(ZOSMF_CSRF_HEADER, ""); - - try { - ResponseEntity response = restTemplate.exchange( - uri + ZOSMF_END_POINT, - HttpMethod.GET, - new HttpEntity<>(null, headers), - String.class); - - List cookies = response.getHeaders().get(HttpHeaders.SET_COOKIE); + final String user = authentication.getPrincipal().toString(); - String ltpaToken = readLtpaToken(cookies); - String domain = readDomain(response.getBody()); - String jwtToken = authenticationService.createJwtToken(user, domain, ltpaToken); + final ZosmfService.AuthenticationResponse ar = zosmfService.authenticate(authentication); - TokenAuthentication tokenAuthentication = new TokenAuthentication(user, jwtToken); - tokenAuthentication.setAuthenticated(true); - - return tokenAuthentication; - } catch (ResourceAccessException e) { - apimlLog.log("apiml.security.serviceUnavailable", uri, e.getMessage()); - throw new ServiceNotAccessibleException("Could not get an access to z/OSMF service."); - } catch (RestClientException e) { - apimlLog.log("apiml.security.generic", e.getMessage(), uri); - throw new AuthenticationServiceException("A failure occurred when authenticating.", e); + // if z/OSMF support JWT, use it as Zowe JWT token + if (ar.getTokens().containsKey(JWT)) { + return authenticationService.createTokenAuthentication(user, ar.getTokens().get(JWT)); } - } - - /** - * Return z/OSMF instance uri - * - * @param zosmf the z/OSMF service id - * @return the uri - */ - private String getURI(String zosmf) { - Supplier authenticationServiceExceptionSupplier = () -> { - apimlLog.log("apiml.security.zosmfInstanceNotFound", zosmf); - return new ServiceNotAccessibleException("z/OSMF instance not found or incorrectly configured."); - }; - - return Optional.ofNullable(discovery.getInstances(zosmf)) - .orElseThrow(authenticationServiceExceptionSupplier) - .stream() - .filter(Objects::nonNull) - .findFirst() - .map(zosmfInstance -> zosmfInstance.getUri().toString()) - .orElseThrow(authenticationServiceExceptionSupplier); - } - - /** - * Read the LTPA token from the cookies - * - * @param cookies the cookies - * @return the LPTA token - * @throws BadCredentialsException if the cookie does not contain valid LTPA token - */ - private String readLtpaToken(List cookies) { - Supplier exceptionSupplier = () -> new BadCredentialsException("Username or password are invalid."); - - return Optional.ofNullable(cookies) - .orElseThrow(exceptionSupplier) - .stream() - .filter(cookie -> cookie != null && cookie.contains("LtpaToken2")) - .map(this::convertCookieToLtpaToken) - .findFirst() - .orElseThrow(exceptionSupplier); - } - private String convertCookieToLtpaToken(String content) { - int end = content.indexOf(';'); - return (end > 0) ? content.substring(0, end) : content; - } + if (ar.getTokens().containsKey(LTPA)) { + // construct own JWT token, including LTPA from z/OSMF + final String domain = ar.getDomain(); + final String jwtToken = authenticationService.createJwtToken(user, domain, ar.getTokens().get(LTPA)); - /** - * Read the z/OSMF domain from the content in the response - * - * @param content the response body - * @return the z/OSMF domain - * @throws AuthenticationServiceException if the zosmf domain cannot be read - */ - private String readDomain(String content) { - try { - ObjectNode zosmfNode = securityObjectMapper.readValue(content, ObjectNode.class); - - return Optional.ofNullable(zosmfNode) - .filter(zn -> zn.has(ZOSMF_DOMAIN)) - .map(zn -> zn.get(ZOSMF_DOMAIN).asText()) - .orElseThrow(() -> { - apimlLog.log("apiml.security.zosmfDomainIsEmpty", ZOSMF_DOMAIN); - return new AuthenticationServiceException("z/OSMF domain cannot be read."); - }); - } catch (IOException e) { - apimlLog.log("apiml.security.errorParsingZosmfResponse", e.getMessage()); - throw new AuthenticationServiceException("z/OSMF domain cannot be read."); + return authenticationService.createTokenAuthentication(user, jwtToken); } + + // JWT and LTPA tokens are missing, authentication was wrong + throw new BadCredentialsException("Username or password are invalid."); } @Override public boolean supports(Class auth) { - return auth.equals(UsernamePasswordAuthenticationToken.class); + return auth == UsernamePasswordAuthenticationToken.class; } + } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java index cec49a346e..d400db1d54 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/AuthenticationService.java @@ -27,6 +27,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.EnableAspectJAutoProxy; @@ -39,7 +40,13 @@ import javax.annotation.PostConstruct; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; -import java.util.*; +import java.util.Arrays; +import java.util.Date; +import java.util.Optional; +import java.util.UUID; + +import static com.ca.mfaas.gateway.security.service.ZosmfService.TokenType.JWT; +import static com.ca.mfaas.gateway.security.service.ZosmfService.TokenType.LTPA; /** * Service for the JWT and LTPA tokens operations @@ -50,12 +57,14 @@ @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) @EnableAspectJAutoProxy(proxyTargetClass = true) public class AuthenticationService { + private static final String LTPA_CLAIM_NAME = "ltpa"; private static final String DOMAIN_CLAIM_NAME = "dom"; private final ApplicationContext applicationContext; private final AuthConfigurationProperties authConfigurationProperties; private final JwtSecurityInitializer jwtSecurityInitializer; + private final ZosmfService zosmfService; private final EurekaClient discoveryClient; private final RestTemplate restTemplate; @@ -121,6 +130,18 @@ public Boolean invalidateJwtToken(String jwtToken, boolean distribute) { } } + // invalidate token in z/OSMF + final QueryResponse queryResponse = parseJwtToken(jwtToken); + switch (queryResponse.getSource()) { + case ZOWE: + final String ltpaToken = getLtpaToken(jwtToken); + if (ltpaToken != null) zosmfService.invalidate(LTPA, ltpaToken); + break; + case ZOSMF: + zosmfService.invalidate(JWT, jwtToken); + break; + } + return Boolean.TRUE; } @@ -129,14 +150,68 @@ public Boolean isInvalidated(String jwtToken) { return Boolean.FALSE; } + protected RuntimeException handleJwtParserException(RuntimeException exception) { + if (exception instanceof ExpiredJwtException) { + final ExpiredJwtException expiredJwtException = (ExpiredJwtException) exception; + log.debug("Token with id '{}' for user '{}' is expired.", expiredJwtException.getClaims().getId(), expiredJwtException.getClaims().getSubject()); + return new TokenExpireException("Token is expired."); + } + if (exception instanceof JwtException) { + log.debug("Token is not valid due to: {}.", exception.getMessage()); + return new TokenNotValidException("Token is not valid."); + } + + log.debug("Token is not valid due to: {}.", exception.getMessage()); + return new TokenNotValidException("An internal error occurred while validating the token therefor the token is no longer valid."); + } + + private Claims validateAndParseLocalJwtToken(String jwtToken) { + try { + return Jwts.parser() + .setSigningKey(jwtSecurityInitializer.getJwtPublicKey()) + .parseClaimsJws(jwtToken) + .getBody(); + } catch (RuntimeException exception) { + throw handleJwtParserException(exception); + } + } + @Cacheable(value = "validationJwtToken", key = "#jwtToken", condition = "#jwtToken != null") public TokenAuthentication validateJwtToken(String jwtToken) { - TokenAuthentication validTokenAuthentication = new TokenAuthentication(getClaims(jwtToken).getSubject(), jwtToken); + QueryResponse queryResponse = parseJwtToken(jwtToken); + + switch (queryResponse.getSource()) { + case ZOWE: + validateAndParseLocalJwtToken(jwtToken); + break; + case ZOSMF: + zosmfService.validate(JWT, jwtToken); + break; + } + + TokenAuthentication tokenAuthentication = new TokenAuthentication(queryResponse.getUserId(), jwtToken); // without a proxy cache aspect is not working, thus it is necessary get bean from application context final boolean authenticated = !meAsProxy.isInvalidated(jwtToken); - validTokenAuthentication.setAuthenticated(authenticated); + tokenAuthentication.setAuthenticated(authenticated); - return validTokenAuthentication; + return tokenAuthentication; + } + + /** + * Method construct {@link TokenAuthentication} marked as valid. It also store JWT token on the cache to + * speed up next call to validate token. + * + * @param user + * @param jwtToken + * @return {@link TokenAuthentication}, as authenticated use information about invalidating of token + */ + @CachePut(value = "validationJwtToken", key = "#jwtToken", condition = "#jwtToken != null") + public TokenAuthentication createTokenAuthentication(String user, String jwtToken) { + final TokenAuthentication out = new TokenAuthentication(user, jwtToken); + // without a proxy cache aspect is not working, thus it is necessary get bean from application context + final boolean authenticated = !meAsProxy.isInvalidated(jwtToken); + out.setAuthenticated(authenticated); + return out; } /** @@ -152,19 +227,52 @@ public TokenAuthentication validateJwtToken(TokenAuthentication token) { } /** - * Parse the JWT token and return a {@link QueryResponse} object containing the domain, user id, date of creation and date of expiration + * This method is for removing if sign. Each JWT token is concatenation of three parts (header, body, sign) joined + * with ".". JWT library required on parse also validation step. For validation is needed defined public key, but + * we use also JWT tokens from another application (z/OSMF) and we don't have it. + * @param jwtToken token to modify + * @return jwt token without sign part + */ + private String removeSign(String jwtToken) { + if (jwtToken == null) return null; + + final int index = jwtToken.indexOf('.'); + final int index2 = jwtToken.indexOf('.', index + 1); + if (index2 > 0) return jwtToken.substring(0, index2 + 1); + + return jwtToken; + } + + /** + * Parse the JWT token and return a {@link QueryResponse} object containing the domain, user id, type (Zowe / z/OSMF), + * date of creation and date of expiration * * @param jwtToken the JWT token * @return the query response */ public QueryResponse parseJwtToken(String jwtToken) { - Claims claims = getClaims(jwtToken); + /** + * Remove signature, because fo z/OSMF we dont have key to verify certificate and + * we just need to read claim. Verification is realized via REST call to z/OSMF. + * JWT library doesn't parse signed key without verification. + */ + final String withoutSign = removeSign(jwtToken); - return new QueryResponse( - claims.get(DOMAIN_CLAIM_NAME, String.class), - claims.getSubject(), - claims.getIssuedAt(), - claims.getExpiration()); + // parse to claims and construct QueryResponse + try { + Claims claims = Jwts.parser() + .parseClaimsJwt(withoutSign) + .getBody(); + return new QueryResponse( + claims.get(DOMAIN_CLAIM_NAME, String.class), + claims.getSubject(), + claims.getIssuedAt(), + claims.getExpiration(), + QueryResponse.Source.valueByIssuer(claims.getIssuer()) + ); + } catch (RuntimeException exception) { + throw handleJwtParserException(exception); + } } /** @@ -190,6 +298,16 @@ public Optional getJwtTokenFromRequest(HttpServletRequest request) { .map(Cookie::getValue); } + /** + * This method validate if JWT token is valid and if yes, then get claim with LTPA token. + * For purpose, where is not needed validation, you can use method {@link #getLtpaToken(String)} + * @param jwtToken the JWT token + * @return LTPA token extracted from JWT + */ + public String getLtpaTokenWithValidation(String jwtToken) { + return validateAndParseLocalJwtToken(jwtToken).get(LTPA_CLAIM_NAME, String.class); + } + /** * Get the LTPA token from the JWT token * @@ -197,8 +315,19 @@ public Optional getJwtTokenFromRequest(HttpServletRequest request) { * @return the LTPA token * @throws TokenNotValidException if the JWT token is not valid */ - public String getLtpaTokenFromJwtToken(String jwtToken) { - return getClaims(jwtToken).get(LTPA_CLAIM_NAME, String.class); + public String getLtpaToken(String jwtToken) { + // remove sign to avoid validation of sign + final String withoutSign = removeSign(jwtToken); + + // parse to claims and construct QueryResponse + try { + return Jwts.parser() + .parseClaimsJwt(withoutSign) + .getBody() + .get(LTPA_CLAIM_NAME, String.class); + } catch (RuntimeException exception) { + throw handleJwtParserException(exception); + } } /** @@ -239,21 +368,4 @@ private long calculateExpiration(long now, String username) { return expiration; } - private Claims getClaims(String jwtToken) { - try { - return Jwts.parser() - .setSigningKey(jwtSecurityInitializer.getJwtPublicKey()) - .parseClaimsJws(jwtToken) - .getBody(); - } catch (ExpiredJwtException exception) { - log.debug("Token with id '{}' for user '{}' is expired.", exception.getClaims().getId(), exception.getClaims().getSubject()); - throw new TokenExpireException("Token is expired."); - } catch (JwtException exception) { - log.debug("Token is not valid due to: {}.", exception.getMessage()); - throw new TokenNotValidException("Token is not valid."); - } catch (Exception exception) { - log.debug("Token is not valid due to: {}.", exception.getMessage()); - throw new TokenNotValidException("An internal error occurred while validating the token therefor the token is no longer valid."); - } - } } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ZosmfService.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ZosmfService.java new file mode 100644 index 0000000000..f95185c539 --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ZosmfService.java @@ -0,0 +1,84 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.security.service; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Value; +import org.springframework.security.core.Authentication; + +import java.util.Map; + +/** + * Common interface for z/OSMF operations about authentication. This interface is implemented with each bean for + * authentication to z/OSMF and also in {@link com.ca.mfaas.gateway.security.service.zosmf.ZosmfServiceFacade}, which + * provides calls by version of z/OSMF. + */ +public interface ZosmfService { + + /** + * Make authentication in z/OSMF. The result contains all supported token (JWT, LTPA) if they are available and + * domain (required to construct Zowe's JWT token) + * @param authentication user authentication + * @return AuthenticationResponse, collections of supported tokens and domain + */ + public AuthenticationResponse authenticate(Authentication authentication); + + /** + * Validate in z/OSMF is the token is valid there or not. If token is invalid or any other error occurred it + * throws an exception. + * @param type Type of token (JWT, LTPA) + * @param token Token to verify + */ + public void validate(ZosmfService.TokenType type, String token); + + /** + * This method invalidate token in z/OSMF if the service to deactivate is available + * @param type Type of token (JWT, LTPA) + * @param token Token to verify + */ + public void invalidate(ZosmfService.TokenType type, String token); + + /** + * Method is to decide which version of z/OSMF are supported by implementation. If bean is not real implementation + * but delegate it has to return false (see {@link com.ca.mfaas.gateway.security.service.zosmf.ZosmfServiceFacade}). + * @param version version of z/OSMF + * @return if bean provides implementation for specific version of z/OSMF + */ + public boolean matchesVersion(int version); + + /** + * Enumeration of supported security tokens + */ + @AllArgsConstructor + @Getter + public enum TokenType { + + JWT("jwtToken"), + LTPA("LtpaToken2") + + ; + + private final String cookieName; + + } + + /** + * Response of authentication, contains all data to next processing + */ + @Value + public static class AuthenticationResponse { + + private final String domain; + private final Map tokens; + + } + +} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ZosmfScheme.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ZosmfScheme.java index 15e6c29d14..03f05607e5 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ZosmfScheme.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/schema/ZosmfScheme.java @@ -60,16 +60,26 @@ public void apply(InstanceInfo instanceInfo) { Optional jwtToken = authenticationService.getJwtTokenFromRequest(context.getRequest()); jwtToken.ifPresent(token -> { - String ltpaToken = authenticationService.getLtpaTokenFromJwtToken(token); - - String cookie = context.getZuulRequestHeaders().get(COOKIE_HEADER); - if (cookie != null) { - cookie += "; " + ltpaToken; - } else { - cookie = ltpaToken; + // parse JWT token to detect the source (z/OSMF / Zowe) + QueryResponse queryResponse = authenticationService.parseJwtToken(token); + switch ( queryResponse.getSource()) { + case ZOSMF: + // token is generated by z/OSMF, no action is required, just bypass, verification will be by itself + break; + case ZOWE: + // user use Zowe own JWT token, for communication with z/OSMF there should be LTPA token, use it + String ltpaToken = authenticationService.getLtpaTokenWithValidation(token); + + String cookie = context.getZuulRequestHeaders().get(COOKIE_HEADER); + if (cookie != null) { + cookie += "; " + ltpaToken; + } else { + cookie = ltpaToken; + } + + context.addZuulRequestHeader(COOKIE_HEADER, cookie); + break; } - - context.addZuulRequestHeader(COOKIE_HEADER, cookie); }); } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/AbstractZosmfService.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/AbstractZosmfService.java new file mode 100644 index 0000000000..7c520e9ba0 --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/AbstractZosmfService.java @@ -0,0 +1,165 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.security.service.zosmf; + +import com.ca.apiml.security.common.config.AuthConfigurationProperties; +import com.ca.apiml.security.common.error.ServiceNotAccessibleException; +import com.ca.mfaas.gateway.security.service.ZosmfService; +import com.ca.mfaas.message.log.ApimlLogger; +import com.ca.mfaas.product.logging.annotations.InjectApimlLogger; +import com.ca.mfaas.util.EurekaUtils; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.netflix.discovery.DiscoveryClient; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.core.Authentication; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.util.*; +import java.util.function.Supplier; + +public abstract class AbstractZosmfService implements ZosmfService { + + protected static final String ZOSMF_INFO_END_POINT = "/zosmf/info"; + protected static final String ZOSMF_AUTHENTICATE_END_POINT = "/zosmf/services/authenticate"; + protected static final String ZOSMF_CSRF_HEADER = "X-CSRF-ZOSMF-HEADER"; + protected static final String ZOSMF_DOMAIN = "zosmf_saf_realm"; + + @InjectApimlLogger + protected ApimlLogger apimlLog = ApimlLogger.empty(); + + protected final AuthConfigurationProperties authConfigurationProperties; + protected final DiscoveryClient discovery; + protected final RestTemplate restTemplate; + protected final ObjectMapper securityObjectMapper; + + public AbstractZosmfService( + AuthConfigurationProperties authConfigurationProperties, + DiscoveryClient discovery, + RestTemplate restTemplate, + ObjectMapper securityObjectMapper + ) { + this.authConfigurationProperties = authConfigurationProperties; + this.discovery = discovery; + this.restTemplate = restTemplate; + this.securityObjectMapper = securityObjectMapper; + } + + protected String getZosmfServiceId() { + return authConfigurationProperties.validatedZosmfServiceId(); + } + + protected String getAuthenticationValue(Authentication authentication) { + final String user = authentication.getPrincipal().toString(); + final String password = authentication.getCredentials().toString(); + + final String credentials = user + ":" + password; + final String authorization = "Basic " + Base64.getEncoder().encodeToString(credentials.getBytes()); + + return authorization; + } + + /** + * Return z/OSMF instance uri + * + * @param zosmf the z/OSMF service id + * @return the uri + */ + protected String getURI(String zosmf) { + Supplier authenticationServiceExceptionSupplier = () -> { + apimlLog.log("apiml.security.zosmfInstanceNotFound", zosmf); + return new ServiceNotAccessibleException("z/OSMF instance not found or incorrectly configured."); + }; + + return Optional.ofNullable(discovery.getApplication(zosmf)) + .orElseThrow(authenticationServiceExceptionSupplier) + .getInstances() + .stream() + .filter(Objects::nonNull) + .findFirst() + .map(zosmfInstance -> EurekaUtils.getUrl(zosmfInstance)) + .orElseThrow(authenticationServiceExceptionSupplier); + } + + protected RuntimeException handleExceptionOnCall(String url, RuntimeException re) { + if (re instanceof ResourceAccessException) { + apimlLog.log("apiml.security.serviceUnavailable", url, re.getMessage()); + return new ServiceNotAccessibleException("Could not get an access to z/OSMF service."); + } + + if (re instanceof RestClientException) { + apimlLog.log("apiml.security.generic", re.getMessage(), url); + return new AuthenticationServiceException("A failure occurred when authenticating.", re); + } + + return re; + } + + /** + * Read the z/OSMF domain from the content in the response + * + * @param content the response body + * @return the z/OSMF domain + * @throws AuthenticationServiceException if the zosmf domain cannot be read + */ + protected String readDomain(String content) { + try { + ObjectNode zosmfNode = securityObjectMapper.readValue(content, ObjectNode.class); + + return Optional.ofNullable(zosmfNode) + .filter(zn -> zn.has(ZOSMF_DOMAIN)) + .map(zn -> zn.get(ZOSMF_DOMAIN).asText()) + .orElseThrow(() -> { + apimlLog.log("apiml.security.zosmfDomainIsEmpty", ZOSMF_DOMAIN); + return new AuthenticationServiceException("z/OSMF domain cannot be read."); + }); + } catch (IOException e) { + apimlLog.log("apiml.security.errorParsingZosmfResponse", e.getMessage()); + throw new AuthenticationServiceException("z/OSMF domain cannot be read."); + } + } + + /** + * Read the token with name cookieName from the cookies + * + * @param cookies the cookies + * @return the token if is set in cookies, otherwise null + */ + protected String readTokenFromCookie(List cookies, String cookieName) { + if (cookies == null) return null; + + return cookies.stream() + .filter(x -> x.startsWith(cookieName + "=")) + .findFirst() + .map(x -> { + final int beginIndex = cookieName.length() + 1; + final int endIndex = x.indexOf(';'); + return endIndex > 0 ? x.substring(beginIndex, endIndex) : x.substring(beginIndex); + }) + .orElse(null); + } + + protected AuthenticationResponse getAuthenticationResponse(ResponseEntity responseEntity) { + final List cookies = responseEntity.getHeaders().get(HttpHeaders.SET_COOKIE); + final Map tokens = new HashMap<>(); + for (final TokenType tokenType : TokenType.values()) { + final String token = readTokenFromCookie(cookies, tokenType.getCookieName()); + if (token != null) tokens.put(tokenType, token); + } + final String domain = readDomain(responseEntity.getBody()); + return new AuthenticationResponse(domain, tokens); + } + +} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceFacade.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceFacade.java new file mode 100644 index 0000000000..8b285172ac --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceFacade.java @@ -0,0 +1,146 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.security.service.zosmf; + +import com.ca.apiml.security.common.config.AuthConfigurationProperties; +import com.ca.mfaas.gateway.security.service.ZosmfService; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.netflix.discovery.DiscoveryClient; +import lombok.Data; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Primary; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import javax.annotation.PostConstruct; +import java.util.List; + +/** + * This bean is default implementation of access to z/OSMF authentication. It collect all other implementation and + * select right implementation by z/OSMF version. This bean is facade for those implementation and all its own methods + * are delegates to implementation based of z/OSMF version. + * + * see also: + * - {@link ZosmfServiceV2} + * - new version supporting https://www.ibm.com/support/knowledgecenter/SSLTBW_2.4.0/com.ibm.zos.v2r4.izua700/izuprog_API_WebTokenAuthServices.htm + * - {@link ZosmfServiceV1} + * - old version using endpoint /zosmf/info + */ +@Primary +@Service +public class ZosmfServiceFacade extends AbstractZosmfService { + + private final ApplicationContext applicationContext; + private final List implementations; + + private ZosmfServiceFacade meProxy; + + public ZosmfServiceFacade( + final AuthConfigurationProperties authConfigurationProperties, + final DiscoveryClient discovery, + final RestTemplate restTemplate, + final ObjectMapper securityObjectMapper, + final ApplicationContext applicationContext, + final List implementations + ) { + super( + authConfigurationProperties, + discovery, + restTemplate, + securityObjectMapper + ); + this.applicationContext = applicationContext; + this.implementations = implementations; + } + + @PostConstruct + public void afterPropertiesSet() { + meProxy = applicationContext.getBean(ZosmfServiceFacade.class); + } + + @CacheEvict(value = {"zosmfVersion", "zosmfServiceImplementation"}, allEntries = true) + public void evictCaches() { + // evict all caches + } + + @Cacheable("zosmfVersion") + public int getVersion(String zosmfServiceId) { + final String url = getURI(zosmfServiceId) + ZOSMF_INFO_END_POINT; + final HttpHeaders headers = new HttpHeaders(); + headers.add(ZOSMF_CSRF_HEADER, ""); + + try { + final ResponseEntity info = restTemplate.exchange( + url, HttpMethod.GET, new HttpEntity<>(headers), ZosmfInfo.class + ); + return info.getBody().getVersion(); + } catch (RuntimeException re) { + meProxy.evictCaches(); + throw handleExceptionOnCall(url, re); + } + } + + @Cacheable("zosmfServiceImplementation") + public ZosmfService getImplementation(String zosmfServiceId) { + final int version = meProxy.getVersion(zosmfServiceId); + for (final ZosmfService zosmfService : implementations) { + if (zosmfService.matchesVersion(version)) return zosmfService; + } + + meProxy.evictCaches(); + throw new IllegalArgumentException("Unknown version of z/OSMF : " + version); + } + + protected ZosmfService getImplementation() { + return meProxy.getImplementation(getZosmfServiceId()); + } + + @Override + public AuthenticationResponse authenticate(Authentication authentication) { + return getImplementation().authenticate(authentication); + } + + @Override + public void validate(TokenType type, String token) { + getImplementation().validate(type, token); + } + + @Override + public void invalidate(TokenType type, String token) { + getImplementation().invalidate(type, token); + } + + @Override + public boolean matchesVersion(int version) { + return false; + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ZosmfInfo { + + @JsonProperty("zosmf_version") + private int version; + + @JsonProperty("zosmf_full_version") + private String fullVersion; + + } + +} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV1.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV1.java new file mode 100644 index 0000000000..41fa9b094d --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV1.java @@ -0,0 +1,92 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.security.service.zosmf; + +import com.ca.apiml.security.common.config.AuthConfigurationProperties; +import com.ca.apiml.security.common.error.ServiceNotAccessibleException; +import com.ca.apiml.security.common.token.TokenNotValidException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.netflix.discovery.DiscoveryClient; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +@Service +public class ZosmfServiceV1 extends AbstractZosmfService { + + public ZosmfServiceV1( + AuthConfigurationProperties authConfigurationProperties, + DiscoveryClient discovery, + RestTemplate restTemplate, + ObjectMapper securityObjectMapper + ) { + super(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); + } + + @Override + public AuthenticationResponse authenticate(Authentication authentication) { + final String url = getURI(getZosmfServiceId()) + ZOSMF_INFO_END_POINT; + + final HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.AUTHORIZATION, getAuthenticationValue(authentication)); + headers.add(ZOSMF_CSRF_HEADER, ""); + + try { + final ResponseEntity responseEntity = restTemplate.exchange( + url, + HttpMethod.GET, + new HttpEntity<>(null, headers), + String.class); + return getAuthenticationResponse(responseEntity); + } catch (RuntimeException re) { + throw handleExceptionOnCall(url, re); + } + } + + @Override + public void validate(TokenType type, String token) { + final String url = getURI(getZosmfServiceId()) + ZOSMF_INFO_END_POINT; + + final HttpHeaders headers = new HttpHeaders(); + headers.add(ZOSMF_CSRF_HEADER, ""); + headers.add(HttpHeaders.COOKIE, type.getCookieName() + "=" + token); + + try { + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + new HttpEntity<>(null, headers), + String.class); + + if (response.getStatusCode().is2xxSuccessful()) return; + if (response.getStatusCodeValue() == 401) { + throw new TokenNotValidException("Token is not valid."); + } + apimlLog.log("apiml.security.serviceUnavailable", url, response.getStatusCodeValue()); + throw new ServiceNotAccessibleException("Could not get an access to z/OSMF service."); + } catch (RuntimeException re) { + throw handleExceptionOnCall(url, re); + } + } + + @Override + public void invalidate(TokenType type, String token) { + // not supported by z/OSMF + } + + @Override + public boolean matchesVersion(int version) { + return version <= 25; + } + +} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV2.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV2.java new file mode 100644 index 0000000000..9205e039f4 --- /dev/null +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV2.java @@ -0,0 +1,115 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.security.service.zosmf; + +import com.ca.apiml.security.common.config.AuthConfigurationProperties; +import com.ca.apiml.security.common.error.ServiceNotAccessibleException; +import com.ca.apiml.security.common.token.TokenNotValidException; +import com.ca.mfaas.gateway.security.service.ZosmfService; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.netflix.discovery.DiscoveryClient; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +/** + * + */ +@Service +public class ZosmfServiceV2 extends AbstractZosmfService { + + public ZosmfServiceV2( + AuthConfigurationProperties authConfigurationProperties, + DiscoveryClient discovery, + RestTemplate restTemplate, + ObjectMapper securityObjectMapper + ) { + super(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); + } + + @Override + public AuthenticationResponse authenticate(Authentication authentication) { + String url = getURI(getZosmfServiceId()) + ZOSMF_AUTHENTICATE_END_POINT; + + final HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.AUTHORIZATION, getAuthenticationValue(authentication)); + headers.add(ZOSMF_CSRF_HEADER, ""); + + try { + final ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.POST, + new HttpEntity<>(null, headers), + String.class); + return getAuthenticationResponse(response); + } catch (RuntimeException re) { + throw handleExceptionOnCall(url, re); + } + } + + @Override + public void validate(ZosmfService.TokenType type, String token) { + final String url = getURI(getZosmfServiceId()) + ZOSMF_AUTHENTICATE_END_POINT; + + final HttpHeaders headers = new HttpHeaders(); + headers.add(ZOSMF_CSRF_HEADER, ""); + headers.add(HttpHeaders.COOKIE, type.getCookieName() + "=" + token); + + try { + ResponseEntity re = restTemplate.exchange( + url , + HttpMethod.POST, + new HttpEntity<>(null, headers), + String.class); + + if (re.getStatusCode().is2xxSuccessful()) return; + if (re.getStatusCodeValue() == 401) { + throw new TokenNotValidException("Token is not valid."); + } + apimlLog.log("apiml.security.serviceUnavailable", url, re.getStatusCodeValue()); + throw new ServiceNotAccessibleException("Could not get an access to z/OSMF service."); + } catch (RuntimeException re) { + throw handleExceptionOnCall(url, re); + } + } + + @Override + public void invalidate(ZosmfService.TokenType type, String token) { + final String url = getURI(getZosmfServiceId()) + ZOSMF_AUTHENTICATE_END_POINT; + + final HttpHeaders headers = new HttpHeaders(); + headers.add(ZOSMF_CSRF_HEADER, ""); + headers.add(HttpHeaders.COOKIE, type.getCookieName() + "=" + token); + + try { + ResponseEntity re = restTemplate.exchange( + url, + HttpMethod.DELETE, + new HttpEntity<>(null, headers), + String.class); + + if (re.getStatusCode().is2xxSuccessful()) return; + apimlLog.log("apiml.security.serviceUnavailable", url, re.getStatusCodeValue()); + throw new ServiceNotAccessibleException("Could not get an access to z/OSMF service."); + } catch (RuntimeException re) { + throw handleExceptionOnCall(url, re); + } + } + + @Override + public boolean matchesVersion(int version) { + return version >= 26; + } + +} diff --git a/gateway-service/src/main/resources/ehcache.xml b/gateway-service/src/main/resources/ehcache.xml index a4d4f0cb0f..fdaad48cf3 100644 --- a/gateway-service/src/main/resources/ehcache.xml +++ b/gateway-service/src/main/resources/ehcache.xml @@ -10,5 +10,7 @@ + + diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ZosmfFilterTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ZosmfFilterTest.java deleted file mode 100644 index 3f5a9696e0..0000000000 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/filters/pre/ZosmfFilterTest.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ -package com.ca.mfaas.gateway.filters.pre; - -import static org.junit.Assert.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SERVICE_ID_KEY; - -import com.ca.mfaas.gateway.security.service.AuthenticationService; -import com.netflix.zuul.context.RequestContext; - -import org.junit.Before; -import org.junit.Test; -import org.springframework.mock.web.MockHttpServletResponse; - -import java.util.Optional; - -public class ZosmfFilterTest { - - private static final String TOKEN = "token"; - private static final String LTPA_TOKEN = "ltpaToken"; - private static final String MY_COOKIE = "myCookie=MYCOOKIE"; - - private ZosmfFilter filter; - private AuthenticationService authenticationService; - - @Before - public void setUp() { - this.authenticationService = mock(AuthenticationService.class); - this.filter = new ZosmfFilter(authenticationService); - RequestContext ctx = RequestContext.getCurrentContext(); - ctx.clear(); - ctx.setResponse(new MockHttpServletResponse()); - } - - @Test - public void shouldFilterZosmfRequests() { - final RequestContext ctx = RequestContext.getCurrentContext(); - ctx.set(SERVICE_ID_KEY, "zosmftest"); - - assertTrue(this.filter.shouldFilter()); - } - - @Test - public void shouldNotFilterOtherServiceRequests() { - final RequestContext ctx = RequestContext.getCurrentContext(); - ctx.set(SERVICE_ID_KEY, "testservice"); - - assertFalse(this.filter.shouldFilter()); - } - - @Test - public void shouldAddLtpaTokenToZosmfRequests() { - final RequestContext ctx = RequestContext.getCurrentContext(); - ctx.set(SERVICE_ID_KEY, "zosmftest"); - when(authenticationService.getJwtTokenFromRequest(ctx.getRequest())).thenReturn( - Optional.of(TOKEN) - ); - when(authenticationService.getLtpaTokenFromJwtToken(TOKEN)).thenReturn(LTPA_TOKEN); - - this.filter.run(); - - assertTrue(ctx.getZuulRequestHeaders().get("cookie").contains(LTPA_TOKEN)); - } - - @Test - public void shouldPassWhenLtpaTokenIsMissing() { - final RequestContext ctx = RequestContext.getCurrentContext(); - ctx.set(SERVICE_ID_KEY, "zosmftest"); - when(authenticationService.getJwtTokenFromRequest(ctx.getRequest())).thenReturn( - Optional.of(TOKEN) - ); - when(authenticationService.getLtpaTokenFromJwtToken(TOKEN)).thenReturn(null); - - this.filter.run(); - - assertNull(ctx.getZuulRequestHeaders().get("cookie")); - } - - @Test - public void shouldPassWhenJwtTokenIsMissing() { - final RequestContext ctx = RequestContext.getCurrentContext(); - ctx.set(SERVICE_ID_KEY, "zosmftest"); - when(authenticationService.getJwtTokenFromRequest(ctx.getRequest())).thenReturn( - Optional.empty() - ); - when(authenticationService.getLtpaTokenFromJwtToken(null)).thenReturn(null); - - this.filter.run(); - - assertNull(ctx.getZuulRequestHeaders().get("cookie")); - } - - @Test - public void shouldKeepExistingCookies() { - final RequestContext ctx = RequestContext.getCurrentContext(); - ctx.set(SERVICE_ID_KEY, "zosmftest"); - ctx.addZuulRequestHeader("Cookie", MY_COOKIE); - - when(authenticationService.getJwtTokenFromRequest(ctx.getRequest())).thenReturn( - Optional.of(TOKEN) - ); - when(authenticationService.getLtpaTokenFromJwtToken(TOKEN)).thenReturn(LTPA_TOKEN); - - this.filter.run(); - - assertTrue(ctx.getZuulRequestHeaders().get("cookie").contains(LTPA_TOKEN)); - assertTrue(ctx.getZuulRequestHeaders().get("cookie").contains(MY_COOKIE)); - } -} diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java index 68e3e1355a..3b71711a8a 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java @@ -12,16 +12,20 @@ import com.ca.apiml.security.common.config.AuthConfigurationProperties; import com.ca.apiml.security.common.error.ServiceNotAccessibleException; +import com.ca.apiml.security.common.token.TokenAuthentication; import com.ca.mfaas.gateway.security.service.AuthenticationService; +import com.ca.mfaas.gateway.security.service.zosmf.ZosmfServiceV2; import com.fasterxml.jackson.databind.ObjectMapper; +import com.netflix.appinfo.InstanceInfo; +import com.netflix.discovery.DiscoveryClient; +import com.netflix.discovery.shared.Application; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.Mockito; -import org.springframework.cloud.client.DefaultServiceInstance; -import org.springframework.cloud.client.ServiceInstance; -import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -34,22 +38,21 @@ import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; -import java.util.Collections; -import java.util.List; +import java.util.Arrays; import static junit.framework.TestCase.assertTrue; import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; public class ZosmfAuthenticationProviderTest { + private static final String USERNAME = "user"; private static final String PASSWORD = "password"; private static final String SERVICE_ID = "service"; private static final String HOST = "localhost"; private static final int PORT = 0; private static final String ZOSMF = "zosmf"; - private static final String COOKIE1 = "JwtToken=test"; + private static final String COOKIE1 = "Cookie1=testCookie1"; private static final String COOKIE2 = "LtpaToken2=test"; private static final String DOMAIN = "realm"; private static final String RESPONSE = "{\"zosmf_saf_realm\": \"" + DOMAIN + "\"}"; @@ -59,8 +62,9 @@ public class ZosmfAuthenticationProviderTest { private DiscoveryClient discovery; private ObjectMapper mapper; private RestTemplate restTemplate; - private ServiceInstance zosmfInstance; + private InstanceInfo zosmfInstance; private AuthenticationService authenticationService; + private ObjectMapper securityObjectMapper = new ObjectMapper(); @Rule public final ExpectedException exception = ExpectedException.none(); @@ -73,27 +77,50 @@ public void setUp() { authenticationService = mock(AuthenticationService.class); mapper = new ObjectMapper(); restTemplate = mock(RestTemplate.class); - zosmfInstance = new DefaultServiceInstance(SERVICE_ID, HOST, PORT, false); + zosmfInstance = createInstanceInfo(SERVICE_ID, HOST, PORT); + + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return TokenAuthentication.createAuthenticated(invocation.getArgument(0), invocation.getArgument(1)); + } + }).when(authenticationService).createTokenAuthentication(anyString(), anyString()); + when(authenticationService.createJwtToken(anyString(), anyString(), anyString())).thenReturn("someJwtToken"); + } + + private InstanceInfo createInstanceInfo(String serviceId, String host, int port) { + InstanceInfo out = mock(InstanceInfo.class); + when(out.getAppName()).thenReturn(serviceId); + when(out.getHostName()).thenReturn(host); + when(out.getPort()).thenReturn(port); + return out; + } + + private Application createApplication(InstanceInfo...instanceInfos) { + Application out = mock(Application.class); + when(out.getInstances()).thenReturn(Arrays.asList(instanceInfos)); + return out; } @Test public void loginWithExistingUser() { authConfigurationProperties.setZosmfServiceId(ZOSMF); - List zosmfInstances = Collections.singletonList(zosmfInstance); - when(discovery.getInstances(ZOSMF)).thenReturn(zosmfInstances); + final Application application = createApplication(zosmfInstance); + when(discovery.getApplication(ZOSMF)).thenReturn(application); HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.SET_COOKIE, COOKIE1); headers.add(HttpHeaders.SET_COOKIE, COOKIE2); when(restTemplate.exchange(Mockito.anyString(), - Mockito.eq(HttpMethod.GET), + Mockito.eq(HttpMethod.POST), Mockito.any(), Mockito.>any())) .thenReturn(new ResponseEntity<>(RESPONSE, headers, HttpStatus.OK)); - ZosmfAuthenticationProvider zosmfAuthenticationProvider - = new ZosmfAuthenticationProvider(authConfigurationProperties, authenticationService, discovery, mapper, restTemplate); + ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); + ZosmfAuthenticationProvider zosmfAuthenticationProvider = + new ZosmfAuthenticationProvider(authenticationService, zosmfService, mapper); Authentication tokenAuthentication = zosmfAuthenticationProvider.authenticate(usernamePasswordAuthentication); @@ -106,18 +133,19 @@ public void loginWithExistingUser() { public void loginWithBadUser() { authConfigurationProperties.setZosmfServiceId(ZOSMF); - List zosmfInstances = Collections.singletonList(zosmfInstance); - when(discovery.getInstances(ZOSMF)).thenReturn(zosmfInstances); + final Application application = createApplication(zosmfInstance); + when(discovery.getApplication(ZOSMF)).thenReturn(application); HttpHeaders headers = new HttpHeaders(); when(restTemplate.exchange(Mockito.anyString(), - Mockito.eq(HttpMethod.GET), + Mockito.eq(HttpMethod.POST), Mockito.any(), Mockito.>any())) .thenReturn(new ResponseEntity<>(RESPONSE, headers, HttpStatus.OK)); - ZosmfAuthenticationProvider zosmfAuthenticationProvider - = new ZosmfAuthenticationProvider(authConfigurationProperties, authenticationService, discovery, mapper, restTemplate); + ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); + ZosmfAuthenticationProvider zosmfAuthenticationProvider = + new ZosmfAuthenticationProvider(authenticationService, zosmfService, mapper); exception.expect(BadCredentialsException.class); exception.expectMessage("Username or password are invalid."); @@ -128,11 +156,13 @@ public void loginWithBadUser() { @Test public void noZosmfInstance() { authConfigurationProperties.setZosmfServiceId(ZOSMF); - List zosmfInstances = Collections.singletonList(null); - when(discovery.getInstances(ZOSMF)).thenReturn(zosmfInstances); - ZosmfAuthenticationProvider zosmfAuthenticationProvider - = new ZosmfAuthenticationProvider(authConfigurationProperties, authenticationService, discovery, mapper, restTemplate); + final Application application = createApplication(); + when(discovery.getApplication(ZOSMF)).thenReturn(application); + + ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); + ZosmfAuthenticationProvider zosmfAuthenticationProvider = + new ZosmfAuthenticationProvider(authenticationService, zosmfService, mapper); exception.expect(ServiceNotAccessibleException.class); exception.expectMessage("z/OSMF instance not found or incorrectly configured."); @@ -142,8 +172,9 @@ public void noZosmfInstance() { @Test public void noZosmfServiceId() { - ZosmfAuthenticationProvider zosmfAuthenticationProvider - = new ZosmfAuthenticationProvider(authConfigurationProperties, authenticationService, discovery, mapper, restTemplate); + ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); + ZosmfAuthenticationProvider zosmfAuthenticationProvider = + new ZosmfAuthenticationProvider(authenticationService, zosmfService, mapper); exception.expect(AuthenticationServiceException.class); exception.expectMessage("The parameter 'zosmfServiceId' is not configured."); @@ -155,20 +186,21 @@ public void noZosmfServiceId() { public void notValidZosmfResponse() { authConfigurationProperties.setZosmfServiceId(ZOSMF); - List zosmfInstances = Collections.singletonList(zosmfInstance); - when(discovery.getInstances(ZOSMF)).thenReturn(zosmfInstances); + final Application application = createApplication(zosmfInstance); + when(discovery.getApplication(ZOSMF)).thenReturn(application); HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.SET_COOKIE, COOKIE1); headers.add(HttpHeaders.SET_COOKIE, COOKIE2); when(restTemplate.exchange(Mockito.anyString(), - Mockito.eq(HttpMethod.GET), + Mockito.eq(HttpMethod.POST), Mockito.any(), Mockito.>any())) .thenReturn(new ResponseEntity<>("", headers, HttpStatus.OK)); - ZosmfAuthenticationProvider zosmfAuthenticationProvider - = new ZosmfAuthenticationProvider(authConfigurationProperties, authenticationService, discovery, mapper, restTemplate); + ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); + ZosmfAuthenticationProvider zosmfAuthenticationProvider = + new ZosmfAuthenticationProvider(authenticationService, zosmfService, mapper); exception.expect(AuthenticationServiceException.class); exception.expectMessage("z/OSMF domain cannot be read."); @@ -182,20 +214,21 @@ public void noDomainInResponse() { authConfigurationProperties.setZosmfServiceId(ZOSMF); - List zosmfInstances = Collections.singletonList(zosmfInstance); - when(discovery.getInstances(ZOSMF)).thenReturn(zosmfInstances); + final Application application = createApplication(zosmfInstance); + when(discovery.getApplication(ZOSMF)).thenReturn(application); HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.SET_COOKIE, COOKIE1); headers.add(HttpHeaders.SET_COOKIE, COOKIE2); when(restTemplate.exchange(Mockito.anyString(), - Mockito.eq(HttpMethod.GET), + Mockito.eq(HttpMethod.POST), Mockito.any(), Mockito.>any())) .thenReturn(new ResponseEntity<>(invalidResponse, headers, HttpStatus.OK)); - ZosmfAuthenticationProvider zosmfAuthenticationProvider - = new ZosmfAuthenticationProvider(authConfigurationProperties, authenticationService, discovery, mapper, restTemplate); + ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); + ZosmfAuthenticationProvider zosmfAuthenticationProvider = + new ZosmfAuthenticationProvider(authenticationService, zosmfService, mapper); exception.expect(AuthenticationServiceException.class); exception.expectMessage("z/OSMF domain cannot be read."); @@ -209,19 +242,20 @@ public void invalidCookieInResponse() { authConfigurationProperties.setZosmfServiceId(ZOSMF); - List zosmfInstances = Collections.singletonList(zosmfInstance); - when(discovery.getInstances(ZOSMF)).thenReturn(zosmfInstances); + final Application application = createApplication(zosmfInstance); + when(discovery.getApplication(ZOSMF)).thenReturn(application); HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.SET_COOKIE, invalidCookie); when(restTemplate.exchange(Mockito.anyString(), - Mockito.eq(HttpMethod.GET), + Mockito.eq(HttpMethod.POST), Mockito.any(), Mockito.>any())) .thenReturn(new ResponseEntity<>(RESPONSE, headers, HttpStatus.OK)); - ZosmfAuthenticationProvider zosmfAuthenticationProvider - = new ZosmfAuthenticationProvider(authConfigurationProperties, authenticationService, discovery, mapper, restTemplate); + ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); + ZosmfAuthenticationProvider zosmfAuthenticationProvider = + new ZosmfAuthenticationProvider(authenticationService, zosmfService, mapper); exception.expect(BadCredentialsException.class); exception.expectMessage("Username or password are invalid."); @@ -235,19 +269,20 @@ public void cookieWithSemicolon() { authConfigurationProperties.setZosmfServiceId(ZOSMF); - List zosmfInstances = Collections.singletonList(zosmfInstance); - when(discovery.getInstances(ZOSMF)).thenReturn(zosmfInstances); + final Application application = createApplication(zosmfInstance); + when(discovery.getApplication(ZOSMF)).thenReturn(application); HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.SET_COOKIE, cookie); when(restTemplate.exchange(Mockito.anyString(), - Mockito.eq(HttpMethod.GET), + Mockito.eq(HttpMethod.POST), Mockito.any(), Mockito.>any())) .thenReturn(new ResponseEntity<>(RESPONSE, headers, HttpStatus.OK)); - ZosmfAuthenticationProvider zosmfAuthenticationProvider - = new ZosmfAuthenticationProvider(authConfigurationProperties, authenticationService, discovery, mapper, restTemplate); + ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); + ZosmfAuthenticationProvider zosmfAuthenticationProvider = + new ZosmfAuthenticationProvider(authenticationService, zosmfService, mapper); Authentication tokenAuthentication = zosmfAuthenticationProvider.authenticate(usernamePasswordAuthentication); @@ -260,15 +295,16 @@ public void cookieWithSemicolon() { public void shouldThrowNewExceptionIfRestClientException() { authConfigurationProperties.setZosmfServiceId(ZOSMF); - List zosmfInstances = Collections.singletonList(zosmfInstance); - when(discovery.getInstances(ZOSMF)).thenReturn(zosmfInstances); + final Application application = createApplication(zosmfInstance); + when(discovery.getApplication(ZOSMF)).thenReturn(application); when(restTemplate.exchange(Mockito.anyString(), - Mockito.eq(HttpMethod.GET), + Mockito.eq(HttpMethod.POST), Mockito.any(), Mockito.>any())) .thenThrow(RestClientException.class); - ZosmfAuthenticationProvider zosmfAuthenticationProvider - = new ZosmfAuthenticationProvider(authConfigurationProperties, authenticationService, discovery, mapper, restTemplate); + ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); + ZosmfAuthenticationProvider zosmfAuthenticationProvider = + new ZosmfAuthenticationProvider(authenticationService, zosmfService, mapper); exception.expect(AuthenticationServiceException.class); exception.expectMessage("A failure occurred when authenticating."); @@ -281,15 +317,16 @@ public void shouldThrowNewExceptionIfRestClientException() { public void shouldThrowNewExceptionIfResourceAccessException() { authConfigurationProperties.setZosmfServiceId(ZOSMF); - List zosmfInstances = Collections.singletonList(zosmfInstance); - when(discovery.getInstances(ZOSMF)).thenReturn(zosmfInstances); + final Application application = createApplication(zosmfInstance); + when(discovery.getApplication(ZOSMF)).thenReturn(application); when(restTemplate.exchange(Mockito.anyString(), - Mockito.eq(HttpMethod.GET), + Mockito.eq(HttpMethod.POST), Mockito.any(), Mockito.>any())) .thenThrow(ResourceAccessException.class); - ZosmfAuthenticationProvider zosmfAuthenticationProvider - = new ZosmfAuthenticationProvider(authConfigurationProperties, authenticationService, discovery, mapper, restTemplate); + ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); + ZosmfAuthenticationProvider zosmfAuthenticationProvider = + new ZosmfAuthenticationProvider(authenticationService, zosmfService, mapper); exception.expect(ServiceNotAccessibleException.class); exception.expectMessage("Could not get an access to z/OSMF service."); @@ -302,13 +339,15 @@ public void shouldThrowNewExceptionIfResourceAccessException() { public void shouldReturnTrueWhenSupportMethodIsCalledWithCorrectClass() { authConfigurationProperties.setZosmfServiceId(ZOSMF); - List zosmfInstances = Collections.singletonList(zosmfInstance); - when(discovery.getInstances(ZOSMF)).thenReturn(zosmfInstances); - ZosmfAuthenticationProvider zosmfAuthenticationProvider - = new ZosmfAuthenticationProvider(authConfigurationProperties, authenticationService, discovery, mapper, restTemplate); + final Application application = createApplication(zosmfInstance); + when(discovery.getApplication(ZOSMF)).thenReturn(application); + ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); + ZosmfAuthenticationProvider zosmfAuthenticationProvider = + new ZosmfAuthenticationProvider(authenticationService, zosmfService, mapper); boolean supports = zosmfAuthenticationProvider.supports(usernamePasswordAuthentication.getClass()); assertTrue(supports); } + } diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/query/SuccessfulQueryHandlerTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/query/SuccessfulQueryHandlerTest.java index 985e55e06d..badc1c45ee 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/query/SuccessfulQueryHandlerTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/query/SuccessfulQueryHandlerTest.java @@ -13,9 +13,10 @@ import com.ca.apiml.security.common.token.TokenAuthentication; import com.ca.mfaas.gateway.security.service.AuthenticationService; import com.ca.mfaas.gateway.security.service.JwtSecurityInitializer; +import com.ca.mfaas.gateway.security.service.zosmf.ZosmfServiceV2; import com.ca.mfaas.security.SecurityUtils; import com.fasterxml.jackson.databind.ObjectMapper; -import com.netflix.discovery.EurekaClient; +import com.netflix.discovery.DiscoveryClient; import io.jsonwebtoken.SignatureAlgorithm; import org.junit.Before; import org.junit.Test; @@ -57,7 +58,7 @@ public class SuccessfulQueryHandlerTest { private RestTemplate restTemplate; @Mock - private EurekaClient discoveryClient; + private DiscoveryClient discoveryClient; @Before public void setup() { @@ -73,10 +74,12 @@ public void setup() { privateKey = keyPair.getPrivate(); publicKey = keyPair.getPublic(); } - AuthenticationService authenticationService = new AuthenticationService(applicationContext, authConfigurationProperties, jwtSecurityInitializer, discoveryClient, restTemplate); + ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discoveryClient, restTemplate, new ObjectMapper()); + AuthenticationService authenticationService = new AuthenticationService( + applicationContext, authConfigurationProperties, jwtSecurityInitializer, zosmfService, discoveryClient, restTemplate + ); when(jwtSecurityInitializer.getSignatureAlgorithm()).thenReturn(algorithm); when(jwtSecurityInitializer.getJwtSecret()).thenReturn(privateKey); - when(jwtSecurityInitializer.getJwtPublicKey()).thenReturn(publicKey); jwtToken = authenticationService.createJwtToken(USER, DOMAIN, LTPA); diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/AuthenticationServiceTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/AuthenticationServiceTest.java index cb4af675c2..785036b04c 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/AuthenticationServiceTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/AuthenticationServiceTest.java @@ -15,24 +15,30 @@ import com.ca.apiml.security.common.token.TokenExpireException; import com.ca.apiml.security.common.token.TokenNotValidException; import com.ca.mfaas.gateway.config.CacheConfig; +import com.ca.mfaas.gateway.security.service.zosmf.ZosmfServiceV2; import com.ca.mfaas.security.SecurityUtils; +import com.ca.mfaas.util.EurekaUtils; +import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.appinfo.ApplicationInfoManager; import com.netflix.appinfo.InstanceInfo; -import com.netflix.discovery.EurekaClient; +import com.netflix.discovery.DiscoveryClient; import com.netflix.discovery.shared.Application; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.*; import org.apache.commons.lang.time.DateUtils; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.*; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.client.RestTemplate; import javax.servlet.http.Cookie; @@ -43,6 +49,7 @@ import java.util.Date; import java.util.List; import java.util.Optional; +import java.util.function.Consumer; import static org.junit.Assert.*; import static org.mockito.Mockito.*; @@ -54,6 +61,10 @@ }) public class AuthenticationServiceTest { + private static final String ZOSMF = "zosmf"; + private static final String ZOSMF_HOSTNAME = "zosmfhostname"; + private static final int ZOSMF_PORT = 1433; + private static final String USER = "Me"; private static final String DOMAIN = "this.com"; private static final String LTPA = "ltpaToken"; @@ -62,6 +73,11 @@ public class AuthenticationServiceTest { private Key privateKey; private PublicKey publicKey; + private String zosmfUrl; + + @Autowired + private ApplicationContext applicationContext; + @Autowired private AuthenticationService authService; @@ -75,9 +91,13 @@ public class AuthenticationServiceTest { private RestTemplate restTemplate; @Autowired - private EurekaClient discoveryClient; + private DiscoveryClient discoveryClient; + + private static ObjectMapper securityObjectMapper = new ObjectMapper(); private void mockJwtSecurityInitializer() { + reset(restTemplate); + reset(jwtSecurityInitializer); KeyPair keyPair = SecurityUtils.generateKeyPair("RSA", 2048); if (keyPair != null) { privateKey = keyPair.getPrivate(); @@ -86,6 +106,19 @@ private void mockJwtSecurityInitializer() { when(jwtSecurityInitializer.getSignatureAlgorithm()).thenReturn(ALGORITHM); when(jwtSecurityInitializer.getJwtSecret()).thenReturn(privateKey); when(jwtSecurityInitializer.getJwtPublicKey()).thenReturn(publicKey); + zosmfUrl = mockZosmfUrl(discoveryClient); + } + + private String mockZosmfUrl(DiscoveryClient discoveryClient) { + final InstanceInfo instanceInfo = mock(InstanceInfo.class); + when(instanceInfo.getHostName()).thenReturn(ZOSMF_HOSTNAME); + when(instanceInfo.getPort()).thenReturn(ZOSMF_PORT); + + final Application application = mock(Application.class); + when(application.getInstances()).thenReturn(Arrays.asList(instanceInfo)); + when(discoveryClient.getApplication(ZOSMF)).thenReturn(application); + + return EurekaUtils.getUrl(instanceInfo); } @Before @@ -191,19 +224,19 @@ public void shouldExtractJwtFromRequestHeader() { @Test public void shouldReadLtpaTokenFromJwtToken() { String jwtToken = authService.createJwtToken(USER, DOMAIN, LTPA); - assertEquals(LTPA, authService.getLtpaTokenFromJwtToken(jwtToken)); + assertEquals(LTPA, authService.getLtpaTokenWithValidation(jwtToken)); } @Test(expected = TokenNotValidException.class) public void shouldThrowExceptionWhenTokenIsInvalidWhileExtractingLtpa() { String jwtToken = authService.createJwtToken(USER, DOMAIN, LTPA); String brokenToken = jwtToken + "not"; - authService.getLtpaTokenFromJwtToken(brokenToken); + authService.getLtpaTokenWithValidation(brokenToken); } @Test(expected = TokenExpireException.class) public void shouldThrowExceptionWhenTokenIsExpiredWhileExtractingLtpa() { - authService.getLtpaTokenFromJwtToken(createExpiredJwtToken(privateKey)); + authService.getLtpaTokenWithValidation(createExpiredJwtToken(privateKey)); } private String createExpiredJwtToken(Key secretKey) { @@ -229,9 +262,6 @@ private InstanceInfo createInstanceInfo(String instanceId, String hostName, int public void invalidateToken() { TokenAuthentication tokenAuthentication; - reset(discoveryClient); - reset(restTemplate); - String jwt1 = authService.createJwtToken("user1", "domain1", "ltpa1"); assertFalse(authService.isInvalidated(jwt1)); tokenAuthentication = authService.validateJwtToken(jwt1); @@ -242,6 +272,8 @@ public void invalidateToken() { ApplicationInfoManager applicationInfoManager = mock(ApplicationInfoManager.class); when(applicationInfoManager.getInfo()).thenReturn(myInstance); when(discoveryClient.getApplicationInfoManager()).thenReturn(applicationInfoManager); + when(restTemplate.exchange(eq(zosmfUrl + "/zosmf/services/authenticate"), eq(HttpMethod.DELETE), any(), eq(String.class))) + .thenReturn(new ResponseEntity(HttpStatus.OK)); Application application = mock(Application.class); List instances = Arrays.asList( @@ -259,13 +291,12 @@ public void invalidateToken() { verify(restTemplate, times(2)).delete(anyString(), (Object[]) any()); verify(restTemplate).delete("https://hostname1:10433/auth/invalidate/{}", jwt1); verify(restTemplate).delete("http://hostname2:10001/auth/invalidate/{}", jwt1); + verify(restTemplate, times(1)) + .exchange(eq(zosmfUrl + "/zosmf/services/authenticate"), eq(HttpMethod.DELETE), any(), eq(String.class)); } @Test public void invalidateTokenCache() { - reset(jwtSecurityInitializer); - mockJwtSecurityInitializer(); - String jwtToken01 = authService.createJwtToken("user01", "domain01", "ltpa01"); String jwtToken02 = authService.createJwtToken("user02", "domain02", "ltpa02"); @@ -282,14 +313,186 @@ public void invalidateTokenCache() { assertTrue(authService.validateJwtToken(jwtToken02).isAuthenticated()); verify(jwtSecurityInitializer, times(2)).getJwtPublicKey(); + when(restTemplate.exchange(eq(zosmfUrl + "/zosmf/services/authenticate"), eq(HttpMethod.DELETE), any(), eq(String.class))) + .thenReturn(new ResponseEntity(HttpStatus.OK)); authService.invalidateJwtToken(jwtToken01, false); assertTrue(authService.validateJwtToken(jwtToken02).isAuthenticated()); verify(jwtSecurityInitializer, times(2)).getJwtPublicKey(); + verify(restTemplate, times(1)) + .exchange(eq(zosmfUrl + "/zosmf/services/authenticate"), eq(HttpMethod.DELETE), any(), eq(String.class)); assertFalse(authService.validateJwtToken(jwtToken01).isAuthenticated()); verify(jwtSecurityInitializer, times(3)).getJwtPublicKey(); } + private ZosmfServiceV2 getSpiedZosmfService() { + return spy( + new ZosmfServiceV2( + authConfigurationProperties, + discoveryClient, + restTemplate, + securityObjectMapper + ) + ); + } + + private AuthenticationService getSpiedAuthenticationService(ZosmfServiceV2 spiedZosmfService) { + AuthenticationService out = new AuthenticationService( + applicationContext, authConfigurationProperties, jwtSecurityInitializer, + spiedZosmfService, discoveryClient, restTemplate + ); + ReflectionTestUtils.setField(out, "meAsProxy", out); + return spy(out); + } + + @Test + public void invalidateZosmfJwtToken() { + final String token = "zosmfJwtToken"; + final String url = zosmfUrl + "/zosmf/services/authenticate"; + + final ZosmfServiceV2 zosmfService = getSpiedZosmfService(); + final AuthenticationService authService = getSpiedAuthenticationService(zosmfService); + doReturn(new QueryResponse( + "domain", "userId", new Date(), new Date(), QueryResponse.Source.ZOSMF + )).when(authService).parseJwtToken(token); + HttpHeaders headers = new HttpHeaders(); + headers.add("X-CSRF-ZOSMF-HEADER", ""); + headers.add(HttpHeaders.COOKIE, "jwtToken=" + token); + HttpEntity httpEntity = new HttpEntity<>(null, headers); + doReturn(new ResponseEntity<>(HttpStatus.OK)).when(restTemplate) + .exchange(url, HttpMethod.DELETE, httpEntity, String.class); + + assertTrue(authService.invalidateJwtToken(token, false)); + verify(zosmfService, times(1)).invalidate(ZosmfServiceV2.TokenType.JWT, token); + verify(restTemplate, times(1)) + .exchange(url, HttpMethod.DELETE, httpEntity, String.class); + } + + @Test + public void invalidateZosmfLtpaToken() { + final String jwtToken = "zosmfJwtToken"; + final String ltpaToken = "zosmfLtpaToken"; + final String url = zosmfUrl + "/zosmf/services/authenticate"; + + final ZosmfServiceV2 zosmfService = getSpiedZosmfService(); + final AuthenticationService authService = getSpiedAuthenticationService(zosmfService); + doReturn(new QueryResponse( + "domain", "userId", new Date(), new Date(), QueryResponse.Source.ZOWE + )).when(authService).parseJwtToken(jwtToken); + doReturn(ltpaToken).when(authService).getLtpaToken(jwtToken); + HttpHeaders headers = new HttpHeaders(); + headers.add("X-CSRF-ZOSMF-HEADER", ""); + headers.add(HttpHeaders.COOKIE, "LtpaToken2=" + ltpaToken); + HttpEntity httpEntity = new HttpEntity<>(null, headers); + doReturn(new ResponseEntity<>(HttpStatus.OK)).when(restTemplate) + .exchange(url, HttpMethod.DELETE, httpEntity, String.class); + + assertTrue(authService.invalidateJwtToken(jwtToken, false)); + verify(zosmfService, times(1)).invalidate(ZosmfServiceV2.TokenType.LTPA, ltpaToken); + } + + @Test + public void testValidateZosmfJwtToken() { + final String jwtToken = "jwtTokenSource"; + final String userId = "userIdSource"; + final QueryResponse queryResponse = new QueryResponse("domain", userId, new Date(), new Date(), QueryResponse.Source.ZOSMF); + + final ZosmfServiceV2 zosmfService = getSpiedZosmfService(); + final AuthenticationService authService = getSpiedAuthenticationService(zosmfService); + + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + assertEquals(jwtToken, invocation.getArgument(0)); + return queryResponse; + } + }).when(authService).parseJwtToken(jwtToken); + + when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) + .thenReturn(new ResponseEntity(HttpStatus.OK)); + + TokenAuthentication tokenAuthentication = authService.validateJwtToken(jwtToken); + assertTrue(tokenAuthentication.isAuthenticated()); + assertEquals(jwtToken, tokenAuthentication.getCredentials()); + assertEquals(userId, tokenAuthentication.getPrincipal()); + verify(zosmfService, times(1)).validate(ZosmfServiceV2.TokenType.JWT, jwtToken); + } + + @Test + public void testCreateTokenAuthentication() { + Consumer assertTokenAuthentication = x -> { + assertNotNull(x); + assertTrue(x.isAuthenticated()); + assertEquals("userXYZ", x.getPrincipal()); + assertEquals("jwtTokenXYZ", x.getCredentials()); + }; + + TokenAuthentication tokenAuthentication; + + tokenAuthentication = authService.createTokenAuthentication("userXYZ", "jwtTokenXYZ"); + assertTokenAuthentication.accept(tokenAuthentication); + + tokenAuthentication = authService.validateJwtToken("jwtTokenXYZ"); + assertTokenAuthentication.accept(tokenAuthentication); + } + + @Test + public void testGetLtpaTokenException() { + for (String jwtToken : new String[] {"header.body.sign", "wrongJwtToken", ""}) { + try { + authService.getLtpaToken(jwtToken); + fail(); + } catch (TokenNotValidException e) { + assertTrue(e.getMessage().contains("Token is not valid.")); + } + } + } + + @Test + public void testCreateJwtTokenUserExpire() { + String jwt1 = authService.createJwtToken("user", "domain", "ltpaToken"); + String jwt2 = authService.createJwtToken("expire", "domain", "ltpaToken"); + + QueryResponse qr1 = authService.parseJwtToken(jwt1); + QueryResponse qr2 = authService.parseJwtToken(jwt2); + + assertEquals( + qr1.getExpiration().getTime() - authConfigurationProperties.getTokenProperties().getExpirationInSeconds() * 1000, + qr2.getExpiration().getTime() - authConfigurationProperties.getTokenProperties().getShortTtlExpirationInSeconds() * 1000 + ); + } + + @Test + public void testHandleJwtParserException() { + class AuthenticationServiceExceptionHanlderTest extends AuthenticationService { + + AuthenticationServiceExceptionHanlderTest() { + super(null, null, null, null, null, null); + } + + @Override + public RuntimeException handleJwtParserException(RuntimeException re) { + return super.handleJwtParserException(re); + } + + } + + AuthenticationServiceExceptionHanlderTest as = new AuthenticationServiceExceptionHanlderTest(); + Exception exception; + + exception = as.handleJwtParserException(new ExpiredJwtException(mock(Header.class), mock(Claims.class), "msg")); + assertTrue(exception instanceof TokenExpireException); + assertEquals("Token is expired.", exception.getMessage()); + + exception = as.handleJwtParserException(new JwtException("msg")); + assertTrue(exception instanceof TokenNotValidException); + assertEquals("Token is not valid.", exception.getMessage()); + + exception = as.handleJwtParserException(new RuntimeException("msg")); + assertTrue(exception instanceof TokenNotValidException); + assertEquals("An internal error occurred while validating the token therefor the token is no longer valid.", exception.getMessage()); + } + @Configuration public static class Context { @@ -298,7 +501,9 @@ public static class Context { @Bean public AuthConfigurationProperties getAuthConfigurationProperties() { - return new AuthConfigurationProperties(); + final AuthConfigurationProperties authConfigurationProperties = new AuthConfigurationProperties(); + authConfigurationProperties.setZosmfServiceId(ZOSMF); + return authConfigurationProperties; } @Bean @@ -312,13 +517,26 @@ public RestTemplate getRestTemplate() { } @Bean - public EurekaClient getDiscoveryClient() { - return mock(EurekaClient.class); + public DiscoveryClient getDiscoveryClient() { + return mock(DiscoveryClient.class); + } + + @Bean + public ZosmfServiceV2 getZosmfService() { + return new ZosmfServiceV2( + getAuthConfigurationProperties(), + getDiscoveryClient(), + getRestTemplate(), + securityObjectMapper + ); } @Bean public AuthenticationService getAuthenticationService() { - return new AuthenticationService(applicationContext, getAuthConfigurationProperties(), getJwtSecurityInitializer(), getDiscoveryClient(), getRestTemplate()); + return new AuthenticationService( + applicationContext, getAuthConfigurationProperties(), getJwtSecurityInitializer(), + getZosmfService(), getDiscoveryClient(), getRestTemplate() + ); } } diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java index ad1c7777e7..a50b6249de 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/ServiceAuthenticationServiceImplTest.java @@ -155,12 +155,14 @@ public void testGetAuthenticationCommand() throws Exception { // token1 - valid QueryResponse qr1 = new QueryResponse("domain", "userId", Date.valueOf(LocalDate.of(1900, 1, 1)), - Date.valueOf(LocalDate.of(2100, 1, 1)) + Date.valueOf(LocalDate.of(2100, 1, 1)), + QueryResponse.Source.ZOWE ); // token2 - expired QueryResponse qr2 = new QueryResponse("domain", "userId", Date.valueOf(LocalDate.of(1900, 1, 1)), - Date.valueOf(LocalDate.of(2000, 1, 1)) + Date.valueOf(LocalDate.of(2000, 1, 1)), + QueryResponse.Source.ZOWE ); AuthenticationCommand acValid = spy(new AuthenticationCommandTest(false)); AuthenticationCommand acExpired = spy(new AuthenticationCommandTest(true)); diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java index 3201eb50df..daf5ea6145 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/AuthenticationSchemeFactoryTest.java @@ -150,7 +150,7 @@ public void testGetAuthenticationCommand() throws Exception { RequestContext requestContext = new RequestContext(); requestContext.setRequest(request); - QueryResponse qr = new QueryResponse("domain", "userId", new Date(), new Date()); + QueryResponse qr = new QueryResponse("domain", "userId", new Date(), new Date(), QueryResponse.Source.ZOWE); when(as.getJwtTokenFromRequest(request)).thenReturn(Optional.of("jwtToken123")); when(as.getJwtTokenFromRequest(null)).thenReturn(Optional.empty()); when(as.parseJwtToken("jwtToken123")).thenReturn(qr); diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java index a88e0e86a2..723d0ced57 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/HttpBasicPassTicketSchemeTest.java @@ -53,7 +53,7 @@ public void init() { public void testCreateCommand() throws Exception { Calendar calendar = Calendar.getInstance(); Authentication authentication = new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, "applid"); - QueryResponse queryResponse = new QueryResponse("domain", "username", calendar.getTime(), calendar.getTime()); + QueryResponse queryResponse = new QueryResponse("domain", "username", calendar.getTime(), calendar.getTime(), QueryResponse.Source.ZOWE); AuthenticationCommand ac = httpBasicPassTicketScheme.createCommand(authentication, queryResponse); assertNotNull(ac); @@ -69,18 +69,18 @@ public void testCreateCommand() throws Exception { // JWT token expired one minute ago (command expired also if JWT token expired) calendar.add(Calendar.MINUTE, -1); - queryResponse = new QueryResponse("domain", "username", calendar.getTime(), calendar.getTime()); + queryResponse = new QueryResponse("domain", "username", calendar.getTime(), calendar.getTime(), QueryResponse.Source.ZOWE); ac = httpBasicPassTicketScheme.createCommand(authentication, queryResponse); assertTrue(ac.isExpired()); // JWT token will expire in one minute (command expired also if JWT token expired) calendar.add(Calendar.MINUTE, 2); - queryResponse = new QueryResponse("domain", "username", calendar.getTime(), calendar.getTime()); + queryResponse = new QueryResponse("domain", "username", calendar.getTime(), calendar.getTime(), QueryResponse.Source.ZOWE); ac = httpBasicPassTicketScheme.createCommand(authentication, queryResponse); assertFalse(ac.isExpired()); calendar.add(Calendar.MINUTE, 100); - queryResponse = new QueryResponse("domain", "username", calendar.getTime(), calendar.getTime()); + queryResponse = new QueryResponse("domain", "username", calendar.getTime(), calendar.getTime(), QueryResponse.Source.ZOWE); ac = httpBasicPassTicketScheme.createCommand(authentication, queryResponse); calendar = Calendar.getInstance(); @@ -100,7 +100,7 @@ public void getExceptionWhenUserIdNotValid() throws AuthenticationException { Calendar calendar = Calendar.getInstance(); Authentication authentication = new Authentication(AuthenticationScheme.HTTP_BASIC_PASSTICKET, applId); - QueryResponse queryResponse = new QueryResponse("domain", UNKNOWN_USER, calendar.getTime(), calendar.getTime()); + QueryResponse queryResponse = new QueryResponse("domain", UNKNOWN_USER, calendar.getTime(), calendar.getTime(), QueryResponse.Source.ZOWE); exceptionRule.expect(AuthenticationException.class); exceptionRule.expectMessage(String.format("Could not generate PassTicket for user ID %s and APPLID %s", UNKNOWN_USER, applId)); diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZosmfSchemeTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZosmfSchemeTest.java index 021ec10506..959239969d 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZosmfSchemeTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/schema/ZosmfSchemeTest.java @@ -13,6 +13,7 @@ import com.ca.apiml.security.common.auth.AuthenticationScheme; import com.ca.apiml.security.common.token.QueryResponse; import com.ca.apiml.security.common.token.TokenNotValidException; +import com.ca.mfaas.gateway.security.service.AuthenticationException; import com.ca.mfaas.gateway.security.service.AuthenticationService; import com.ca.mfaas.gateway.utils.CleanCurrentRequestContextTest; import com.netflix.zuul.context.RequestContext; @@ -48,7 +49,7 @@ public class ZosmfSchemeTest extends CleanCurrentRequestContextTest { public void testCreateCommand() throws Exception { Calendar calendar = Calendar.getInstance(); Authentication authentication = new Authentication(AuthenticationScheme.ZOSMF, null); - QueryResponse queryResponse = new QueryResponse("domain", "username", calendar.getTime(), calendar.getTime()); + QueryResponse queryResponse = new QueryResponse("domain", "username", calendar.getTime(), calendar.getTime(), QueryResponse.Source.ZOWE); RequestContext requestContext = spy(new RequestContext()); RequestContext.testSetCurrentContext(requestContext); @@ -64,7 +65,8 @@ public void testCreateCommand() throws Exception { // no cookies is set now reset(authenticationService); when(authenticationService.getJwtTokenFromRequest(request)).thenReturn(Optional.of("jwtToken1")); - when(authenticationService.getLtpaTokenFromJwtToken("jwtToken1")).thenReturn("ltpa1"); + when(authenticationService.getLtpaTokenWithValidation("jwtToken1")).thenReturn("ltpa1"); + when(authenticationService.parseJwtToken("jwtToken1")).thenReturn(queryResponse); requestContext.getZuulRequestHeaders().put(COOKIE_HEADER, null); zosmfScheme.createCommand(authentication, queryResponse).apply(null); assertEquals("ltpa1", requestContext.getZuulRequestHeaders().get(COOKIE_HEADER)); @@ -72,7 +74,8 @@ public void testCreateCommand() throws Exception { // a cookies is set now reset(authenticationService); when(authenticationService.getJwtTokenFromRequest(request)).thenReturn(Optional.of("jwtToken2")); - when(authenticationService.getLtpaTokenFromJwtToken("jwtToken2")).thenReturn("ltpa2"); + when(authenticationService.getLtpaTokenWithValidation("jwtToken2")).thenReturn("ltpa2"); + when(authenticationService.parseJwtToken("jwtToken2")).thenReturn(queryResponse); requestContext.getZuulRequestHeaders().put(COOKIE_HEADER, "cookie1=1"); zosmfScheme.createCommand(authentication, queryResponse).apply(null); assertEquals("cookie1=1; ltpa2", requestContext.getZuulRequestHeaders().get(COOKIE_HEADER)); @@ -81,7 +84,8 @@ public void testCreateCommand() throws Exception { try { reset(authenticationService); when(authenticationService.getJwtTokenFromRequest(request)).thenReturn(Optional.of("jwtToken3")); - when(authenticationService.getLtpaTokenFromJwtToken("jwtToken3")).thenThrow(new TokenNotValidException("Token is not valid")); + when(authenticationService.getLtpaTokenWithValidation("jwtToken3")).thenThrow(new TokenNotValidException("Token is not valid")); + when(authenticationService.parseJwtToken("jwtToken3")).thenReturn(queryResponse); zosmfScheme.createCommand(authentication, queryResponse).apply(null); fail(); } catch (TokenNotValidException e) { @@ -92,7 +96,8 @@ public void testCreateCommand() throws Exception { try { reset(authenticationService); when(authenticationService.getJwtTokenFromRequest(request)).thenReturn(Optional.of("jwtToken3")); - when(authenticationService.getLtpaTokenFromJwtToken("jwtToken3")).thenThrow(new JwtException("Token is expired")); + when(authenticationService.getLtpaTokenWithValidation("jwtToken3")).thenThrow(new JwtException("Token is expired")); + when(authenticationService.parseJwtToken("jwtToken3")).thenReturn(queryResponse); zosmfScheme.createCommand(authentication, queryResponse).apply(null); fail(); } catch (JwtException e) { @@ -144,4 +149,19 @@ public void testExpiration() { assertFalse(command.isExpired()); } + @Test + public void testZosmfToken() throws AuthenticationException { + ZosmfScheme scheme = new ZosmfScheme(authenticationService); + QueryResponse queryResponse = new QueryResponse("domain", "username", new Date(), new Date(), QueryResponse.Source.ZOSMF); + when(authenticationService.getJwtTokenFromRequest(any())).thenReturn(Optional.of("jwtTokenZosmf")); + when(authenticationService.parseJwtToken("jwtTokenZosmf")).thenReturn(queryResponse); + + AuthenticationCommand command = scheme.createCommand(new Authentication(AuthenticationScheme.ZOSMF, null), queryResponse); + + command.apply(null); + verify(authenticationService, times(1)).getJwtTokenFromRequest(any()); + verify(authenticationService, times(1)).parseJwtToken("jwtTokenZosmf"); + verify(authenticationService, never()).getLtpaTokenWithValidation("jwtTokenZosmf"); + } + } diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceFacadeTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceFacadeTest.java new file mode 100644 index 0000000000..624df49794 --- /dev/null +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceFacadeTest.java @@ -0,0 +1,230 @@ +package com.ca.mfaas.gateway.security.service.zosmf;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import com.ca.apiml.security.common.config.AuthConfigurationProperties; +import com.ca.mfaas.gateway.security.service.ZosmfService; +import lombok.AllArgsConstructor; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.client.RestTemplate; + +import java.util.Arrays; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@RunWith(SpringJUnit4ClassRunner.class) +public class ZosmfServiceFacadeTest { + + private RestTemplate restTemplate = mock(RestTemplate.class); + private ZosmfServiceFacade zosmfService; + + @Before + public void setUp() { + zosmfService = getZosmfServiceFacade(); + } + + public ZosmfServiceFacade getZosmfServiceFacade() { + AuthConfigurationProperties authConfigurationProperties = mock(AuthConfigurationProperties.class); + ApplicationContext applicationContext = mock(ApplicationContext.class); + + ZosmfServiceFacade out = new ZosmfServiceFacade( + authConfigurationProperties, + null, + restTemplate, + null, + applicationContext, + Arrays.asList( + new ZosmfServiceTest(1), + new ZosmfServiceTest(2) + ) + ) { + @Override + protected String getURI(String zosmf) { + return "http://zosmf:1433"; + } + }; + out = spy(out); + + when(applicationContext.getBean(ZosmfServiceFacade.class)).thenReturn(out); + out.afterPropertiesSet(); + return out; + } + + private void mockVersion(int version) { + ZosmfServiceFacade.ZosmfInfo response = new ZosmfServiceFacade.ZosmfInfo(); + response.setVersion(version); + + when(restTemplate.exchange( + eq("http://zosmf:1433/zosmf/info"), + eq(HttpMethod.GET), + (HttpEntity) any(), + eq(ZosmfServiceFacade.ZosmfInfo.class) + )).thenReturn(new ResponseEntity<>(response, HttpStatus.OK)); + } + + @Test + public void testValidate() { + mockVersion(1); + + try { + zosmfService.validate(ZosmfService.TokenType.JWT, "jwt"); + fail(); + } catch (TestException te) { + assertEquals(1, te.version); + assertEquals("validate", te.method); + assertEquals(ZosmfService.TokenType.JWT, te.arguments[0]); + assertEquals("jwt", te.arguments[1]); + } + + mockVersion(2); + + try { + zosmfService.validate(ZosmfService.TokenType.JWT, "jwt"); + fail(); + } catch (TestException te) { + assertEquals(2, te.version); + } + } + + @Test + public void testInvalidate() { + mockVersion(1); + + try { + zosmfService.invalidate(ZosmfService.TokenType.JWT, "jwt"); + fail(); + } catch (TestException te) { + assertEquals(1, te.version); + assertEquals("invalidate", te.method); + assertEquals(ZosmfService.TokenType.JWT, te.arguments[0]); + assertEquals("jwt", te.arguments[1]); + } + + mockVersion(2); + + try { + zosmfService.invalidate(ZosmfService.TokenType.JWT, "jwt"); + fail(); + } catch (TestException te) { + assertEquals(2, te.version); + } + } + + @Test + public void testAuthenticate() { + mockVersion(1); + + Authentication authentication = new UsernamePasswordAuthenticationToken("user", "pass"); + + try { + zosmfService.authenticate(authentication); + fail(); + } catch (TestException te) { + assertEquals(1, te.version); + assertEquals("authenticate", te.method); + assertSame(authentication, te.arguments[0]); + } + + mockVersion(2); + + try { + zosmfService.authenticate(authentication); + fail(); + } catch (TestException te) { + assertEquals(2, te.version); + } + } + + @Test + public void testValidateException() { + ZosmfServiceFacade.ZosmfInfo response = new ZosmfServiceFacade.ZosmfInfo(); + response.setVersion(1); + + when(restTemplate.exchange( + anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any() + )).thenThrow(new IllegalArgumentException("msg")); + + try { + zosmfService.validate(ZosmfService.TokenType.JWT, "jwt"); + fail(); + } catch (IllegalArgumentException te) { + } + verify(zosmfService, times(1)).evictCaches(); + } + + @Test + public void testUnknownVersion() { + mockVersion(-1); + + try { + zosmfService.validate(ZosmfService.TokenType.JWT, "jwt"); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Unknown version of z/OSMF : -1", e.getMessage()); + } + verify(zosmfService, times(1)).evictCaches(); + } + + @Test + public void testMatchesVersion() { + assertFalse(zosmfService.matchesVersion(1)); + assertFalse(zosmfService.matchesVersion(25)); + } + + @AllArgsConstructor + private static class TestException extends RuntimeException { + + private static final long serialVersionUID = -5481719144373455488L; + + private final String method; + private final int version; + private final Object[] arguments; + + } + + @AllArgsConstructor + private static class ZosmfServiceTest implements ZosmfService { + + private final int version; + + @Override + public AuthenticationResponse authenticate(Authentication authentication) { + throw new TestException("authenticate", version, new Object[] {authentication}); + } + + @Override + public void validate(TokenType type, String token) { + throw new TestException("validate", version, new Object[] {type, token}); + } + + @Override + public void invalidate(TokenType type, String token) { + throw new TestException("invalidate", version, new Object[] {type, token}); + } + + @Override + public boolean matchesVersion(int version) { + return this.version == version; + } + + } + +} diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV1Test.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV1Test.java new file mode 100644 index 0000000000..b5a41240f3 --- /dev/null +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV1Test.java @@ -0,0 +1,209 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gateway.security.service.zosmf; + +import com.ca.apiml.security.common.config.AuthConfigurationProperties; +import com.ca.apiml.security.common.error.ServiceNotAccessibleException; +import com.ca.apiml.security.common.token.TokenNotValidException; +import com.ca.mfaas.gateway.security.service.ZosmfService; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.netflix.appinfo.InstanceInfo; +import com.netflix.discovery.DiscoveryClient; +import com.netflix.discovery.shared.Application; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.http.*; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.util.Arrays; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ZosmfServiceV1Test { + + private static final String ZOSMF_ID = "zosmf"; + + @Mock + private AuthConfigurationProperties authConfigurationProperties; + + @Mock + private DiscoveryClient discovery; + + @Mock + private RestTemplate restTemplate; + + @Spy + private ObjectMapper securityObjectMapper; + + @InjectMocks + private ZosmfServiceV1 zosmfService; + + @Before + public void setUp() { + when(authConfigurationProperties.validatedZosmfServiceId()).thenReturn(ZOSMF_ID); + } + + private void mockZosmfService(String hostname, int port) { + InstanceInfo instanceInfo = mock(InstanceInfo.class); + when(instanceInfo.getHostName()).thenReturn(hostname); + when(instanceInfo.getPort()).thenReturn(port); + Application application = mock(Application.class); + when(application.getInstances()).thenReturn(Arrays.asList(instanceInfo)); + when(discovery.getApplication(ZOSMF_ID)).thenReturn(application); + } + + @Test + public void testAuthenticate() { + mockZosmfService("zosmf", 1433); + + HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.add(HttpHeaders.AUTHORIZATION, "Basic dXNlcjpwYXNz"); + requestHeaders.add("X-CSRF-ZOSMF-HEADER", ""); + + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.add(HttpHeaders.SET_COOKIE, "LtpaToken2=lt"); + ResponseEntity responseEntity = new ResponseEntity("{\"zosmf_saf_realm\":\"domain\"}", responseHeaders, HttpStatus.OK); + + when(restTemplate.exchange( + "http://zosmf:1433/zosmf/info", + HttpMethod.GET, + new HttpEntity<>(null, requestHeaders), + String.class + )).thenReturn(responseEntity); + + ZosmfService.AuthenticationResponse response = zosmfService.authenticate( + new UsernamePasswordAuthenticationToken("user", "pass") + ); + assertNotNull(response); + assertNotNull(response.getTokens()); + assertEquals(1, response.getTokens().size()); + assertEquals("lt", response.getTokens().get(ZosmfService.TokenType.LTPA)); + assertEquals("domain", response.getDomain()); + } + + @Test(expected = AuthenticationServiceException.class) + public void testAuthenticateException() { + mockZosmfService("zosmf", 1433); + when(restTemplate.exchange( + anyString(), + (HttpMethod) any(), + (HttpEntity) any(), + (Class) any() + )).thenThrow(new RestClientException("any exception")); + + zosmfService.validate(ZosmfService.TokenType.LTPA, "anyLtpaToken"); + } + + @Test + public void testValidateValid() { + mockZosmfService("zosmf", 1433); + + HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.add(HttpHeaders.COOKIE, "LtpaToken2=validToken"); + requestHeaders.add("X-CSRF-ZOSMF-HEADER", ""); + + when(restTemplate.exchange( + "http://zosmf:1433/zosmf/info", + HttpMethod.GET, + new HttpEntity<>(null, requestHeaders), + String.class + )).thenReturn(new ResponseEntity<>(HttpStatus.OK)); + + zosmfService.validate(ZosmfService.TokenType.LTPA, "validToken"); + } + + @Test + public void testValidateInvalid() { + mockZosmfService("zosmf", 1433); + + HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.add(HttpHeaders.COOKIE, "LtpaToken2=invalidToken"); + requestHeaders.add("X-CSRF-ZOSMF-HEADER", ""); + + when(restTemplate.exchange( + "http://zosmf:1433/zosmf/info", + HttpMethod.GET, + new HttpEntity<>(null, requestHeaders), + String.class + )).thenReturn(new ResponseEntity<>(HttpStatus.UNAUTHORIZED)); + + try { + zosmfService.validate(ZosmfService.TokenType.LTPA, "invalidToken"); + fail(); + } catch (TokenNotValidException e) { + // access denied - not valid + } + } + + @Test + public void testValidateInternalError() { + mockZosmfService("zosmf", 1433); + + when(restTemplate.exchange( + eq("http://zosmf:1433/zosmf/info"), + eq(HttpMethod.GET), + (HttpEntity) any(), + (Class) any() + )).thenReturn(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); + + try { + zosmfService.validate(ZosmfService.TokenType.LTPA, "invalidToken"); + fail(); + } catch (ServiceNotAccessibleException e) { + // access denied - not valid + } + } + + @Test + public void testValidateRuntimeException() { + mockZosmfService("zosmf", 1433); + + when(restTemplate.exchange( + eq("http://zosmf:1433/zosmf/info"), + eq(HttpMethod.GET), + (HttpEntity) any(), + (Class) any() + )).thenThrow(new IllegalArgumentException("exception")); + + try { + zosmfService.validate(ZosmfService.TokenType.LTPA, "invalidToken"); + fail(); + } catch (IllegalArgumentException e) { + // access denied - not valid + } + } + + @Test + public void testInvalidate() { + ZosmfServiceV1 service = new ZosmfServiceV1(null, null, null, null); + service.invalidate(null, null); + } + + @Test + public void testMatchesVersion() { + assertTrue(zosmfService.matchesVersion(Integer.MIN_VALUE)); + assertTrue(zosmfService.matchesVersion(25)); + assertFalse(zosmfService.matchesVersion(26)); + assertFalse(zosmfService.matchesVersion(Integer.MAX_VALUE)); + } + +} diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV2Test.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV2Test.java new file mode 100644 index 0000000000..443ffb2e5d --- /dev/null +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV2Test.java @@ -0,0 +1,285 @@ +package com.ca.mfaas.gateway.security.service.zosmf;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import com.ca.apiml.security.common.config.AuthConfigurationProperties; +import com.ca.apiml.security.common.error.ServiceNotAccessibleException; +import com.ca.apiml.security.common.token.TokenNotValidException; +import com.ca.mfaas.gateway.security.service.ZosmfService; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.netflix.appinfo.InstanceInfo; +import com.netflix.discovery.DiscoveryClient; +import com.netflix.discovery.shared.Application; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.http.*; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.util.Arrays; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class ZosmfServiceV2Test { + + private static final String ZOSMF_ID = "zosmf"; + + @Mock + private AuthConfigurationProperties authConfigurationProperties; + + @Mock + private DiscoveryClient discovery; + + @Mock + private RestTemplate restTemplate; + + @Spy + private ObjectMapper securityObjectMapper; + + @InjectMocks + private ZosmfServiceV2 zosmfService; + + @Before + public void setUp() { + when(authConfigurationProperties.validatedZosmfServiceId()).thenReturn(ZOSMF_ID); + } + + @Test(expected = ServiceNotAccessibleException.class) + public void testInvalidateNoZosmf() { + zosmfService.invalidate(ZosmfServiceV2.TokenType.JWT, "token"); + } + + private void mockZosmfService(String hostname, int port) { + InstanceInfo instanceInfo = mock(InstanceInfo.class); + when(instanceInfo.getHostName()).thenReturn(hostname); + when(instanceInfo.getPort()).thenReturn(port); + Application application = mock(Application.class); + when(application.getInstances()).thenReturn(Arrays.asList(instanceInfo)); + when(discovery.getApplication(ZOSMF_ID)).thenReturn(application); + } + + @Test + public void testInvalidateJwt() { + mockZosmfService("zosmf", 1433); + + HttpHeaders headers = new HttpHeaders(); + headers.add("X-CSRF-ZOSMF-HEADER", ""); + headers.add(HttpHeaders.COOKIE, "jwtToken=x.y.z"); + + when(restTemplate.exchange( + "http://zosmf:1433/zosmf/services/authenticate", + HttpMethod.DELETE, + new HttpEntity<>(null, headers), + String.class + )).thenReturn(new ResponseEntity(HttpStatus.OK)); + + zosmfService.invalidate(ZosmfServiceV2.TokenType.JWT, "x.y.z"); + verify(restTemplate, times(1)). + exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any()); + } + + @Test + public void testInvalidateLtpa() { + mockZosmfService("zosmf", 1433); + + HttpHeaders headers = new HttpHeaders(); + headers.add("X-CSRF-ZOSMF-HEADER", ""); + headers.add(HttpHeaders.COOKIE, "LtpaToken2=ltpa"); + + when(restTemplate.exchange( + "http://zosmf:1433/zosmf/services/authenticate", + HttpMethod.DELETE, + new HttpEntity<>(null, headers), + String.class + )).thenReturn(new ResponseEntity(HttpStatus.OK)); + + zosmfService.invalidate(ZosmfServiceV2.TokenType.LTPA, "ltpa"); + verify(restTemplate, times(1)). + exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any()); + } + + @Test + public void testInvalidateError500() { + mockZosmfService("zosmf", 1433); + when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) + .thenReturn(new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR)); + + try { + zosmfService.invalidate(ZosmfServiceV2.TokenType.LTPA, "ltpa"); + fail(); + } catch (ServiceNotAccessibleException e) { + assertEquals("Could not get an access to z/OSMF service.", e.getMessage()); + } + } + + @Test + public void testInvalidateException() { + mockZosmfService("zosmf", 1433); + + try { + zosmfService.invalidate(ZosmfServiceV2.TokenType.LTPA, "ltpa"); + fail(); + } catch (NullPointerException e) { + // response is null + } + + when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) + .thenThrow(new ResourceAccessException("msg")); + + try { + zosmfService.invalidate(ZosmfServiceV2.TokenType.LTPA, "ltpa"); + fail(); + } catch (ServiceNotAccessibleException e) { + assertEquals("Could not get an access to z/OSMF service.", e.getMessage()); + } + + reset(restTemplate); + when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) + .thenThrow(new RestClientException("msg")); + try { + zosmfService.invalidate(ZosmfServiceV2.TokenType.LTPA, "ltpa"); + fail(); + } catch (AuthenticationServiceException e) { + assertEquals("A failure occurred when authenticating.", e.getMessage()); + assertTrue(e.getCause() instanceof RestClientException); + } + } + + @Test + public void testValidate() { + mockZosmfService("zosmf", 1433); + + HttpHeaders headers = new HttpHeaders(); + headers.add("X-CSRF-ZOSMF-HEADER", ""); + headers.add(HttpHeaders.COOKIE, "LtpaToken2=ltpa"); + + when(restTemplate.exchange( + "http://zosmf:1433/zosmf/services/authenticate", + HttpMethod.POST, + new HttpEntity<>(null, headers), + String.class + )).thenReturn(new ResponseEntity(HttpStatus.OK)); + + zosmfService.validate(ZosmfServiceV2.TokenType.LTPA, "ltpa");; + } + + @Test + public void testValidateException() { + mockZosmfService("zosmf", 1433); + + try { + zosmfService.validate(ZosmfServiceV2.TokenType.LTPA, "token"); + } catch (NullPointerException ne) { + // response is null + } + + // response 401 + when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) + .thenReturn(new ResponseEntity(HttpStatus.UNAUTHORIZED)); + try { + zosmfService.validate(ZosmfServiceV2.TokenType.LTPA, "token"); + } catch (TokenNotValidException e) { + assertEquals("Token is not valid.", e.getMessage()); + } + + // response 500 + when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) + .thenReturn(new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR)); + try { + zosmfService.validate(ZosmfServiceV2.TokenType.LTPA, "token"); + } catch (ServiceNotAccessibleException e) { + assertEquals("Could not get an access to z/OSMF service.", e.getMessage()); + } + } + + @Test + public void testAuthenticate() { + mockZosmfService("zosmf", 1433); + + HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.add(HttpHeaders.AUTHORIZATION, "Basic dXNlcjpwYXNz"); + requestHeaders.add("X-CSRF-ZOSMF-HEADER", ""); + + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.add(HttpHeaders.SET_COOKIE, "jwtToken=jt"); + responseHeaders.add(HttpHeaders.SET_COOKIE, "LtpaToken2=lt"); + ResponseEntity responseEntity = new ResponseEntity("{\"zosmf_saf_realm\":\"domain\"}", responseHeaders, HttpStatus.OK); + + when(restTemplate.exchange( + "http://zosmf:1433/zosmf/services/authenticate", + HttpMethod.POST, + new HttpEntity<>(null, requestHeaders), + String.class + )).thenReturn(responseEntity); + + ZosmfService.AuthenticationResponse response = zosmfService.authenticate( + new UsernamePasswordAuthenticationToken("user", "pass") + ); + assertNotNull(response); + assertNotNull(response.getTokens()); + assertEquals(2, response.getTokens().size()); + assertEquals("lt", response.getTokens().get(ZosmfService.TokenType.LTPA)); + assertEquals("jt", response.getTokens().get(ZosmfService.TokenType.JWT)); + assertEquals("domain", response.getDomain()); + } + + @Test + public void testAuthenticateToZosmfException() { + mockZosmfService("zosmf", 1433); + + // unsupported runtime exception + when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) + .thenThrow(new IllegalArgumentException("msg")); + try { + zosmfService.validate(ZosmfServiceV2.TokenType.LTPA, "token"); + } catch (IllegalArgumentException e) { + assertEquals("msg", e.getMessage()); + } + + // ResourceAccessException + reset(restTemplate); + when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) + .thenThrow(new ResourceAccessException("msg")); + try { + zosmfService.validate(ZosmfServiceV2.TokenType.LTPA, "token"); + } catch (ServiceNotAccessibleException e) { + assertEquals("Could not get an access to z/OSMF service.", e.getMessage()); + } + + // RestClientException + reset(restTemplate); + when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) + .thenThrow(new RestClientException("msg")); + try { + zosmfService.validate(ZosmfServiceV2.TokenType.LTPA, "token"); + } catch (AuthenticationServiceException e) { + assertEquals("A failure occurred when authenticating.", e.getMessage()); + assertTrue(e.getCause() instanceof RestClientException); + } + } + + @Test + public void testMatchesVersion() { + assertFalse(zosmfService.matchesVersion(Integer.MIN_VALUE)); + assertFalse(zosmfService.matchesVersion(25)); + assertTrue(zosmfService.matchesVersion(26)); + assertTrue(zosmfService.matchesVersion(Integer.MAX_VALUE)); + } + +} diff --git a/security-service-client-spring/src/test/java/com/ca/apiml/security/client/service/GatewaySecurityServiceTest.java b/security-service-client-spring/src/test/java/com/ca/apiml/security/client/service/GatewaySecurityServiceTest.java index a0cac63ca9..e64ecab6a0 100644 --- a/security-service-client-spring/src/test/java/com/ca/apiml/security/client/service/GatewaySecurityServiceTest.java +++ b/security-service-client-spring/src/test/java/com/ca/apiml/security/client/service/GatewaySecurityServiceTest.java @@ -134,7 +134,7 @@ public void doLoginWhenGatewayUnauthorized() { @Test public void doSuccessfulQuery() { - QueryResponse expectedQueryResponse = new QueryResponse("domain", "user", new Date(), new Date()); + QueryResponse expectedQueryResponse = new QueryResponse("domain", "user", new Date(), new Date(), QueryResponse.Source.ZOWE); String uri = String.format("%s://%s%s", gatewayConfigProperties.getScheme(), gatewayConfigProperties.getHostname(), authConfigurationProperties.getGatewayQueryEndpoint()); diff --git a/security-service-client-spring/src/test/java/com/ca/apiml/security/client/token/GatewayTokenProviderTest.java b/security-service-client-spring/src/test/java/com/ca/apiml/security/client/token/GatewayTokenProviderTest.java index be74f55128..19bfea70a6 100644 --- a/security-service-client-spring/src/test/java/com/ca/apiml/security/client/token/GatewayTokenProviderTest.java +++ b/security-service-client-spring/src/test/java/com/ca/apiml/security/client/token/GatewayTokenProviderTest.java @@ -38,7 +38,7 @@ public class GatewayTokenProviderTest { @Test public void shouldAuthenticateValidToken() { - when(gatewaySecurityService.query(VALID_TOKEN)).thenReturn(new QueryResponse(DOMAIN, USER, new Date(), new Date())); + when(gatewaySecurityService.query(VALID_TOKEN)).thenReturn(new QueryResponse(DOMAIN, USER, new Date(), new Date(), QueryResponse.Source.ZOWE)); TokenAuthentication tokenAuthentication = new TokenAuthentication(VALID_TOKEN); Authentication processedAuthentication = gatewayTokenProvider.authenticate(tokenAuthentication); From 426584324359ec60074793fd340694eb5612a91c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Wed, 29 Jan 2020 09:11:05 +0100 Subject: [PATCH 104/122] fix Sonar issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../login/zosmf/ZosmfAuthenticationProvider.java | 4 +--- .../security/service/zosmf/AbstractZosmfService.java | 8 +++----- .../security/service/zosmf/ZosmfServiceV1Test.java | 9 +++++---- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/login/zosmf/ZosmfAuthenticationProvider.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/login/zosmf/ZosmfAuthenticationProvider.java index a82b709df6..4f9b74b3fc 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/login/zosmf/ZosmfAuthenticationProvider.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/login/zosmf/ZosmfAuthenticationProvider.java @@ -13,7 +13,6 @@ import com.ca.mfaas.gateway.security.service.ZosmfService; import com.ca.mfaas.message.log.ApimlLogger; import com.ca.mfaas.product.logging.annotations.InjectApimlLogger; -import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -37,8 +36,7 @@ public class ZosmfAuthenticationProvider implements AuthenticationProvider { public ZosmfAuthenticationProvider( AuthenticationService authenticationService, - ZosmfService zosmfService, - ObjectMapper securityObjectMapper + ZosmfService zosmfService ) { this.authenticationService = authenticationService; this.zosmfService = zosmfService; diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/AbstractZosmfService.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/AbstractZosmfService.java index 7c520e9ba0..00ad54976a 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/AbstractZosmfService.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/AbstractZosmfService.java @@ -66,9 +66,7 @@ protected String getAuthenticationValue(Authentication authentication) { final String password = authentication.getCredentials().toString(); final String credentials = user + ":" + password; - final String authorization = "Basic " + Base64.getEncoder().encodeToString(credentials.getBytes()); - - return authorization; + return "Basic " + Base64.getEncoder().encodeToString(credentials.getBytes()); } /** @@ -89,7 +87,7 @@ protected String getURI(String zosmf) { .stream() .filter(Objects::nonNull) .findFirst() - .map(zosmfInstance -> EurekaUtils.getUrl(zosmfInstance)) + .map(EurekaUtils::getUrl) .orElseThrow(authenticationServiceExceptionSupplier); } @@ -153,7 +151,7 @@ protected String readTokenFromCookie(List cookies, String cookieName) { protected AuthenticationResponse getAuthenticationResponse(ResponseEntity responseEntity) { final List cookies = responseEntity.getHeaders().get(HttpHeaders.SET_COOKIE); - final Map tokens = new HashMap<>(); + final EnumMap tokens = new EnumMap<>(TokenType.class); for (final TokenType tokenType : TokenType.values()) { final String token = readTokenFromCookie(cookies, tokenType.getCookieName()); if (token != null) tokens.put(tokenType, token); diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV1Test.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV1Test.java index b5a41240f3..ca01748b21 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV1Test.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV1Test.java @@ -30,7 +30,7 @@ import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; -import java.util.Arrays; +import java.util.Collections; import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.*; @@ -67,7 +67,7 @@ private void mockZosmfService(String hostname, int port) { when(instanceInfo.getHostName()).thenReturn(hostname); when(instanceInfo.getPort()).thenReturn(port); Application application = mock(Application.class); - when(application.getInstances()).thenReturn(Arrays.asList(instanceInfo)); + when(application.getInstances()).thenReturn(Collections.singletonList(instanceInfo)); when(discovery.getApplication(ZOSMF_ID)).thenReturn(application); } @@ -81,7 +81,7 @@ public void testAuthenticate() { HttpHeaders responseHeaders = new HttpHeaders(); responseHeaders.add(HttpHeaders.SET_COOKIE, "LtpaToken2=lt"); - ResponseEntity responseEntity = new ResponseEntity("{\"zosmf_saf_realm\":\"domain\"}", responseHeaders, HttpStatus.OK); + ResponseEntity responseEntity = new ResponseEntity<>("{\"zosmf_saf_realm\":\"domain\"}", responseHeaders, HttpStatus.OK); when(restTemplate.exchange( "http://zosmf:1433/zosmf/info", @@ -106,7 +106,7 @@ public void testAuthenticateException() { when(restTemplate.exchange( anyString(), (HttpMethod) any(), - (HttpEntity) any(), + (HttpEntity) any(), (Class) any() )).thenThrow(new RestClientException("any exception")); @@ -196,6 +196,7 @@ public void testValidateRuntimeException() { public void testInvalidate() { ZosmfServiceV1 service = new ZosmfServiceV1(null, null, null, null); service.invalidate(null, null); + assertNull(service.restTemplate); } @Test From 7df261acc0768e9c1157cca4c8396bae8d5de860 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Fri, 31 Jan 2020 17:29:22 +0100 Subject: [PATCH 105/122] merge with master MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../common/token/QueryResponseTest.java | 3 +- .../ApplicationConfiguration.java | 1 - .../configuration/WebSecurityConfig.java | 32 ------------------- .../gateway/routing/ApimlRoutingConfig.java | 9 ++---- .../security/service/ZosmfService.java | 4 +-- .../service/zosmf/AbstractZosmfService.java | 6 ++-- .../service/zosmf/ZosmfServiceFacade.java | 11 ++++--- .../service/zosmf/ZosmfServiceV1.java | 4 +-- .../service/zosmf/ZosmfServiceV2.java | 6 ++-- ...RibbonLoadBalancingHttpClientImplTest.java | 1 + .../ZosmfAuthenticationProviderTest.java | 26 +++++++-------- .../query/SuccessfulQueryHandlerTest.java | 4 --- .../service/zosmf/ZosmfServiceV1Test.java | 2 +- 13 files changed, 36 insertions(+), 73 deletions(-) delete mode 100644 discoverable-client/src/main/java/com/ca/mfaas/client/configuration/WebSecurityConfig.java diff --git a/apiml-security-common/src/test/java/com/ca/apiml/security/common/token/QueryResponseTest.java b/apiml-security-common/src/test/java/com/ca/apiml/security/common/token/QueryResponseTest.java index dc1b77d0d6..df7b58b776 100644 --- a/apiml-security-common/src/test/java/com/ca/apiml/security/common/token/QueryResponseTest.java +++ b/apiml-security-common/src/test/java/com/ca/apiml/security/common/token/QueryResponseTest.java @@ -13,8 +13,7 @@ import java.util.Calendar; import java.util.Date; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; public class QueryResponseTest { diff --git a/discoverable-client/src/main/java/com/ca/mfaas/client/configuration/ApplicationConfiguration.java b/discoverable-client/src/main/java/com/ca/mfaas/client/configuration/ApplicationConfiguration.java index 0b7ed09ca1..d930023246 100644 --- a/discoverable-client/src/main/java/com/ca/mfaas/client/configuration/ApplicationConfiguration.java +++ b/discoverable-client/src/main/java/com/ca/mfaas/client/configuration/ApplicationConfiguration.java @@ -19,7 +19,6 @@ public class ApplicationConfiguration { @Bean public MessageService messageService() { MessageService messageService = YamlMessageServiceInstance.getInstance(); - messageService.loadMessages("/common-log-messages.yml"); messageService.loadMessages("/api-messages.yml"); messageService.loadMessages("/log-messages.yml"); return messageService; diff --git a/discoverable-client/src/main/java/com/ca/mfaas/client/configuration/WebSecurityConfig.java b/discoverable-client/src/main/java/com/ca/mfaas/client/configuration/WebSecurityConfig.java deleted file mode 100644 index d2260a4ad2..0000000000 --- a/discoverable-client/src/main/java/com/ca/mfaas/client/configuration/WebSecurityConfig.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ - -package com.ca.mfaas.client.configuration; - -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.builders.WebSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.stereotype.Component; - -@Component -@EnableWebSecurity -public class WebSecurityConfig extends WebSecurityConfigurerAdapter { - - @Override - public void configure(WebSecurity web) throws Exception { - web.ignoring().anyRequest(); - } - - @Override - protected void configure(HttpSecurity http) throws Exception { - http.authorizeRequests().anyRequest().permitAll(); - } -} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/routing/ApimlRoutingConfig.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/routing/ApimlRoutingConfig.java index 72954b3ccf..1f3413ae0d 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/routing/ApimlRoutingConfig.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/routing/ApimlRoutingConfig.java @@ -39,7 +39,9 @@ public LocationFilter locationFilter() { } @Bean - public EncodedCharactersFilter encodedCharactersFilterFilter(DiscoveryClient discovery) { return new EncodedCharactersFilter(discovery); } + public EncodedCharactersFilter encodedCharactersFilterFilter(DiscoveryClient discovery) { + return new EncodedCharactersFilter(discovery); + } @Bean public SlashFilter slashFilter() { @@ -51,11 +53,6 @@ public ServiceAuthenticationFilter serviceAuthenticationFilter() { return new ServiceAuthenticationFilter(); } - @Bean - public ServiceAuthenticationFilter serviceAuthenticationFilter() { - return new ServiceAuthenticationFilter(); - } - @Bean @Autowired public PageRedirectionFilter pageRedirectionFilter(DiscoveryClient discovery, diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ZosmfService.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ZosmfService.java index f95185c539..68101b6b1b 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ZosmfService.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/ZosmfService.java @@ -61,8 +61,8 @@ public interface ZosmfService { @Getter public enum TokenType { - JWT("jwtToken"), - LTPA("LtpaToken2") + JWT("jwtToken"), + LTPA("LtpaToken2") ; diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/AbstractZosmfService.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/AbstractZosmfService.java index 00ad54976a..0c831cfc80 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/AbstractZosmfService.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/AbstractZosmfService.java @@ -42,18 +42,18 @@ public abstract class AbstractZosmfService implements ZosmfService { protected final AuthConfigurationProperties authConfigurationProperties; protected final DiscoveryClient discovery; - protected final RestTemplate restTemplate; + protected final RestTemplate restTemplateWithoutKeystore; protected final ObjectMapper securityObjectMapper; public AbstractZosmfService( AuthConfigurationProperties authConfigurationProperties, DiscoveryClient discovery, - RestTemplate restTemplate, + RestTemplate restTemplateWithoutKeystore, ObjectMapper securityObjectMapper ) { this.authConfigurationProperties = authConfigurationProperties; this.discovery = discovery; - this.restTemplate = restTemplate; + this.restTemplateWithoutKeystore = restTemplateWithoutKeystore; this.securityObjectMapper = securityObjectMapper; } diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceFacade.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceFacade.java index 8b285172ac..7bee0a1001 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceFacade.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceFacade.java @@ -16,6 +16,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.discovery.DiscoveryClient; import lombok.Data; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.context.ApplicationContext; @@ -54,7 +55,7 @@ public class ZosmfServiceFacade extends AbstractZosmfService { public ZosmfServiceFacade( final AuthConfigurationProperties authConfigurationProperties, final DiscoveryClient discovery, - final RestTemplate restTemplate, + final @Qualifier("restTemplateWithoutKeystore") RestTemplate restTemplateWithoutKeystore, final ObjectMapper securityObjectMapper, final ApplicationContext applicationContext, final List implementations @@ -62,7 +63,7 @@ public ZosmfServiceFacade( super( authConfigurationProperties, discovery, - restTemplate, + restTemplateWithoutKeystore, securityObjectMapper ); this.applicationContext = applicationContext; @@ -86,10 +87,12 @@ public int getVersion(String zosmfServiceId) { headers.add(ZOSMF_CSRF_HEADER, ""); try { - final ResponseEntity info = restTemplate.exchange( + final ResponseEntity info = restTemplateWithoutKeystore.exchange( url, HttpMethod.GET, new HttpEntity<>(headers), ZosmfInfo.class ); - return info.getBody().getVersion(); + final ZosmfInfo body = info.getBody(); + if (body == null) return 0; + return body.getVersion(); } catch (RuntimeException re) { meProxy.evictCaches(); throw handleExceptionOnCall(url, re); diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV1.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV1.java index 41fa9b094d..cc56c1360e 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV1.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV1.java @@ -42,7 +42,7 @@ public AuthenticationResponse authenticate(Authentication authentication) { headers.add(ZOSMF_CSRF_HEADER, ""); try { - final ResponseEntity responseEntity = restTemplate.exchange( + final ResponseEntity responseEntity = restTemplateWithoutKeystore.exchange( url, HttpMethod.GET, new HttpEntity<>(null, headers), @@ -62,7 +62,7 @@ public void validate(TokenType type, String token) { headers.add(HttpHeaders.COOKIE, type.getCookieName() + "=" + token); try { - ResponseEntity response = restTemplate.exchange( + ResponseEntity response = restTemplateWithoutKeystore.exchange( url, HttpMethod.GET, new HttpEntity<>(null, headers), diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV2.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV2.java index 9205e039f4..c839bedce9 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV2.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV2.java @@ -47,7 +47,7 @@ public AuthenticationResponse authenticate(Authentication authentication) { headers.add(ZOSMF_CSRF_HEADER, ""); try { - final ResponseEntity response = restTemplate.exchange( + final ResponseEntity response = restTemplateWithoutKeystore.exchange( url, HttpMethod.POST, new HttpEntity<>(null, headers), @@ -67,7 +67,7 @@ public void validate(ZosmfService.TokenType type, String token) { headers.add(HttpHeaders.COOKIE, type.getCookieName() + "=" + token); try { - ResponseEntity re = restTemplate.exchange( + ResponseEntity re = restTemplateWithoutKeystore.exchange( url , HttpMethod.POST, new HttpEntity<>(null, headers), @@ -93,7 +93,7 @@ public void invalidate(ZosmfService.TokenType type, String token) { headers.add(HttpHeaders.COOKIE, type.getCookieName() + "=" + token); try { - ResponseEntity re = restTemplate.exchange( + ResponseEntity re = restTemplateWithoutKeystore.exchange( url, HttpMethod.DELETE, new HttpEntity<>(null, headers), diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClientImplTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClientImplTest.java index 06bec3883c..63e39c402c 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClientImplTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/ribbon/GatewayRibbonLoadBalancingHttpClientImplTest.java @@ -21,6 +21,7 @@ import com.netflix.loadbalancer.reactive.ExecutionInfo; import com.netflix.loadbalancer.reactive.ExecutionListener; import com.netflix.loadbalancer.reactive.LoadBalancerCommand; +import com.netflix.niws.loadbalancer.DiscoveryEnabledServer; import com.netflix.zuul.context.RequestContext; import lombok.Getter; import org.apache.http.impl.client.CloseableHttpClient; diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java index 3b71711a8a..7ec4a78c7a 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java @@ -79,9 +79,9 @@ public void setUp() { restTemplate = mock(RestTemplate.class); zosmfInstance = createInstanceInfo(SERVICE_ID, HOST, PORT); - doAnswer(new Answer() { + doAnswer(new Answer() { @Override - public Object answer(InvocationOnMock invocation) throws Throwable { + public TokenAuthentication answer(InvocationOnMock invocation) { return TokenAuthentication.createAuthenticated(invocation.getArgument(0), invocation.getArgument(1)); } }).when(authenticationService).createTokenAuthentication(anyString(), anyString()); @@ -120,7 +120,7 @@ public void loginWithExistingUser() { ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); ZosmfAuthenticationProvider zosmfAuthenticationProvider = - new ZosmfAuthenticationProvider(authenticationService, zosmfService, mapper); + new ZosmfAuthenticationProvider(authenticationService, zosmfService); Authentication tokenAuthentication = zosmfAuthenticationProvider.authenticate(usernamePasswordAuthentication); @@ -145,7 +145,7 @@ public void loginWithBadUser() { ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); ZosmfAuthenticationProvider zosmfAuthenticationProvider = - new ZosmfAuthenticationProvider(authenticationService, zosmfService, mapper); + new ZosmfAuthenticationProvider(authenticationService, zosmfService); exception.expect(BadCredentialsException.class); exception.expectMessage("Username or password are invalid."); @@ -162,7 +162,7 @@ public void noZosmfInstance() { ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); ZosmfAuthenticationProvider zosmfAuthenticationProvider = - new ZosmfAuthenticationProvider(authenticationService, zosmfService, mapper); + new ZosmfAuthenticationProvider(authenticationService, zosmfService); exception.expect(ServiceNotAccessibleException.class); exception.expectMessage("z/OSMF instance not found or incorrectly configured."); @@ -174,7 +174,7 @@ public void noZosmfInstance() { public void noZosmfServiceId() { ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); ZosmfAuthenticationProvider zosmfAuthenticationProvider = - new ZosmfAuthenticationProvider(authenticationService, zosmfService, mapper); + new ZosmfAuthenticationProvider(authenticationService, zosmfService); exception.expect(AuthenticationServiceException.class); exception.expectMessage("The parameter 'zosmfServiceId' is not configured."); @@ -200,7 +200,7 @@ public void notValidZosmfResponse() { ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); ZosmfAuthenticationProvider zosmfAuthenticationProvider = - new ZosmfAuthenticationProvider(authenticationService, zosmfService, mapper); + new ZosmfAuthenticationProvider(authenticationService, zosmfService); exception.expect(AuthenticationServiceException.class); exception.expectMessage("z/OSMF domain cannot be read."); @@ -228,7 +228,7 @@ public void noDomainInResponse() { ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); ZosmfAuthenticationProvider zosmfAuthenticationProvider = - new ZosmfAuthenticationProvider(authenticationService, zosmfService, mapper); + new ZosmfAuthenticationProvider(authenticationService, zosmfService); exception.expect(AuthenticationServiceException.class); exception.expectMessage("z/OSMF domain cannot be read."); @@ -255,7 +255,7 @@ public void invalidCookieInResponse() { ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); ZosmfAuthenticationProvider zosmfAuthenticationProvider = - new ZosmfAuthenticationProvider(authenticationService, zosmfService, mapper); + new ZosmfAuthenticationProvider(authenticationService, zosmfService); exception.expect(BadCredentialsException.class); exception.expectMessage("Username or password are invalid."); @@ -282,7 +282,7 @@ public void cookieWithSemicolon() { ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); ZosmfAuthenticationProvider zosmfAuthenticationProvider = - new ZosmfAuthenticationProvider(authenticationService, zosmfService, mapper); + new ZosmfAuthenticationProvider(authenticationService, zosmfService); Authentication tokenAuthentication = zosmfAuthenticationProvider.authenticate(usernamePasswordAuthentication); @@ -304,7 +304,7 @@ public void shouldThrowNewExceptionIfRestClientException() { .thenThrow(RestClientException.class); ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); ZosmfAuthenticationProvider zosmfAuthenticationProvider = - new ZosmfAuthenticationProvider(authenticationService, zosmfService, mapper); + new ZosmfAuthenticationProvider(authenticationService, zosmfService); exception.expect(AuthenticationServiceException.class); exception.expectMessage("A failure occurred when authenticating."); @@ -326,7 +326,7 @@ public void shouldThrowNewExceptionIfResourceAccessException() { .thenThrow(ResourceAccessException.class); ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); ZosmfAuthenticationProvider zosmfAuthenticationProvider = - new ZosmfAuthenticationProvider(authenticationService, zosmfService, mapper); + new ZosmfAuthenticationProvider(authenticationService, zosmfService); exception.expect(ServiceNotAccessibleException.class); exception.expectMessage("Could not get an access to z/OSMF service."); @@ -343,7 +343,7 @@ public void shouldReturnTrueWhenSupportMethodIsCalledWithCorrectClass() { when(discovery.getApplication(ZOSMF)).thenReturn(application); ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); ZosmfAuthenticationProvider zosmfAuthenticationProvider = - new ZosmfAuthenticationProvider(authenticationService, zosmfService, mapper); + new ZosmfAuthenticationProvider(authenticationService, zosmfService); boolean supports = zosmfAuthenticationProvider.supports(usernamePasswordAuthentication.getClass()); assertTrue(supports); diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/query/SuccessfulQueryHandlerTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/query/SuccessfulQueryHandlerTest.java index acef7b6881..fc15a0dbbd 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/query/SuccessfulQueryHandlerTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/query/SuccessfulQueryHandlerTest.java @@ -32,7 +32,6 @@ import java.security.Key; import java.security.KeyPair; -import java.security.PublicKey; import static org.junit.Assert.*; import static org.mockito.Mockito.when; @@ -69,10 +68,8 @@ public void setup() { SignatureAlgorithm algorithm = SignatureAlgorithm.RS256; KeyPair keyPair = SecurityUtils.generateKeyPair("RSA", 2048); Key privateKey = null; - PublicKey publicKey = null; if (keyPair != null) { privateKey = keyPair.getPrivate(); - publicKey = keyPair.getPublic(); } ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discoveryClient, restTemplate, new ObjectMapper()); AuthenticationService authenticationService = new AuthenticationService( @@ -80,7 +77,6 @@ public void setup() { ); when(jwtSecurityInitializer.getSignatureAlgorithm()).thenReturn(algorithm); when(jwtSecurityInitializer.getJwtSecret()).thenReturn(privateKey); - when(jwtSecurityInitializer.getJwtPublicKey()).thenReturn(publicKey); jwtToken = authenticationService.createJwtToken(USER, DOMAIN, LTPA); diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV1Test.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV1Test.java index ca01748b21..b6682da76a 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV1Test.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV1Test.java @@ -196,7 +196,7 @@ public void testValidateRuntimeException() { public void testInvalidate() { ZosmfServiceV1 service = new ZosmfServiceV1(null, null, null, null); service.invalidate(null, null); - assertNull(service.restTemplate); + assertNull(service.restTemplateWithoutKeystore); } @Test From 2fbd4556e201d5d1895170e59b72126905d25df1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Mon, 3 Feb 2020 10:49:36 +0100 Subject: [PATCH 106/122] improve test coverage and remove duplicities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../common/service/PassTicketService.java | 129 ------------ .../common/service/PassTicketServiceTest.java | 198 ------------------ .../service/zosmf/ZosmfServiceFacade.java | 4 +- .../ZosmfAuthenticationProviderTest.java | 42 +++- .../service/zosmf/ZosmfServiceFacadeTest.java | 81 ++++++- 5 files changed, 112 insertions(+), 342 deletions(-) delete mode 100644 apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java delete mode 100644 apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java diff --git a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java b/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java deleted file mode 100644 index b8b32624eb..0000000000 --- a/apiml-security-common/src/main/java/com/ca/apiml/security/common/service/PassTicketService.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ -package com.ca.apiml.security.common.service; - -import com.ca.mfaas.util.ClassOrDefaultProxyUtils; -import com.ca.mfaas.util.ObjectUtil; -import lombok.AllArgsConstructor; -import lombok.Value; -import org.apache.commons.lang.StringUtils; -import org.springframework.stereotype.Service; - -import javax.annotation.PostConstruct; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -/** - * This method allow to get a PassTicket from SAF. - */ -@Service -public class PassTicketService { - - private IRRPassTicket irrPassTicket; - - @PostConstruct - public void init() { - this.irrPassTicket = ClassOrDefaultProxyUtils.createProxy(IRRPassTicket.class, - "com.ibm.eserver.zos.racf.IRRPassTicket", DefaultPassTicketImpl::new, - new ClassOrDefaultProxyUtils.ByMethodName<>( - "com.ibm.eserver.zos.racf.IRRPassTicketEvaluationException", - IRRPassTicketEvaluationException.class, "getSafRc", "getRacfRc", "getRacfRsn"), - new ClassOrDefaultProxyUtils.ByMethodName<>( - "com.ibm.eserver.zos.racf.IRRPassTicketGenerationException", - IRRPassTicketGenerationException.class, "getSafRc", "getRacfRc", "getRacfRsn")); - } - - public void evaluate(String userId, String applId, String passTicket) throws IRRPassTicketEvaluationException { - irrPassTicket.evaluate(userId, applId, passTicket); - } - - public String generate(String userId, String applId) throws IRRPassTicketGenerationException { - return irrPassTicket.generate(userId, applId); - } - - public boolean isUsingSafImplementation() { - ClassOrDefaultProxyUtils.ClassOrDefaultProxyState stateInterface = (ClassOrDefaultProxyUtils.ClassOrDefaultProxyState) irrPassTicket; - return stateInterface.isUsingBaseImplementation(); - } - - public static class DefaultPassTicketImpl implements IRRPassTicket { - - private static int id = 0; - - public static final String ZOWE_DUMMY_USERID = "user"; - public static final String ZOWE_DUMMY_PASS_TICKET_PREFIX = "ZoweDummyPassTicket"; - - public static final String DUMMY_USER = "user"; - public static final String UNKNOWN_USER = "unknownUser"; - public static final String UNKNOWN_APPLID = "XBADAPPL"; - - private final Map> userAppToPasstickets = new HashMap<>(); - - @Override - public void evaluate(String userId, String applId, String passTicket) throws IRRPassTicketEvaluationException { - ObjectUtil.requireNotNull(userId, "Parameter userId is empty"); - ObjectUtil.requireNotNull(applId, "Parameter applId is empty"); - ObjectUtil.requireNotNull(passTicket, "Parameter passTicket is empty"); - - if (StringUtils.equalsIgnoreCase(UNKNOWN_APPLID, applId)) { - throw new IRRPassTicketEvaluationException(AbstractIRRPassTicketException.ErrorCode.ERR_8_16_28); - } - - if (userId.equals(ZOWE_DUMMY_USERID) && passTicket.startsWith(ZOWE_DUMMY_PASS_TICKET_PREFIX)) { - return; - } - - final Set passTickets = userAppToPasstickets.get(new UserApp(userId, applId)); - - if ((passTickets == null) || !passTickets.contains(passTicket)) { - throw new IRRPassTicketEvaluationException(AbstractIRRPassTicketException.ErrorCode.ERR_8_16_32); - } - } - - @Override - public String generate(String userId, String applId) throws IRRPassTicketGenerationException { - if (StringUtils.equalsIgnoreCase(UNKNOWN_USER, userId)) { - throw new IRRPassTicketGenerationException(AbstractIRRPassTicketException.ErrorCode.ERR_8_8_16); - } - - if (StringUtils.equalsIgnoreCase(UNKNOWN_APPLID, applId)) { - throw new IRRPassTicketGenerationException(AbstractIRRPassTicketException.ErrorCode.ERR_8_16_28); - } - - if (StringUtils.equalsIgnoreCase(DUMMY_USER, userId)) { - return ZOWE_DUMMY_PASS_TICKET_PREFIX; - } - - final UserApp userApp = new UserApp(userId, applId); - final int currentId; - synchronized (DefaultPassTicketImpl.class) { - currentId = DefaultPassTicketImpl.id++; - } - final String passTicket = ZOWE_DUMMY_PASS_TICKET_PREFIX + "_" + applId + "_" + userId + "_" + currentId; - - final Set passTickets = userAppToPasstickets.computeIfAbsent(userApp, x -> new HashSet<>()); - passTickets.add(passTicket); - - return passTicket; - } - - @AllArgsConstructor - @Value - private static class UserApp { - - private final String userId; - private final String applId; - - } - } - -} diff --git a/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java b/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java deleted file mode 100644 index 98712a5494..0000000000 --- a/apiml-security-common/src/test/java/com/ca/apiml/security/common/service/PassTicketServiceTest.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * This program and the accompanying materials are made available under the terms of the - * Eclipse Public License v2.0 which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-v20.html - * - * SPDX-License-Identifier: EPL-2.0 - * - * Copyright Contributors to the Zowe Project. - */ -package com.ca.apiml.security.common.service; - -import com.ca.mfaas.util.ClassOrDefaultProxyUtils; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.util.ReflectionTestUtils; - -import static com.ca.apiml.security.common.service.PassTicketService.DefaultPassTicketImpl.*; -import static org.junit.Assert.*; - -@RunWith(SpringRunner.class) -@ContextConfiguration -public class PassTicketServiceTest { - - private static final String TEST_USERID = "userId"; - - @Autowired - private PassTicketService passTicketService; - - private static String evaluated; - - @Test - public void testIsUsingSafImplementation() { - IRRPassTicket irrPassTicket = (IRRPassTicket) ReflectionTestUtils.getField(passTicketService, "irrPassTicket"); - ClassOrDefaultProxyUtils.ClassOrDefaultProxyState stateInterface = (ClassOrDefaultProxyUtils.ClassOrDefaultProxyState) irrPassTicket; - assertEquals(stateInterface.isUsingBaseImplementation(), passTicketService.isUsingSafImplementation()); - } - - @Test - public void testInit() throws IRRPassTicketEvaluationException, IRRPassTicketGenerationException { - PassTicketService passTicketService = new PassTicketService(); - ReflectionTestUtils.setField(passTicketService, "irrPassTicket", new IRRPassTicket() { - @Override - public void evaluate(String userId, String applId, String passTicket) { - evaluated = userId + "-" + applId + "-" + passTicket; - } - - @Override - public String generate(String userId, String applId) { - return userId + "-" + applId; - } - }); - - evaluated = null; - passTicketService.evaluate("userId", "applId", "passTicket"); - assertEquals("userId-applId-passTicket", evaluated); - passTicketService.evaluate("1", "2", "3"); - assertEquals("1-2-3", evaluated); - - assertEquals("userId-applId", passTicketService.generate("userId", "applId")); - assertEquals("1-2", passTicketService.generate("1", "2")); - } - - @Test - public void testProxy() throws IRRPassTicketGenerationException { - IRRPassTicket irrPassTicket = ClassOrDefaultProxyUtils.createProxy( - IRRPassTicket.class, - "notExistingClass", - Impl::new - ); - - try { - irrPassTicket.evaluate(TEST_USERID, "applId", "passTicket"); - fail(); - } catch (Exception e) { - assertEquals("Dummy implementation of evaluate : userId x applId x passTicket", e.getMessage()); - } - - assertEquals("success", irrPassTicket.generate(TEST_USERID, "applId")); - } - - @Test - public void testDefaultPassTicketImpl() throws IRRPassTicketEvaluationException, IRRPassTicketGenerationException { - PassTicketService.DefaultPassTicketImpl dpti = new PassTicketService.DefaultPassTicketImpl(); - - try { - dpti.evaluate(TEST_USERID, "applId", "passticket"); - fail(); - } catch (IRRPassTicketEvaluationException e) { - assertEquals(8, e.getSafRc()); - assertEquals(16, e.getRacfRc()); - assertEquals(32, e.getRacfRsn()); - } - - String passTicket1 = dpti.generate(TEST_USERID, "applId"); - String passTicket2 = dpti.generate(TEST_USERID, "applId"); - - assertNotNull(passTicket1); - assertNotNull(passTicket2); - assertNotEquals(passTicket1, passTicket2); - - dpti.evaluate(TEST_USERID, "applId", passTicket1); - dpti.evaluate(TEST_USERID, "applId", passTicket2); - - try { - dpti.evaluate(TEST_USERID, "applId", "wrongPassTicket"); - fail(); - } catch (IRRPassTicketEvaluationException e) { - assertEquals(AbstractIRRPassTicketException.ErrorCode.ERR_8_16_32, e.getErrorCode()); - } - - try { - dpti.evaluate("anyUser", UNKNOWN_APPLID, "anyPassTicket"); - fail(); - } catch (IRRPassTicketEvaluationException e) { - assertEquals(AbstractIRRPassTicketException.ErrorCode.ERR_8_16_28, e.getErrorCode()); - } - - dpti.evaluate(ZOWE_DUMMY_USERID, "anyApplId", ZOWE_DUMMY_PASS_TICKET_PREFIX); - dpti.evaluate(ZOWE_DUMMY_USERID, "anyApplId", ZOWE_DUMMY_PASS_TICKET_PREFIX + "xyz"); - - try { - dpti.evaluate("unknownUser", "anyApplId", ZOWE_DUMMY_PASS_TICKET_PREFIX); - fail(); - } catch (IRRPassTicketEvaluationException e) { - } - try { - dpti.evaluate(ZOWE_DUMMY_USERID, "anyApplId", "wrongPassticket"); - fail(); - } catch (IRRPassTicketEvaluationException e) { - } - - try { - dpti.evaluate("userx", "applId", passTicket1); - fail(); - } catch (IRRPassTicketEvaluationException e) { - // different user, should throw exception - } - - try { - dpti.evaluate(TEST_USERID, "applIdx", passTicket1); - fail(); - } catch (IRRPassTicketEvaluationException e) { - // different applId, should throw exception - } - - try { - dpti.generate(PassTicketService.DefaultPassTicketImpl.UNKNOWN_USER, "anyApplId"); - fail(); - } catch (IRRPassTicketGenerationException e) { - assertEquals(8, e.getSafRc()); - assertEquals(8, e.getRacfRc()); - assertEquals(16, e.getRacfRsn()); - assertNotNull(e.getErrorCode()); - assertEquals(AbstractIRRPassTicketException.ErrorCode.ERR_8_8_16, e.getErrorCode()); - assertEquals( - "Error on generation of PassTicket: Not authorized to use this service." - , e.getMessage() - ); - } - - assertEquals(ZOWE_DUMMY_PASS_TICKET_PREFIX, dpti.generate(DUMMY_USER, "anyApplid")); - - try { - dpti.generate("anyUser", UNKNOWN_APPLID); - fail(); - } catch (IRRPassTicketGenerationException e) { - assertEquals(8, e.getSafRc()); - assertEquals(16, e.getRacfRc()); - assertEquals(28, e.getRacfRsn()); - assertNotNull(e.getErrorCode()); - assertEquals(AbstractIRRPassTicketException.ErrorCode.ERR_8_16_28, e.getErrorCode()); - } - } - - public static class Impl implements IRRPassTicket { - @Override - public void evaluate(String userId, String applId, String passTicket) { - throw new RuntimeException("Dummy implementation of evaluate : " + userId + " x " + applId + " x " + passTicket); - } - - @Override - public String generate(String userId, String applId) { - return "success"; - } - } - - @Configuration - @ComponentScan(basePackageClasses = {PassTicketService.class}) - public static class SpringConfig { - - } - -} diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceFacade.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceFacade.java index 7bee0a1001..51e95d7223 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceFacade.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceFacade.java @@ -47,8 +47,8 @@ @Service public class ZosmfServiceFacade extends AbstractZosmfService { - private final ApplicationContext applicationContext; - private final List implementations; + protected final ApplicationContext applicationContext; + protected final List implementations; private ZosmfServiceFacade meProxy; diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java index 7ec4a78c7a..ebe64b5237 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java @@ -14,6 +14,7 @@ import com.ca.apiml.security.common.error.ServiceNotAccessibleException; import com.ca.apiml.security.common.token.TokenAuthentication; import com.ca.mfaas.gateway.security.service.AuthenticationService; +import com.ca.mfaas.gateway.security.service.ZosmfService; import com.ca.mfaas.gateway.security.service.zosmf.ZosmfServiceV2; import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.appinfo.InstanceInfo; @@ -24,24 +25,26 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.jaas.JaasAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import java.util.Arrays; +import java.util.Collections; import static junit.framework.TestCase.assertTrue; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; import static org.mockito.Mockito.*; public class ZosmfAuthenticationProviderTest { @@ -79,12 +82,7 @@ public void setUp() { restTemplate = mock(RestTemplate.class); zosmfInstance = createInstanceInfo(SERVICE_ID, HOST, PORT); - doAnswer(new Answer() { - @Override - public TokenAuthentication answer(InvocationOnMock invocation) { - return TokenAuthentication.createAuthenticated(invocation.getArgument(0), invocation.getArgument(1)); - } - }).when(authenticationService).createTokenAuthentication(anyString(), anyString()); + doAnswer((Answer) invocation -> TokenAuthentication.createAuthenticated(invocation.getArgument(0), invocation.getArgument(1))).when(authenticationService).createTokenAuthentication(anyString(), anyString()); when(authenticationService.createJwtToken(anyString(), anyString(), anyString())).thenReturn("someJwtToken"); } @@ -347,7 +345,35 @@ public void shouldReturnTrueWhenSupportMethodIsCalledWithCorrectClass() { boolean supports = zosmfAuthenticationProvider.supports(usernamePasswordAuthentication.getClass()); assertTrue(supports); + } + @Test + public void testSupports() { + ZosmfAuthenticationProvider mock = new ZosmfAuthenticationProvider(null, null); + + assertTrue(mock.supports(UsernamePasswordAuthenticationToken.class)); + assertFalse(mock.supports(Object.class)); + assertFalse(mock.supports(AbstractAuthenticationToken.class)); + assertFalse(mock.supports(JaasAuthenticationToken.class)); + assertFalse(mock.supports(null)); + } + + @Test + public void testAuthenticateJwt() { + AuthenticationService authenticationService = mock(AuthenticationService.class); + ZosmfService zosmfService = mock(ZosmfService.class); + ZosmfAuthenticationProvider zosmfAuthenticationProvider = new ZosmfAuthenticationProvider(authenticationService, zosmfService); + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn("user1"); + TokenAuthentication authentication2 = mock(TokenAuthentication.class); + + when(zosmfService.authenticate(authentication)).thenReturn(new ZosmfService.AuthenticationResponse( + "domain1", + Collections.singletonMap(ZosmfService.TokenType.JWT, "jwtToken1") + )); + when(authenticationService.createTokenAuthentication("user1", "jwtToken1")).thenReturn(authentication2); + + assertSame(authentication2, zosmfAuthenticationProvider.authenticate(authentication)); } } diff --git a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceFacadeTest.java b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceFacadeTest.java index 624df49794..204e63094f 100644 --- a/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceFacadeTest.java +++ b/gateway-service/src/test/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceFacadeTest.java @@ -10,6 +10,8 @@ import com.ca.apiml.security.common.config.AuthConfigurationProperties; import com.ca.mfaas.gateway.security.service.ZosmfService; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.netflix.discovery.DiscoveryClient; import lombok.AllArgsConstructor; import org.junit.Before; import org.junit.Test; @@ -25,6 +27,7 @@ import org.springframework.web.client.RestTemplate; import java.util.Arrays; +import java.util.List; import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.*; @@ -34,18 +37,18 @@ public class ZosmfServiceFacadeTest { private RestTemplate restTemplate = mock(RestTemplate.class); - private ZosmfServiceFacade zosmfService; + private ZosmfServiceFacadeTestExt zosmfService; @Before public void setUp() { zosmfService = getZosmfServiceFacade(); } - public ZosmfServiceFacade getZosmfServiceFacade() { + public ZosmfServiceFacadeTestExt getZosmfServiceFacade() { AuthConfigurationProperties authConfigurationProperties = mock(AuthConfigurationProperties.class); ApplicationContext applicationContext = mock(ApplicationContext.class); - ZosmfServiceFacade out = new ZosmfServiceFacade( + ZosmfServiceFacadeTestExt out = new ZosmfServiceFacadeTestExt( authConfigurationProperties, null, restTemplate, @@ -53,7 +56,8 @@ public ZosmfServiceFacade getZosmfServiceFacade() { applicationContext, Arrays.asList( new ZosmfServiceTest(1), - new ZosmfServiceTest(2) + new ZosmfServiceTest(2), + spy(new ZosmfServiceTest2(3)) ) ) { @Override @@ -75,7 +79,7 @@ private void mockVersion(int version) { when(restTemplate.exchange( eq("http://zosmf:1433/zosmf/info"), eq(HttpMethod.GET), - (HttpEntity) any(), + any(), eq(ZosmfServiceFacade.ZosmfInfo.class) )).thenReturn(new ResponseEntity<>(response, HttpStatus.OK)); } @@ -102,6 +106,11 @@ public void testValidate() { } catch (TestException te) { assertEquals(2, te.version); } + + mockVersion(3); + + zosmfService.validate(ZosmfService.TokenType.LTPA, "lpta"); + verify(zosmfService.getService(3), times(1)).validate(ZosmfService.TokenType.LTPA, "lpta"); } @Test @@ -126,6 +135,11 @@ public void testInvalidate() { } catch (TestException te) { assertEquals(2, te.version); } + + mockVersion(3); + + zosmfService.invalidate(ZosmfService.TokenType.LTPA, "lpta"); + verify(zosmfService.getService(3), times(1)).invalidate(ZosmfService.TokenType.LTPA, "lpta"); } @Test @@ -151,6 +165,11 @@ public void testAuthenticate() { } catch (TestException te) { assertEquals(2, te.version); } + + mockVersion(3); + + zosmfService.authenticate(authentication); + verify(zosmfService.getService(3), times(1)).authenticate(authentication); } @Test @@ -166,6 +185,7 @@ public void testValidateException() { zosmfService.validate(ZosmfService.TokenType.JWT, "jwt"); fail(); } catch (IllegalArgumentException te) { + // IllegalArgumentException is expected } verify(zosmfService, times(1)).evictCaches(); } @@ -189,6 +209,15 @@ public void testMatchesVersion() { assertFalse(zosmfService.matchesVersion(25)); } + @Test + public void testNoBody() { + when(restTemplate.exchange( + anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any() + )).thenReturn(new ResponseEntity<>(null, HttpStatus.NO_CONTENT)); + + assertEquals(0, zosmfService.getVersion("zosmf")); + } + @AllArgsConstructor private static class TestException extends RuntimeException { @@ -227,4 +256,46 @@ public boolean matchesVersion(int version) { } + @AllArgsConstructor + private static class ZosmfServiceTest2 implements ZosmfService { + + private int version; + + @Override + public AuthenticationResponse authenticate(Authentication authentication) { + return null; + } + + @Override + public void validate(TokenType type, String token) { + + } + + @Override + public void invalidate(TokenType type, String token) { + + } + + @Override + public boolean matchesVersion(int version) { + return this.version == version; + } + + } + + private static class ZosmfServiceFacadeTestExt extends ZosmfServiceFacade { + + public ZosmfServiceFacadeTestExt(AuthConfigurationProperties authConfigurationProperties, DiscoveryClient discovery, RestTemplate restTemplateWithoutKeystore, ObjectMapper securityObjectMapper, ApplicationContext applicationContext, List implementations) { + super(authConfigurationProperties, discovery, restTemplateWithoutKeystore, securityObjectMapper, applicationContext, implementations); + } + + public ZosmfService getService(int version) { + for (final ZosmfService zosmfService : implementations) { + if (zosmfService.matchesVersion(version)) return zosmfService; + } + return null; + } + + } + } From 186fb63f95e8f730ae9f56b6ca5ed0f41f9da8af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Mon, 3 Feb 2020 17:33:41 +0100 Subject: [PATCH 107/122] integration test for z/OSMF and small fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../com/ca/mfaas/product/web/HttpConfig.java | 1 + .../service/zosmf/AbstractZosmfService.java | 18 +- .../service/zosmf/ZosmfServiceFacade.java | 16 +- .../service/zosmf/ZosmfServiceV1.java | 5 +- .../service/zosmf/ZosmfServiceV2.java | 5 +- .../ZosmfAuthenticationTest.java | 275 ++++++++++++++++++ .../ca/mfaas/util/service/VirtualService.java | 4 + 7 files changed, 315 insertions(+), 9 deletions(-) create mode 100644 integration-tests/src/test/java/com/ca/mfaas/gatewayservice/ZosmfAuthenticationTest.java diff --git a/apiml-common/src/main/java/com/ca/mfaas/product/web/HttpConfig.java b/apiml-common/src/main/java/com/ca/mfaas/product/web/HttpConfig.java index 7420a5c1fe..c6b9bebbf7 100644 --- a/apiml-common/src/main/java/com/ca/mfaas/product/web/HttpConfig.java +++ b/apiml-common/src/main/java/com/ca/mfaas/product/web/HttpConfig.java @@ -153,6 +153,7 @@ public RestTemplate restTemplateWithKeystore() { } @Bean + @Qualifier("restTemplateWithoutKeystore") public RestTemplate restTemplateWithoutKeystore() { HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(secureHttpClientWithoutKeystore); return new RestTemplate(factory); diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/AbstractZosmfService.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/AbstractZosmfService.java index 0c831cfc80..b8f12402b8 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/AbstractZosmfService.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/AbstractZosmfService.java @@ -18,10 +18,13 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.netflix.discovery.DiscoveryClient; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.Authentication; +import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; @@ -48,7 +51,7 @@ public abstract class AbstractZosmfService implements ZosmfService { public AbstractZosmfService( AuthConfigurationProperties authConfigurationProperties, DiscoveryClient discovery, - RestTemplate restTemplateWithoutKeystore, + @Qualifier("restTemplateWithKeystore") RestTemplate restTemplateWithoutKeystore, ObjectMapper securityObjectMapper ) { this.authConfigurationProperties = authConfigurationProperties; @@ -97,6 +100,10 @@ protected RuntimeException handleExceptionOnCall(String url, RuntimeException re return new ServiceNotAccessibleException("Could not get an access to z/OSMF service."); } + if (re instanceof HttpClientErrorException.Unauthorized) { + return new BadCredentialsException("Username or password are invalid."); + } + if (re instanceof RestClientException) { apimlLog.log("apiml.security.generic", re.getMessage(), url); return new AuthenticationServiceException("A failure occurred when authenticating.", re); @@ -114,6 +121,7 @@ protected RuntimeException handleExceptionOnCall(String url, RuntimeException re */ protected String readDomain(String content) { try { + if (content == null) return null; ObjectNode zosmfNode = securityObjectMapper.readValue(content, ObjectNode.class); return Optional.ofNullable(zosmfNode) @@ -152,9 +160,11 @@ protected String readTokenFromCookie(List cookies, String cookieName) { protected AuthenticationResponse getAuthenticationResponse(ResponseEntity responseEntity) { final List cookies = responseEntity.getHeaders().get(HttpHeaders.SET_COOKIE); final EnumMap tokens = new EnumMap<>(TokenType.class); - for (final TokenType tokenType : TokenType.values()) { - final String token = readTokenFromCookie(cookies, tokenType.getCookieName()); - if (token != null) tokens.put(tokenType, token); + if (cookies != null) { + for (final TokenType tokenType : TokenType.values()) { + final String token = readTokenFromCookie(cookies, tokenType.getCookieName()); + if (token != null) tokens.put(tokenType, token); + } } final String domain = readDomain(responseEntity.getBody()); return new AuthenticationResponse(domain, tokens); diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceFacade.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceFacade.java index 51e95d7223..d7d3901036 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceFacade.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceFacade.java @@ -10,12 +10,14 @@ package com.ca.mfaas.gateway.security.service.zosmf; import com.ca.apiml.security.common.config.AuthConfigurationProperties; +import com.ca.mfaas.gateway.security.service.ServiceCacheEvict; import com.ca.mfaas.gateway.security.service.ZosmfService; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.discovery.DiscoveryClient; import lombok.Data; +import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; @@ -45,7 +47,7 @@ */ @Primary @Service -public class ZosmfServiceFacade extends AbstractZosmfService { +public class ZosmfServiceFacade extends AbstractZosmfService implements ServiceCacheEvict { protected final ApplicationContext applicationContext; protected final List implementations; @@ -134,6 +136,18 @@ public boolean matchesVersion(int version) { return false; } + @Override + public void evictCacheAllService() { + meProxy.evictCaches(); + } + + @Override + public void evictCacheService(String serviceId) { + if (StringUtils.equalsIgnoreCase(getZosmfServiceId(), serviceId)) { + meProxy.evictCaches(); + } + } + @Data @JsonIgnoreProperties(ignoreUnknown = true) public static class ZosmfInfo { diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV1.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV1.java index cc56c1360e..6c5453442e 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV1.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV1.java @@ -14,6 +14,7 @@ import com.ca.apiml.security.common.token.TokenNotValidException; import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.discovery.DiscoveryClient; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -27,10 +28,10 @@ public class ZosmfServiceV1 extends AbstractZosmfService { public ZosmfServiceV1( AuthConfigurationProperties authConfigurationProperties, DiscoveryClient discovery, - RestTemplate restTemplate, + @Qualifier("restTemplateWithKeystore") RestTemplate restTemplateWithKeystore, ObjectMapper securityObjectMapper ) { - super(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); + super(authConfigurationProperties, discovery, restTemplateWithKeystore, securityObjectMapper); } @Override diff --git a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV2.java b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV2.java index c839bedce9..0971764b7b 100644 --- a/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV2.java +++ b/gateway-service/src/main/java/com/ca/mfaas/gateway/security/service/zosmf/ZosmfServiceV2.java @@ -15,6 +15,7 @@ import com.ca.mfaas.gateway.security.service.ZosmfService; import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.discovery.DiscoveryClient; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -32,10 +33,10 @@ public class ZosmfServiceV2 extends AbstractZosmfService { public ZosmfServiceV2( AuthConfigurationProperties authConfigurationProperties, DiscoveryClient discovery, - RestTemplate restTemplate, + @Qualifier("restTemplateWithKeystore") RestTemplate restTemplateWithKeystore, ObjectMapper securityObjectMapper ) { - super(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); + super(authConfigurationProperties, discovery, restTemplateWithKeystore, securityObjectMapper); } @Override diff --git a/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/ZosmfAuthenticationTest.java b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/ZosmfAuthenticationTest.java new file mode 100644 index 0000000000..8786b3d7b9 --- /dev/null +++ b/integration-tests/src/test/java/com/ca/mfaas/gatewayservice/ZosmfAuthenticationTest.java @@ -0,0 +1,275 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package com.ca.mfaas.gatewayservice; + +import com.ca.mfaas.util.categories.AdditionalLocalTest; +import com.ca.mfaas.util.service.DiscoveryUtils; +import com.ca.mfaas.util.service.VirtualService; +import io.restassured.RestAssured; +import lombok.AllArgsConstructor; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Base64; + +import static com.ca.mfaas.gatewayservice.SecurityUtils.getConfiguredSslConfig; +import static io.restassured.RestAssured.given; + +/** + * Those tests simulate different version of z/OSMF. + * + * For right execution is required: + * - set provider in gateway to zosmf with serviceId zosmfca32 + * - start discovery service and gateway locally + */ +@RunWith(JUnit4.class) +@Category(AdditionalLocalTest.class) +public class ZosmfAuthenticationTest { + + private static final String ZOSMF_ID = "zosmfca32"; + private static final String LOGIN_ENDPOINT = "/api/v1/gateway/auth/login"; + + private static final String USER_ID = "user"; + private static final String PASSWORD = "secret"; + private static final String WRONG_PASSWORD = "public"; + + private static final int TIMEOUT_REGISTRATION = 10; + + @Before + public void setUp() { + RestAssured.useRelaxedHTTPSValidation(); + RestAssured.config = RestAssured.config().sslConfig(getConfiguredSslConfig()); + } + + @Test + public void testNoResponse() throws Exception { + try (VirtualService zosmf = new VirtualService(ZOSMF_ID)) { + zosmf + .addServlet("info", "/zosmf/info", new AuthServletGet( + null, null, "LtpaToken2=ltpaToken", HttpStatus.OK + )) + .addRoute("/api", "/") + .start() + .waitForGatewayRegistration(1, TIMEOUT_REGISTRATION); + + given() + .auth().preemptive().basic(USER_ID, PASSWORD) + .post(DiscoveryUtils.getGatewayUrls().get(0) + LOGIN_ENDPOINT) + .then().statusCode(HttpStatus.NO_CONTENT.value()); + } + } + + @Test + public void testOldAuthenticationEndpoint() throws Exception { + try (VirtualService zosmf = new VirtualService(ZOSMF_ID)) { + zosmf + .addServlet("info", "/zosmf/info", new AuthServletGet( + "{\"zosmf_version\":\"25\",\"zosmf_full_version\": \"25.2\",\"zosmf_saf_realm\": \"SAFRealm\",\"otherAttribute\":\"someValue\"}", + null, "LtpaToken2=ltpaToken", HttpStatus.OK + )) + .addRoute("/api", "/") + .start() + .waitForGatewayRegistration(1, TIMEOUT_REGISTRATION); + + given() + .auth().preemptive().basic(USER_ID, PASSWORD) + .post(DiscoveryUtils.getGatewayUrls().get(0) + LOGIN_ENDPOINT) + .then().statusCode(HttpStatus.NO_CONTENT.value()).cookie("apimlAuthenticationToken"); + } + } + + @Test + public void testOldAuthenticationEndpointInvalid() throws Exception { + try (VirtualService zosmf = new VirtualService(ZOSMF_ID)) { + zosmf + .addServlet("info", "/zosmf/info", new AuthServletGet( + "{\"zosmf_version\":\"25\",\"zosmf_full_version\": \"25.2\",\"zosmf_saf_realm\": \"SAFRealm\",\"otherAttribute\":\"someValue\"}", + null, "LtpaToken2=ltpaToken", HttpStatus.OK + )) + .addRoute("/api", "/") + .start() + .waitForGatewayRegistration(1, TIMEOUT_REGISTRATION); + + given() + .auth().preemptive().basic(USER_ID, WRONG_PASSWORD) + .post(DiscoveryUtils.getGatewayUrls().get(0) + LOGIN_ENDPOINT) + .then().statusCode(HttpStatus.UNAUTHORIZED.value()); + } + } + + @Test + public void testNewAuthenticationEndpointLtpa() throws Exception { + try (VirtualService zosmf = new VirtualService(ZOSMF_ID)) { + zosmf + .addServlet("info", "/zosmf/info", new AuthServletGet( + "{\"zosmf_version\":\"27\",\"zosmf_full_version\": \"27.0\",\"zosmf_saf_realm\": \"SAFRealm\",\"otherAttribute\":\"someValue\"}", + null, null, HttpStatus.OK + )) + .addServlet("auth", "/zosmf/services/authenticate", new AuthServletPost( + null, + null, "LtpaToken2=ltpaToken", HttpStatus.UNAUTHORIZED + )) + .addRoute("/api", "/") + .start() + .waitForGatewayRegistration(1, TIMEOUT_REGISTRATION); + + given() + .auth().preemptive().basic(USER_ID, PASSWORD) + .post(DiscoveryUtils.getGatewayUrls().get(0) + LOGIN_ENDPOINT) + .then().statusCode(HttpStatus.NO_CONTENT.value()).cookie("apimlAuthenticationToken"); + } + } + + @Test + public void testNewAuthenticationEndpointJwt() throws Exception { + try (VirtualService zosmf = new VirtualService(ZOSMF_ID)) { + zosmf + .addServlet("info", "/zosmf/info", new AuthServletGet( + "{\"zosmf_version\":\"27\",\"zosmf_full_version\": \"27.0\",\"zosmf_saf_realm\": \"SAFRealm\",\"otherAttribute\":\"someValue\"}", + null, null, HttpStatus.OK + )) + .addServlet("auth", "/zosmf/services/authenticate", new AuthServletPost( + null, + null, "jwtToken=jwtToken", HttpStatus.UNAUTHORIZED + )) + .addRoute("/api", "/") + .start() + .waitForGatewayRegistration(1, TIMEOUT_REGISTRATION); + + given() + .auth().preemptive().basic(USER_ID, PASSWORD) + .post(DiscoveryUtils.getGatewayUrls().get(0) + LOGIN_ENDPOINT) + .then().statusCode(HttpStatus.NO_CONTENT.value()).cookie("apimlAuthenticationToken", "jwtToken"); + } + } + + @Test + public void testNewAuthenticationEndpointInvalid() throws Exception { + try (VirtualService zosmf = new VirtualService(ZOSMF_ID)) { + zosmf + .addServlet("info", "/zosmf/info", new AuthServletGet( + "{\"zosmf_version\":\"27\",\"zosmf_full_version\": \"27.0\",\"zosmf_saf_realm\": \"SAFRealm\",\"otherAttribute\":\"someValue\"}", + null, null, HttpStatus.OK + )) + .addServlet("auth", "/zosmf/services/authenticate", new AuthServletPost( + null, + null, "jwtToken=jwtToken", HttpStatus.UNAUTHORIZED + )) + .addRoute("/api", "/") + .start() + .waitForGatewayRegistration(1, TIMEOUT_REGISTRATION); + + given() + .auth().preemptive().basic(USER_ID, WRONG_PASSWORD) + .post(DiscoveryUtils.getGatewayUrls().get(0) + LOGIN_ENDPOINT) + .then().statusCode(HttpStatus.UNAUTHORIZED.value()); + } + } + + @AllArgsConstructor + private static class AuthServlet extends HttpServlet { + + protected final String bodyOnSuccess; + protected final String bodyOnError; + protected final String cookiesOnSuccess; + protected final HttpStatus unauthorizedStatus; + + protected boolean isAuthorized(HttpServletRequest req) { + final String authorization = req.getHeader(HttpHeaders.AUTHORIZATION).split(" ", 2)[1]; + final String basic = new String(Base64.getDecoder().decode(authorization)); + + return + USER_ID.equalsIgnoreCase(basic.split(":", 2)[0]) && + PASSWORD.equalsIgnoreCase(basic.split(":", 2)[1]); + } + + protected void success(HttpServletResponse resp, boolean authorized) throws IOException { + if (authorized && (cookiesOnSuccess != null)) { + resp.setHeader(HttpHeaders.SET_COOKIE, cookiesOnSuccess); + } + if (bodyOnSuccess != null) { + resp.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE); + resp.getOutputStream().write(bodyOnSuccess.getBytes()); + resp.setStatus(HttpStatus.OK.value()); + } else { + resp.setStatus(HttpStatus.NO_CONTENT.value()); + } + } + + protected void fail(HttpServletResponse resp) throws IOException { + if (bodyOnError != null) { + resp.getOutputStream().write(bodyOnSuccess.getBytes()); + } + resp.setStatus(unauthorizedStatus.value()); + } + } + + private static class AuthServletGet extends AuthServlet { + + public AuthServletGet(String bodyOnSuccess, String bodyOnError, String cookiesOnSuccess, HttpStatus unauthorizedStatus) { + super(bodyOnSuccess, bodyOnError, cookiesOnSuccess, unauthorizedStatus); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + if (req.getHeader("X-CSRF-ZOSMF-HEADER") == null) { + resp.setStatus(HttpStatus.BAD_REQUEST.value()); + return; + } + + if (req.getHeader(HttpHeaders.AUTHORIZATION) != null) { + if (isAuthorized(req)) { + success(resp, true); + } else { + fail(resp); + } + } else { + success(resp, false); + } + } + + } + + private static class AuthServletPost extends AuthServlet { + + public AuthServletPost(String bodyOnSuccess, String bodyOnError, String cookiesOnSuccess, HttpStatus unauthorizedStatus) { + super(bodyOnSuccess, bodyOnError, cookiesOnSuccess, unauthorizedStatus); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + if (req.getHeader("X-CSRF-ZOSMF-HEADER") == null) { + resp.setStatus(HttpStatus.BAD_REQUEST.value()); + return; + } + + if (req.getHeader(HttpHeaders.AUTHORIZATION) == null) { + fail(resp); + } else if (isAuthorized(req)) { + success(resp, true); + } else { + fail(resp); + } + } + + } + +} diff --git a/integration-tests/src/test/java/com/ca/mfaas/util/service/VirtualService.java b/integration-tests/src/test/java/com/ca/mfaas/util/service/VirtualService.java index ebc5914c68..b7148d4d9a 100644 --- a/integration-tests/src/test/java/com/ca/mfaas/util/service/VirtualService.java +++ b/integration-tests/src/test/java/com/ca/mfaas/util/service/VirtualService.java @@ -358,6 +358,10 @@ private void register() throws UnknownHostException { .put("$", getPort()) .put("@enabled", "true") ) + .put("securePort", new JSONObject() + .put("$", 0) + .put("@enabled", "true") + ) .put("healthCheckUrl", getUrl() + "/application/health") .put("dataCenterInfo", new JSONObject() .put("@class", "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo") From 189a70e7f734fcb8ac9b20e97dd3d8a8e173623d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Tue, 4 Feb 2020 15:57:26 +0100 Subject: [PATCH 108/122] merge with master MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../common/token/TokenAuthenticationTest.java | 2 +- .../metadata/MetadataTranslationService.java | 2 +- .../MetadataTranslationServiceTest.java | 4 +- .../gateway/routing/ApimlRoutingConfig.java | 25 +++++----- .../zosmf/ZosmfAuthenticationProvider.java | 23 +++------ .../service/AuthenticationService.java | 22 ++++----- .../security/service/ZosmfService.java | 6 +-- .../service/zosmf/AbstractZosmfService.java | 16 +++---- .../service/zosmf/ZosmfServiceFacade.java | 8 ++-- .../service/zosmf/ZosmfServiceV1.java | 8 ++-- .../service/zosmf/ZosmfServiceV2.java | 10 ++-- .../ZosmfAuthenticationProviderTest.java | 9 ++-- .../query/SuccessfulQueryHandlerTest.java | 11 +++-- .../service/AuthenticationServiceTest.java | 48 +++++++------------ .../service/schema/ZosmfSchemeTest.java | 15 +++--- .../zosmf/ZosmfServiceFacadeTest.java | 6 +-- .../zosmf/ZosmfServiceV1Test.java | 10 ++-- .../zosmf/ZosmfServiceV2Test.java | 48 +++++++++---------- .../ZosmfAuthenticationTest.java | 21 ++++---- 19 files changed, 138 insertions(+), 156 deletions(-) rename gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/{service => }/zosmf/ZosmfServiceFacadeTest.java (97%) rename gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/{service => }/zosmf/ZosmfServiceV1Test.java (95%) rename gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/{service => }/zosmf/ZosmfServiceV2Test.java (87%) diff --git a/apiml-security-common/src/test/java/org/zowe/apiml/security/common/token/TokenAuthenticationTest.java b/apiml-security-common/src/test/java/org/zowe/apiml/security/common/token/TokenAuthenticationTest.java index 782fd1e09a..b4293d97d4 100644 --- a/apiml-security-common/src/test/java/org/zowe/apiml/security/common/token/TokenAuthenticationTest.java +++ b/apiml-security-common/src/test/java/org/zowe/apiml/security/common/token/TokenAuthenticationTest.java @@ -1,4 +1,4 @@ -package com.ca.apiml.security.common.token;/* +package org.zowe.apiml.security.common.token;/* * This program and the accompanying materials are made available under the terms of the * Eclipse Public License v2.0 which accompanies this distribution, and is available at * https://www.eclipse.org/legal/epl-v20.html diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/metadata/MetadataTranslationService.java b/discovery-service/src/main/java/org/zowe/apiml/discovery/metadata/MetadataTranslationService.java index 993a6fff6e..39a7be4d12 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/metadata/MetadataTranslationService.java +++ b/discovery-service/src/main/java/org/zowe/apiml/discovery/metadata/MetadataTranslationService.java @@ -9,9 +9,9 @@ */ package org.zowe.apiml.discovery.metadata; -import com.ca.apiml.security.common.auth.AuthenticationScheme; import org.apache.commons.lang.StringUtils; import org.springframework.stereotype.Service; +import org.zowe.apiml.security.common.auth.AuthenticationScheme; import java.util.Map; import java.util.stream.Collectors; diff --git a/discovery-service/src/test/java/org/zowe/apiml/discovery/metadata/MetadataTranslationServiceTest.java b/discovery-service/src/test/java/org/zowe/apiml/discovery/metadata/MetadataTranslationServiceTest.java index c203c00ce8..60062e6dc3 100644 --- a/discovery-service/src/test/java/org/zowe/apiml/discovery/metadata/MetadataTranslationServiceTest.java +++ b/discovery-service/src/test/java/org/zowe/apiml/discovery/metadata/MetadataTranslationServiceTest.java @@ -9,17 +9,17 @@ */ package org.zowe.apiml.discovery.metadata; -import com.ca.apiml.security.common.auth.AuthenticationScheme; import org.junit.Test; +import org.zowe.apiml.security.common.auth.AuthenticationScheme; import java.util.HashMap; import java.util.Map; -import static org.zowe.apiml.constants.EurekaMetadataDefinition.*; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.collection.IsMapContaining.hasEntry; import static org.junit.Assert.*; import static org.mockito.Mockito.*; +import static org.zowe.apiml.constants.EurekaMetadataDefinition.*; public class MetadataTranslationServiceTest { diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/routing/ApimlRoutingConfig.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/routing/ApimlRoutingConfig.java index 33e367f9e6..99e528896d 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/routing/ApimlRoutingConfig.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/routing/ApimlRoutingConfig.java @@ -9,15 +9,6 @@ */ package org.zowe.apiml.gateway.routing; -import org.zowe.apiml.gateway.filters.pre.*; -import org.zowe.apiml.message.core.MessageService; -import org.zowe.apiml.security.common.config.AuthConfigurationProperties; -import org.zowe.apiml.gateway.filters.post.ConvertAuthTokenInUriToCookieFilter; -import org.zowe.apiml.gateway.filters.post.PageRedirectionFilter; -import org.zowe.apiml.gateway.security.service.AuthenticationService; -import org.zowe.apiml.gateway.ws.WebSocketProxyServerHandler; -import org.zowe.apiml.product.gateway.GatewayConfigProperties; -import org.zowe.apiml.product.routing.RoutedServicesUser; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; @@ -25,6 +16,17 @@ import org.springframework.cloud.netflix.zuul.filters.discovery.ServiceRouteMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.zowe.apiml.gateway.filters.post.ConvertAuthTokenInUriToCookieFilter; +import org.zowe.apiml.gateway.filters.post.PageRedirectionFilter; +import org.zowe.apiml.gateway.filters.pre.EncodedCharactersFilter; +import org.zowe.apiml.gateway.filters.pre.LocationFilter; +import org.zowe.apiml.gateway.filters.pre.ServiceAuthenticationFilter; +import org.zowe.apiml.gateway.filters.pre.SlashFilter; +import org.zowe.apiml.gateway.ws.WebSocketProxyServerHandler; +import org.zowe.apiml.message.core.MessageService; +import org.zowe.apiml.product.gateway.GatewayConfigProperties; +import org.zowe.apiml.product.routing.RoutedServicesUser; +import org.zowe.apiml.security.common.config.AuthConfigurationProperties; import java.util.ArrayList; import java.util.List; @@ -45,11 +47,6 @@ public SlashFilter slashFilter() { return new SlashFilter(); } - @Bean - public ZosmfFilter zosmfFilter(AuthenticationService authenticationService) { - return new ZosmfFilter(authenticationService); - } - @Bean public ServiceAuthenticationFilter serviceAuthenticationFilter() { return new ServiceAuthenticationFilter(); diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProvider.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProvider.java index 78e8e47ebe..2709160722 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProvider.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProvider.java @@ -9,29 +9,18 @@ */ package org.zowe.apiml.gateway.security.login.zosmf; -import org.zowe.apiml.security.common.config.AuthConfigurationProperties; -import org.zowe.apiml.security.common.error.ServiceNotAccessibleException; -import org.zowe.apiml.security.common.token.TokenAuthentication; -import org.zowe.apiml.gateway.security.service.AuthenticationService; -import org.zowe.apiml.message.log.ApimlLogger; -import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; - -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.cloud.client.discovery.DiscoveryClient; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; +import org.zowe.apiml.gateway.security.service.AuthenticationService; +import org.zowe.apiml.gateway.security.service.ZosmfService; +import org.zowe.apiml.message.log.ApimlLogger; +import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; -import static com.ca.mfaas.gateway.security.service.ZosmfService.TokenType.JWT; -import static com.ca.mfaas.gateway.security.service.ZosmfService.TokenType.LTPA; +import static org.zowe.apiml.gateway.security.service.ZosmfService.TokenType.JWT; +import static org.zowe.apiml.gateway.security.service.ZosmfService.TokenType.LTPA; /** * Authentication provider that verifies credentials against z/OSMF service diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/AuthenticationService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/AuthenticationService.java index f8b2019f09..818abecd9a 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/AuthenticationService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/AuthenticationService.java @@ -9,13 +9,6 @@ */ package org.zowe.apiml.gateway.security.service; -import org.zowe.apiml.security.common.config.AuthConfigurationProperties; -import org.zowe.apiml.security.common.token.QueryResponse; -import org.zowe.apiml.security.common.token.TokenAuthentication; -import org.zowe.apiml.security.common.token.TokenExpireException; -import org.zowe.apiml.security.common.token.TokenNotValidException; -import org.zowe.apiml.constants.ApimlConstants; -import org.zowe.apiml.util.EurekaUtils; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.EurekaClient; import com.netflix.discovery.shared.Application; @@ -36,6 +29,13 @@ import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; +import org.zowe.apiml.constants.ApimlConstants; +import org.zowe.apiml.security.common.config.AuthConfigurationProperties; +import org.zowe.apiml.security.common.token.QueryResponse; +import org.zowe.apiml.security.common.token.TokenAuthentication; +import org.zowe.apiml.security.common.token.TokenExpireException; +import org.zowe.apiml.security.common.token.TokenNotValidException; +import org.zowe.apiml.util.EurekaUtils; import javax.annotation.PostConstruct; import javax.servlet.http.Cookie; @@ -45,8 +45,8 @@ import java.util.Optional; import java.util.UUID; -import static com.ca.mfaas.gateway.security.service.ZosmfService.TokenType.JWT; -import static com.ca.mfaas.gateway.security.service.ZosmfService.TokenType.LTPA; +import static org.zowe.apiml.gateway.security.service.ZosmfService.TokenType.JWT; +import static org.zowe.apiml.gateway.security.service.ZosmfService.TokenType.LTPA; /** * Service for the JWT and LTPA tokens operations @@ -201,8 +201,8 @@ public TokenAuthentication validateJwtToken(String jwtToken) { * Method construct {@link TokenAuthentication} marked as valid. It also store JWT token on the cache to * speed up next call to validate token. * - * @param user - * @param jwtToken + * @param user username to login + * @param jwtToken token of user * @return {@link TokenAuthentication}, as authenticated use information about invalidating of token */ @CachePut(value = "validationJwtToken", key = "#jwtToken", condition = "#jwtToken != null") diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/ZosmfService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/ZosmfService.java index 68101b6b1b..3cc3f71078 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/ZosmfService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/ZosmfService.java @@ -7,7 +7,7 @@ * * Copyright Contributors to the Zowe Project. */ -package com.ca.mfaas.gateway.security.service; +package org.zowe.apiml.gateway.security.service; import lombok.AllArgsConstructor; import lombok.Getter; @@ -18,7 +18,7 @@ /** * Common interface for z/OSMF operations about authentication. This interface is implemented with each bean for - * authentication to z/OSMF and also in {@link com.ca.mfaas.gateway.security.service.zosmf.ZosmfServiceFacade}, which + * authentication to z/OSMF and also in {@link org.zowe.apiml.gateway.security.service.zosmf.ZosmfServiceFacade}, which * provides calls by version of z/OSMF. */ public interface ZosmfService { @@ -48,7 +48,7 @@ public interface ZosmfService { /** * Method is to decide which version of z/OSMF are supported by implementation. If bean is not real implementation - * but delegate it has to return false (see {@link com.ca.mfaas.gateway.security.service.zosmf.ZosmfServiceFacade}). + * but delegate it has to return false (see {@link org.zowe.apiml.gateway.security.service.zosmf.ZosmfServiceFacade}). * @param version version of z/OSMF * @return if bean provides implementation for specific version of z/OSMF */ diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/AbstractZosmfService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/AbstractZosmfService.java index b8f12402b8..67f8566dca 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/AbstractZosmfService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/AbstractZosmfService.java @@ -7,14 +7,8 @@ * * Copyright Contributors to the Zowe Project. */ -package com.ca.mfaas.gateway.security.service.zosmf; - -import com.ca.apiml.security.common.config.AuthConfigurationProperties; -import com.ca.apiml.security.common.error.ServiceNotAccessibleException; -import com.ca.mfaas.gateway.security.service.ZosmfService; -import com.ca.mfaas.message.log.ApimlLogger; -import com.ca.mfaas.product.logging.annotations.InjectApimlLogger; -import com.ca.mfaas.util.EurekaUtils; +package org.zowe.apiml.gateway.security.service.zosmf; + import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.netflix.discovery.DiscoveryClient; @@ -28,6 +22,12 @@ import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; +import org.zowe.apiml.gateway.security.service.ZosmfService; +import org.zowe.apiml.message.log.ApimlLogger; +import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; +import org.zowe.apiml.security.common.config.AuthConfigurationProperties; +import org.zowe.apiml.security.common.error.ServiceNotAccessibleException; +import org.zowe.apiml.util.EurekaUtils; import java.io.IOException; import java.util.*; diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceFacade.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceFacade.java index d7d3901036..e200b2d7ec 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceFacade.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceFacade.java @@ -7,11 +7,8 @@ * * Copyright Contributors to the Zowe Project. */ -package com.ca.mfaas.gateway.security.service.zosmf; +package org.zowe.apiml.gateway.security.service.zosmf; -import com.ca.apiml.security.common.config.AuthConfigurationProperties; -import com.ca.mfaas.gateway.security.service.ServiceCacheEvict; -import com.ca.mfaas.gateway.security.service.ZosmfService; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; @@ -30,6 +27,9 @@ import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; +import org.zowe.apiml.gateway.security.service.ServiceCacheEvict; +import org.zowe.apiml.gateway.security.service.ZosmfService; +import org.zowe.apiml.security.common.config.AuthConfigurationProperties; import javax.annotation.PostConstruct; import java.util.List; diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1.java index 6c5453442e..0797511cef 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1.java @@ -7,11 +7,8 @@ * * Copyright Contributors to the Zowe Project. */ -package com.ca.mfaas.gateway.security.service.zosmf; +package org.zowe.apiml.gateway.security.service.zosmf; -import com.ca.apiml.security.common.config.AuthConfigurationProperties; -import com.ca.apiml.security.common.error.ServiceNotAccessibleException; -import com.ca.apiml.security.common.token.TokenNotValidException; import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.discovery.DiscoveryClient; import org.springframework.beans.factory.annotation.Qualifier; @@ -22,6 +19,9 @@ import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; +import org.zowe.apiml.security.common.config.AuthConfigurationProperties; +import org.zowe.apiml.security.common.error.ServiceNotAccessibleException; +import org.zowe.apiml.security.common.token.TokenNotValidException; @Service public class ZosmfServiceV1 extends AbstractZosmfService { diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV2.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV2.java index 0971764b7b..f55fcd4dae 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV2.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV2.java @@ -7,12 +7,12 @@ * * Copyright Contributors to the Zowe Project. */ -package com.ca.mfaas.gateway.security.service.zosmf; +package org.zowe.apiml.gateway.security.service.zosmf; -import com.ca.apiml.security.common.config.AuthConfigurationProperties; -import com.ca.apiml.security.common.error.ServiceNotAccessibleException; -import com.ca.apiml.security.common.token.TokenNotValidException; -import com.ca.mfaas.gateway.security.service.ZosmfService; +import org.zowe.apiml.security.common.config.AuthConfigurationProperties; +import org.zowe.apiml.security.common.error.ServiceNotAccessibleException; +import org.zowe.apiml.security.common.token.TokenNotValidException; +import org.zowe.apiml.gateway.security.service.ZosmfService; import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.discovery.DiscoveryClient; import org.springframework.beans.factory.annotation.Qualifier; diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java index 5948688644..acb32cfb61 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java @@ -10,9 +10,6 @@ package org.zowe.apiml.gateway.security.login.zosmf; -import org.zowe.apiml.security.common.config.AuthConfigurationProperties; -import org.zowe.apiml.security.common.error.ServiceNotAccessibleException; -import org.zowe.apiml.gateway.security.service.AuthenticationService; import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.DiscoveryClient; @@ -36,6 +33,12 @@ import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; +import org.zowe.apiml.gateway.security.service.AuthenticationService; +import org.zowe.apiml.gateway.security.service.ZosmfService; +import org.zowe.apiml.gateway.security.service.zosmf.ZosmfServiceV2; +import org.zowe.apiml.security.common.config.AuthConfigurationProperties; +import org.zowe.apiml.security.common.error.ServiceNotAccessibleException; +import org.zowe.apiml.security.common.token.TokenAuthentication; import java.util.Arrays; import java.util.Collections; diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/query/SuccessfulQueryHandlerTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/query/SuccessfulQueryHandlerTest.java index eb0bc71eb0..2fd7a2c9d8 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/query/SuccessfulQueryHandlerTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/query/SuccessfulQueryHandlerTest.java @@ -9,11 +9,6 @@ */ package org.zowe.apiml.gateway.security.query; -import org.zowe.apiml.security.common.config.AuthConfigurationProperties; -import org.zowe.apiml.security.common.token.TokenAuthentication; -import org.zowe.apiml.gateway.security.service.AuthenticationService; -import org.zowe.apiml.gateway.security.service.JwtSecurityInitializer; -import org.zowe.apiml.security.SecurityUtils; import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.discovery.DiscoveryClient; import io.jsonwebtoken.SignatureAlgorithm; @@ -28,6 +23,12 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.web.client.RestTemplate; +import org.zowe.apiml.gateway.security.service.AuthenticationService; +import org.zowe.apiml.gateway.security.service.JwtSecurityInitializer; +import org.zowe.apiml.gateway.security.service.zosmf.ZosmfServiceV2; +import org.zowe.apiml.security.SecurityUtils; +import org.zowe.apiml.security.common.config.AuthConfigurationProperties; +import org.zowe.apiml.security.common.token.TokenAuthentication; import java.security.Key; import java.security.KeyPair; diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/AuthenticationServiceTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/AuthenticationServiceTest.java index e74c69c131..9e71ff109c 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/AuthenticationServiceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/AuthenticationServiceTest.java @@ -9,23 +9,7 @@ */ package org.zowe.apiml.gateway.security.service; -import com.ca.apiml.security.common.config.AuthConfigurationProperties; -import com.ca.apiml.security.common.token.QueryResponse; -import com.ca.apiml.security.common.token.TokenAuthentication; -import com.ca.apiml.security.common.token.TokenExpireException; -import com.ca.apiml.security.common.token.TokenNotValidException; -import com.ca.mfaas.gateway.config.CacheConfig; -import com.ca.mfaas.gateway.security.service.zosmf.ZosmfServiceV2; -import com.ca.mfaas.security.SecurityUtils; -import com.ca.mfaas.util.EurekaUtils; import com.fasterxml.jackson.databind.ObjectMapper; -import org.zowe.apiml.security.common.config.AuthConfigurationProperties; -import org.zowe.apiml.security.common.token.QueryResponse; -import org.zowe.apiml.security.common.token.TokenAuthentication; -import org.zowe.apiml.security.common.token.TokenExpireException; -import org.zowe.apiml.security.common.token.TokenNotValidException; -import org.zowe.apiml.gateway.config.CacheConfig; -import org.zowe.apiml.security.SecurityUtils; import com.netflix.appinfo.ApplicationInfoManager; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.DiscoveryClient; @@ -35,7 +19,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -47,15 +30,21 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.client.RestTemplate; +import org.zowe.apiml.gateway.config.CacheConfig; +import org.zowe.apiml.gateway.security.service.zosmf.ZosmfServiceV2; +import org.zowe.apiml.security.SecurityUtils; +import org.zowe.apiml.security.common.config.AuthConfigurationProperties; +import org.zowe.apiml.security.common.token.QueryResponse; +import org.zowe.apiml.security.common.token.TokenAuthentication; +import org.zowe.apiml.security.common.token.TokenExpireException; +import org.zowe.apiml.security.common.token.TokenNotValidException; +import org.zowe.apiml.util.EurekaUtils; import javax.servlet.http.Cookie; import java.security.Key; import java.security.KeyPair; import java.security.PublicKey; -import java.util.Arrays; -import java.util.Date; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.function.Consumer; import static org.junit.Assert.*; @@ -122,7 +111,7 @@ private String mockZosmfUrl(DiscoveryClient discoveryClient) { when(instanceInfo.getPort()).thenReturn(ZOSMF_PORT); final Application application = mock(Application.class); - when(application.getInstances()).thenReturn(Arrays.asList(instanceInfo)); + when(application.getInstances()).thenReturn(Collections.singletonList(instanceInfo)); when(discoveryClient.getApplication(ZOSMF)).thenReturn(application); return EurekaUtils.getUrl(instanceInfo); @@ -280,7 +269,7 @@ public void invalidateToken() { when(applicationInfoManager.getInfo()).thenReturn(myInstance); when(discoveryClient.getApplicationInfoManager()).thenReturn(applicationInfoManager); when(restTemplate.exchange(eq(zosmfUrl + "/zosmf/services/authenticate"), eq(HttpMethod.DELETE), any(), eq(String.class))) - .thenReturn(new ResponseEntity(HttpStatus.OK)); + .thenReturn(new ResponseEntity<>(HttpStatus.OK)); Application application = mock(Application.class); List instances = Arrays.asList( @@ -321,7 +310,7 @@ public void invalidateTokenCache() { verify(jwtSecurityInitializer, times(2)).getJwtPublicKey(); when(restTemplate.exchange(eq(zosmfUrl + "/zosmf/services/authenticate"), eq(HttpMethod.DELETE), any(), eq(String.class))) - .thenReturn(new ResponseEntity(HttpStatus.OK)); + .thenReturn(new ResponseEntity<>(HttpStatus.OK)); authService.invalidateJwtToken(jwtToken01, false); assertTrue(authService.validateJwtToken(jwtToken02).isAuthenticated()); verify(jwtSecurityInitializer, times(2)).getJwtPublicKey(); @@ -407,16 +396,13 @@ public void testValidateZosmfJwtToken() { final ZosmfServiceV2 zosmfService = getSpiedZosmfService(); final AuthenticationService authService = getSpiedAuthenticationService(zosmfService); - doAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - assertEquals(jwtToken, invocation.getArgument(0)); - return queryResponse; - } + doAnswer((Answer) invocation -> { + assertEquals(jwtToken, invocation.getArgument(0)); + return queryResponse; }).when(authService).parseJwtToken(jwtToken); when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) - .thenReturn(new ResponseEntity(HttpStatus.OK)); + .thenReturn(new ResponseEntity<>(HttpStatus.OK)); TokenAuthentication tokenAuthentication = authService.validateJwtToken(jwtToken); assertTrue(tokenAuthentication.isAuthenticated()); diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/schema/ZosmfSchemeTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/schema/ZosmfSchemeTest.java index f2db00f337..239f49fef0 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/schema/ZosmfSchemeTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/schema/ZosmfSchemeTest.java @@ -9,12 +9,6 @@ */ package org.zowe.apiml.gateway.security.service.schema; -import org.zowe.apiml.security.common.auth.Authentication; -import org.zowe.apiml.security.common.auth.AuthenticationScheme; -import org.zowe.apiml.security.common.token.QueryResponse; -import org.zowe.apiml.security.common.token.TokenNotValidException; -import org.zowe.apiml.gateway.security.service.AuthenticationService; -import org.zowe.apiml.gateway.utils.CleanCurrentRequestContextTest; import com.netflix.zuul.context.RequestContext; import io.jsonwebtoken.JwtException; import org.junit.Test; @@ -24,16 +18,23 @@ import org.mockito.junit.MockitoJUnitRunner; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.test.util.ReflectionTestUtils; +import org.zowe.apiml.gateway.security.service.AuthenticationException; +import org.zowe.apiml.gateway.security.service.AuthenticationService; +import org.zowe.apiml.gateway.utils.CleanCurrentRequestContextTest; +import org.zowe.apiml.security.common.auth.Authentication; +import org.zowe.apiml.security.common.auth.AuthenticationScheme; +import org.zowe.apiml.security.common.token.QueryResponse; +import org.zowe.apiml.security.common.token.TokenNotValidException; import javax.servlet.http.HttpServletRequest; import java.util.Calendar; import java.util.Date; import java.util.Optional; -import static org.zowe.apiml.gateway.security.service.schema.ZosmfScheme.ZosmfCommand.COOKIE_HEADER; import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; +import static org.zowe.apiml.gateway.security.service.schema.ZosmfScheme.ZosmfCommand.COOKIE_HEADER; @RunWith(MockitoJUnitRunner.class) public class ZosmfSchemeTest extends CleanCurrentRequestContextTest { diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/service/zosmf/ZosmfServiceFacadeTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceFacadeTest.java similarity index 97% rename from gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/service/zosmf/ZosmfServiceFacadeTest.java rename to gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceFacadeTest.java index 204e63094f..92d706c8d3 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/service/zosmf/ZosmfServiceFacadeTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceFacadeTest.java @@ -1,4 +1,4 @@ -package com.ca.mfaas.gateway.security.service.zosmf;/* +package org.zowe.apiml.gateway.security.service.zosmf;/* * This program and the accompanying materials are made available under the terms of the * Eclipse Public License v2.0 which accompanies this distribution, and is available at * https://www.eclipse.org/legal/epl-v20.html @@ -8,8 +8,8 @@ * Copyright Contributors to the Zowe Project. */ -import com.ca.apiml.security.common.config.AuthConfigurationProperties; -import com.ca.mfaas.gateway.security.service.ZosmfService; +import org.zowe.apiml.security.common.config.AuthConfigurationProperties; +import org.zowe.apiml.gateway.security.service.ZosmfService; import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.discovery.DiscoveryClient; import lombok.AllArgsConstructor; diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/service/zosmf/ZosmfServiceV1Test.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1Test.java similarity index 95% rename from gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/service/zosmf/ZosmfServiceV1Test.java rename to gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1Test.java index b6682da76a..5acb01de3b 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/service/zosmf/ZosmfServiceV1Test.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1Test.java @@ -7,12 +7,12 @@ * * Copyright Contributors to the Zowe Project. */ -package com.ca.mfaas.gateway.security.service.zosmf; +package org.zowe.apiml.gateway.security.service.zosmf; -import com.ca.apiml.security.common.config.AuthConfigurationProperties; -import com.ca.apiml.security.common.error.ServiceNotAccessibleException; -import com.ca.apiml.security.common.token.TokenNotValidException; -import com.ca.mfaas.gateway.security.service.ZosmfService; +import org.zowe.apiml.security.common.config.AuthConfigurationProperties; +import org.zowe.apiml.security.common.error.ServiceNotAccessibleException; +import org.zowe.apiml.security.common.token.TokenNotValidException; +import org.zowe.apiml.gateway.security.service.ZosmfService; import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.DiscoveryClient; diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/service/zosmf/ZosmfServiceV2Test.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV2Test.java similarity index 87% rename from gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/service/zosmf/ZosmfServiceV2Test.java rename to gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV2Test.java index 443ffb2e5d..53cd00176e 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/service/zosmf/ZosmfServiceV2Test.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV2Test.java @@ -1,4 +1,4 @@ -package com.ca.mfaas.gateway.security.service.zosmf;/* +package org.zowe.apiml.gateway.security.service.zosmf;/* * This program and the accompanying materials are made available under the terms of the * Eclipse Public License v2.0 which accompanies this distribution, and is available at * https://www.eclipse.org/legal/epl-v20.html @@ -8,10 +8,6 @@ * Copyright Contributors to the Zowe Project. */ -import com.ca.apiml.security.common.config.AuthConfigurationProperties; -import com.ca.apiml.security.common.error.ServiceNotAccessibleException; -import com.ca.apiml.security.common.token.TokenNotValidException; -import com.ca.mfaas.gateway.security.service.ZosmfService; import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.DiscoveryClient; @@ -29,8 +25,12 @@ import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; +import org.zowe.apiml.gateway.security.service.ZosmfService; +import org.zowe.apiml.security.common.config.AuthConfigurationProperties; +import org.zowe.apiml.security.common.error.ServiceNotAccessibleException; +import org.zowe.apiml.security.common.token.TokenNotValidException; -import java.util.Arrays; +import java.util.Collections; import static org.junit.Assert.*; import static org.mockito.Mockito.*; @@ -70,7 +70,7 @@ private void mockZosmfService(String hostname, int port) { when(instanceInfo.getHostName()).thenReturn(hostname); when(instanceInfo.getPort()).thenReturn(port); Application application = mock(Application.class); - when(application.getInstances()).thenReturn(Arrays.asList(instanceInfo)); + when(application.getInstances()).thenReturn(Collections.singletonList(instanceInfo)); when(discovery.getApplication(ZOSMF_ID)).thenReturn(application); } @@ -87,11 +87,11 @@ public void testInvalidateJwt() { HttpMethod.DELETE, new HttpEntity<>(null, headers), String.class - )).thenReturn(new ResponseEntity(HttpStatus.OK)); + )).thenReturn(new ResponseEntity<>(HttpStatus.OK)); zosmfService.invalidate(ZosmfServiceV2.TokenType.JWT, "x.y.z"); verify(restTemplate, times(1)). - exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any()); + exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any()); } @Test @@ -107,18 +107,18 @@ public void testInvalidateLtpa() { HttpMethod.DELETE, new HttpEntity<>(null, headers), String.class - )).thenReturn(new ResponseEntity(HttpStatus.OK)); + )).thenReturn(new ResponseEntity<>(HttpStatus.OK)); zosmfService.invalidate(ZosmfServiceV2.TokenType.LTPA, "ltpa"); verify(restTemplate, times(1)). - exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any()); + exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any()); } @Test public void testInvalidateError500() { mockZosmfService("zosmf", 1433); when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) - .thenReturn(new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR)); + .thenReturn(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); try { zosmfService.invalidate(ZosmfServiceV2.TokenType.LTPA, "ltpa"); @@ -139,7 +139,7 @@ public void testInvalidateException() { // response is null } - when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) + when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) .thenThrow(new ResourceAccessException("msg")); try { @@ -150,7 +150,7 @@ public void testInvalidateException() { } reset(restTemplate); - when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) + when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) .thenThrow(new RestClientException("msg")); try { zosmfService.invalidate(ZosmfServiceV2.TokenType.LTPA, "ltpa"); @@ -174,9 +174,9 @@ public void testValidate() { HttpMethod.POST, new HttpEntity<>(null, headers), String.class - )).thenReturn(new ResponseEntity(HttpStatus.OK)); + )).thenReturn(new ResponseEntity<>(HttpStatus.OK)); - zosmfService.validate(ZosmfServiceV2.TokenType.LTPA, "ltpa");; + zosmfService.validate(ZosmfServiceV2.TokenType.LTPA, "ltpa"); } @Test @@ -190,8 +190,8 @@ public void testValidateException() { } // response 401 - when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) - .thenReturn(new ResponseEntity(HttpStatus.UNAUTHORIZED)); + when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) + .thenReturn(new ResponseEntity<>(HttpStatus.UNAUTHORIZED)); try { zosmfService.validate(ZosmfServiceV2.TokenType.LTPA, "token"); } catch (TokenNotValidException e) { @@ -199,8 +199,8 @@ public void testValidateException() { } // response 500 - when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) - .thenReturn(new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR)); + when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) + .thenReturn(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); try { zosmfService.validate(ZosmfServiceV2.TokenType.LTPA, "token"); } catch (ServiceNotAccessibleException e) { @@ -219,7 +219,7 @@ public void testAuthenticate() { HttpHeaders responseHeaders = new HttpHeaders(); responseHeaders.add(HttpHeaders.SET_COOKIE, "jwtToken=jt"); responseHeaders.add(HttpHeaders.SET_COOKIE, "LtpaToken2=lt"); - ResponseEntity responseEntity = new ResponseEntity("{\"zosmf_saf_realm\":\"domain\"}", responseHeaders, HttpStatus.OK); + ResponseEntity responseEntity = new ResponseEntity<>("{\"zosmf_saf_realm\":\"domain\"}", responseHeaders, HttpStatus.OK); when(restTemplate.exchange( "http://zosmf:1433/zosmf/services/authenticate", @@ -244,7 +244,7 @@ public void testAuthenticateToZosmfException() { mockZosmfService("zosmf", 1433); // unsupported runtime exception - when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) + when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) .thenThrow(new IllegalArgumentException("msg")); try { zosmfService.validate(ZosmfServiceV2.TokenType.LTPA, "token"); @@ -254,7 +254,7 @@ public void testAuthenticateToZosmfException() { // ResourceAccessException reset(restTemplate); - when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) + when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) .thenThrow(new ResourceAccessException("msg")); try { zosmfService.validate(ZosmfServiceV2.TokenType.LTPA, "token"); @@ -264,7 +264,7 @@ public void testAuthenticateToZosmfException() { // RestClientException reset(restTemplate); - when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) + when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) .thenThrow(new RestClientException("msg")); try { zosmfService.validate(ZosmfServiceV2.TokenType.LTPA, "token"); diff --git a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfAuthenticationTest.java b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfAuthenticationTest.java index 8786b3d7b9..e9fa5610c3 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfAuthenticationTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfAuthenticationTest.java @@ -7,11 +7,8 @@ * * Copyright Contributors to the Zowe Project. */ -package com.ca.mfaas.gatewayservice; +package org.zowe.apiml.gatewayservice; -import com.ca.mfaas.util.categories.AdditionalLocalTest; -import com.ca.mfaas.util.service.DiscoveryUtils; -import com.ca.mfaas.util.service.VirtualService; import io.restassured.RestAssured; import lombok.AllArgsConstructor; import org.junit.Before; @@ -22,16 +19,18 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.zowe.apiml.util.categories.AdditionalLocalTest; +import org.zowe.apiml.util.service.DiscoveryUtils; +import org.zowe.apiml.util.service.VirtualService; -import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Base64; -import static com.ca.mfaas.gatewayservice.SecurityUtils.getConfiguredSslConfig; import static io.restassured.RestAssured.given; +import static org.zowe.apiml.gatewayservice.SecurityUtils.getConfiguredSslConfig; /** * Those tests simulate different version of z/OSMF. @@ -187,6 +186,8 @@ public void testNewAuthenticationEndpointInvalid() throws Exception { @AllArgsConstructor private static class AuthServlet extends HttpServlet { + private static final long serialVersionUID = 376774806451036298L; + protected final String bodyOnSuccess; protected final String bodyOnError; protected final String cookiesOnSuccess; @@ -224,12 +225,14 @@ protected void fail(HttpServletResponse resp) throws IOException { private static class AuthServletGet extends AuthServlet { + private static final long serialVersionUID = 1148934437865771827L; + public AuthServletGet(String bodyOnSuccess, String bodyOnError, String cookiesOnSuccess, HttpStatus unauthorizedStatus) { super(bodyOnSuccess, bodyOnError, cookiesOnSuccess, unauthorizedStatus); } @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { if (req.getHeader("X-CSRF-ZOSMF-HEADER") == null) { resp.setStatus(HttpStatus.BAD_REQUEST.value()); return; @@ -250,12 +253,14 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se private static class AuthServletPost extends AuthServlet { + private static final long serialVersionUID = -693089744639154436L; + public AuthServletPost(String bodyOnSuccess, String bodyOnError, String cookiesOnSuccess, HttpStatus unauthorizedStatus) { super(bodyOnSuccess, bodyOnError, cookiesOnSuccess, unauthorizedStatus); } @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { if (req.getHeader("X-CSRF-ZOSMF-HEADER") == null) { resp.setStatus(HttpStatus.BAD_REQUEST.value()); return; From 7d98322b152f7b5aaa80b5e799ff3daf1249b618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Mon, 10 Feb 2020 16:49:09 +0100 Subject: [PATCH 109/122] fix unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../security/service/ZosmfService.java | 9 +- .../service/zosmf/AbstractZosmfService.java | 30 +----- .../service/zosmf/ZosmfServiceFacade.java | 57 ++++++++--- .../src/main/resources/ehcache.xml | 2 +- .../ZosmfAuthenticationProviderTest.java | 97 ++++++++++++------- .../service/zosmf/ZosmfServiceFacadeTest.java | 24 +++-- .../service/zosmf/ZosmfServiceV1Test.java | 3 +- .../service/zosmf/ZosmfServiceV2Test.java | 7 +- .../AuthenticationOnDeploymentTest.java | 2 +- .../ZosmfAuthenticationTest.java | 20 +++- 10 files changed, 154 insertions(+), 97 deletions(-) diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/ZosmfService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/ZosmfService.java index 3cc3f71078..e2bbea9168 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/ZosmfService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/ZosmfService.java @@ -10,8 +10,9 @@ package org.zowe.apiml.gateway.security.service; import lombok.AllArgsConstructor; +import lombok.Data; import lombok.Getter; -import lombok.Value; +import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import java.util.Map; @@ -73,10 +74,12 @@ public enum TokenType { /** * Response of authentication, contains all data to next processing */ - @Value + @Data + @AllArgsConstructor + @RequiredArgsConstructor public static class AuthenticationResponse { - private final String domain; + private String domain; private final Map tokens; } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/AbstractZosmfService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/AbstractZosmfService.java index 67f8566dca..11fba12c85 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/AbstractZosmfService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/AbstractZosmfService.java @@ -10,7 +10,6 @@ package org.zowe.apiml.gateway.security.service.zosmf; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.netflix.discovery.DiscoveryClient; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpHeaders; @@ -29,7 +28,6 @@ import org.zowe.apiml.security.common.error.ServiceNotAccessibleException; import org.zowe.apiml.util.EurekaUtils; -import java.io.IOException; import java.util.*; import java.util.function.Supplier; @@ -112,31 +110,6 @@ protected RuntimeException handleExceptionOnCall(String url, RuntimeException re return re; } - /** - * Read the z/OSMF domain from the content in the response - * - * @param content the response body - * @return the z/OSMF domain - * @throws AuthenticationServiceException if the zosmf domain cannot be read - */ - protected String readDomain(String content) { - try { - if (content == null) return null; - ObjectNode zosmfNode = securityObjectMapper.readValue(content, ObjectNode.class); - - return Optional.ofNullable(zosmfNode) - .filter(zn -> zn.has(ZOSMF_DOMAIN)) - .map(zn -> zn.get(ZOSMF_DOMAIN).asText()) - .orElseThrow(() -> { - apimlLog.log("apiml.security.zosmfDomainIsEmpty", ZOSMF_DOMAIN); - return new AuthenticationServiceException("z/OSMF domain cannot be read."); - }); - } catch (IOException e) { - apimlLog.log("apiml.security.errorParsingZosmfResponse", e.getMessage()); - throw new AuthenticationServiceException("z/OSMF domain cannot be read."); - } - } - /** * Read the token with name cookieName from the cookies * @@ -166,8 +139,7 @@ protected AuthenticationResponse getAuthenticationResponse(ResponseEntity info = restTemplateWithoutKeystore.exchange( url, HttpMethod.GET, new HttpEntity<>(headers), ZosmfInfo.class ); - final ZosmfInfo body = info.getBody(); - if (body == null) return 0; - return body.getVersion(); + + ZosmfInfo zosmfInfo = info.getBody(); + if ((zosmfInfo != null) && StringUtils.isEmpty(zosmfInfo.getSafRealm())) { + apimlLog.log("apiml.security.zosmfDomainIsEmpty", ZOSMF_DOMAIN); + throw new AuthenticationServiceException("z/OSMF domain cannot be read."); + } + + return zosmfInfo; } catch (RuntimeException re) { meProxy.evictCaches(); throw handleExceptionOnCall(url, re); } } + protected int getVersion(ZosmfInfo zosmfInfo) { + if (zosmfInfo == null) return 0; + return zosmfInfo.getVersion(); + } + @Cacheable("zosmfServiceImplementation") - public ZosmfService getImplementation(String zosmfServiceId) { - final int version = meProxy.getVersion(zosmfServiceId); + public ImplementationWrapper getImplementation(String zosmfServiceId) { + final ZosmfInfo zosmfInfo = meProxy.getZosmfInfo(zosmfServiceId); + final int version = getVersion(zosmfInfo); for (final ZosmfService zosmfService : implementations) { - if (zosmfService.matchesVersion(version)) return zosmfService; + if (zosmfService.matchesVersion(version)) return new ImplementationWrapper(zosmfInfo, zosmfService); } meProxy.evictCaches(); throw new IllegalArgumentException("Unknown version of z/OSMF : " + version); } - protected ZosmfService getImplementation() { + protected ImplementationWrapper getImplementation() { return meProxy.getImplementation(getZosmfServiceId()); } @Override public AuthenticationResponse authenticate(Authentication authentication) { - return getImplementation().authenticate(authentication); + final ImplementationWrapper implementation = getImplementation(); + final ZosmfInfo zosmfInfo = implementation.getZosmfInfo(); + final AuthenticationResponse output = implementation.getZosmfService().authenticate(authentication); + if (zosmfInfo != null) { + output.setDomain(zosmfInfo.getSafRealm()); + } + return output; } @Override public void validate(TokenType type, String token) { - getImplementation().validate(type, token); + getImplementation().getZosmfService().validate(type, token); } @Override public void invalidate(TokenType type, String token) { - getImplementation().invalidate(type, token); + getImplementation().getZosmfService().invalidate(type, token); } @Override @@ -158,6 +177,18 @@ public static class ZosmfInfo { @JsonProperty("zosmf_full_version") private String fullVersion; + @JsonProperty(ZOSMF_DOMAIN) + private String safRealm; + + } + + @Value + public static class ImplementationWrapper { + + private final ZosmfInfo zosmfInfo; + + private final ZosmfService zosmfService; + } } diff --git a/gateway-service/src/main/resources/ehcache.xml b/gateway-service/src/main/resources/ehcache.xml index fdaad48cf3..2ad560efd7 100644 --- a/gateway-service/src/main/resources/ehcache.xml +++ b/gateway-service/src/main/resources/ehcache.xml @@ -10,7 +10,7 @@ - + diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java index acb32cfb61..1fe03f4880 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java @@ -20,6 +20,7 @@ import org.junit.rules.ExpectedException; import org.mockito.Mockito; import org.mockito.stubbing.Answer; +import org.springframework.context.ApplicationContext; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -35,11 +36,13 @@ import org.springframework.web.client.RestTemplate; import org.zowe.apiml.gateway.security.service.AuthenticationService; import org.zowe.apiml.gateway.security.service.ZosmfService; -import org.zowe.apiml.gateway.security.service.zosmf.ZosmfServiceV2; +import org.zowe.apiml.gateway.security.service.zosmf.ZosmfServiceFacade; +import org.zowe.apiml.gateway.security.service.zosmf.ZosmfServiceV1; import org.zowe.apiml.security.common.config.AuthConfigurationProperties; import org.zowe.apiml.security.common.error.ServiceNotAccessibleException; import org.zowe.apiml.security.common.token.TokenAuthentication; +import java.io.IOException; import java.util.Arrays; import java.util.Collections; @@ -59,6 +62,7 @@ public class ZosmfAuthenticationProviderTest { private static final String COOKIE2 = "LtpaToken2=test"; private static final String DOMAIN = "realm"; private static final String RESPONSE = "{\"zosmf_saf_realm\": \"" + DOMAIN + "\"}"; + private static final String INVALID_RESPONSE = "{\"saf_realm\": \"" + DOMAIN + "\"}"; private UsernamePasswordAuthenticationToken usernamePasswordAuthentication; private AuthConfigurationProperties authConfigurationProperties; @@ -72,6 +76,12 @@ public class ZosmfAuthenticationProviderTest { @Rule public final ExpectedException exception = ExpectedException.none(); + private ZosmfServiceFacade.ZosmfInfo getResponse(boolean valid) throws IOException { + return securityObjectMapper.reader().forType(ZosmfServiceFacade.ZosmfInfo.class).readValue( + valid ? RESPONSE : INVALID_RESPONSE + ); + } + @Before public void setUp() { usernamePasswordAuthentication = new UsernamePasswordAuthenticationToken(USERNAME, PASSWORD); @@ -100,8 +110,31 @@ private Application createApplication(InstanceInfo...instanceInfos) { return out; } + private ZosmfService createZosmfService() { + ZosmfServiceV1 zosmfServiceV1 = new ZosmfServiceV1(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); + ApplicationContext applicationContext = mock(ApplicationContext.class); + ZosmfServiceFacade output = new ZosmfServiceFacade(authConfigurationProperties, discovery, restTemplate, securityObjectMapper, applicationContext, Collections.singletonList(zosmfServiceV1)); + output = spy(output); + when(applicationContext.getBean(ZosmfServiceFacade.class)).thenReturn(output); + output.afterPropertiesSet(); + + /*ZosmfServiceFacade.ZosmfInfo zosmfInfo = new ZosmfServiceFacade.ZosmfInfo(); + zosmfInfo.setVersion(27); + zosmfInfo.setFullVersion("27.0"); + zosmfInfo.setSafRealm(DOMAIN); + doReturn(zosmfInfo).when(output).getZosmfInfo(ZOSMF); + + when(restTemplate.exchange(Mockito.anyString(), + Mockito.eq(HttpMethod.GET), + Mockito.any(), + Mockito.>any())) + .thenReturn(new ResponseEntity<>(infoAnswer, new HttpHeaders(), HttpStatus.OK));*/ + + return output; + } + @Test - public void loginWithExistingUser() { + public void loginWithExistingUser() throws IOException { authConfigurationProperties.setZosmfServiceId(ZOSMF); final Application application = createApplication(zosmfInstance); @@ -111,12 +144,12 @@ public void loginWithExistingUser() { headers.add(HttpHeaders.SET_COOKIE, COOKIE1); headers.add(HttpHeaders.SET_COOKIE, COOKIE2); when(restTemplate.exchange(Mockito.anyString(), - Mockito.eq(HttpMethod.POST), + Mockito.eq(HttpMethod.GET), Mockito.any(), Mockito.>any())) - .thenReturn(new ResponseEntity<>(RESPONSE, headers, HttpStatus.OK)); + .thenReturn(new ResponseEntity<>(getResponse(true), headers, HttpStatus.OK)); - ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); + ZosmfService zosmfService = createZosmfService(); ZosmfAuthenticationProvider zosmfAuthenticationProvider = new ZosmfAuthenticationProvider(authenticationService, zosmfService); @@ -128,7 +161,7 @@ public void loginWithExistingUser() { } @Test - public void loginWithBadUser() { + public void loginWithBadUser() throws IOException { authConfigurationProperties.setZosmfServiceId(ZOSMF); final Application application = createApplication(zosmfInstance); @@ -136,12 +169,12 @@ public void loginWithBadUser() { HttpHeaders headers = new HttpHeaders(); when(restTemplate.exchange(Mockito.anyString(), - Mockito.eq(HttpMethod.POST), + Mockito.eq(HttpMethod.GET), Mockito.any(), Mockito.>any())) - .thenReturn(new ResponseEntity<>(RESPONSE, headers, HttpStatus.OK)); + .thenReturn(new ResponseEntity<>(getResponse(true), headers, HttpStatus.OK)); - ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); + ZosmfService zosmfService = createZosmfService(); ZosmfAuthenticationProvider zosmfAuthenticationProvider = new ZosmfAuthenticationProvider(authenticationService, zosmfService); @@ -158,7 +191,7 @@ public void noZosmfInstance() { final Application application = createApplication(); when(discovery.getApplication(ZOSMF)).thenReturn(application); - ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); + ZosmfService zosmfService = createZosmfService(); ZosmfAuthenticationProvider zosmfAuthenticationProvider = new ZosmfAuthenticationProvider(authenticationService, zosmfService); @@ -170,7 +203,7 @@ public void noZosmfInstance() { @Test public void noZosmfServiceId() { - ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); + ZosmfService zosmfService = createZosmfService(); ZosmfAuthenticationProvider zosmfAuthenticationProvider = new ZosmfAuthenticationProvider(authenticationService, zosmfService); @@ -191,12 +224,12 @@ public void notValidZosmfResponse() { headers.add(HttpHeaders.SET_COOKIE, COOKIE1); headers.add(HttpHeaders.SET_COOKIE, COOKIE2); when(restTemplate.exchange(Mockito.anyString(), - Mockito.eq(HttpMethod.POST), + Mockito.eq(HttpMethod.GET), Mockito.any(), Mockito.>any())) - .thenReturn(new ResponseEntity<>("", headers, HttpStatus.OK)); + .thenReturn(new ResponseEntity<>(new ZosmfServiceFacade.ZosmfInfo(), headers, HttpStatus.OK)); - ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); + ZosmfService zosmfService = createZosmfService(); ZosmfAuthenticationProvider zosmfAuthenticationProvider = new ZosmfAuthenticationProvider(authenticationService, zosmfService); @@ -207,9 +240,7 @@ public void notValidZosmfResponse() { } @Test - public void noDomainInResponse() { - String invalidResponse = "{\"saf_realm\": \"" + DOMAIN + "\"}"; - + public void noDomainInResponse() throws IOException { authConfigurationProperties.setZosmfServiceId(ZOSMF); final Application application = createApplication(zosmfInstance); @@ -219,12 +250,12 @@ public void noDomainInResponse() { headers.add(HttpHeaders.SET_COOKIE, COOKIE1); headers.add(HttpHeaders.SET_COOKIE, COOKIE2); when(restTemplate.exchange(Mockito.anyString(), - Mockito.eq(HttpMethod.POST), + Mockito.eq(HttpMethod.GET), Mockito.any(), Mockito.>any())) - .thenReturn(new ResponseEntity<>(invalidResponse, headers, HttpStatus.OK)); + .thenReturn(new ResponseEntity<>(getResponse(false), headers, HttpStatus.OK)); - ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); + ZosmfService zosmfService = createZosmfService(); ZosmfAuthenticationProvider zosmfAuthenticationProvider = new ZosmfAuthenticationProvider(authenticationService, zosmfService); @@ -235,7 +266,7 @@ public void noDomainInResponse() { } @Test - public void invalidCookieInResponse() { + public void invalidCookieInResponse() throws IOException { String invalidCookie = "LtpaToken=test"; authConfigurationProperties.setZosmfServiceId(ZOSMF); @@ -246,12 +277,12 @@ public void invalidCookieInResponse() { HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.SET_COOKIE, invalidCookie); when(restTemplate.exchange(Mockito.anyString(), - Mockito.eq(HttpMethod.POST), + Mockito.eq(HttpMethod.GET), Mockito.any(), Mockito.>any())) - .thenReturn(new ResponseEntity<>(RESPONSE, headers, HttpStatus.OK)); + .thenReturn(new ResponseEntity<>(getResponse(true), headers, HttpStatus.OK)); - ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); + ZosmfService zosmfService = createZosmfService(); ZosmfAuthenticationProvider zosmfAuthenticationProvider = new ZosmfAuthenticationProvider(authenticationService, zosmfService); @@ -262,7 +293,7 @@ public void invalidCookieInResponse() { } @Test - public void cookieWithSemicolon() { + public void cookieWithSemicolon() throws IOException { String cookie = "LtpaToken2=test;"; authConfigurationProperties.setZosmfServiceId(ZOSMF); @@ -273,12 +304,12 @@ public void cookieWithSemicolon() { HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.SET_COOKIE, cookie); when(restTemplate.exchange(Mockito.anyString(), - Mockito.eq(HttpMethod.POST), + Mockito.eq(HttpMethod.GET), Mockito.any(), Mockito.>any())) - .thenReturn(new ResponseEntity<>(RESPONSE, headers, HttpStatus.OK)); + .thenReturn(new ResponseEntity<>(getResponse(true), headers, HttpStatus.OK)); - ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); + ZosmfService zosmfService = createZosmfService(); ZosmfAuthenticationProvider zosmfAuthenticationProvider = new ZosmfAuthenticationProvider(authenticationService, zosmfService); @@ -296,11 +327,11 @@ public void shouldThrowNewExceptionIfRestClientException() { final Application application = createApplication(zosmfInstance); when(discovery.getApplication(ZOSMF)).thenReturn(application); when(restTemplate.exchange(Mockito.anyString(), - Mockito.eq(HttpMethod.POST), + Mockito.eq(HttpMethod.GET), Mockito.any(), Mockito.>any())) .thenThrow(RestClientException.class); - ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); + ZosmfService zosmfService = createZosmfService(); ZosmfAuthenticationProvider zosmfAuthenticationProvider = new ZosmfAuthenticationProvider(authenticationService, zosmfService); @@ -318,11 +349,11 @@ public void shouldThrowNewExceptionIfResourceAccessException() { final Application application = createApplication(zosmfInstance); when(discovery.getApplication(ZOSMF)).thenReturn(application); when(restTemplate.exchange(Mockito.anyString(), - Mockito.eq(HttpMethod.POST), + Mockito.eq(HttpMethod.GET), Mockito.any(), Mockito.>any())) .thenThrow(ResourceAccessException.class); - ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); + ZosmfService zosmfService = createZosmfService(); ZosmfAuthenticationProvider zosmfAuthenticationProvider = new ZosmfAuthenticationProvider(authenticationService, zosmfService); @@ -339,7 +370,7 @@ public void shouldReturnTrueWhenSupportMethodIsCalledWithCorrectClass() { final Application application = createApplication(zosmfInstance); when(discovery.getApplication(ZOSMF)).thenReturn(application); - ZosmfServiceV2 zosmfService = new ZosmfServiceV2(authConfigurationProperties, discovery, restTemplate, securityObjectMapper); + ZosmfService zosmfService = createZosmfService(); ZosmfAuthenticationProvider zosmfAuthenticationProvider = new ZosmfAuthenticationProvider(authenticationService, zosmfService); diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceFacadeTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceFacadeTest.java index 92d706c8d3..ef19caf141 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceFacadeTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceFacadeTest.java @@ -8,8 +8,6 @@ * Copyright Contributors to the Zowe Project. */ -import org.zowe.apiml.security.common.config.AuthConfigurationProperties; -import org.zowe.apiml.gateway.security.service.ZosmfService; import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.discovery.DiscoveryClient; import lombok.AllArgsConstructor; @@ -17,14 +15,13 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.context.ApplicationContext; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; +import org.springframework.http.*; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.web.client.RestTemplate; +import org.zowe.apiml.gateway.security.service.ZosmfService; +import org.zowe.apiml.security.common.config.AuthConfigurationProperties; import java.util.Arrays; import java.util.List; @@ -75,11 +72,15 @@ protected String getURI(String zosmf) { private void mockVersion(int version) { ZosmfServiceFacade.ZosmfInfo response = new ZosmfServiceFacade.ZosmfInfo(); response.setVersion(version); + response.setSafRealm("domain"); + + final HttpHeaders headers = new HttpHeaders(); + headers.add("X-CSRF-ZOSMF-HEADER", ""); when(restTemplate.exchange( eq("http://zosmf:1433/zosmf/info"), eq(HttpMethod.GET), - any(), + eq(new HttpEntity<>(headers)), eq(ZosmfServiceFacade.ZosmfInfo.class) )).thenReturn(new ResponseEntity<>(response, HttpStatus.OK)); } @@ -215,7 +216,12 @@ public void testNoBody() { anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any() )).thenReturn(new ResponseEntity<>(null, HttpStatus.NO_CONTENT)); - assertEquals(0, zosmfService.getVersion("zosmf")); + try { + zosmfService.getImplementation(); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Unknown version of z/OSMF : 0", e.getMessage()); + } } @AllArgsConstructor @@ -263,7 +269,7 @@ private static class ZosmfServiceTest2 implements ZosmfService { @Override public AuthenticationResponse authenticate(Authentication authentication) { - return null; + return mock(AuthenticationResponse.class); } @Override diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1Test.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1Test.java index 5acb01de3b..4ed634a9fd 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1Test.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1Test.java @@ -97,7 +97,8 @@ public void testAuthenticate() { assertNotNull(response.getTokens()); assertEquals(1, response.getTokens().size()); assertEquals("lt", response.getTokens().get(ZosmfService.TokenType.LTPA)); - assertEquals("domain", response.getDomain()); + //provided via Facade + assertNull(response.getDomain()); } @Test(expected = AuthenticationServiceException.class) diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV2Test.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV2Test.java index 53cd00176e..437f79a400 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV2Test.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV2Test.java @@ -117,7 +117,7 @@ public void testInvalidateLtpa() { @Test public void testInvalidateError500() { mockZosmfService("zosmf", 1433); - when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) + when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) .thenReturn(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); try { @@ -219,7 +219,7 @@ public void testAuthenticate() { HttpHeaders responseHeaders = new HttpHeaders(); responseHeaders.add(HttpHeaders.SET_COOKIE, "jwtToken=jt"); responseHeaders.add(HttpHeaders.SET_COOKIE, "LtpaToken2=lt"); - ResponseEntity responseEntity = new ResponseEntity<>("{\"zosmf_saf_realm\":\"domain\"}", responseHeaders, HttpStatus.OK); + ResponseEntity responseEntity = new ResponseEntity<>("{}", responseHeaders, HttpStatus.OK); when(restTemplate.exchange( "http://zosmf:1433/zosmf/services/authenticate", @@ -236,7 +236,8 @@ public void testAuthenticate() { assertEquals(2, response.getTokens().size()); assertEquals("lt", response.getTokens().get(ZosmfService.TokenType.LTPA)); assertEquals("jt", response.getTokens().get(ZosmfService.TokenType.JWT)); - assertEquals("domain", response.getDomain()); + // provided via Facade + assertNull(response.getDomain()); } @Test diff --git a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/AuthenticationOnDeploymentTest.java b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/AuthenticationOnDeploymentTest.java index 8b14d3cda4..90c392f2c2 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/AuthenticationOnDeploymentTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/AuthenticationOnDeploymentTest.java @@ -39,7 +39,7 @@ @Category(AdditionalLocalTest.class) public class AuthenticationOnDeploymentTest { - private static final int TIMEOUT = 10; + private static final int TIMEOUT = 100; private RequestVerifier verifier; diff --git a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfAuthenticationTest.java b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfAuthenticationTest.java index e9fa5610c3..c2198a665a 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfAuthenticationTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfAuthenticationTest.java @@ -59,7 +59,7 @@ public void setUp() { } @Test - public void testNoResponse() throws Exception { + public void testNoContent() throws Exception { try (VirtualService zosmf = new VirtualService(ZOSMF_ID)) { zosmf .addServlet("info", "/zosmf/info", new AuthServletGet( @@ -73,6 +73,8 @@ public void testNoResponse() throws Exception { .auth().preemptive().basic(USER_ID, PASSWORD) .post(DiscoveryUtils.getGatewayUrls().get(0) + LOGIN_ENDPOINT) .then().statusCode(HttpStatus.NO_CONTENT.value()); + + zosmf.unregister().waitForGatewayUnregistering(1, TIMEOUT_REGISTRATION); } } @@ -92,6 +94,8 @@ public void testOldAuthenticationEndpoint() throws Exception { .auth().preemptive().basic(USER_ID, PASSWORD) .post(DiscoveryUtils.getGatewayUrls().get(0) + LOGIN_ENDPOINT) .then().statusCode(HttpStatus.NO_CONTENT.value()).cookie("apimlAuthenticationToken"); + + zosmf.unregister().waitForGatewayUnregistering(1, TIMEOUT_REGISTRATION); } } @@ -111,6 +115,8 @@ public void testOldAuthenticationEndpointInvalid() throws Exception { .auth().preemptive().basic(USER_ID, WRONG_PASSWORD) .post(DiscoveryUtils.getGatewayUrls().get(0) + LOGIN_ENDPOINT) .then().statusCode(HttpStatus.UNAUTHORIZED.value()); + + zosmf.unregister().waitForGatewayUnregistering(1, TIMEOUT_REGISTRATION); } } @@ -123,7 +129,7 @@ public void testNewAuthenticationEndpointLtpa() throws Exception { null, null, HttpStatus.OK )) .addServlet("auth", "/zosmf/services/authenticate", new AuthServletPost( - null, + "{}", null, "LtpaToken2=ltpaToken", HttpStatus.UNAUTHORIZED )) .addRoute("/api", "/") @@ -134,6 +140,8 @@ public void testNewAuthenticationEndpointLtpa() throws Exception { .auth().preemptive().basic(USER_ID, PASSWORD) .post(DiscoveryUtils.getGatewayUrls().get(0) + LOGIN_ENDPOINT) .then().statusCode(HttpStatus.NO_CONTENT.value()).cookie("apimlAuthenticationToken"); + + zosmf.unregister().waitForGatewayUnregistering(1, TIMEOUT_REGISTRATION); } } @@ -146,7 +154,7 @@ public void testNewAuthenticationEndpointJwt() throws Exception { null, null, HttpStatus.OK )) .addServlet("auth", "/zosmf/services/authenticate", new AuthServletPost( - null, + "{}", null, "jwtToken=jwtToken", HttpStatus.UNAUTHORIZED )) .addRoute("/api", "/") @@ -157,6 +165,8 @@ public void testNewAuthenticationEndpointJwt() throws Exception { .auth().preemptive().basic(USER_ID, PASSWORD) .post(DiscoveryUtils.getGatewayUrls().get(0) + LOGIN_ENDPOINT) .then().statusCode(HttpStatus.NO_CONTENT.value()).cookie("apimlAuthenticationToken", "jwtToken"); + + zosmf.unregister().waitForGatewayUnregistering(1, TIMEOUT_REGISTRATION); } } @@ -169,7 +179,7 @@ public void testNewAuthenticationEndpointInvalid() throws Exception { null, null, HttpStatus.OK )) .addServlet("auth", "/zosmf/services/authenticate", new AuthServletPost( - null, + "{}", null, "jwtToken=jwtToken", HttpStatus.UNAUTHORIZED )) .addRoute("/api", "/") @@ -180,6 +190,8 @@ public void testNewAuthenticationEndpointInvalid() throws Exception { .auth().preemptive().basic(USER_ID, WRONG_PASSWORD) .post(DiscoveryUtils.getGatewayUrls().get(0) + LOGIN_ENDPOINT) .then().statusCode(HttpStatus.UNAUTHORIZED.value()); + + zosmf.unregister().waitForGatewayUnregistering(1, TIMEOUT_REGISTRATION); } } From 2417db495d5395fe11fb33831d36e241e1840d01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Mon, 10 Feb 2020 16:55:27 +0100 Subject: [PATCH 110/122] merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../security/service/zosmf/AbstractZosmfService.java | 6 +++--- .../gateway/security/service/zosmf/ZosmfServiceV1.java | 2 +- .../gateway/security/service/zosmf/ZosmfServiceV2.java | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/AbstractZosmfService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/AbstractZosmfService.java index 11fba12c85..484db30b6e 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/AbstractZosmfService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/AbstractZosmfService.java @@ -78,7 +78,7 @@ protected String getAuthenticationValue(Authentication authentication) { */ protected String getURI(String zosmf) { Supplier authenticationServiceExceptionSupplier = () -> { - apimlLog.log("apiml.security.zosmfInstanceNotFound", zosmf); + apimlLog.log("org.zowe.apiml.security.zosmfInstanceNotFound", zosmf); return new ServiceNotAccessibleException("z/OSMF instance not found or incorrectly configured."); }; @@ -94,7 +94,7 @@ protected String getURI(String zosmf) { protected RuntimeException handleExceptionOnCall(String url, RuntimeException re) { if (re instanceof ResourceAccessException) { - apimlLog.log("apiml.security.serviceUnavailable", url, re.getMessage()); + apimlLog.log("org.zowe.apiml.security.serviceUnavailable", url, re.getMessage()); return new ServiceNotAccessibleException("Could not get an access to z/OSMF service."); } @@ -103,7 +103,7 @@ protected RuntimeException handleExceptionOnCall(String url, RuntimeException re } if (re instanceof RestClientException) { - apimlLog.log("apiml.security.generic", re.getMessage(), url); + apimlLog.log("org.zowe.apiml.security.generic", re.getMessage(), url); return new AuthenticationServiceException("A failure occurred when authenticating.", re); } diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1.java index 0797511cef..0af14e9d84 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1.java @@ -73,7 +73,7 @@ public void validate(TokenType type, String token) { if (response.getStatusCodeValue() == 401) { throw new TokenNotValidException("Token is not valid."); } - apimlLog.log("apiml.security.serviceUnavailable", url, response.getStatusCodeValue()); + apimlLog.log("org.zowe.apiml.security.serviceUnavailable", url, response.getStatusCodeValue()); throw new ServiceNotAccessibleException("Could not get an access to z/OSMF service."); } catch (RuntimeException re) { throw handleExceptionOnCall(url, re); diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV2.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV2.java index f55fcd4dae..d2a04c1fbb 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV2.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV2.java @@ -78,7 +78,7 @@ public void validate(ZosmfService.TokenType type, String token) { if (re.getStatusCodeValue() == 401) { throw new TokenNotValidException("Token is not valid."); } - apimlLog.log("apiml.security.serviceUnavailable", url, re.getStatusCodeValue()); + apimlLog.log("org.zowe.apiml.security.serviceUnavailable", url, re.getStatusCodeValue()); throw new ServiceNotAccessibleException("Could not get an access to z/OSMF service."); } catch (RuntimeException re) { throw handleExceptionOnCall(url, re); @@ -101,7 +101,7 @@ public void invalidate(ZosmfService.TokenType type, String token) { String.class); if (re.getStatusCode().is2xxSuccessful()) return; - apimlLog.log("apiml.security.serviceUnavailable", url, re.getStatusCodeValue()); + apimlLog.log("org.zowe.apiml.security.serviceUnavailable", url, re.getStatusCodeValue()); throw new ServiceNotAccessibleException("Could not get an access to z/OSMF service."); } catch (RuntimeException re) { throw handleExceptionOnCall(url, re); From a573d6dd41f569f1806c46fb8ffdbc89f9dfd34d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Mon, 10 Feb 2020 17:34:43 +0100 Subject: [PATCH 111/122] fix using restTemplate without keystore in z/OSMF services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../gateway/security/service/zosmf/AbstractZosmfService.java | 2 +- .../apiml/gateway/security/service/zosmf/ZosmfServiceV1.java | 4 ++-- .../apiml/gateway/security/service/zosmf/ZosmfServiceV2.java | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/AbstractZosmfService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/AbstractZosmfService.java index 484db30b6e..8a335da3ee 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/AbstractZosmfService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/AbstractZosmfService.java @@ -49,7 +49,7 @@ public abstract class AbstractZosmfService implements ZosmfService { public AbstractZosmfService( AuthConfigurationProperties authConfigurationProperties, DiscoveryClient discovery, - @Qualifier("restTemplateWithKeystore") RestTemplate restTemplateWithoutKeystore, + @Qualifier("restTemplateWithoutKeystore") RestTemplate restTemplateWithoutKeystore, ObjectMapper securityObjectMapper ) { this.authConfigurationProperties = authConfigurationProperties; diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1.java index 0af14e9d84..6f554d82d2 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1.java @@ -28,10 +28,10 @@ public class ZosmfServiceV1 extends AbstractZosmfService { public ZosmfServiceV1( AuthConfigurationProperties authConfigurationProperties, DiscoveryClient discovery, - @Qualifier("restTemplateWithKeystore") RestTemplate restTemplateWithKeystore, + @Qualifier("restTemplateWithoutKeystore") RestTemplate restTemplateWithoutKeystore, ObjectMapper securityObjectMapper ) { - super(authConfigurationProperties, discovery, restTemplateWithKeystore, securityObjectMapper); + super(authConfigurationProperties, discovery, restTemplateWithoutKeystore, securityObjectMapper); } @Override diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV2.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV2.java index d2a04c1fbb..85a978dd06 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV2.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV2.java @@ -33,10 +33,10 @@ public class ZosmfServiceV2 extends AbstractZosmfService { public ZosmfServiceV2( AuthConfigurationProperties authConfigurationProperties, DiscoveryClient discovery, - @Qualifier("restTemplateWithKeystore") RestTemplate restTemplateWithKeystore, + @Qualifier("restTemplateWithoutKeystore") RestTemplate restTemplateWithoutKeystore, ObjectMapper securityObjectMapper ) { - super(authConfigurationProperties, discovery, restTemplateWithKeystore, securityObjectMapper); + super(authConfigurationProperties, discovery, restTemplateWithoutKeystore, securityObjectMapper); } @Override From fe0753d2d2a22137bf2eed488580c360ca2a7714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Tue, 11 Feb 2020 16:01:47 +0100 Subject: [PATCH 112/122] z/OSMF schema fix - cookies structure, also attempt to fix integration tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../java/org/zowe/apiml/util/CookieUtil.java | 64 +++++++++++++++++++ .../org/zowe/apiml/util/CookieUtilTest.java | 38 +++++++++++ .../security/service/schema/ZosmfScheme.java | 53 ++++++++++----- .../service/schema/ZosmfSchemeTest.java | 22 +++++-- 4 files changed, 156 insertions(+), 21 deletions(-) create mode 100644 common-service-core/src/main/java/org/zowe/apiml/util/CookieUtil.java create mode 100644 common-service-core/src/test/java/org/zowe/apiml/util/CookieUtilTest.java diff --git a/common-service-core/src/main/java/org/zowe/apiml/util/CookieUtil.java b/common-service-core/src/main/java/org/zowe/apiml/util/CookieUtil.java new file mode 100644 index 0000000000..4a6989f6ff --- /dev/null +++ b/common-service-core/src/main/java/org/zowe/apiml/util/CookieUtil.java @@ -0,0 +1,64 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package org.zowe.apiml.util; + +import org.apache.commons.lang.StringUtils; + +/** + * This utilities allowe base work with cookies and its string representation. + */ +public final class CookieUtil { + + private CookieUtil() { + } + + public static String setCookie(String cookieHeader, String name, String value) { + StringBuilder sb = new StringBuilder(); + + int counter = 0; + if (StringUtils.isNotBlank(cookieHeader)) { + for (final String cookie : cookieHeader.split(";")) { + final String[] cookieParts = cookie.split("=", 2); + if (StringUtils.equals(StringUtils.trim(cookieParts[0]), name)) continue; + + if (counter++ > 0) sb.append(';'); + sb.append(cookie); + } + } + + + if (counter > 0) sb.append(';'); + sb.append(name).append('=').append(value); + return sb.toString(); + } + + public static String removeCookie(String cookieHeader, String name) { + StringBuilder sb = new StringBuilder(); + + int counter = 0; + boolean changed = false; + if (StringUtils.isNotBlank(cookieHeader)) { + for (final String cookie : cookieHeader.split(";")) { + final String[] cookieParts = cookie.split("=", 2); + if (StringUtils.equals(StringUtils.trim(cookieParts[0]), name)) { + changed = true; + continue; + } + + if (counter++ > 0) sb.append(';'); + sb.append(cookie); + } + } + + if (!changed) return cookieHeader; + return sb.toString(); + } + +} diff --git a/common-service-core/src/test/java/org/zowe/apiml/util/CookieUtilTest.java b/common-service-core/src/test/java/org/zowe/apiml/util/CookieUtilTest.java new file mode 100644 index 0000000000..73b440648e --- /dev/null +++ b/common-service-core/src/test/java/org/zowe/apiml/util/CookieUtilTest.java @@ -0,0 +1,38 @@ +package org.zowe.apiml.util;/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import static org.junit.Assert.*; +@RunWith(JUnit4.class) +public class CookieUtilTest { + + @Test + public void testSetCookie() { + assertEquals("a=bc", CookieUtil.setCookie(null, "a", "bc")); + assertEquals("a=bc", CookieUtil.setCookie("", "a", "bc")); + assertEquals("a=1;b=2;c=4", CookieUtil.setCookie("a=1;c=3;b=2", "c", "4")); + assertEquals("name=value", CookieUtil.setCookie(";", "name", "value")); + assertEquals("name=value", CookieUtil.setCookie(";;;", "name", "value")); + assertEquals("a=1;b=2;null=null", CookieUtil.setCookie("a=1;b=2", null, null)); + } + + @Test + public void removeCookie() { + String c = "a=1;b=2"; + assertSame(c, CookieUtil.removeCookie(c, "c")); + assertEquals("", CookieUtil.removeCookie("a=b", "a")); + assertEquals("a=1;c=3", CookieUtil.removeCookie("a=1;b=2;c=3", "b")); + assertEquals("", CookieUtil.removeCookie("a=1;a=2;a=3", "a")); + } + +} diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/ZosmfScheme.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/ZosmfScheme.java index 71d83ca1f1..8e7c562ab8 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/ZosmfScheme.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/ZosmfScheme.java @@ -9,15 +9,19 @@ */ package org.zowe.apiml.gateway.security.service.schema; -import org.zowe.apiml.security.common.auth.Authentication; -import org.zowe.apiml.security.common.auth.AuthenticationScheme; -import org.zowe.apiml.security.common.token.QueryResponse; -import org.zowe.apiml.gateway.security.service.AuthenticationService; import com.netflix.appinfo.InstanceInfo; import com.netflix.zuul.context.RequestContext; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; +import org.apache.http.HttpHeaders; import org.springframework.stereotype.Component; +import org.zowe.apiml.gateway.security.service.AuthenticationService; +import org.zowe.apiml.gateway.security.service.ZosmfService; +import org.zowe.apiml.security.common.auth.Authentication; +import org.zowe.apiml.security.common.auth.AuthenticationScheme; +import org.zowe.apiml.security.common.config.AuthConfigurationProperties; +import org.zowe.apiml.security.common.token.QueryResponse; +import org.zowe.apiml.util.CookieUtil; import java.util.Date; import java.util.Optional; @@ -31,6 +35,7 @@ public class ZosmfScheme implements AbstractAuthenticationScheme { private final AuthenticationService authenticationService; + private final AuthConfigurationProperties authConfigurationProperties; @Override public AuthenticationScheme getScheme() { @@ -54,6 +59,25 @@ public class ZosmfCommand extends AuthenticationCommand { private final Long expireAt; + private void setCookie(RequestContext context, String name, String value) { + context.addZuulRequestHeader(COOKIE_HEADER, + CookieUtil.setCookie( + context.getZuulRequestHeaders().get(COOKIE_HEADER), + name, + value + ) + ); + } + + private void removeCookie(RequestContext context, String name) { + context.addZuulRequestHeader(COOKIE_HEADER, + CookieUtil.removeCookie( + context.getZuulRequestHeaders().get(COOKIE_HEADER), + name + ) + ); + } + @Override public void apply(InstanceInfo instanceInfo) { final RequestContext context = RequestContext.getCurrentContext(); @@ -62,24 +86,21 @@ public void apply(InstanceInfo instanceInfo) { jwtToken.ifPresent(token -> { // parse JWT token to detect the source (z/OSMF / Zowe) QueryResponse queryResponse = authenticationService.parseJwtToken(token); - switch ( queryResponse.getSource()) { + switch (queryResponse.getSource()) { case ZOSMF: - // token is generated by z/OSMF, no action is required, just bypass, verification will be by itself + // token is generated by z/OSMF, fix set cookies + removeCookie(context, authConfigurationProperties.getCookieProperties().getCookieName()); + setCookie(context, ZosmfService.TokenType.JWT.getCookieName(), token); break; case ZOWE: // user use Zowe own JWT token, for communication with z/OSMF there should be LTPA token, use it - String ltpaToken = authenticationService.getLtpaTokenWithValidation(token); - - String cookie = context.getZuulRequestHeaders().get(COOKIE_HEADER); - if (cookie != null) { - cookie += "; " + ltpaToken; - } else { - cookie = ltpaToken; - } - - context.addZuulRequestHeader(COOKIE_HEADER, cookie); + final String ltpaToken = authenticationService.getLtpaTokenWithValidation(token); + setCookie(context, ZosmfService.TokenType.LTPA.getCookieName(), ltpaToken); break; } + + // remove authentication part + context.getZuulRequestHeaders().remove(HttpHeaders.AUTHORIZATION); }); } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/schema/ZosmfSchemeTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/schema/ZosmfSchemeTest.java index 239f49fef0..00758d8902 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/schema/ZosmfSchemeTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/schema/ZosmfSchemeTest.java @@ -11,6 +11,7 @@ import com.netflix.zuul.context.RequestContext; import io.jsonwebtoken.JwtException; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; @@ -23,6 +24,7 @@ import org.zowe.apiml.gateway.utils.CleanCurrentRequestContextTest; import org.zowe.apiml.security.common.auth.Authentication; import org.zowe.apiml.security.common.auth.AuthenticationScheme; +import org.zowe.apiml.security.common.config.AuthConfigurationProperties; import org.zowe.apiml.security.common.token.QueryResponse; import org.zowe.apiml.security.common.token.TokenNotValidException; @@ -42,9 +44,19 @@ public class ZosmfSchemeTest extends CleanCurrentRequestContextTest { @Mock private AuthenticationService authenticationService; + @Mock + private AuthConfigurationProperties authConfigurationProperties; + @InjectMocks private ZosmfScheme zosmfScheme; + @Before + public void setUp() { + AuthConfigurationProperties.CookieProperties cookieProperties = mock(AuthConfigurationProperties.CookieProperties.class); + when(cookieProperties.getCookieName()).thenReturn("apimlAuthenticationToken"); + when(authConfigurationProperties.getCookieProperties()).thenReturn(cookieProperties); + } + @Test public void testCreateCommand() throws Exception { Calendar calendar = Calendar.getInstance(); @@ -69,7 +81,7 @@ public void testCreateCommand() throws Exception { when(authenticationService.parseJwtToken("jwtToken1")).thenReturn(queryResponse); requestContext.getZuulRequestHeaders().put(COOKIE_HEADER, null); zosmfScheme.createCommand(authentication, queryResponse).apply(null); - assertEquals("ltpa1", requestContext.getZuulRequestHeaders().get(COOKIE_HEADER)); + assertEquals("LtpaToken2=ltpa1", requestContext.getZuulRequestHeaders().get(COOKIE_HEADER)); // a cookies is set now reset(authenticationService); @@ -78,7 +90,7 @@ public void testCreateCommand() throws Exception { when(authenticationService.parseJwtToken("jwtToken2")).thenReturn(queryResponse); requestContext.getZuulRequestHeaders().put(COOKIE_HEADER, "cookie1=1"); zosmfScheme.createCommand(authentication, queryResponse).apply(null); - assertEquals("cookie1=1; ltpa2", requestContext.getZuulRequestHeaders().get(COOKIE_HEADER)); + assertEquals("cookie1=1;LtpaToken2=ltpa2", requestContext.getZuulRequestHeaders().get(COOKIE_HEADER)); // JWT token is not valid anymore - TokenNotValidException try { @@ -107,13 +119,13 @@ public void testCreateCommand() throws Exception { @Test public void testScheme() { - ZosmfScheme scheme = new ZosmfScheme(authenticationService); + ZosmfScheme scheme = new ZosmfScheme(authenticationService, authConfigurationProperties); assertEquals(AuthenticationScheme.ZOSMF, scheme.getScheme()); } @Test public void testExpiration() { - ZosmfScheme scheme = new ZosmfScheme(authenticationService); + ZosmfScheme scheme = new ZosmfScheme(authenticationService, authConfigurationProperties); AuthenticationCommand command; QueryResponse queryResponse = new QueryResponse(); @@ -151,7 +163,7 @@ public void testExpiration() { @Test public void testZosmfToken() throws AuthenticationException { - ZosmfScheme scheme = new ZosmfScheme(authenticationService); + ZosmfScheme scheme = new ZosmfScheme(authenticationService, authConfigurationProperties); QueryResponse queryResponse = new QueryResponse("domain", "username", new Date(), new Date(), QueryResponse.Source.ZOSMF); when(authenticationService.getJwtTokenFromRequest(any())).thenReturn(Optional.of("jwtTokenZosmf")); when(authenticationService.parseJwtToken("jwtTokenZosmf")).thenReturn(queryResponse); From c5311c5f5ff0d031d7202ac2cd3f5a4f199e5c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Wed, 12 Feb 2020 13:21:05 +0100 Subject: [PATCH 113/122] improve code coverage and allow integration tests with VirtualService on mainframe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../service/zosmf/ZosmfServiceFacadeTest.java | 52 +++++++++++++++++++ .../service/zosmf/ZosmfServiceV1Test.java | 2 +- .../service/zosmf/ZosmfServiceV2Test.java | 18 ++++++- .../AuthenticationOnDeploymentTest.java | 15 +++--- .../ZosmfAuthenticationTest.java | 24 +++++++-- 5 files changed, 96 insertions(+), 15 deletions(-) diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceFacadeTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceFacadeTest.java index ef19caf141..2eb75b345a 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceFacadeTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceFacadeTest.java @@ -33,6 +33,8 @@ @RunWith(SpringJUnit4ClassRunner.class) public class ZosmfServiceFacadeTest { + private static final String SERVICE_ID = "zosmfca32"; + private RestTemplate restTemplate = mock(RestTemplate.class); private ZosmfServiceFacadeTestExt zosmfService; @@ -43,6 +45,8 @@ public void setUp() { public ZosmfServiceFacadeTestExt getZosmfServiceFacade() { AuthConfigurationProperties authConfigurationProperties = mock(AuthConfigurationProperties.class); + when(authConfigurationProperties.getZosmfServiceId()).thenReturn(SERVICE_ID); + when(authConfigurationProperties.validatedZosmfServiceId()).thenReturn(SERVICE_ID); ApplicationContext applicationContext = mock(ApplicationContext.class); ZosmfServiceFacadeTestExt out = new ZosmfServiceFacadeTestExt( @@ -224,6 +228,50 @@ public void testNoBody() { } } + @Test + public void testEvictCacheAllService() { + zosmfService.evictCacheAllService(); + verify(zosmfService, times(1)).evictCaches(); + } + + @Test + public void testEvictCacheService() { + ZosmfServiceFacade zosmfServiceFacade = getZosmfServiceFacade(); + zosmfServiceFacade.evictCacheService(null); + zosmfServiceFacade.evictCacheService("anyService"); + verify(zosmfServiceFacade, never()).evictCaches(); + zosmfServiceFacade.evictCacheService(SERVICE_ID); + verify(zosmfServiceFacade, times(1)).evictCaches(); + } + + @Test + public void testSetDomain() { + ZosmfService.AuthenticationResponse authenticationResponse = mock(ZosmfService.AuthenticationResponse.class); + + ZosmfService zosmfService = mock(ZosmfService.class); + when(zosmfService.authenticate(any())).thenReturn(authenticationResponse); + + ZosmfServiceFacade zosmfServiceFacade = getZosmfServiceFacade(); + doReturn(new ZosmfServiceFacade.ImplementationWrapper(null, zosmfService)) + .when(zosmfServiceFacade).getImplementation(); + + zosmfServiceFacade.authenticate(null); + verify(authenticationResponse, never()).setDomain(any()); + + ZosmfServiceFacade.ZosmfInfo zosmfInfo = new ZosmfServiceFacade.ZosmfInfo(); + zosmfInfo.setSafRealm("realm"); + doReturn(new ZosmfServiceFacade.ImplementationWrapper(zosmfInfo, zosmfService)) + .when(zosmfServiceFacade).getImplementation(); + + zosmfServiceFacade.authenticate(null); + verify(authenticationResponse, times(1)).setDomain("realm"); + } + + @Test + public void testReadTokenFromCookie() { + assertNull(new ZosmfServiceFacadeTestExt(null, null, null, null, null, null).readTokenFromCookie(null, null)); + } + @AllArgsConstructor private static class TestException extends RuntimeException { @@ -302,6 +350,10 @@ public ZosmfService getService(int version) { return null; } + @Override + protected String readTokenFromCookie(List cookies, String cookieName) { + return super.readTokenFromCookie(cookies, cookieName); + } } } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1Test.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1Test.java index 4ed634a9fd..f57a6a04de 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1Test.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1Test.java @@ -111,7 +111,7 @@ public void testAuthenticateException() { (Class) any() )).thenThrow(new RestClientException("any exception")); - zosmfService.validate(ZosmfService.TokenType.LTPA, "anyLtpaToken"); + zosmfService.authenticate(new UsernamePasswordAuthenticationToken("user", "pass")); } @Test diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV2Test.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV2Test.java index 437f79a400..cbe9b077c1 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV2Test.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV2Test.java @@ -21,7 +21,9 @@ import org.mockito.junit.MockitoJUnitRunner; import org.springframework.http.*; import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; @@ -30,10 +32,12 @@ import org.zowe.apiml.security.common.error.ServiceNotAccessibleException; import org.zowe.apiml.security.common.token.TokenNotValidException; +import java.nio.charset.Charset; import java.util.Collections; import static org.junit.Assert.*; import static org.mockito.Mockito.*; +import static org.springframework.http.HttpStatus.UNAUTHORIZED; @RunWith(MockitoJUnitRunner.class) public class ZosmfServiceV2Test { @@ -191,7 +195,7 @@ public void testValidateException() { // response 401 when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) - .thenReturn(new ResponseEntity<>(HttpStatus.UNAUTHORIZED)); + .thenReturn(new ResponseEntity<>(UNAUTHORIZED)); try { zosmfService.validate(ZosmfServiceV2.TokenType.LTPA, "token"); } catch (TokenNotValidException e) { @@ -248,7 +252,7 @@ public void testAuthenticateToZosmfException() { when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) .thenThrow(new IllegalArgumentException("msg")); try { - zosmfService.validate(ZosmfServiceV2.TokenType.LTPA, "token"); + zosmfService.authenticate(new UsernamePasswordAuthenticationToken("user", "pass")); } catch (IllegalArgumentException e) { assertEquals("msg", e.getMessage()); } @@ -273,6 +277,16 @@ public void testAuthenticateToZosmfException() { assertEquals("A failure occurred when authenticating.", e.getMessage()); assertTrue(e.getCause() instanceof RestClientException); } + + // HttpClientErrorException.Unauthorized + reset(restTemplate); + when(restTemplate.exchange(anyString(), (HttpMethod) any(), (HttpEntity) any(), (Class) any())) + .thenThrow(HttpClientErrorException.create(UNAUTHORIZED, "msg", new HttpHeaders(), new byte[0], Charset.defaultCharset())); + try { + zosmfService.validate(ZosmfServiceV2.TokenType.LTPA, "token"); + } catch (BadCredentialsException e) { + assertEquals("Username or password are invalid.", e.getMessage()); + } } @Test diff --git a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/AuthenticationOnDeploymentTest.java b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/AuthenticationOnDeploymentTest.java index 90c392f2c2..e6ca937978 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/AuthenticationOnDeploymentTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/AuthenticationOnDeploymentTest.java @@ -9,34 +9,31 @@ */ package org.zowe.apiml.gatewayservice; -import org.zowe.apiml.security.common.auth.Authentication; -import org.zowe.apiml.security.common.auth.AuthenticationScheme; -import org.zowe.apiml.passticket.PassTicketService; -import org.zowe.apiml.util.categories.AdditionalLocalTest; -import org.zowe.apiml.util.service.RequestVerifier; -import org.zowe.apiml.util.service.VirtualService; import io.restassured.RestAssured; import org.apache.http.HttpHeaders; import org.junit.Before; import org.junit.Test; -import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.zowe.apiml.passticket.PassTicketService; +import org.zowe.apiml.security.common.auth.Authentication; +import org.zowe.apiml.security.common.auth.AuthenticationScheme; +import org.zowe.apiml.util.service.RequestVerifier; +import org.zowe.apiml.util.service.VirtualService; import java.nio.charset.StandardCharsets; import java.util.Base64; -import static org.zowe.apiml.gatewayservice.SecurityUtils.*; import static io.restassured.RestAssured.given; import static org.apache.http.HttpStatus.SC_OK; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertEquals; +import static org.zowe.apiml.gatewayservice.SecurityUtils.*; /** * This test requires to allow endpoint routes on gateway (ie profile dev) */ @RunWith(JUnit4.class) -@Category(AdditionalLocalTest.class) public class AuthenticationOnDeploymentTest { private static final int TIMEOUT = 100; diff --git a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfAuthenticationTest.java b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfAuthenticationTest.java index c2198a665a..891df0d5de 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfAuthenticationTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfAuthenticationTest.java @@ -11,15 +11,14 @@ import io.restassured.RestAssured; import lombok.AllArgsConstructor; +import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.zowe.apiml.util.categories.AdditionalLocalTest; import org.zowe.apiml.util.service.DiscoveryUtils; import org.zowe.apiml.util.service.VirtualService; @@ -30,6 +29,7 @@ import java.util.Base64; import static io.restassured.RestAssured.given; +import static org.apache.http.HttpStatus.SC_OK; import static org.zowe.apiml.gatewayservice.SecurityUtils.getConfiguredSslConfig; /** @@ -40,7 +40,6 @@ * - start discovery service and gateway locally */ @RunWith(JUnit4.class) -@Category(AdditionalLocalTest.class) public class ZosmfAuthenticationTest { private static final String ZOSMF_ID = "zosmfca32"; @@ -56,6 +55,25 @@ public class ZosmfAuthenticationTest { public void setUp() { RestAssured.useRelaxedHTTPSValidation(); RestAssured.config = RestAssured.config().sslConfig(getConfiguredSslConfig()); + + // unregister current z/OSMF + DiscoveryUtils.getDiscoveryUrls().forEach(ds -> + DiscoveryUtils.getInstances(ZOSMF_ID).forEach(zosmf -> { + given().when() + .delete(ds + "/eureka/apps/{appId}/{instanceId}", zosmf.getApp(), zosmf.getInstanceId()) + .then().statusCode(SC_OK); + }) + ); + } + + @After + public void after() { + // reload static clients to use default one again + DiscoveryUtils.getDiscoveryUrls().forEach(ds -> { + given().when() + .post(ds + "/discovery/api/v1/staticApi") + .then().statusCode(SC_OK); + }); } @Test From 3a13a5e0a49457bcdec751fa7da76d7b7e540fea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Thu, 13 Feb 2020 11:43:53 +0100 Subject: [PATCH 114/122] new integration test - logout and disable tests with VirtualService on mainframe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../security/SecurityConfiguration.java | 2 +- .../config/AuthConfigurationProperties.java | 6 +- config/local/api-defs/zosmf-sample.yml_ | 2 - .../config/SecurityConfiguration.java | 2 +- .../AuthenticationOnDeploymentTest.java | 3 + .../zowe/apiml/gatewayservice/LogoutTest.java | 90 +++++++++++++++++++ .../ZosmfAuthenticationTest.java | 3 + 7 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 integration-tests/src/test/java/org/zowe/apiml/gatewayservice/LogoutTest.java diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/SecurityConfiguration.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/SecurityConfiguration.java index 8bd2989fe0..778c0b94dc 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/SecurityConfiguration.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/SecurityConfiguration.java @@ -106,7 +106,7 @@ protected void configure(HttpSecurity http) throws Exception { // logout endpoint .and() .logout() - .logoutUrl(authConfigurationProperties.getServiceLogoutEndpoint()) + .logoutUrl(authConfigurationProperties.getGatewayQueryEndpoint()) .logoutSuccessHandler(logoutSuccessHandler()) // endpoints protection diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/config/AuthConfigurationProperties.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/config/AuthConfigurationProperties.java index 4700501854..4f0542cef8 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/config/AuthConfigurationProperties.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/config/AuthConfigurationProperties.java @@ -9,12 +9,12 @@ */ package org.zowe.apiml.security.common.config; -import org.zowe.apiml.message.log.ApimlLogger; -import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.stereotype.Component; +import org.zowe.apiml.message.log.ApimlLogger; +import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; /** @@ -30,11 +30,11 @@ public class AuthConfigurationProperties { // General properties private String gatewayLoginEndpoint = "/api/v1/gateway/auth/login"; + private String gatewayLogoutEndpoint = "/api/v1/gateway/auth/logout"; private String gatewayQueryEndpoint = "/api/v1/gateway/auth/query"; private String gatewayTicketEndpoint = "/api/v1/gateway/auth/ticket"; private String serviceLoginEndpoint = "/auth/login"; - private String serviceLogoutEndpoint = "/auth/logout"; private AuthConfigurationProperties.TokenProperties tokenProperties; private AuthConfigurationProperties.CookieProperties cookieProperties; diff --git a/config/local/api-defs/zosmf-sample.yml_ b/config/local/api-defs/zosmf-sample.yml_ index 2072b18de4..71d4363221 100644 --- a/config/local/api-defs/zosmf-sample.yml_ +++ b/config/local/api-defs/zosmf-sample.yml_ @@ -20,8 +20,6 @@ services: catalogUiTileId: zosmf # ID of the API Catalog UI tile for z/OSMF services instanceBaseUrls: # list of base URLs for each instance - # scheme://hostname:port/contextPath - authentication: - scheme: zosmf # This service expects z/OSMF LTPA token homePageRelativeUrl: # Home page of the z/OSMF service routes: - gatewayUrl: api # [api/ui/ws]/v{majorVersion} diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/SecurityConfiguration.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/SecurityConfiguration.java index a01f795e78..5287a94513 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/SecurityConfiguration.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/SecurityConfiguration.java @@ -94,7 +94,7 @@ protected void configure(HttpSecurity http) throws Exception { // logout endpoint .and() .logout() - .logoutUrl(authConfigurationProperties.getServiceLogoutEndpoint()) + .logoutUrl(authConfigurationProperties.getGatewayLogoutEndpoint()) .addLogoutHandler(logoutHandler()) // endpoint protection diff --git a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/AuthenticationOnDeploymentTest.java b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/AuthenticationOnDeploymentTest.java index e6ca937978..92fc84df1d 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/AuthenticationOnDeploymentTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/AuthenticationOnDeploymentTest.java @@ -13,11 +13,13 @@ import org.apache.http.HttpHeaders; import org.junit.Before; import org.junit.Test; +import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.zowe.apiml.passticket.PassTicketService; import org.zowe.apiml.security.common.auth.Authentication; import org.zowe.apiml.security.common.auth.AuthenticationScheme; +import org.zowe.apiml.util.categories.AdditionalLocalTest; import org.zowe.apiml.util.service.RequestVerifier; import org.zowe.apiml.util.service.VirtualService; @@ -34,6 +36,7 @@ * This test requires to allow endpoint routes on gateway (ie profile dev) */ @RunWith(JUnit4.class) +@Category(AdditionalLocalTest.class) public class AuthenticationOnDeploymentTest { private static final int TIMEOUT = 100; diff --git a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/LogoutTest.java b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/LogoutTest.java new file mode 100644 index 0000000000..d4c1f25431 --- /dev/null +++ b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/LogoutTest.java @@ -0,0 +1,90 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ +package org.zowe.apiml.gatewayservice; + +import io.restassured.RestAssured; +import io.restassured.http.Cookie; +import org.apache.http.HttpHeaders; +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpStatus; +import org.zowe.apiml.util.config.ConfigReader; +import org.zowe.apiml.util.service.DiscoveryUtils; + +import static io.restassured.RestAssured.given; +import static org.apache.http.HttpStatus.SC_NO_CONTENT; +import static org.hamcrest.Matchers.isEmptyString; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNot.not; +import static org.zowe.apiml.gatewayservice.SecurityUtils.getConfiguredSslConfig; + +public class LogoutTest { + + private final static String USERNAME = ConfigReader.environmentConfiguration().getCredentials().getUser(); + private final static String PASSWORD = ConfigReader.environmentConfiguration().getCredentials().getPassword(); + private final static int PORT = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration().getPort(); + private final static String BASE_PATH = "/api/v1/gateway"; + private final static String LOGIN_ENDPOINT = "/auth/login"; + private final static String LOGOUT_ENDPOINT = "/auth/logout"; + private final static String QUERY_ENDPOINT = "/auth/query"; + private final static String COOKIE_NAME = "apimlAuthenticationToken"; + + @Before + public void setUp() { + RestAssured.useRelaxedHTTPSValidation(); + RestAssured.config = RestAssured.config().sslConfig(getConfiguredSslConfig()); + } + + private String getJwt() { + Cookie cookie = given() + .auth().preemptive().basic(USERNAME, PASSWORD) + .when() + .post(String.format("%s%s%s", DiscoveryUtils.getGatewayUrls().get(0), BASE_PATH, LOGIN_ENDPOINT)) + .then() + .statusCode(is(SC_NO_CONTENT)) + .cookie(COOKIE_NAME, not(isEmptyString())) + .extract().detailedCookie(COOKIE_NAME); + return cookie.getValue(); + } + + private void assertIfLogged(String jwt, boolean logged) { + final HttpStatus status = logged ? HttpStatus.OK : HttpStatus.UNAUTHORIZED; + + DiscoveryUtils.getGatewayUrls().forEach(gw -> { + given() + .header(HttpHeaders.AUTHORIZATION, "Bearer " + jwt) + .when() + .get(String.format("%s%s%s", gw, BASE_PATH, QUERY_ENDPOINT)) + .then() + .statusCode(status.value()); + }); + } + + @Test + public void testLogout() { + // make login + String jwt = getJwt(); + + // check if it is logged in + assertIfLogged(jwt, true); + + // make logout + given() + .cookie(COOKIE_NAME, jwt) + .when() + .post(String.format("%s%s%s", DiscoveryUtils.getGatewayUrls().get(0), BASE_PATH, LOGOUT_ENDPOINT)) + .then() + .statusCode(is(SC_NO_CONTENT)); + + // check if it is logged in + assertIfLogged(jwt, false); + } + +} diff --git a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfAuthenticationTest.java b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfAuthenticationTest.java index 891df0d5de..5e284a5035 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfAuthenticationTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfAuthenticationTest.java @@ -14,11 +14,13 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.zowe.apiml.util.categories.AdditionalLocalTest; import org.zowe.apiml.util.service.DiscoveryUtils; import org.zowe.apiml.util.service.VirtualService; @@ -40,6 +42,7 @@ * - start discovery service and gateway locally */ @RunWith(JUnit4.class) +@Category(AdditionalLocalTest.class) public class ZosmfAuthenticationTest { private static final String ZOSMF_ID = "zosmfca32"; From 425fd3a29f31ae0c9df557ba64f9473b7f9fb6de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Tue, 18 Feb 2020 15:02:08 +0100 Subject: [PATCH 115/122] fix logout integration test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../config/SecurityConfiguration.java | 25 +++++++++++-------- .../security/service/schema/ZosmfScheme.java | 2 +- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/SecurityConfiguration.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/SecurityConfiguration.java index 5287a94513..4840f26979 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/SecurityConfiguration.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/SecurityConfiguration.java @@ -9,19 +9,11 @@ */ package org.zowe.apiml.gateway.security.config; -import org.zowe.apiml.security.common.config.AuthConfigurationProperties; -import org.zowe.apiml.security.common.config.HandlerInitializer; -import org.zowe.apiml.security.common.content.BasicContentFilter; -import org.zowe.apiml.security.common.content.CookieContentFilter; -import org.zowe.apiml.security.common.login.LoginFilter; -import org.zowe.apiml.gateway.security.query.QueryFilter; -import org.zowe.apiml.gateway.security.query.SuccessfulQueryHandler; -import org.zowe.apiml.gateway.security.service.AuthenticationService; -import org.zowe.apiml.gateway.security.ticket.SuccessfulTicketHandler; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; @@ -31,8 +23,19 @@ import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler; import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.security.web.firewall.StrictHttpFirewall; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.zowe.apiml.gateway.security.query.QueryFilter; +import org.zowe.apiml.gateway.security.query.SuccessfulQueryHandler; +import org.zowe.apiml.gateway.security.service.AuthenticationService; +import org.zowe.apiml.gateway.security.ticket.SuccessfulTicketHandler; +import org.zowe.apiml.security.common.config.AuthConfigurationProperties; +import org.zowe.apiml.security.common.config.HandlerInitializer; +import org.zowe.apiml.security.common.content.BasicContentFilter; +import org.zowe.apiml.security.common.content.CookieContentFilter; +import org.zowe.apiml.security.common.login.LoginFilter; import java.util.Collections; @@ -94,8 +97,10 @@ protected void configure(HttpSecurity http) throws Exception { // logout endpoint .and() .logout() - .logoutUrl(authConfigurationProperties.getGatewayLogoutEndpoint()) + .logoutRequestMatcher(new AntPathRequestMatcher(authConfigurationProperties.getGatewayLogoutEndpoint(), HttpMethod.POST.name())) .addLogoutHandler(logoutHandler()) + .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.NO_CONTENT)) + .permitAll() // endpoint protection .and() diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/ZosmfScheme.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/ZosmfScheme.java index 8e7c562ab8..9baf629908 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/ZosmfScheme.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/ZosmfScheme.java @@ -100,7 +100,7 @@ public void apply(InstanceInfo instanceInfo) { } // remove authentication part - context.getZuulRequestHeaders().remove(HttpHeaders.AUTHORIZATION); + context.addZuulRequestHeader(HttpHeaders.AUTHORIZATION, null); }); } From 11f710e3e8a0b5288e2dc706a53111b719d84fc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Tue, 18 Feb 2020 16:30:39 +0100 Subject: [PATCH 116/122] fix integration tests - using JWT token from z/OSMF have different issuer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../zowe/apiml/apicatalog/ApiCatalogLoginIntegrationTest.java | 2 +- .../org/zowe/apiml/gatewayservice/LoginIntegrationTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/integration-tests/src/test/java/org/zowe/apiml/apicatalog/ApiCatalogLoginIntegrationTest.java b/integration-tests/src/test/java/org/zowe/apiml/apicatalog/ApiCatalogLoginIntegrationTest.java index bc8c9f7dc7..04454ef85e 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/apicatalog/ApiCatalogLoginIntegrationTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/apicatalog/ApiCatalogLoginIntegrationTest.java @@ -39,7 +39,7 @@ public class ApiCatalogLoginIntegrationTest { private final static String CATALOG_SERVICE_ID = "/apicatalog"; private final static String LOGIN_ENDPOINT = "/auth/login"; private final static String COOKIE_NAME = "apimlAuthenticationToken"; - private final static String ISSUER = "APIML"; + private final static String ISSUER = "zOSMF"; private final static String USERNAME = ConfigReader.environmentConfiguration().getCredentials().getUser(); private final static String PASSWORD = ConfigReader.environmentConfiguration().getCredentials().getPassword(); private final static String INVALID_USERNAME = "incorrectUser"; diff --git a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/LoginIntegrationTest.java b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/LoginIntegrationTest.java index 1c8968bbe1..846f2b50be 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/LoginIntegrationTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/LoginIntegrationTest.java @@ -37,7 +37,7 @@ public class LoginIntegrationTest { private final static String BASE_PATH = "/api/v1/gateway"; private final static String LOGIN_ENDPOINT = "/auth/login"; private final static String COOKIE_NAME = "apimlAuthenticationToken"; - private final static String ISSUER = "APIML"; + private final static String ISSUER = "zOSMF"; private final static String USERNAME = ConfigReader.environmentConfiguration().getCredentials().getUser(); private final static String PASSWORD = ConfigReader.environmentConfiguration().getCredentials().getPassword(); private final static String INVALID_USERNAME = "incorrectUser"; From b2e62f73d1805dedb397ec36333d58a3c456de09 Mon Sep 17 00:00:00 2001 From: JirkaAichler Date: Fri, 21 Feb 2020 15:39:32 +0100 Subject: [PATCH 117/122] Minor changes Signed-off-by: JirkaAichler --- .../java/org/zowe/apiml/util/CookieUtil.java | 9 +++-- .../org/zowe/apiml/util/CookieUtilTest.java | 5 ++- .../zosmf/ZosmfAuthenticationProvider.java | 15 ++------ .../service/AuthenticationService.java | 32 ++++++++++------- .../security/service/ZosmfService.java | 36 ++++++++++--------- .../security/service/schema/ZosmfScheme.java | 22 ++++++------ .../service/zosmf/ZosmfServiceFacade.java | 16 ++++----- .../service/zosmf/ZosmfServiceV1.java | 1 + .../service/zosmf/ZosmfServiceFacadeTest.java | 2 +- 9 files changed, 70 insertions(+), 68 deletions(-) diff --git a/common-service-core/src/main/java/org/zowe/apiml/util/CookieUtil.java b/common-service-core/src/main/java/org/zowe/apiml/util/CookieUtil.java index 4a6989f6ff..8333314c2e 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/util/CookieUtil.java +++ b/common-service-core/src/main/java/org/zowe/apiml/util/CookieUtil.java @@ -9,16 +9,15 @@ */ package org.zowe.apiml.util; +import lombok.experimental.UtilityClass; import org.apache.commons.lang.StringUtils; /** - * This utilities allowe base work with cookies and its string representation. + * A utility class for Cookies administration */ +@UtilityClass public final class CookieUtil { - private CookieUtil() { - } - public static String setCookie(String cookieHeader, String name, String value) { StringBuilder sb = new StringBuilder(); @@ -33,9 +32,9 @@ public static String setCookie(String cookieHeader, String name, String value) { } } - if (counter > 0) sb.append(';'); sb.append(name).append('=').append(value); + return sb.toString(); } diff --git a/common-service-core/src/test/java/org/zowe/apiml/util/CookieUtilTest.java b/common-service-core/src/test/java/org/zowe/apiml/util/CookieUtilTest.java index 73b440648e..9456d6c48c 100644 --- a/common-service-core/src/test/java/org/zowe/apiml/util/CookieUtilTest.java +++ b/common-service-core/src/test/java/org/zowe/apiml/util/CookieUtilTest.java @@ -1,4 +1,4 @@ -package org.zowe.apiml.util;/* +/* * This program and the accompanying materials are made available under the terms of the * Eclipse Public License v2.0 which accompanies this distribution, and is available at * https://www.eclipse.org/legal/epl-v20.html @@ -7,12 +7,15 @@ * * Copyright Contributors to the Zowe Project. */ +package org.zowe.apiml.util; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import static org.junit.Assert.*; + + @RunWith(JUnit4.class) public class CookieUtilTest { diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProvider.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProvider.java index 2709160722..d91334915e 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProvider.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProvider.java @@ -9,6 +9,7 @@ */ package org.zowe.apiml.gateway.security.login.zosmf; +import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -16,8 +17,6 @@ import org.springframework.stereotype.Component; import org.zowe.apiml.gateway.security.service.AuthenticationService; import org.zowe.apiml.gateway.security.service.ZosmfService; -import org.zowe.apiml.message.log.ApimlLogger; -import org.zowe.apiml.product.logging.annotations.InjectApimlLogger; import static org.zowe.apiml.gateway.security.service.ZosmfService.TokenType.JWT; import static org.zowe.apiml.gateway.security.service.ZosmfService.TokenType.LTPA; @@ -26,22 +25,12 @@ * Authentication provider that verifies credentials against z/OSMF service */ @Component +@RequiredArgsConstructor public class ZosmfAuthenticationProvider implements AuthenticationProvider { - @InjectApimlLogger - private ApimlLogger apimlLog = ApimlLogger.empty(); - private final AuthenticationService authenticationService; private final ZosmfService zosmfService; - public ZosmfAuthenticationProvider( - AuthenticationService authenticationService, - ZosmfService zosmfService - ) { - this.authenticationService = authenticationService; - this.zosmfService = zosmfService; - } - /** * Authenticate the credentials with the z/OSMF service * diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/AuthenticationService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/AuthenticationService.java index 818abecd9a..370966faa1 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/AuthenticationService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/AuthenticationService.java @@ -140,6 +140,8 @@ public Boolean invalidateJwtToken(String jwtToken, boolean distribute) { case ZOSMF: zosmfService.invalidate(JWT, jwtToken); break; + default: + throw new TokenNotValidException("Unknown token type."); } return Boolean.TRUE; @@ -187,6 +189,8 @@ public TokenAuthentication validateJwtToken(String jwtToken) { case ZOSMF: zosmfService.validate(JWT, jwtToken); break; + default: + throw new TokenNotValidException("Unknown token type."); } TokenAuthentication tokenAuthentication = new TokenAuthentication(queryResponse.getUserId(), jwtToken); @@ -198,12 +202,12 @@ public TokenAuthentication validateJwtToken(String jwtToken) { } /** - * Method construct {@link TokenAuthentication} marked as valid. It also store JWT token on the cache to - * speed up next call to validate token. + * Method constructs {@link TokenAuthentication} marked as valid. It also stores JWT token to the cache to + * speed up next validation call. * - * @param user username to login + * @param user username to login * @param jwtToken token of user - * @return {@link TokenAuthentication}, as authenticated use information about invalidating of token + * @return authenticated {@link TokenAuthentication} using information about invalidating of token */ @CachePut(value = "validationJwtToken", key = "#jwtToken", condition = "#jwtToken != null") public TokenAuthentication createTokenAuthentication(String user, String jwtToken) { @@ -227,9 +231,10 @@ public TokenAuthentication validateJwtToken(TokenAuthentication token) { } /** - * This method is for removing if sign. Each JWT token is concatenation of three parts (header, body, sign) joined - * with ".". JWT library required on parse also validation step. For validation is needed defined public key, but - * we use also JWT tokens from another application (z/OSMF) and we don't have it. + * This method removes the token signature. Each JWT token is concatenated of three parts (header, body, sign) joined + * with ".". JWT library used for parsing contains also validation. A public key is needed for validation, but + * we are also using JWT tokens from another application (z/OSMF) and we don't have it. + * * @param jwtToken token to modify * @return jwt token without sign part */ @@ -244,18 +249,18 @@ private String removeSign(String jwtToken) { } /** - * Parse the JWT token and return a {@link QueryResponse} object containing the domain, user id, type (Zowe / z/OSMF), + * Parses the JWT token and return a {@link QueryResponse} object containing the domain, user id, type (Zowe / z/OSMF), * date of creation and date of expiration * * @param jwtToken the JWT token * @return the query response */ public QueryResponse parseJwtToken(String jwtToken) { - /** - * Remove signature, because fo z/OSMF we dont have key to verify certificate and + /* + * Removes signature, because of z/OSMF we don't have key to verify certificate and * we just need to read claim. Verification is realized via REST call to z/OSMF. * JWT library doesn't parse signed key without verification. - */ + */ final String withoutSign = removeSign(jwtToken); // parse to claims and construct QueryResponse @@ -299,8 +304,9 @@ public Optional getJwtTokenFromRequest(HttpServletRequest request) { } /** - * This method validate if JWT token is valid and if yes, then get claim with LTPA token. - * For purpose, where is not needed validation, you can use method {@link #getLtpaToken(String)} + * This method validates if JWT token is valid and if yes, then get claim from LTPA token. + * For purpose, when is not needed validation, you can use method {@link #getLtpaToken(String)} + * * @param jwtToken the JWT token * @return LTPA token extracted from JWT */ diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/ZosmfService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/ZosmfService.java index e2bbea9168..4467a749dc 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/ZosmfService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/ZosmfService.java @@ -18,54 +18,56 @@ import java.util.Map; /** - * Common interface for z/OSMF operations about authentication. This interface is implemented with each bean for + * Common interface for z/OSMF authentication operations. This interface is implemented with each bean for * authentication to z/OSMF and also in {@link org.zowe.apiml.gateway.security.service.zosmf.ZosmfServiceFacade}, which * provides calls by version of z/OSMF. */ public interface ZosmfService { /** - * Make authentication in z/OSMF. The result contains all supported token (JWT, LTPA) if they are available and + * Make authentication in z/OSMF. The result contains all supported tokens (JWT, LTPA) if they are available and * domain (required to construct Zowe's JWT token) + * * @param authentication user authentication * @return AuthenticationResponse, collections of supported tokens and domain */ - public AuthenticationResponse authenticate(Authentication authentication); + AuthenticationResponse authenticate(Authentication authentication); /** - * Validate in z/OSMF is the token is valid there or not. If token is invalid or any other error occurred it + * Check whether the token is valid in z/OSMF. If token is invalid or any other error occurs it * throws an exception. - * @param type Type of token (JWT, LTPA) + * + * @param type Type of token (JWT, LTPA) * @param token Token to verify */ - public void validate(ZosmfService.TokenType type, String token); + void validate(ZosmfService.TokenType type, String token); /** - * This method invalidate token in z/OSMF if the service to deactivate is available - * @param type Type of token (JWT, LTPA) + * This method invalidates token in z/OSMF when deactivate functionality is available + * + * @param type Type of token (JWT, LTPA) * @param token Token to verify */ - public void invalidate(ZosmfService.TokenType type, String token); + void invalidate(ZosmfService.TokenType type, String token); /** * Method is to decide which version of z/OSMF are supported by implementation. If bean is not real implementation - * but delegate it has to return false (see {@link org.zowe.apiml.gateway.security.service.zosmf.ZosmfServiceFacade}). + * but delegates it has to return false (see {@link org.zowe.apiml.gateway.security.service.zosmf.ZosmfServiceFacade}). + * * @param version version of z/OSMF * @return if bean provides implementation for specific version of z/OSMF */ - public boolean matchesVersion(int version); + boolean matchesVersion(int version); /** * Enumeration of supported security tokens */ @AllArgsConstructor @Getter - public enum TokenType { + enum TokenType { - JWT("jwtToken"), - LTPA("LtpaToken2") - - ; + JWT("jwtToken"), + LTPA("LtpaToken2"); private final String cookieName; @@ -77,7 +79,7 @@ public enum TokenType { @Data @AllArgsConstructor @RequiredArgsConstructor - public static class AuthenticationResponse { + class AuthenticationResponse { private String domain; private final Map tokens; diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/ZosmfScheme.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/ZosmfScheme.java index 9baf629908..512defc30d 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/ZosmfScheme.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/schema/ZosmfScheme.java @@ -87,16 +87,18 @@ public void apply(InstanceInfo instanceInfo) { // parse JWT token to detect the source (z/OSMF / Zowe) QueryResponse queryResponse = authenticationService.parseJwtToken(token); switch (queryResponse.getSource()) { - case ZOSMF: - // token is generated by z/OSMF, fix set cookies - removeCookie(context, authConfigurationProperties.getCookieProperties().getCookieName()); - setCookie(context, ZosmfService.TokenType.JWT.getCookieName(), token); - break; - case ZOWE: - // user use Zowe own JWT token, for communication with z/OSMF there should be LTPA token, use it - final String ltpaToken = authenticationService.getLtpaTokenWithValidation(token); - setCookie(context, ZosmfService.TokenType.LTPA.getCookieName(), ltpaToken); - break; + case ZOSMF: + // token is generated by z/OSMF, fix set cookies + removeCookie(context, authConfigurationProperties.getCookieProperties().getCookieName()); + setCookie(context, ZosmfService.TokenType.JWT.getCookieName(), token); + break; + case ZOWE: + // user use Zowe own JWT token, for communication with z/OSMF there should be LTPA token, use it + final String ltpaToken = authenticationService.getLtpaTokenWithValidation(token); + setCookie(context, ZosmfService.TokenType.LTPA.getCookieName(), ltpaToken); + break; + default: + return; } // remove authentication part diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceFacade.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceFacade.java index 5fabcabeb1..316eef2610 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceFacade.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceFacade.java @@ -37,15 +37,15 @@ import java.util.List; /** - * This bean is default implementation of access to z/OSMF authentication. It collect all other implementation and - * select right implementation by z/OSMF version. This bean is facade for those implementation and all its own methods - * are delegates to implementation based of z/OSMF version. - * + * This bean is default implementation of access to z/OSMF authentication. It collects all other implementation and + * select right implementation by z/OSMF version. This bean is facade for those implementations and all its own methods + * are delegated to implementation based of z/OSMF version. + *

* see also: - * - {@link ZosmfServiceV2} - * - new version supporting https://www.ibm.com/support/knowledgecenter/SSLTBW_2.4.0/com.ibm.zos.v2r4.izua700/izuprog_API_WebTokenAuthServices.htm - * - {@link ZosmfServiceV1} - * - old version using endpoint /zosmf/info + * - {@link ZosmfServiceV2} + * - new version supporting https://www.ibm.com/support/knowledgecenter/SSLTBW_2.4.0/com.ibm.zos.v2r4.izua700/izuprog_API_WebTokenAuthServices.htm + * - {@link ZosmfServiceV1} + * - old version using endpoint /zosmf/info */ @Primary @Service diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1.java index 6f554d82d2..cc1007a938 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1.java @@ -22,6 +22,7 @@ import org.zowe.apiml.security.common.config.AuthConfigurationProperties; import org.zowe.apiml.security.common.error.ServiceNotAccessibleException; import org.zowe.apiml.security.common.token.TokenNotValidException; + @Service public class ZosmfServiceV1 extends AbstractZosmfService { diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceFacadeTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceFacadeTest.java index 2eb75b345a..eab8bd6bb4 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceFacadeTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceFacadeTest.java @@ -33,7 +33,7 @@ @RunWith(SpringJUnit4ClassRunner.class) public class ZosmfServiceFacadeTest { - private static final String SERVICE_ID = "zosmfca32"; + private static final String SERVICE_ID = "zosmf"; private RestTemplate restTemplate = mock(RestTemplate.class); private ZosmfServiceFacadeTestExt zosmfService; From 8fdb410b1ecdd320933d116aa8f9b1bd627641f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Tue, 25 Feb 2020 11:22:53 +0100 Subject: [PATCH 118/122] minor changes (remove commented code, using configuration for gateway in standard way, adding of comments to public values, remove changes in JSON of QueryResponse, better detection of z/OSMF's realm) based on code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../security/SecurityConfiguration.java | 18 ++++----- .../zowe/apiml/product/web/HttpConfig.java | 19 ++++++++++ .../config/AuthConfigurationProperties.java | 1 + .../security/common/token/QueryResponse.java | 21 +++++++++-- .../common/token/TokenAuthentication.java | 6 +++ .../common/token/QueryResponseTest.java | 16 +++++++- .../java/org/zowe/apiml/util/CookieUtil.java | 16 ++++++++ .../zowe/apiml/discovery/GatewayNotifier.java | 11 ++++++ .../metadata/MetadataTranslationService.java | 9 ++++- .../config/ComponentsConfiguration.java | 5 +++ .../service/AuthenticationService.java | 27 ++++++++++++++ .../service/zosmf/AbstractZosmfService.java | 23 ++++++++++++ .../service/zosmf/ZosmfServiceFacade.java | 37 +++++++++++++++++++ .../service/zosmf/ZosmfServiceV1.java | 6 +++ .../service/zosmf/ZosmfServiceV2.java | 4 ++ .../ZosmfAuthenticationProviderTest.java | 13 ------- .../ApiCatalogLoginIntegrationTest.java | 7 +--- .../gatewayservice/LoginIntegrationTest.java | 7 +--- .../zowe/apiml/gatewayservice/LogoutTest.java | 24 +----------- .../apiml/gatewayservice/SecurityUtils.java | 16 +++++--- 20 files changed, 220 insertions(+), 66 deletions(-) diff --git a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/SecurityConfiguration.java b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/SecurityConfiguration.java index 778c0b94dc..25d188fb53 100644 --- a/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/SecurityConfiguration.java +++ b/api-catalog-services/src/main/java/org/zowe/apiml/apicatalog/security/SecurityConfiguration.java @@ -9,14 +9,6 @@ */ package org.zowe.apiml.apicatalog.security; -import org.zowe.apiml.security.client.EnableApimlAuth; -import org.zowe.apiml.security.client.login.GatewayLoginProvider; -import org.zowe.apiml.security.client.token.GatewayTokenProvider; -import org.zowe.apiml.security.common.config.AuthConfigurationProperties; -import org.zowe.apiml.security.common.config.HandlerInitializer; -import org.zowe.apiml.security.common.content.BasicContentFilter; -import org.zowe.apiml.security.common.content.CookieContentFilter; -import org.zowe.apiml.security.common.login.LoginFilter; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; @@ -31,6 +23,14 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.zowe.apiml.security.client.EnableApimlAuth; +import org.zowe.apiml.security.client.login.GatewayLoginProvider; +import org.zowe.apiml.security.client.token.GatewayTokenProvider; +import org.zowe.apiml.security.common.config.AuthConfigurationProperties; +import org.zowe.apiml.security.common.config.HandlerInitializer; +import org.zowe.apiml.security.common.content.BasicContentFilter; +import org.zowe.apiml.security.common.content.CookieContentFilter; +import org.zowe.apiml.security.common.login.LoginFilter; /** * Main configuration class of Spring web security for Api Catalog @@ -106,7 +106,7 @@ protected void configure(HttpSecurity http) throws Exception { // logout endpoint .and() .logout() - .logoutUrl(authConfigurationProperties.getGatewayQueryEndpoint()) + .logoutUrl(authConfigurationProperties.getServiceLogoutEndpoint()) .logoutSuccessHandler(logoutSuccessHandler()) // endpoints protection diff --git a/apiml-common/src/main/java/org/zowe/apiml/product/web/HttpConfig.java b/apiml-common/src/main/java/org/zowe/apiml/product/web/HttpConfig.java index 5eed775644..b55b9f823d 100644 --- a/apiml-common/src/main/java/org/zowe/apiml/product/web/HttpConfig.java +++ b/apiml-common/src/main/java/org/zowe/apiml/product/web/HttpConfig.java @@ -144,6 +144,13 @@ public SslContextFactory jettySslContextFactory() { return sslContextFactory; } + /** + * Returns RestTemplate without keystore. The purpose is to call z/OSMF (or other systems), which accept login by + * certificate. In case of login into z/OSMF can certificate has higher priority. It breaks credentials + * verification. + * + * @return default RestTemplate, which doesn't use certificate from keystore + */ @Bean @Primary @Qualifier("restTemplateWithKeystore") @@ -152,6 +159,12 @@ public RestTemplate restTemplateWithKeystore() { return new RestTemplate(factory); } + /** + * Returns RestTemplate with keystore. This RestTemplate makes calls to other systems with a certificate to sign to + * other systems by certificate. It is necessary to call systems like DiscoverySystem etc. + * + * @return RestTemplate, which uses certificate from keystore to authenticate + */ @Bean @Qualifier("restTemplateWithoutKeystore") public RestTemplate restTemplateWithoutKeystore() { @@ -159,12 +172,18 @@ public RestTemplate restTemplateWithoutKeystore() { return new RestTemplate(factory); } + /** + * @return HttpCLient, which doesn't use certificate to authenticate + */ @Bean @Primary public CloseableHttpClient secureHttpClient() { return secureHttpClient; } + /** + * @return HttpCLient, which doesn't use certificate to authenticate + */ @Bean @Qualifier("secureHttpClientWithoutKeystore") public CloseableHttpClient secureHttpClientWithoutKeystore() { diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/config/AuthConfigurationProperties.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/config/AuthConfigurationProperties.java index 4f0542cef8..1536640887 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/config/AuthConfigurationProperties.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/config/AuthConfigurationProperties.java @@ -35,6 +35,7 @@ public class AuthConfigurationProperties { private String gatewayTicketEndpoint = "/api/v1/gateway/auth/ticket"; private String serviceLoginEndpoint = "/auth/login"; + private String serviceLogoutEndpoint = "/auth/logout"; private AuthConfigurationProperties.TokenProperties tokenProperties; private AuthConfigurationProperties.CookieProperties cookieProperties; diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/token/QueryResponse.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/token/QueryResponse.java index fc774dbd11..dff6cba5d4 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/token/QueryResponse.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/token/QueryResponse.java @@ -9,11 +9,12 @@ */ package org.zowe.apiml.security.common.token; -import org.zowe.apiml.cache.EntryExpiration; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.apache.commons.lang.StringUtils; +import org.zowe.apiml.cache.EntryExpiration; import java.util.Date; @@ -29,6 +30,7 @@ public class QueryResponse implements EntryExpiration { private String userId; private Date creation; private Date expiration; + @JsonIgnore private Source source; @Override @@ -36,19 +38,32 @@ public boolean isExpired() { return expiration.before(new Date()); } + /** + * An enumeration defines all possible sources of JWT token using to user authentication into Gateway, Discovery + * service and catalog. + */ public enum Source { + // JWT token is generated by Zowe (including ie. LTPA token from z/OSMF) ZOWE, + // Zowe uses JWT token generated by z/OSMF ZOSMF ; + /** + * Find the source of JWT token by issuer inside the JWT tokens (see claims) + * @param issuer issuer claim from JWT token + * @return which system generated the JWT token + */ public static Source valueByIssuer(String issuer) { if (StringUtils.equalsIgnoreCase(issuer, "zOSMF")) { return ZOSMF; } - - return ZOWE; + if (StringUtils.equalsIgnoreCase(issuer, "APIML")) { + return ZOWE; + } + throw new TokenNotValidException("Unknown token type : " + issuer); } } diff --git a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/token/TokenAuthentication.java b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/token/TokenAuthentication.java index 21712b5b2a..0842d88c5e 100644 --- a/apiml-security-common/src/main/java/org/zowe/apiml/security/common/token/TokenAuthentication.java +++ b/apiml-security-common/src/main/java/org/zowe/apiml/security/common/token/TokenAuthentication.java @@ -54,6 +54,12 @@ public String getPrincipal() { return username; } + /** + * Creates the TokenAuthentication with fulfilled username (principal), token and marked as authenticated. + * @param username Username, who is authenticated + * @param token Token, which authenticate the user + * @return TokenAuthentication marked as authenticated with username, token + */ public static TokenAuthentication createAuthenticated(String username, String token) { final TokenAuthentication out = new TokenAuthentication(username, token); out.setAuthenticated(true); diff --git a/apiml-security-common/src/test/java/org/zowe/apiml/security/common/token/QueryResponseTest.java b/apiml-security-common/src/test/java/org/zowe/apiml/security/common/token/QueryResponseTest.java index 3b2860d76f..9f4bb01550 100644 --- a/apiml-security-common/src/test/java/org/zowe/apiml/security/common/token/QueryResponseTest.java +++ b/apiml-security-common/src/test/java/org/zowe/apiml/security/common/token/QueryResponseTest.java @@ -34,8 +34,20 @@ public void testIsExpired() { public void testSource() { assertEquals(QueryResponse.Source.ZOSMF, QueryResponse.Source.valueByIssuer("zosmf")); assertEquals(QueryResponse.Source.ZOSMF, QueryResponse.Source.valueByIssuer("zOSMF")); - assertEquals(QueryResponse.Source.ZOWE, QueryResponse.Source.valueByIssuer("zosmfX")); - assertEquals(QueryResponse.Source.ZOWE, QueryResponse.Source.valueByIssuer(null)); + assertEquals(QueryResponse.Source.ZOWE, QueryResponse.Source.valueByIssuer("apiml")); + assertEquals(QueryResponse.Source.ZOWE, QueryResponse.Source.valueByIssuer("APIML")); + try { + assertEquals(QueryResponse.Source.ZOWE, QueryResponse.Source.valueByIssuer(null)); + fail(); + } catch (TokenNotValidException tnve) { + assertEquals("Unknown token type : null", tnve.getMessage()); + } + try { + assertEquals(QueryResponse.Source.ZOWE, QueryResponse.Source.valueByIssuer("unknown")); + fail(); + } catch (TokenNotValidException tnve) { + assertEquals("Unknown token type : unknown", tnve.getMessage()); + } } } diff --git a/common-service-core/src/main/java/org/zowe/apiml/util/CookieUtil.java b/common-service-core/src/main/java/org/zowe/apiml/util/CookieUtil.java index 8333314c2e..c9253af3a6 100644 --- a/common-service-core/src/main/java/org/zowe/apiml/util/CookieUtil.java +++ b/common-service-core/src/main/java/org/zowe/apiml/util/CookieUtil.java @@ -18,6 +18,14 @@ @UtilityClass public final class CookieUtil { + /** + * It replace or add cookie into header string value (see header with name "Cookie"). + * + * @param cookieHeader original header string value + * @param name name of cookie to add or replace + * @param value value of cookie to add or replace + * @return new header string, which contains a new cookie + */ public static String setCookie(String cookieHeader, String name, String value) { StringBuilder sb = new StringBuilder(); @@ -38,6 +46,14 @@ public static String setCookie(String cookieHeader, String name, String value) { return sb.toString(); } + /** + * It remove cookie from header value string (see header with name "Cookie") if exists. In case of missing + * cookie, it return original header value string. + * + * @param cookieHeader original header string value + * @param name name of cookie to remove + * @return new header string, without a specified cookie + */ public static String removeCookie(String cookieHeader, String name) { StringBuilder sb = new StringBuilder(); diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/GatewayNotifier.java b/discovery-service/src/main/java/org/zowe/apiml/discovery/GatewayNotifier.java index af604b5674..1e19db7546 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/GatewayNotifier.java +++ b/discovery-service/src/main/java/org/zowe/apiml/discovery/GatewayNotifier.java @@ -22,6 +22,11 @@ import java.util.List; +/** + * This component is responsible for notifying all gateways about service changes. It is the easiest way how discovery + * service sends information about a change to all gateways to make a delay between registration and stoping of service + * and using or releasing on gateways sides the shortest. + */ @Component public class GatewayNotifier { @@ -42,6 +47,12 @@ private PeerAwareInstanceRegistry getRegistry() { return getServerContext().getRegistry(); } + /** + * Method notify all gateways about any change of service serviceId. This method should be call immediately after + * any modification of service instance happened. + * + * @param serviceId id of modified service + */ public void serviceUpdated(String serviceId) { final PeerAwareInstanceRegistry registry = getRegistry(); final Application application = registry.getApplication("GATEWAY"); diff --git a/discovery-service/src/main/java/org/zowe/apiml/discovery/metadata/MetadataTranslationService.java b/discovery-service/src/main/java/org/zowe/apiml/discovery/metadata/MetadataTranslationService.java index 39a7be4d12..2505cb3c64 100644 --- a/discovery-service/src/main/java/org/zowe/apiml/discovery/metadata/MetadataTranslationService.java +++ b/discovery-service/src/main/java/org/zowe/apiml/discovery/metadata/MetadataTranslationService.java @@ -91,7 +91,14 @@ private void translateParameter(String oldParameter, String newParameter, Map metadata) { + /** + * This method support automatically mapping of z/OSMF's authentication scheme. It means, this method set the + * right authentication scheme to z/OSMF on registration if value is missing in the static service definition. + * + * @param serviceId Id of service to check (if contains zosmf ignoring case it will be applied) + * @param metadata metadata of service + */ + protected void updateZosmfAuthentication(String serviceId, Map metadata) { if (!StringUtils.containsIgnoreCase(serviceId, "zosmf")) return; if (metadata.containsKey(AUTHENTICATION_SCHEME)) return; diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/ComponentsConfiguration.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/ComponentsConfiguration.java index 739b452be1..e051f85220 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/ComponentsConfiguration.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/config/ComponentsConfiguration.java @@ -28,6 +28,11 @@ public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(10); } + /** + * Service to call generating and validating of passTickets. If JVM contains mainframe's class, it uses it, + * otherwise method returns dummy implementation + * @return mainframe / dummy implementation of passTicket's generation and validation + */ @Bean public PassTicketService passTicketService() { return new PassTicketService(); diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/AuthenticationService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/AuthenticationService.java index 370966faa1..16a3837b3f 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/AuthenticationService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/AuthenticationService.java @@ -147,11 +147,23 @@ public Boolean invalidateJwtToken(String jwtToken, boolean distribute) { return Boolean.TRUE; } + /** + * Checks if jwtToken is in the list of invalidated tokens. + * + * @param jwtToken token to check + * @return true - token is invalidated, otherwise token is still valid + */ @Cacheable(value = "invalidatedJwtTokens", unless = "true", key = "#jwtToken", condition = "#jwtToken != null") public Boolean isInvalidated(String jwtToken) { return Boolean.FALSE; } + /** + * Method to translate original exception to internal one. It is used in case of parsing and verifying of JWT tokens. + * + * @param exception original exception + * @return translated exception (better messaging and allow subsequent handling) + */ protected RuntimeException handleJwtParserException(RuntimeException exception) { if (exception instanceof ExpiredJwtException) { final ExpiredJwtException expiredJwtException = (ExpiredJwtException) exception; @@ -178,6 +190,21 @@ private Claims validateAndParseLocalJwtToken(String jwtToken) { } } + /** + * Method validate if jwtToken is valid or not. This method contains two types of verification: + * - Zowe + * - it checks validity of signature + * - it checks if token is not expired + * - it checks if token was not removed (see logout) + * - z/OSMF + * - it uses validation via REST directly in z/OSMF + * + * Method uses cache to speedup validation. In case of invalidating jwtToken in z/OSMF without Zowe, method + * can return still true until cache will expired or be evicted. + * + * @param jwtToken token to verification + * @return true if token is still valid, otherwise false + */ @Cacheable(value = "validationJwtToken", key = "#jwtToken", condition = "#jwtToken != null") public TokenAuthentication validateJwtToken(String jwtToken) { QueryResponse queryResponse = parseJwtToken(jwtToken); diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/AbstractZosmfService.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/AbstractZosmfService.java index 8a335da3ee..11581d0512 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/AbstractZosmfService.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/AbstractZosmfService.java @@ -58,10 +58,18 @@ public AbstractZosmfService( this.securityObjectMapper = securityObjectMapper; } + /** + * @return serviceId of z/OSMF service from configuration, which is used + */ protected String getZosmfServiceId() { return authConfigurationProperties.validatedZosmfServiceId(); } + /** + * Methods construct the value of authentication header by credentials + * @param authentication credentials to generates header value + * @return prepared header value (see header Authentication) + */ protected String getAuthenticationValue(Authentication authentication) { final String user = authentication.getPrincipal().toString(); final String password = authentication.getCredentials().toString(); @@ -92,6 +100,14 @@ protected String getURI(String zosmf) { .orElseThrow(authenticationServiceExceptionSupplier); } + /** + * Method handles exception from REST call to z/OSMF into internal exception. It convert original exception into + * custom one with better messages and types for subsequent treatment. + * + * @param url URL of invoked REST endpoint + * @param re original exception + * @return translated exception + */ protected RuntimeException handleExceptionOnCall(String url, RuntimeException re) { if (re instanceof ResourceAccessException) { apimlLog.log("org.zowe.apiml.security.serviceUnavailable", url, re.getMessage()); @@ -130,6 +146,13 @@ protected String readTokenFromCookie(List cookies, String cookieName) { .orElse(null); } + /** + * Method reads authentication values from answer of REST call. It read all supported tokens, which are returned + * from z/OSMF. + * + * @param responseEntity answer of REST call + * @return AuthenticationResponse with all supported tokens from responseEntity + */ protected AuthenticationResponse getAuthenticationResponse(ResponseEntity responseEntity) { final List cookies = responseEntity.getHeaders().get(HttpHeaders.SET_COOKIE); final EnumMap tokens = new EnumMap<>(TokenType.class); diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceFacade.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceFacade.java index 316eef2610..0c5fe6de59 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceFacade.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceFacade.java @@ -54,6 +54,10 @@ public class ZosmfServiceFacade extends AbstractZosmfService implements ServiceC protected final ApplicationContext applicationContext; protected final List implementations; + /** + * This attribute is used for calling of methods in the bean, which are covered by AOP (cache). Direct call will + * bypass those AOP. + */ private ZosmfServiceFacade meProxy; public ZosmfServiceFacade( @@ -79,11 +83,21 @@ public void afterPropertiesSet() { meProxy = applicationContext.getBean(ZosmfServiceFacade.class); } + /** + * It evicts all records in caches used for z/OSMF (information about version, domain and current used + * implementation). + */ @CacheEvict(value = {"zosmfInfo", "zosmfServiceImplementation"}, allEntries = true) public void evictCaches() { // evict all caches } + /** + * Method return base information about z/OSMF which is currently in use. Method use cache to reduce amount of calls. + * + * @param zosmfServiceId id of z/OSMF service (see static definition) + * @return ZosmfInfo, which contains version of z/OSMF, domain and realm (domain) + */ @Cacheable("zosmfInfo") public ZosmfInfo getZosmfInfo(String zosmfServiceId) { final String url = getURI(zosmfServiceId) + ZOSMF_INFO_END_POINT; @@ -108,11 +122,24 @@ public ZosmfInfo getZosmfInfo(String zosmfServiceId) { } } + /** + * Method returns version of current z/OSMF. It uses information from {@link #getZosmfInfo(String)}. If version + * is not available return 0 (as version which doesn't support method to get this information) + * @param zosmfInfo value which can be get with {@link #getZosmfInfo(String)} to extract version + * @return version of z/OSMF or 0 as default + */ protected int getVersion(ZosmfInfo zosmfInfo) { if (zosmfInfo == null) return 0; return zosmfInfo.getVersion(); } + /** + * It return implementation which should be used with current version of z/OSMF. It is determined by version of + * z/OSMF (see {@link #getZosmfInfo(String)}). + * Method use cache to fast response. + * @param zosmfServiceId id of z/OSMF's service (see static definition) + * @return ImplementationWrapper, which contains implementation and ZosmfInfo (version and realm/domain) + */ @Cacheable("zosmfServiceImplementation") public ImplementationWrapper getImplementation(String zosmfServiceId) { final ZosmfInfo zosmfInfo = meProxy.getZosmfInfo(zosmfServiceId); @@ -125,6 +152,9 @@ public ImplementationWrapper getImplementation(String zosmfServiceId) { throw new IllegalArgumentException("Unknown version of z/OSMF : " + version); } + /** + * @return implementation for current version of z/OSMF defined in configuration. + */ protected ImplementationWrapper getImplementation() { return meProxy.getImplementation(getZosmfServiceId()); } @@ -167,6 +197,9 @@ public void evictCacheService(String serviceId) { } } + /** + * DTO with base information about z/OSMF (version and realm/domain) + */ @Data @JsonIgnoreProperties(ignoreUnknown = true) public static class ZosmfInfo { @@ -182,6 +215,10 @@ public static class ZosmfInfo { } + /** + * DTO about implementation. It contains instance of bean implements ZosmfService and base information about + * z/OSMF (version and realm/domain). DTO is using to specify current z/OSMF and implementation, which support it. + */ @Value public static class ImplementationWrapper { diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1.java index cc1007a938..737d52c609 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV1.java @@ -23,6 +23,12 @@ import org.zowe.apiml.security.common.error.ServiceNotAccessibleException; import org.zowe.apiml.security.common.token.TokenNotValidException; +/** + * This implementation is used for version z/OSMF which don't support authentication endpoint. Instant this endpoint + * it use info endpoint for authentication and verification. This implementation doesn't support invalidation (logout). + * + * Bean could be served via {@link ZosmfServiceFacade} + */ @Service public class ZosmfServiceV1 extends AbstractZosmfService { diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV2.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV2.java index 85a978dd06..a8ec1da098 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV2.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/security/service/zosmf/ZosmfServiceV2.java @@ -25,7 +25,11 @@ import org.springframework.web.client.RestTemplate; /** + * This implementation is used for version z/OSMF which support authentication endpoint. This endpoint allows to + * generate new token, verify using token and also deactivation of token. Token could be LTPA or JWT (depends on + * z/OSMF's configuration) * + * Bean could be served via {@link ZosmfServiceFacade} */ @Service public class ZosmfServiceV2 extends AbstractZosmfService { diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java index 1fe03f4880..2acd25c088 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java @@ -117,19 +117,6 @@ private ZosmfService createZosmfService() { output = spy(output); when(applicationContext.getBean(ZosmfServiceFacade.class)).thenReturn(output); output.afterPropertiesSet(); - - /*ZosmfServiceFacade.ZosmfInfo zosmfInfo = new ZosmfServiceFacade.ZosmfInfo(); - zosmfInfo.setVersion(27); - zosmfInfo.setFullVersion("27.0"); - zosmfInfo.setSafRealm(DOMAIN); - doReturn(zosmfInfo).when(output).getZosmfInfo(ZOSMF); - - when(restTemplate.exchange(Mockito.anyString(), - Mockito.eq(HttpMethod.GET), - Mockito.any(), - Mockito.>any())) - .thenReturn(new ResponseEntity<>(infoAnswer, new HttpHeaders(), HttpStatus.OK));*/ - return output; } diff --git a/integration-tests/src/test/java/org/zowe/apiml/apicatalog/ApiCatalogLoginIntegrationTest.java b/integration-tests/src/test/java/org/zowe/apiml/apicatalog/ApiCatalogLoginIntegrationTest.java index 04454ef85e..06f84d923f 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/apicatalog/ApiCatalogLoginIntegrationTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/apicatalog/ApiCatalogLoginIntegrationTest.java @@ -9,8 +9,6 @@ */ package org.zowe.apiml.apicatalog; -import org.zowe.apiml.security.common.login.LoginRequest; -import org.zowe.apiml.util.config.ConfigReader; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.restassured.RestAssured; @@ -18,6 +16,8 @@ import org.json.JSONObject; import org.junit.Before; import org.junit.Test; +import org.zowe.apiml.security.common.login.LoginRequest; +import org.zowe.apiml.util.config.ConfigReader; import static io.restassured.RestAssured.given; import static io.restassured.http.ContentType.JSON; @@ -39,7 +39,6 @@ public class ApiCatalogLoginIntegrationTest { private final static String CATALOG_SERVICE_ID = "/apicatalog"; private final static String LOGIN_ENDPOINT = "/auth/login"; private final static String COOKIE_NAME = "apimlAuthenticationToken"; - private final static String ISSUER = "zOSMF"; private final static String USERNAME = ConfigReader.environmentConfiguration().getCredentials().getUser(); private final static String PASSWORD = ConfigReader.environmentConfiguration().getCredentials().getPassword(); private final static String INVALID_USERNAME = "incorrectUser"; @@ -77,7 +76,6 @@ public void doLoginWithValidBodyLoginRequest() { assertThat(claims.getId(), not(isEmptyString())); assertThat(claims.getSubject(), is(USERNAME)); - assertThat(claims.getIssuer(), is(ISSUER)); } @Test @@ -100,7 +98,6 @@ public void doLoginWithValidHeader() { assertThat(claims.getId(), not(isEmptyString())); assertThat(claims.getSubject(), is(USERNAME)); - assertThat(claims.getIssuer(), is(ISSUER)); } @Test diff --git a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/LoginIntegrationTest.java b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/LoginIntegrationTest.java index 846f2b50be..320317bb02 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/LoginIntegrationTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/LoginIntegrationTest.java @@ -9,8 +9,6 @@ */ package org.zowe.apiml.gatewayservice; -import org.zowe.apiml.security.common.login.LoginRequest; -import org.zowe.apiml.util.config.ConfigReader; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.restassured.RestAssured; @@ -18,6 +16,8 @@ import org.json.JSONObject; import org.junit.Before; import org.junit.Test; +import org.zowe.apiml.security.common.login.LoginRequest; +import org.zowe.apiml.util.config.ConfigReader; import static io.restassured.RestAssured.given; import static io.restassured.http.ContentType.JSON; @@ -37,7 +37,6 @@ public class LoginIntegrationTest { private final static String BASE_PATH = "/api/v1/gateway"; private final static String LOGIN_ENDPOINT = "/auth/login"; private final static String COOKIE_NAME = "apimlAuthenticationToken"; - private final static String ISSUER = "zOSMF"; private final static String USERNAME = ConfigReader.environmentConfiguration().getCredentials().getUser(); private final static String PASSWORD = ConfigReader.environmentConfiguration().getCredentials().getPassword(); private final static String INVALID_USERNAME = "incorrectUser"; @@ -76,7 +75,6 @@ public void doLoginWithValidBodyLoginRequest() { assertThat(claims.getId(), not(isEmptyString())); assertThat(claims.getSubject(), is(USERNAME)); - assertThat(claims.getIssuer(), is(ISSUER)); } @Test @@ -99,7 +97,6 @@ public void doLoginWithValidHeader() { assertThat(claims.getId(), not(isEmptyString())); assertThat(claims.getSubject(), is(USERNAME)); - assertThat(claims.getIssuer(), is(ISSUER)); } @Test diff --git a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/LogoutTest.java b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/LogoutTest.java index d4c1f25431..067ebe432e 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/LogoutTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/LogoutTest.java @@ -10,28 +10,20 @@ package org.zowe.apiml.gatewayservice; import io.restassured.RestAssured; -import io.restassured.http.Cookie; import org.apache.http.HttpHeaders; import org.junit.Before; import org.junit.Test; import org.springframework.http.HttpStatus; -import org.zowe.apiml.util.config.ConfigReader; import org.zowe.apiml.util.service.DiscoveryUtils; import static io.restassured.RestAssured.given; import static org.apache.http.HttpStatus.SC_NO_CONTENT; -import static org.hamcrest.Matchers.isEmptyString; import static org.hamcrest.core.Is.is; -import static org.hamcrest.core.IsNot.not; import static org.zowe.apiml.gatewayservice.SecurityUtils.getConfiguredSslConfig; public class LogoutTest { - private final static String USERNAME = ConfigReader.environmentConfiguration().getCredentials().getUser(); - private final static String PASSWORD = ConfigReader.environmentConfiguration().getCredentials().getPassword(); - private final static int PORT = ConfigReader.environmentConfiguration().getGatewayServiceConfiguration().getPort(); private final static String BASE_PATH = "/api/v1/gateway"; - private final static String LOGIN_ENDPOINT = "/auth/login"; private final static String LOGOUT_ENDPOINT = "/auth/logout"; private final static String QUERY_ENDPOINT = "/auth/query"; private final static String COOKIE_NAME = "apimlAuthenticationToken"; @@ -42,18 +34,6 @@ public void setUp() { RestAssured.config = RestAssured.config().sslConfig(getConfiguredSslConfig()); } - private String getJwt() { - Cookie cookie = given() - .auth().preemptive().basic(USERNAME, PASSWORD) - .when() - .post(String.format("%s%s%s", DiscoveryUtils.getGatewayUrls().get(0), BASE_PATH, LOGIN_ENDPOINT)) - .then() - .statusCode(is(SC_NO_CONTENT)) - .cookie(COOKIE_NAME, not(isEmptyString())) - .extract().detailedCookie(COOKIE_NAME); - return cookie.getValue(); - } - private void assertIfLogged(String jwt, boolean logged) { final HttpStatus status = logged ? HttpStatus.OK : HttpStatus.UNAUTHORIZED; @@ -70,7 +50,7 @@ private void assertIfLogged(String jwt, boolean logged) { @Test public void testLogout() { // make login - String jwt = getJwt(); + String jwt = SecurityUtils.gatewayToken(); // check if it is logged in assertIfLogged(jwt, true); @@ -79,7 +59,7 @@ public void testLogout() { given() .cookie(COOKIE_NAME, jwt) .when() - .post(String.format("%s%s%s", DiscoveryUtils.getGatewayUrls().get(0), BASE_PATH, LOGOUT_ENDPOINT)) + .post(SecurityUtils.getGateWayUrl(LOGOUT_ENDPOINT)) .then() .statusCode(is(SC_NO_CONTENT)); diff --git a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/SecurityUtils.java b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/SecurityUtils.java index f84710631a..407e6f5021 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/SecurityUtils.java +++ b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/SecurityUtils.java @@ -10,16 +10,16 @@ package org.zowe.apiml.gatewayservice; -import org.zowe.apiml.security.common.login.LoginRequest; -import org.zowe.apiml.util.config.ConfigReader; -import org.zowe.apiml.util.config.GatewayServiceConfiguration; -import org.zowe.apiml.util.config.TlsConfiguration; -import org.zowe.apiml.util.config.ZosmfServiceConfiguration; import com.netflix.discovery.shared.transport.jersey.SSLSocketFactoryAdapter; import io.restassured.config.SSLConfig; import org.apache.http.conn.ssl.DefaultHostnameVerifier; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.ssl.SSLContexts; +import org.zowe.apiml.security.common.login.LoginRequest; +import org.zowe.apiml.util.config.ConfigReader; +import org.zowe.apiml.util.config.GatewayServiceConfiguration; +import org.zowe.apiml.util.config.TlsConfiguration; +import org.zowe.apiml.util.config.ZosmfServiceConfiguration; import javax.net.ssl.SSLContext; import java.io.File; @@ -65,6 +65,10 @@ public static String gatewayToken() { return gatewayToken(USERNAME, PASSWORD); } + public static String getGateWayUrl(String path) { + return String.format("%s://%s:%d%s%s", gatewayScheme, gatewayHost, gatewayPort, GATEWAY_BASE_PATH, path); + } + public static String gatewayToken(String username, String password) { LoginRequest loginRequest = new LoginRequest(username, password); @@ -72,7 +76,7 @@ public static String gatewayToken(String username, String password) { .contentType(JSON) .body(loginRequest) .when() - .post(String.format("%s://%s:%d%s%s", gatewayScheme, gatewayHost, gatewayPort, GATEWAY_BASE_PATH, GATEWAY_LOGIN_ENDPOINT)) + .post(getGateWayUrl(GATEWAY_LOGIN_ENDPOINT)) .then() .statusCode(is(SC_NO_CONTENT)) .cookie(GATEWAY_TOKEN_COOKIE_NAME, not(isEmptyString())) From f90596fcbb4e54388aa8d17d91bdf07ebfeda941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Thu, 27 Feb 2020 09:26:30 +0100 Subject: [PATCH 119/122] move serviceId of z/OSMF into configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../InstanceInitializeServiceTest.java | 36 ++++++++----------- .../InstanceRetrievalServiceTest.java | 14 ++++---- .../ZosmfAuthenticationTest.java | 5 +-- .../ZosmfSsoIntegrationTest.java | 3 +- .../zowe/apiml/util/config/ConfigReader.java | 2 +- .../config/ZosmfServiceConfiguration.java | 1 + 6 files changed, 28 insertions(+), 33 deletions(-) diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceInitializeServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceInitializeServiceTest.java index 82308dc04a..bf66db4999 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceInitializeServiceTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceInitializeServiceTest.java @@ -10,12 +10,6 @@ package org.zowe.apiml.apicatalog.instance; -import org.zowe.apiml.apicatalog.services.cached.CachedProductFamilyService; -import org.zowe.apiml.apicatalog.services.cached.CachedServicesService; -import org.zowe.apiml.product.constants.CoreService; -import org.zowe.apiml.product.gateway.GatewayNotAvailableException; -import org.zowe.apiml.product.instance.InstanceInitializationException; -import org.zowe.apiml.product.registry.CannotRegisterServiceException; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.shared.Application; import com.netflix.discovery.shared.Applications; @@ -27,21 +21,22 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.retry.RetryException; +import org.zowe.apiml.apicatalog.services.cached.CachedProductFamilyService; +import org.zowe.apiml.apicatalog.services.cached.CachedServicesService; +import org.zowe.apiml.product.constants.CoreService; +import org.zowe.apiml.product.gateway.GatewayNotAvailableException; +import org.zowe.apiml.product.instance.InstanceInitializationException; +import org.zowe.apiml.product.registry.CannotRegisterServiceException; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Optional; -import static org.zowe.apiml.constants.EurekaMetadataDefinition.CATALOG_ID; -import static org.zowe.apiml.constants.EurekaMetadataDefinition.ROUTES; -import static org.zowe.apiml.constants.EurekaMetadataDefinition.ROUTES_GATEWAY_URL; -import static org.zowe.apiml.constants.EurekaMetadataDefinition.ROUTES_SERVICE_URL; import static junit.framework.TestCase.assertTrue; import static org.hamcrest.Matchers.isA; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; +import static org.zowe.apiml.constants.EurekaMetadataDefinition.*; @RunWith(MockitoJUnitRunner.class) public class InstanceInitializeServiceTest { @@ -165,7 +160,6 @@ private Map createInstances() { "https://localhost:9090/"); instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); - instanceInfo = getStandardInstance( "STATICCLIENT", InstanceInfo.InstanceStatus.UP, @@ -174,7 +168,6 @@ private Map createInstances() { "https://localhost:9090/discoverableclient"); instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); - instanceInfo = getStandardInstance( "STATICCLIENT2", InstanceInfo.InstanceStatus.UP, @@ -183,20 +176,19 @@ private Map createInstances() { null); instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); - instanceInfo = getStandardInstance( - "ZOSMFTSO21", + "ZOSMF1", InstanceInfo.InstanceStatus.UP, - getMetadataByCatalogUiTitleId("zosmf", "/zosmftso21"), - "zosmftso21", + getMetadataByCatalogUiTitleId("zosmf", "/zosmf1"), + "zosmf1", null); instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); instanceInfo = getStandardInstance( - "ZOSMFCA32", + "ZOSMF2", InstanceInfo.InstanceStatus.UP, - getMetadataByCatalogUiTitleId("zosmf", "/zosmfca32"), - "zosmfca32", + getMetadataByCatalogUiTitleId("zosmf", "/zosmf2"), + "zosmf2", null); instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceRetrievalServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceRetrievalServiceTest.java index 8d48251312..b1d12d7414 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceRetrievalServiceTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceRetrievalServiceTest.java @@ -10,11 +10,6 @@ package org.zowe.apiml.apicatalog.instance; -import org.zowe.apiml.apicatalog.discovery.DiscoveryConfigProperties; -import org.zowe.apiml.apicatalog.util.ApplicationsWrapper; -import org.zowe.apiml.product.constants.CoreService; -import org.zowe.apiml.product.instance.InstanceInitializationException; -import org.zowe.apiml.product.registry.ApplicationWrapper; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.appinfo.InstanceInfo; @@ -36,6 +31,11 @@ import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.web.client.RestTemplate; +import org.zowe.apiml.apicatalog.discovery.DiscoveryConfigProperties; +import org.zowe.apiml.apicatalog.util.ApplicationsWrapper; +import org.zowe.apiml.product.constants.CoreService; +import org.zowe.apiml.product.instance.InstanceInitializationException; +import org.zowe.apiml.product.registry.ApplicationWrapper; import java.util.*; @@ -259,10 +259,10 @@ private Map createInstances() { instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); - instanceInfo = getStandardInstance("ZOSMFTSO21", InstanceInfo.InstanceStatus.UP); + instanceInfo = getStandardInstance("ZOSMF1", InstanceInfo.InstanceStatus.UP); instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); - instanceInfo = getStandardInstance("ZOSMFCA32", InstanceInfo.InstanceStatus.UP); + instanceInfo = getStandardInstance("ZOSMF2", InstanceInfo.InstanceStatus.UP); instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); return instanceInfoMap; diff --git a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfAuthenticationTest.java b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfAuthenticationTest.java index 5e284a5035..9a92a06514 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfAuthenticationTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfAuthenticationTest.java @@ -21,6 +21,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.zowe.apiml.util.categories.AdditionalLocalTest; +import org.zowe.apiml.util.config.ConfigReader; import org.zowe.apiml.util.service.DiscoveryUtils; import org.zowe.apiml.util.service.VirtualService; @@ -38,14 +39,14 @@ * Those tests simulate different version of z/OSMF. * * For right execution is required: - * - set provider in gateway to zosmf with serviceId zosmfca32 + * - set provider in gateway to zosmf with using serviceId of z/OSMF * - start discovery service and gateway locally */ @RunWith(JUnit4.class) @Category(AdditionalLocalTest.class) public class ZosmfAuthenticationTest { - private static final String ZOSMF_ID = "zosmfca32"; + private static final String ZOSMF_ID = ConfigReader.environmentConfiguration().getZosmfServiceConfiguration().getServiceId(); private static final String LOGIN_ENDPOINT = "/api/v1/gateway/auth/login"; private static final String USER_ID = "user"; diff --git a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfSsoIntegrationTest.java b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfSsoIntegrationTest.java index 0266e6712c..f29f1349c9 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfSsoIntegrationTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfSsoIntegrationTest.java @@ -26,7 +26,8 @@ public class ZosmfSsoIntegrationTest { private final static String PASSWORD = ConfigReader.environmentConfiguration().getCredentials().getPassword(); private final static String USERNAME = ConfigReader.environmentConfiguration().getCredentials().getUser(); - private final static String BASE_PATH = "/api/zosmfca32"; + private final static String ZOSMF_SERVICE_ID = ConfigReader.environmentConfiguration().getZosmfServiceConfiguration().getServiceId(); + private final static String BASE_PATH = "/api/" + ZOSMF_SERVICE_ID; private final static String ZOSMF_ENDPOINT = "/zosmf/restfiles/ds?dslevel=sys1.p*"; private String token; diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/config/ConfigReader.java b/integration-tests/src/test/java/org/zowe/apiml/util/config/ConfigReader.java index 15b3df6f40..6352843770 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/config/ConfigReader.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/config/ConfigReader.java @@ -51,7 +51,7 @@ public static EnvironmentConfiguration environmentConfiguration() { .trustStorePassword("password") .build(); - ZosmfServiceConfiguration zosmfServiceConfiguration = new ZosmfServiceConfiguration("https", "ca32.ca.com", 1443); + ZosmfServiceConfiguration zosmfServiceConfiguration = new ZosmfServiceConfiguration("https", "zosmf.acme.com", 1443, "zosmf"); configuration = new EnvironmentConfiguration( credentials, gatewayServiceConfiguration, diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/config/ZosmfServiceConfiguration.java b/integration-tests/src/test/java/org/zowe/apiml/util/config/ZosmfServiceConfiguration.java index b473321557..289d800f90 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/config/ZosmfServiceConfiguration.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/config/ZosmfServiceConfiguration.java @@ -20,4 +20,5 @@ public class ZosmfServiceConfiguration { private String scheme; private String host; private int port; + private String serviceId; } From d979e8e6ba257a5722cb66e95a2a0d195c0b4b8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Thu, 27 Feb 2020 11:18:20 +0100 Subject: [PATCH 120/122] merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../EurekaInstanceRegisteredListenerTest.java | 2 +- .../ZosmfAuthenticationProviderTest.java | 12 +---------- .../query/SuccessfulQueryHandlerTest.java | 14 +++++-------- .../service/AuthenticationServiceTest.java | 20 ++++--------------- 4 files changed, 11 insertions(+), 37 deletions(-) diff --git a/discovery-service/src/test/java/org/zowe/apiml/discovery/EurekaInstanceRegisteredListenerTest.java b/discovery-service/src/test/java/org/zowe/apiml/discovery/EurekaInstanceRegisteredListenerTest.java index 37c3c78846..c9f62f0619 100644 --- a/discovery-service/src/test/java/org/zowe/apiml/discovery/EurekaInstanceRegisteredListenerTest.java +++ b/discovery-service/src/test/java/org/zowe/apiml/discovery/EurekaInstanceRegisteredListenerTest.java @@ -55,7 +55,7 @@ public void getServiceId() { verify(metadataTranslationService, times(1)).translateMetadata("serviceName", metadata); verify(metadataDefaultsService, times(1)).updateMetadata("serviceName", metadata); - verify(gatewayNotifier, times(1)).serviceUpdated("serviceName"); + verify(gatewayNotifier, times(1)).serviceUpdated("serviceName", "1:serviceName:2"); } private EurekaInstanceRegisteredEvent createEvent(String instanceId) { diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java index c12f42d9fa..8dc3c495bd 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/login/zosmf/ZosmfAuthenticationProviderTest.java @@ -14,10 +14,6 @@ import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.DiscoveryClient; import com.netflix.discovery.shared.Application; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -48,12 +44,8 @@ import java.util.Arrays; import java.util.Collections; -import static junit.framework.TestCase.assertTrue; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; public class ZosmfAuthenticationProviderTest { @@ -349,8 +341,6 @@ public void shouldThrowNewExceptionIfResourceAccessException() { () -> zosmfAuthenticationProvider.authenticate(usernamePasswordAuthentication), "Expected exception is not ServiceNotAccessibleException"); assertEquals("Could not get an access to z/OSMF service.", exception.getMessage()); - - zosmfAuthenticationProvider.authenticate(usernamePasswordAuthentication); } @Test diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/query/SuccessfulQueryHandlerTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/query/SuccessfulQueryHandlerTest.java index 8431456efb..2a8a1136da 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/query/SuccessfulQueryHandlerTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/query/SuccessfulQueryHandlerTest.java @@ -9,19 +9,14 @@ */ package org.zowe.apiml.gateway.security.query; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; -import org.zowe.apiml.security.common.config.AuthConfigurationProperties; -import org.zowe.apiml.security.common.token.TokenAuthentication; -import org.zowe.apiml.gateway.security.service.AuthenticationService; -import org.zowe.apiml.gateway.security.service.JwtSecurityInitializer; -import org.zowe.apiml.security.SecurityUtils; import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.discovery.DiscoveryClient; import io.jsonwebtoken.SignatureAlgorithm; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.context.ApplicationContext; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -118,4 +113,5 @@ public void shouldWriteModelToBody() throws Exception { assertTrue(response.contains("\"creation\":")); assertTrue(response.contains("\"expiration\":")); } + } diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/AuthenticationServiceTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/AuthenticationServiceTest.java index bc8676effd..63077482af 100644 --- a/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/AuthenticationServiceTest.java +++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/security/service/AuthenticationServiceTest.java @@ -10,27 +10,15 @@ package org.zowe.apiml.gateway.security.service; import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mockito; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.zowe.apiml.security.common.config.AuthConfigurationProperties; -import org.zowe.apiml.security.common.token.QueryResponse; -import org.zowe.apiml.security.common.token.TokenAuthentication; -import org.zowe.apiml.security.common.token.TokenExpireException; -import org.zowe.apiml.security.common.token.TokenNotValidException; -import org.zowe.apiml.gateway.config.CacheConfig; -import org.zowe.apiml.security.SecurityUtils; import com.netflix.appinfo.ApplicationInfoManager; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.DiscoveryClient; import com.netflix.discovery.shared.Application; import io.jsonwebtoken.*; import org.apache.commons.lang.time.DateUtils; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.stubbing.Answer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -39,7 +27,7 @@ import org.springframework.http.*; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.client.RestTemplate; import org.zowe.apiml.gateway.config.CacheConfig; From 34bed3bb93443286135e0db33b9213e1d048bfde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Thu, 27 Feb 2020 09:26:30 +0100 Subject: [PATCH 121/122] move serviceId of z/OSMF into configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../InstanceInitializeServiceTest.java | 36 ++++++++----------- .../InstanceRetrievalServiceTest.java | 14 ++++---- .../ZosmfAuthenticationTest.java | 5 +-- .../ZosmfSsoIntegrationTest.java | 3 +- .../zowe/apiml/util/config/ConfigReader.java | 2 +- .../config/ZosmfServiceConfiguration.java | 1 + 6 files changed, 28 insertions(+), 33 deletions(-) diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceInitializeServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceInitializeServiceTest.java index 82308dc04a..bf66db4999 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceInitializeServiceTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceInitializeServiceTest.java @@ -10,12 +10,6 @@ package org.zowe.apiml.apicatalog.instance; -import org.zowe.apiml.apicatalog.services.cached.CachedProductFamilyService; -import org.zowe.apiml.apicatalog.services.cached.CachedServicesService; -import org.zowe.apiml.product.constants.CoreService; -import org.zowe.apiml.product.gateway.GatewayNotAvailableException; -import org.zowe.apiml.product.instance.InstanceInitializationException; -import org.zowe.apiml.product.registry.CannotRegisterServiceException; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.shared.Application; import com.netflix.discovery.shared.Applications; @@ -27,21 +21,22 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.retry.RetryException; +import org.zowe.apiml.apicatalog.services.cached.CachedProductFamilyService; +import org.zowe.apiml.apicatalog.services.cached.CachedServicesService; +import org.zowe.apiml.product.constants.CoreService; +import org.zowe.apiml.product.gateway.GatewayNotAvailableException; +import org.zowe.apiml.product.instance.InstanceInitializationException; +import org.zowe.apiml.product.registry.CannotRegisterServiceException; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Optional; -import static org.zowe.apiml.constants.EurekaMetadataDefinition.CATALOG_ID; -import static org.zowe.apiml.constants.EurekaMetadataDefinition.ROUTES; -import static org.zowe.apiml.constants.EurekaMetadataDefinition.ROUTES_GATEWAY_URL; -import static org.zowe.apiml.constants.EurekaMetadataDefinition.ROUTES_SERVICE_URL; import static junit.framework.TestCase.assertTrue; import static org.hamcrest.Matchers.isA; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; +import static org.zowe.apiml.constants.EurekaMetadataDefinition.*; @RunWith(MockitoJUnitRunner.class) public class InstanceInitializeServiceTest { @@ -165,7 +160,6 @@ private Map createInstances() { "https://localhost:9090/"); instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); - instanceInfo = getStandardInstance( "STATICCLIENT", InstanceInfo.InstanceStatus.UP, @@ -174,7 +168,6 @@ private Map createInstances() { "https://localhost:9090/discoverableclient"); instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); - instanceInfo = getStandardInstance( "STATICCLIENT2", InstanceInfo.InstanceStatus.UP, @@ -183,20 +176,19 @@ private Map createInstances() { null); instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); - instanceInfo = getStandardInstance( - "ZOSMFTSO21", + "ZOSMF1", InstanceInfo.InstanceStatus.UP, - getMetadataByCatalogUiTitleId("zosmf", "/zosmftso21"), - "zosmftso21", + getMetadataByCatalogUiTitleId("zosmf", "/zosmf1"), + "zosmf1", null); instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); instanceInfo = getStandardInstance( - "ZOSMFCA32", + "ZOSMF2", InstanceInfo.InstanceStatus.UP, - getMetadataByCatalogUiTitleId("zosmf", "/zosmfca32"), - "zosmfca32", + getMetadataByCatalogUiTitleId("zosmf", "/zosmf2"), + "zosmf2", null); instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); diff --git a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceRetrievalServiceTest.java b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceRetrievalServiceTest.java index 8d48251312..b1d12d7414 100644 --- a/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceRetrievalServiceTest.java +++ b/api-catalog-services/src/test/java/org/zowe/apiml/apicatalog/instance/InstanceRetrievalServiceTest.java @@ -10,11 +10,6 @@ package org.zowe.apiml.apicatalog.instance; -import org.zowe.apiml.apicatalog.discovery.DiscoveryConfigProperties; -import org.zowe.apiml.apicatalog.util.ApplicationsWrapper; -import org.zowe.apiml.product.constants.CoreService; -import org.zowe.apiml.product.instance.InstanceInitializationException; -import org.zowe.apiml.product.registry.ApplicationWrapper; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.netflix.appinfo.InstanceInfo; @@ -36,6 +31,11 @@ import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.web.client.RestTemplate; +import org.zowe.apiml.apicatalog.discovery.DiscoveryConfigProperties; +import org.zowe.apiml.apicatalog.util.ApplicationsWrapper; +import org.zowe.apiml.product.constants.CoreService; +import org.zowe.apiml.product.instance.InstanceInitializationException; +import org.zowe.apiml.product.registry.ApplicationWrapper; import java.util.*; @@ -259,10 +259,10 @@ private Map createInstances() { instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); - instanceInfo = getStandardInstance("ZOSMFTSO21", InstanceInfo.InstanceStatus.UP); + instanceInfo = getStandardInstance("ZOSMF1", InstanceInfo.InstanceStatus.UP); instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); - instanceInfo = getStandardInstance("ZOSMFCA32", InstanceInfo.InstanceStatus.UP); + instanceInfo = getStandardInstance("ZOSMF2", InstanceInfo.InstanceStatus.UP); instanceInfoMap.put(instanceInfo.getAppName(), instanceInfo); return instanceInfoMap; diff --git a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfAuthenticationTest.java b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfAuthenticationTest.java index 5e284a5035..9a92a06514 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfAuthenticationTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfAuthenticationTest.java @@ -21,6 +21,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.zowe.apiml.util.categories.AdditionalLocalTest; +import org.zowe.apiml.util.config.ConfigReader; import org.zowe.apiml.util.service.DiscoveryUtils; import org.zowe.apiml.util.service.VirtualService; @@ -38,14 +39,14 @@ * Those tests simulate different version of z/OSMF. * * For right execution is required: - * - set provider in gateway to zosmf with serviceId zosmfca32 + * - set provider in gateway to zosmf with using serviceId of z/OSMF * - start discovery service and gateway locally */ @RunWith(JUnit4.class) @Category(AdditionalLocalTest.class) public class ZosmfAuthenticationTest { - private static final String ZOSMF_ID = "zosmfca32"; + private static final String ZOSMF_ID = ConfigReader.environmentConfiguration().getZosmfServiceConfiguration().getServiceId(); private static final String LOGIN_ENDPOINT = "/api/v1/gateway/auth/login"; private static final String USER_ID = "user"; diff --git a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfSsoIntegrationTest.java b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfSsoIntegrationTest.java index 0266e6712c..f29f1349c9 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfSsoIntegrationTest.java +++ b/integration-tests/src/test/java/org/zowe/apiml/gatewayservice/ZosmfSsoIntegrationTest.java @@ -26,7 +26,8 @@ public class ZosmfSsoIntegrationTest { private final static String PASSWORD = ConfigReader.environmentConfiguration().getCredentials().getPassword(); private final static String USERNAME = ConfigReader.environmentConfiguration().getCredentials().getUser(); - private final static String BASE_PATH = "/api/zosmfca32"; + private final static String ZOSMF_SERVICE_ID = ConfigReader.environmentConfiguration().getZosmfServiceConfiguration().getServiceId(); + private final static String BASE_PATH = "/api/" + ZOSMF_SERVICE_ID; private final static String ZOSMF_ENDPOINT = "/zosmf/restfiles/ds?dslevel=sys1.p*"; private String token; diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/config/ConfigReader.java b/integration-tests/src/test/java/org/zowe/apiml/util/config/ConfigReader.java index 15b3df6f40..6352843770 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/config/ConfigReader.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/config/ConfigReader.java @@ -51,7 +51,7 @@ public static EnvironmentConfiguration environmentConfiguration() { .trustStorePassword("password") .build(); - ZosmfServiceConfiguration zosmfServiceConfiguration = new ZosmfServiceConfiguration("https", "ca32.ca.com", 1443); + ZosmfServiceConfiguration zosmfServiceConfiguration = new ZosmfServiceConfiguration("https", "zosmf.acme.com", 1443, "zosmf"); configuration = new EnvironmentConfiguration( credentials, gatewayServiceConfiguration, diff --git a/integration-tests/src/test/java/org/zowe/apiml/util/config/ZosmfServiceConfiguration.java b/integration-tests/src/test/java/org/zowe/apiml/util/config/ZosmfServiceConfiguration.java index b473321557..289d800f90 100644 --- a/integration-tests/src/test/java/org/zowe/apiml/util/config/ZosmfServiceConfiguration.java +++ b/integration-tests/src/test/java/org/zowe/apiml/util/config/ZosmfServiceConfiguration.java @@ -20,4 +20,5 @@ public class ZosmfServiceConfiguration { private String scheme; private String host; private int port; + private String serviceId; } From f166b61aa48f664d3c7e8bd8328a0bafda5a3ccd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Jare=C5=A1?= Date: Wed, 11 Mar 2020 12:52:23 +0100 Subject: [PATCH 122/122] fix merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pavel Jareš --- .../org/zowe/apiml/gateway/routing/ApimlRoutingConfig.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/routing/ApimlRoutingConfig.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/routing/ApimlRoutingConfig.java index af1b97305a..f6f19db974 100644 --- a/gateway-service/src/main/java/org/zowe/apiml/gateway/routing/ApimlRoutingConfig.java +++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/routing/ApimlRoutingConfig.java @@ -18,10 +18,7 @@ import org.springframework.context.annotation.Configuration; import org.zowe.apiml.gateway.filters.post.ConvertAuthTokenInUriToCookieFilter; import org.zowe.apiml.gateway.filters.post.PageRedirectionFilter; -import org.zowe.apiml.gateway.filters.pre.EncodedCharactersFilter; -import org.zowe.apiml.gateway.filters.pre.LocationFilter; -import org.zowe.apiml.gateway.filters.pre.ServiceAuthenticationFilter; -import org.zowe.apiml.gateway.filters.pre.SlashFilter; +import org.zowe.apiml.gateway.filters.pre.*; import org.zowe.apiml.gateway.ws.WebSocketProxyServerHandler; import org.zowe.apiml.message.core.MessageService; import org.zowe.apiml.product.gateway.GatewayConfigProperties;