Skip to content

Commit 965fcbf

Browse files
author
Chris Wiechmann
committed
Refactore App-OAuth-Scopes handling
Fixes #206
1 parent 71474b0 commit 965fcbf

10 files changed

Lines changed: 337 additions & 108 deletions

File tree

modules/apim-adapter/src/main/java/com/axway/apim/adapter/clientApps/APIMgrAppsAdapter.java

Lines changed: 95 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import java.io.UnsupportedEncodingException;
55
import java.net.URI;
66
import java.net.URISyntaxException;
7+
import java.util.ArrayList;
78
import java.util.HashMap;
9+
import java.util.Iterator;
810
import java.util.List;
911
import java.util.Map;
1012
import java.util.Optional;
@@ -38,6 +40,7 @@
3840
import com.axway.apim.lib.errorHandling.AppException;
3941
import com.axway.apim.lib.errorHandling.ErrorCode;
4042
import com.axway.apim.lib.utils.Utils;
43+
import com.axway.apim.lib.utils.rest.DELRequest;
4144
import com.axway.apim.lib.utils.rest.GETRequest;
4245
import com.axway.apim.lib.utils.rest.POSTRequest;
4346
import com.axway.apim.lib.utils.rest.PUTRequest;
@@ -340,14 +343,14 @@ public ClientApplication createOrUpdateApplication(ClientApplication desiredApp,
340343
RestAPICall request;
341344
if(actualApp==null) {
342345
FilterProvider filter = new SimpleFilterProvider().setDefaultFilter(
343-
SimpleBeanPropertyFilter.serializeAllExcept(new String[] {"credentials", "appQuota", "organization", "image","oauthResources"}));
346+
SimpleBeanPropertyFilter.serializeAllExcept(new String[] {"credentials", "appQuota", "organization", "image","appScopes"}));
344347
mapper.setFilterProvider(filter);
345348
String json = mapper.writeValueAsString(desiredApp);
346349
HttpEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
347350
request = new POSTRequest(entity, uri);
348351
} else {
349352
FilterProvider filter = new SimpleFilterProvider().setDefaultFilter(
350-
SimpleBeanPropertyFilter.serializeAllExcept(new String[] {"credentials", "appQuota", "organization", "image", "apis","oauthResources"}));
353+
SimpleBeanPropertyFilter.serializeAllExcept(new String[] {"credentials", "appQuota", "organization", "image", "apis","appScopes"}));
351354
mapper.setFilterProvider(filter);
352355
String json = mapper.writeValueAsString(desiredApp);
353356
HttpEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
@@ -376,12 +379,12 @@ public ClientApplication createOrUpdateApplication(ClientApplication desiredApp,
376379
saveImage(desiredApp, actualApp);
377380
saveAPIAccess(desiredApp, actualApp);
378381
saveCredentials(desiredApp, actualApp);
379-
saveOauthResources(desiredApp, actualApp);
382+
manageOAuthResources(desiredApp, actualApp);
380383
saveQuota(desiredApp, actualApp);
381384
return createdApp;
382385

383386
} catch (Exception e) {
384-
throw new AppException("Error creating/updating application", ErrorCode.CANT_CREATE_API_PROXY, e);
387+
throw new AppException("Error creating/updating application", ErrorCode.ERR_CREATING_APPLICATION, e);
385388
}
386389
}
387390

@@ -562,31 +565,25 @@ private void saveAPIAccess(ClientApplication app, ClientApplication actualApp) t
562565
accessAdapter.saveAPIAccess(app.getApiAccess(), app, Type.applications);
563566
}
564567

565-
private void saveOauthResources(ClientApplication desiredApp, ClientApplication actualApp) throws AppException {
566-
if(desiredApp.getOauthResources()==null || desiredApp.getOauthResources().size()==0) return;
567-
568+
private void manageOAuthResources(ClientApplication desiredApp, ClientApplication actualApp) throws AppException {
569+
List<ClientAppOauthResource> scopes2Update = new ArrayList<ClientAppOauthResource>();
570+
List<ClientAppOauthResource> scopes2Create = new ArrayList<ClientAppOauthResource>();
571+
List<ClientAppOauthResource> scopes2Delete = new ArrayList<ClientAppOauthResource>();
572+
getScopes2AddOrUpdate(actualApp, desiredApp, scopes2Update, scopes2Create, scopes2Delete);
573+
saveOrUpdateOAuthResources(desiredApp, scopes2Create, false);
574+
saveOrUpdateOAuthResources(desiredApp, scopes2Update, true);
575+
deleteOAuthResources(desiredApp, scopes2Delete);
576+
}
577+
578+
private void saveOrUpdateOAuthResources(ClientApplication desiredApp, List<ClientAppOauthResource> scopes2Create, boolean update) throws AppException {
579+
if(scopes2Create==null || scopes2Create.size()==0) return;
568580
HttpResponse httpResponse = null;
569-
for(ClientAppOauthResource res : desiredApp.getOauthResources()) {
581+
for(ClientAppOauthResource res : scopes2Create) {
570582
String endpoint = "oauthresource";
571-
if(actualApp!=null && actualApp.getOauthResources().contains(res)) //nothing to do
572-
continue;
573-
574583
try {
575-
boolean update = false;
576-
if (actualApp!=null && actualApp.getOauthResources()!=null) {
577-
Optional<ClientAppOauthResource> opt = actualApp.getOauthResources().stream().filter(o -> o.getScope().equals(res.getScope())).findFirst();
578-
if (opt.isPresent()) {
579-
//I found an oauth resource with same scope name but different in some properties, I have to update it
580-
String oauthResourceId = opt.get().getId();
581-
endpoint += "/"+oauthResourceId;
582-
res.setId(oauthResourceId);
583-
res.setApplicationId(opt.get().getApplicationId());
584-
res.setUriprefix(opt.get().getUriprefix());
585-
update = true;
586-
LOG.debug("Oauth resource already exists, updating");
587-
} else {
588-
LOG.debug("Oauth resource not found, creating");
589-
}
584+
if(update) {
585+
endpoint += "/"+res.getId();
586+
LOG.debug("Oauth resource already exists, updating");
590587
} else {
591588
LOG.debug("Oauth resource not found, creating");
592589
}
@@ -612,7 +609,78 @@ private void saveOauthResources(ClientApplication desiredApp, ClientApplication
612609
} catch (Exception ignore) { }
613610
}
614611
}
615-
}
612+
}
613+
614+
private void deleteOAuthResources(ClientApplication desiredApp, List<ClientAppOauthResource> scopes2Delete) throws AppException {
615+
if(scopes2Delete==null || scopes2Delete.size()==0) return;
616+
HttpResponse httpResponse = null;
617+
for(ClientAppOauthResource res : scopes2Delete) {
618+
try {
619+
URI uri = new URIBuilder(cmd.getAPIManagerURL()).setPath(cmd.getApiBasepath()+"/applications/"+desiredApp.getId()+"/oauthresource/"+res.getId()).build();
620+
RestAPICall request = new DELRequest(uri, true);
621+
httpResponse = request.execute();
622+
int statusCode = httpResponse.getStatusLine().getStatusCode();
623+
if(statusCode != 204){
624+
LOG.error("Error deleting application scope. Response-Code: "+statusCode+". Got response: '"+EntityUtils.toString(httpResponse.getEntity())+"'");
625+
throw new AppException("Error deleting application scope. Response-Code: "+statusCode+"", ErrorCode.API_MANAGER_COMMUNICATION);
626+
}
627+
LOG.info("Application scope: "+res.getScope()+" for application: "+desiredApp.getName()+" successfully deleted");
628+
} catch (Exception e) {
629+
throw new AppException("Error deleting application scope", ErrorCode.API_MANAGER_COMMUNICATION, e);
630+
} finally {
631+
try {
632+
((CloseableHttpResponse)httpResponse).close();
633+
} catch (Exception ignore) { }
634+
}
635+
}
636+
}
637+
638+
private void getScopes2AddOrUpdate(ClientApplication actualApp, ClientApplication desiredApp,
639+
List<ClientAppOauthResource> scopes2Update, List<ClientAppOauthResource> scopes2Create, List<ClientAppOauthResource> scopes2Delete) throws AppException {
640+
if((desiredApp.getOauthResources()==null || desiredApp.getOauthResources().size()==0) &&
641+
(actualApp.getOauthResources()==null || actualApp.getOauthResources().size()==0)) return;
642+
List<ClientAppOauthResource> existingScopes = null;
643+
List<ClientAppOauthResource> desiredScopes = desiredApp.getOauthResources();
644+
if(actualApp!=null) {
645+
existingScopes = actualApp.getOauthResources();
646+
}
647+
if(existingScopes==null) existingScopes = new ArrayList<ClientAppOauthResource>();
648+
boolean existingScopeFound = false;
649+
if(desiredScopes!=null) {
650+
// Iterate over the given desired scopes
651+
Iterator<ClientAppOauthResource> it = desiredScopes.iterator();
652+
while(it.hasNext()) {
653+
ClientAppOauthResource desiredScope = it.next();
654+
// Compare each desired scope, if it is already included in the existing scopes
655+
for(ClientAppOauthResource existingScope : existingScopes) {
656+
// If it is the same scope (based on the name) ...
657+
if(desiredScope.getScope().equals(existingScope.getScope())) {
658+
// Check, if the scopes are not equal and if so take over the configuration and put it to the list of scopes to update
659+
if(!desiredScope.equals(existingScope)) {
660+
existingScope.setDefaultScope(desiredScope.isDefaultScope());
661+
existingScope.setEnabled(desiredScope.isEnabled());
662+
scopes2Update.add(existingScope);
663+
}
664+
existingScopeFound = true;
665+
break;
666+
}
667+
}
668+
if(!existingScopeFound) scopes2Create.add(desiredScope);
669+
}
670+
// finally iterate over all existing scopes and check if they are still desired
671+
for(ClientAppOauthResource existingScope : existingScopes) {
672+
boolean actualScopeFound = false;
673+
for(ClientAppOauthResource desiredScope : desiredScopes) {
674+
if(existingScope.getScope().equals(desiredScope.getScope())) {
675+
actualScopeFound = true;
676+
break; // As the actual scope is still desired
677+
}
678+
}
679+
if(!actualScopeFound) scopes2Delete.add(existingScope);
680+
}
681+
}
682+
return;
683+
}
616684

617685

618686
public void setTestApiManagerResponse(ClientAppFilter filter, String apiManagerResponse) {

modules/apim-adapter/src/main/java/com/axway/apim/api/model/apps/ClientAppOauthResource.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,5 +95,11 @@ public boolean equals(Object other) {
9595
otherAppCredential.isDefaultScope()==this.isDefaultScope() );
9696
}
9797
return false;
98-
}
98+
}
99+
100+
@Override
101+
public String toString() {
102+
return "ClientAppOauthResource [scope=" + scope + ", enabled=" + enabled + ", defaultScope=" + defaultScope
103+
+ "]";
104+
}
99105
}

modules/apim-adapter/src/main/java/com/axway/apim/api/model/apps/ClientApplication.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public class ClientApplication extends AbstractEntity implements CustomPropertie
5959

6060
private APIQuota appQuota;
6161

62+
@JsonProperty("appScopes")
6263
private List<ClientAppOauthResource> oauthResources = new ArrayList<ClientAppOauthResource>();
6364

6465
@JsonDeserialize( using = OrganizationDeserializer.class)

modules/apim-adapter/src/main/java/com/axway/apim/lib/errorHandling/ErrorCode.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public enum ErrorCode {
5959
ERR_DELETING_ORG (92, "Organization could not be deleted.", false),
6060
ERR_GRANTING_ACCESS_TO_API (93, "Error granting access to an API.", false),
6161
ERR_EXPORTING_API_DAT_FILE (94, "Error exporting API-Date file.", false),
62+
ERR_CREATING_APPLICATION (95, "Error creating/updating an application.", true),
6263
UNXPECTED_ERROR (99, "An unexpected error occured.");
6364

6465
private final int code;

modules/apps/src/main/java/com/axway/apim/appexport/impl/JsonApplicationExporter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ private void saveApplicationLocally(ExportApplication app) throws AppException {
6969
.setDefaultFilter(SimpleBeanPropertyFilter.serializeAllExcept(new String[] {"id", "apiId", "createdBy", "createdOn", "enabled"}))
7070
.addFilter("APIAccessFilter", SimpleBeanPropertyFilter.filterOutAllExcept(new String[] {"apiName", "apiVersion"}))
7171
.addFilter("ClientAppCredentialFilter", SimpleBeanPropertyFilter.serializeAllExcept(new String[] {"applicationId", "id", "createdOn", "createdBy"}))
72-
.addFilter("ClientAppOauthResourceFilter", SimpleBeanPropertyFilter.serializeAllExcept(new String[] {"applicationId", "id"}));
72+
.addFilter("ClientAppOauthResourceFilter", SimpleBeanPropertyFilter.serializeAllExcept(new String[] {"applicationId", "id", "uriprefix", "scopes", "enabled"}));
7373
mapper.setFilterProvider(filter);
7474
mapper.setSerializationInclusion(Include.NON_NULL);
7575
try {

modules/apps/src/test/java/com/axway/apim/appimport/adapter/JSONClientAppAdapterTest.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.axway.apim.api.model.QuotaRestrictiontype;
2020
import com.axway.apim.api.model.apps.APIKey;
2121
import com.axway.apim.api.model.apps.ClientAppCredential;
22+
import com.axway.apim.api.model.apps.ClientAppOauthResource;
2223
import com.axway.apim.api.model.apps.ClientApplication;
2324
import com.axway.apim.api.model.apps.OAuth;
2425
import com.axway.apim.appimport.lib.AppImportParams;
@@ -140,5 +141,12 @@ public void testCompleteApp() throws AppException {
140141
assertEquals(restr.getConfig().get("messages"), "9999");
141142
assertEquals(restr.getConfig().get("period"), "week");
142143
assertEquals(restr.getConfig().get("per"), "1");
144+
145+
List<ClientAppOauthResource> oauthResources = app.getOauthResources();
146+
assertNotNull(oauthResources, "oauthResources is null");
147+
assertEquals(oauthResources.size(), 2, "Expected two OAuth resources");
148+
ClientAppOauthResource oauthRes = oauthResources.get(0);
149+
assertEquals(oauthRes.getScope(), "resource.READ");
150+
assertEquals(oauthRes.isEnabled(), true);
143151
}
144152
}

modules/apps/src/test/java/com/axway/apim/appimport/it/basic/ImportCompleteApplicationTestIT.java

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public class ImportCompleteApplicationTestIT extends TestNGCitrusTestRunner impl
2424

2525
@CitrusTest
2626
@Test @Parameters("context")
27-
public void importApplicationBasicTest(@Optional @CitrusResource TestContext context) throws IOException, AppException {
27+
public void importApplicationBasicTest(@Optional @CitrusResource TestContext context) throws IOException, AppException, InterruptedException {
2828
description("Import application into API-Manager");
2929

3030
ImportAppTestAction importApp = new ImportAppTestAction(context);
@@ -39,6 +39,14 @@ public void importApplicationBasicTest(@Optional @CitrusResource TestContext con
3939
variable("state", "approved");
4040
variable("appImage", "app-image.jpg");
4141
variable("oauthCorsOrigins","");
42+
43+
variable("scopeName1","scope.READ");
44+
variable("scopeEnabled1",true);
45+
variable("scopeIsDefault1",false);
46+
47+
variable("scopeName2","scope.WRITE");
48+
variable("scopeEnabled2",false);
49+
variable("scopeIsDefault2",false);
4250

4351
echo("####### Import application: '${appName}' #######");
4452
createVariable(PARAM_CONFIGFILE, PACKAGE + "CompleteApplication.json");
@@ -70,7 +78,7 @@ public void importApplicationBasicTest(@Optional @CitrusResource TestContext con
7078
.validate("$.restrictions[0].config.period", "${quotaPeriod}")
7179
.validate("$.restrictions[0].config.per", "1"));
7280

73-
echo("####### Validate application: '${appName}' with id: ${appId} OAuth has been imported #######");
81+
echo("####### Validate application: '${appName}' with id: ${appId} OAuth-Credentials have been imported #######");
7482
http(builder -> builder.client("apiManager").send().get("/applications/${appId}/oauth").header("Content-Type", "application/json"));
7583

7684
http(builder -> builder.client("apiManager").receive().response(HttpStatus.OK).messageType(MessageType.JSON)
@@ -94,6 +102,12 @@ public void importApplicationBasicTest(@Optional @CitrusResource TestContext con
94102
.validate("$.[?(@.clientId=='ClientConfidentialClientID-${appNumber}')].enabled", "true")
95103
.validate("$[0].corsOrigins[0]", ""));
96104

105+
echo("####### Validate application: '${appName}' with id: ${appId} Application-Scopes have been imported #######");
106+
http(builder -> builder.client("apiManager").send().get("/applications/${appId}/oauthresource").header("Content-Type", "application/json"));
107+
http(builder -> builder.client("apiManager").receive().response(HttpStatus.OK).messageType(MessageType.JSON)
108+
.validate("$.[?(@.scope=='${scopeName1}')].isDefault", "${scopeIsDefault1}")
109+
.validate("$.[?(@.scope=='${scopeName2}')].isDefault", "${scopeIsDefault2}"));
110+
97111
echo("####### Re-Import same application - Should be a No-Change #######");
98112
createVariable(PARAM_EXPECTED_RC, "10");
99113
importApp.doExecute(context);
@@ -103,7 +117,14 @@ public void importApplicationBasicTest(@Optional @CitrusResource TestContext con
103117
variable("quotaMessages", "1111");
104118
variable("quotaPeriod", "day");
105119

120+
variable("scopeName2","scope.WRITE");
121+
variable("scopeEnabled2",true);
122+
variable("scopeIsDefault2",true);
123+
124+
// First scope is removed - Second scope becomes the first (See the config file: CompleteApplicationOnlyOneScope.json)
125+
106126
createVariable(PARAM_EXPECTED_RC, "0");
127+
createVariable(PARAM_CONFIGFILE, PACKAGE + "CompleteApplicationOnlyOneScope.json");
107128
importApp.doExecute(context);
108129

109130
echo("####### Validate application: '${appName}' (${appId}) has been updated #######");
@@ -131,14 +152,19 @@ public void importApplicationBasicTest(@Optional @CitrusResource TestContext con
131152
.validate("$.restrictions[0].config.period", "${quotaPeriod}")
132153
.validate("$.restrictions[0].config.per", "1"));
133154

155+
echo("####### Validate modified scopes for application: '${appName}' with id: ${appId} #######");
156+
Thread.sleep(1000);
157+
http(builder -> builder.client("apiManager").send().get("/applications/${appId}/oauthresource").header("Content-Type", "application/json"));
158+
http(builder -> builder.client("apiManager").receive().response(HttpStatus.OK).messageType(MessageType.JSON)
159+
.validate("$.[?(@.scope=='${scopeName2}')].enabled", "${scopeEnabled2}")
160+
.validate("$.[?(@.scope=='${scopeName2}')].isDefault", "${scopeIsDefault2}"));
161+
134162
echo("####### Update the application image only #######");
135163
variable("appImage", "other-app-image.jpg");
136164

137165
createVariable(PARAM_EXPECTED_RC, "0");
138166
importApp.doExecute(context);
139167

140-
141-
142168
echo("####### Re-Import change corsorigins (oauth) - Existing App should be updated #######");
143169
variable("oauthCorsOrigins","*");
144170
createVariable(PARAM_EXPECTED_RC, "0");

0 commit comments

Comments
 (0)