Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IdP functionality for visibility and access control support #1837

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions backoffice/backoffice-domain-service/ballerina/types.bal
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// AUTO-GENERATED FILE.
// This file is auto-generated by the Ballerina OpenAPI tool.

import ballerina/constraint;
import ballerina/http;

Expand Down Expand Up @@ -392,7 +391,7 @@ public type APIInfo record {
string updatedTime?;
boolean hasThumbnail?;
# State of the API. Only published APIs are visible on the Developer Portal
"CREATED"|"PUBLISHED" state?;
string state?;
};

public type APIExternalStore record {
Expand Down
1 change: 1 addition & 0 deletions idp/idp-domain-service/ballerina/IdpConstants.bal
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const string TOKEN_TYPE_CLAIM = "token_type";
const string TOKEN_TYPE_BEARER = "Bearer";
const string REDIRECT_URI_CLAIM = "redirectUri";
const string SCOPES_CLAIM = "scope";
const string GROUPS_CLAIM = "groups";
const string SESSION_KEY_PREFIX = "session-";
const string STATE_KEY_QUERY_PARAM = "stateKey";
const string LOCATION_HEADER = "Location";
Expand Down
2 changes: 2 additions & 0 deletions idp/idp-domain-service/ballerina/Oauth2Types.bal
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ public type Token_body record {
string refresh_token?;
# OAuth scopes
string scope?;
# Visibility groups
string groups?;
# username
string username?;
# password
Expand Down
35 changes: 27 additions & 8 deletions idp/idp-domain-service/ballerina/TokenUtil.bal
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public class TokenUtil {
} else if grantType == AUTHORIZATION_CODE_GRANT_TYPE {
return self.handleAuthorizationCodeGrant(payload, application);
} else if grantType == REFRESH_TOKEN_GRANT_TYPE {
return self.hanleRefreshTokenGrant(payload, application);
return self.handleRefreshTokenGrant(payload, application);
} else {
BadRequestTokenErrorResponse tokenError = {body: {'error: "unsupported_grant_type", error_description: grantType + " not supported by system."}};
return tokenError;
Expand All @@ -86,7 +86,8 @@ public class TokenUtil {
}
public isolated function handleClientCredentialsGrant(Token_body payload, Application application) returns OkTokenResponse|BadRequestTokenErrorResponse|UnauthorizedTokenErrorResponse {
string[] scopeArray = self.filterScopes(payload.scope);
string|jwt:Error tokenResult = self.issueToken(application, (), scopeArray, (), ACCESS_TOKEN_TYPE);
string[] groupsArray = self.filterGroups(payload.groups);
string|jwt:Error tokenResult = self.issueToken(application, (), scopeArray, groupsArray, (), ACCESS_TOKEN_TYPE);
if tokenResult is string {
TokenResponse tokenResponse = {
access_token: tokenResult,
Expand All @@ -103,7 +104,7 @@ public class TokenUtil {

}
}
public isolated function issueToken(Application application, string? username, string[] scopes, string? organization, string tokenType) returns string|jwt:Error {
public isolated function issueToken(Application application, string? username, string[] scopes, string[] groups, string? organization, string tokenType) returns string|jwt:Error {
TokenIssuerConfiguration issuerConfiguration = idpConfiguration.tokenIssuerConfiguration;
string jwtid = uuid:createType1AsString();
decimal exptime = tokenType == ACCESS_TOKEN_TYPE ? issuerConfiguration.expTime : issuerConfiguration.refrshTokenValidity;
Expand All @@ -124,6 +125,7 @@ public class TokenUtil {
map<string> customClaims = {};
customClaims[CLIENT_ID_CLAIM] = <string>application.client_id;
customClaims[SCOPES_CLAIM] = string:'join(" ", ...scopes);
customClaims[GROUPS_CLAIM] = string:'join(" ", ...groups);
if organization is string && organization.toString().trim().length() > 0 {
customClaims[ORGANIZATION_CLAIM] = organization;
}
Expand Down Expand Up @@ -157,6 +159,7 @@ public class TokenUtil {
string requestRedirectUrl = <string>validatedPayload.get(REDIRECT_URI_CLAIM);
string clientId = <string>validatedPayload.get(CLIENT_ID_CLAIM);
json[] scopes = <json[]>validatedPayload.get(SCOPES_CLAIM);
json[] groups = validatedPayload.hasKey(GROUPS_CLAIM) ? <json[]>validatedPayload.get(GROUPS_CLAIM) : [];
string sub = <string>validatedPayload.sub;
string[]? redirectUris = application.redirect_uris;

Expand All @@ -169,9 +172,14 @@ public class TokenUtil {
foreach json scope in scopes {
scopesArray.push(scope.toString());
}

string[] groupsArray = [];
foreach json group in groups {
groupsArray.push(group.toString());
}
do {
string accessToken = check self.issueToken(application, sub, scopesArray, organization, ACCESS_TOKEN_TYPE);
string refreshToken = check self.issueToken(application, sub, scopesArray, organization, REFRESH_TOKEN_TYPE);
string accessToken = check self.issueToken(application, sub, scopesArray, groupsArray, organization, ACCESS_TOKEN_TYPE);
string refreshToken = check self.issueToken(application, sub, scopesArray, groupsArray, organization, REFRESH_TOKEN_TYPE);
TokenResponse token = {access_token: accessToken, refresh_token: refreshToken, expires_in: idpConfiguration.tokenIssuerConfiguration.expTime, token_type: TOKEN_TYPE_BEARER, scope: string:'join(" ", ...scopesArray)};
return {body: token};
} on fail var e {
Expand All @@ -184,7 +192,7 @@ public class TokenUtil {
return tokenError;
}
}
public isolated function hanleRefreshTokenGrant(Token_body payload, Application application) returns BadRequestTokenErrorResponse|OkTokenResponse {
public isolated function handleRefreshTokenGrant(Token_body payload, Application application) returns BadRequestTokenErrorResponse|OkTokenResponse {
string? refresh_token = payload.refresh_token;

if (refresh_token is () || refresh_token.toString().trim().length() == 0) {
Expand All @@ -206,6 +214,8 @@ public class TokenUtil {
}
string clientId = <string>validatedPayload.get(CLIENT_ID_CLAIM);
string scopes = <string>validatedPayload.get(SCOPES_CLAIM);
string groups = <string>validatedPayload.get(GROUPS_CLAIM);

string sub = <string>validatedPayload.sub;

string? organization = validatedPayload.hasKey(ORGANIZATION_CLAIM) ? <string>validatedPayload.get(ORGANIZATION_CLAIM) : ();
Expand All @@ -215,8 +225,9 @@ public class TokenUtil {
}
do {
string[] scopesArray = regex:split(scopes, " ");
string accessToken = check self.issueToken(application, sub, scopesArray, organization, ACCESS_TOKEN_TYPE);
string refreshToken = check self.issueToken(application, sub, scopesArray, organization, REFRESH_TOKEN_TYPE);
string[] groupsArray = regex:split(groups, " ");
string accessToken = check self.issueToken(application, sub, scopesArray, groupsArray, organization, ACCESS_TOKEN_TYPE);
string refreshToken = check self.issueToken(application, sub, scopesArray, groupsArray, organization, REFRESH_TOKEN_TYPE);
TokenResponse token = {access_token: accessToken, refresh_token: refreshToken, expires_in: idpConfiguration.tokenIssuerConfiguration.expTime, token_type: TOKEN_TYPE_BEARER, scope: scopes};
return {body: token};
} on fail var e {
Expand Down Expand Up @@ -303,6 +314,14 @@ public class TokenUtil {
}
return scopeArray;
}

public isolated function filterGroups(string? groups) returns string[] {
string[] groupArray = [];
if groups is string && groups.trim().length() > 0 {
groupArray = regex:split(groups, " ");
}
return groupArray;
}
public isolated function handleOauthCallBackRequest(http:Request request, string sessionKey) returns http:Found {
do {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.wso2.apk.integration.api;

import java.util.HashMap;
import java.util.Map;
import io.cucumber.java.en.When;

import org.apache.http.HttpResponse;
import org.wso2.apk.integration.utils.Constants;
import org.wso2.apk.integration.utils.Utils;
import org.wso2.apk.integration.utils.clients.SimpleHTTPClient;

public class BackOfficeSteps {

private final SharedContext sharedContext;

public BackOfficeSteps(SharedContext sharedContext) {

this.sharedContext = sharedContext;
}

@When("I make the GET APIs call to the backoffice")
public void make_a_deployment_request() throws Exception {
Map<String, String> headers = new HashMap<>();
headers.put(Constants.REQUEST_HEADERS.AUTHORIZATION, "Bearer " + sharedContext.getAccessToken());
headers.put(Constants.REQUEST_HEADERS.HOST, Constants.DEFAULT_API_HOST);

HttpResponse response = sharedContext.getHttpClient().doGet(Utils.getBackOfficeAPIURL(),
headers);

sharedContext.setResponse(response);
sharedContext.setResponseBody(SimpleHTTPClient.responseEntityBodyToString(sharedContext.getResponse()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,18 +94,22 @@ public void systemIsReady() {

@Then("the response body should contain {string}")
public void theResponseBodyShouldContain(String expectedText) throws IOException {
Assert.assertTrue(sharedContext.getResponseBody().contains(expectedText), "Actual response body: " + sharedContext.getResponseBody());
Assert.assertTrue(sharedContext.getResponseBody().contains(expectedText),
"Actual response body: " + sharedContext.getResponseBody());
}

@Then("the response body should not contain {string}")
public void theResponseBodyShouldNotContain(String expectedText) throws IOException {
Assert.assertFalse(sharedContext.getResponseBody().contains(expectedText), "Actual response body: " + sharedContext.getResponseBody());
Assert.assertFalse(sharedContext.getResponseBody().contains(expectedText),
"Actual response body: " + sharedContext.getResponseBody());
}

@Then("the response body should contain")
public void theResponseBodyShouldContain(DataTable dataTable) throws IOException {
List<String> responseBodyLines = dataTable.asList(String.class);
for (String line : responseBodyLines) {
Assert.assertTrue(sharedContext.getResponseBody().contains(line), "Actual response body: " + sharedContext.getResponseBody());
Assert.assertTrue(sharedContext.getResponseBody().contains(line),
"Actual response body: " + sharedContext.getResponseBody());
}
}

Expand Down Expand Up @@ -141,7 +145,8 @@ public void sendHttpRequest(String httpMethod, String url, String body) throws I

// It will send request using a new thread and forget about the response
@Then("I send {string} async request to {string} with body {string}")
public void sendAsyncHttpRequest(String httpMethod, String url, String body) throws IOException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
public void sendAsyncHttpRequest(String httpMethod, String url, String body)
throws IOException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
String finalBody = Utils.resolveVariables(body, sharedContext.getValueStore());
if (sharedContext.getResponse() instanceof CloseableHttpResponse) {
((CloseableHttpResponse) sharedContext.getResponse()).close();
Expand Down Expand Up @@ -222,7 +227,7 @@ public void waitForNextMinute() throws InterruptedException {
if (secondsToWait > MAX_WAIT_FOR_NEXT_MINUTE_IN_SECONDS) {
return;
}
Thread.sleep((secondsToWait+1) * 1000);
Thread.sleep((secondsToWait + 1) * 1000);
logger.info("Current time: " + LocalDateTime.now());
}

Expand All @@ -231,7 +236,7 @@ public void waitForNextMinuteStrictly() throws InterruptedException {
LocalDateTime now = LocalDateTime.now();
LocalDateTime nextMinute = now.plusMinutes(1).withSecond(0).withNano(0);
long secondsToWait = now.until(nextMinute, ChronoUnit.SECONDS);
Thread.sleep((secondsToWait+1) * 1000);
Thread.sleep((secondsToWait + 1) * 1000);
logger.info("Current time: " + LocalDateTime.now());
}

Expand Down Expand Up @@ -261,8 +266,9 @@ public void containsHeader(String key, String value) {
return; // Any value is acceptable
}
String actualValue = header.getValue();
Assert.assertEquals(value, actualValue,"Header with key found but value mismatched.");
Assert.assertEquals(value, actualValue, "Header with key found but value mismatched.");
}

@Then("the response headers not contains key {string}")
public void notContainsHeader(String key) {
key = Utils.resolveVariables(key, sharedContext.getValueStore());
Expand All @@ -271,11 +277,12 @@ public void notContainsHeader(String key) {
Assert.fail("Response is null.");
}
Header header = response.getFirstHeader(key);
Assert.assertNull(header,"header contains in response headers");
Assert.assertNull(header, "header contains in response headers");
}

@Then("the {string} jwt should validate from JWKS {string} and contain")
public void decode_header_and_validate(String header,String jwksEndpoint, DataTable dataTable) throws MalformedURLException {
public void decode_header_and_validate(String header, String jwksEndpoint, DataTable dataTable)
throws MalformedURLException {
List<Map<String, String>> claims = dataTable.asMaps(String.class, String.class);
JsonObject jsonResponse = (JsonObject) JsonParser.parseString(sharedContext.getResponseBody());
String headerValue = jsonResponse.get("headers").getAsJsonObject().get(header).getAsString();
Expand Down Expand Up @@ -310,7 +317,7 @@ public void decode_header_and_validate(String header,String jwksEndpoint, DataTa
Assert.assertEquals(claim.get("value"), claim1.toString(), "Actual " +
"decoded JWT body: " + claimsSet);
}
} catch (BadJOSEException | JOSEException|ParseException e) {
} catch (BadJOSEException | JOSEException | ParseException e) {
logger.error("JWT Signature verification fail", e);
Assert.fail("JWT Signature verification fail");
}
Expand All @@ -321,9 +328,11 @@ public void iHaveValidSubscription() throws Exception {

Map<String, String> headers = new HashMap<>();
headers.put(Constants.REQUEST_HEADERS.HOST, Constants.DEFAULT_IDP_HOST);
headers.put(Constants.REQUEST_HEADERS.AUTHORIZATION, "Basic NDVmMWM1YzgtYTkyZS0xMWVkLWFmYTEtMDI0MmFjMTIwMDAyOjRmYmQ2MmVjLWE5MmUtMTFlZC1hZmExLTAyNDJhYzEyMDAwMg==");
headers.put(Constants.REQUEST_HEADERS.AUTHORIZATION,
"Basic NDVmMWM1YzgtYTkyZS0xMWVkLWFmYTEtMDI0MmFjMTIwMDAyOjRmYmQ2MmVjLWE5MmUtMTFlZC1hZmExLTAyNDJhYzEyMDAwMg==");

HttpResponse httpResponse = httpClient.doPost(Utils.getTokenEndpointURL(), headers, "grant_type=client_credentials&scope=" + Constants.API_CREATE_SCOPE,
HttpResponse httpResponse = httpClient.doPost(Utils.getTokenEndpointURL(), headers,
"grant_type=client_credentials&scope=" + Constants.API_CREATE_SCOPE,
Constants.CONTENT_TYPES.APPLICATION_X_WWW_FORM_URLENCODED);
sharedContext.setAccessToken(Utils.extractToken(httpResponse));
sharedContext.addStoreValue("accessToken", sharedContext.getAccessToken());
Expand All @@ -334,9 +343,11 @@ public void iHaveValidSubscriptionWithAPICreateScope() throws Exception {

Map<String, String> headers = new HashMap<>();
headers.put(Constants.REQUEST_HEADERS.HOST, Constants.DEFAULT_IDP_HOST);
headers.put(Constants.REQUEST_HEADERS.AUTHORIZATION, "Basic NDVmMWM1YzgtYTkyZS0xMWVkLWFmYTEtMDI0MmFjMTIwMDAyOjRmYmQ2MmVjLWE5MmUtMTFlZC1hZmExLTAyNDJhYzEyMDAwMg==");
headers.put(Constants.REQUEST_HEADERS.AUTHORIZATION,
"Basic NDVmMWM1YzgtYTkyZS0xMWVkLWFmYTEtMDI0MmFjMTIwMDAyOjRmYmQ2MmVjLWE5MmUtMTFlZC1hZmExLTAyNDJhYzEyMDAwMg==");

HttpResponse httpResponse = httpClient.doPost(Utils.getTokenEndpointURL(), headers, "grant_type=client_credentials",
HttpResponse httpResponse = httpClient.doPost(Utils.getTokenEndpointURL(), headers,
"grant_type=client_credentials",
Constants.CONTENT_TYPES.APPLICATION_X_WWW_FORM_URLENCODED);
sharedContext.setAccessToken(Utils.extractToken(httpResponse));
sharedContext.addStoreValue("accessToken", sharedContext.getAccessToken());
Expand All @@ -355,9 +366,28 @@ public void iHaveValidSubscriptionWithScope(DataTable dataTable) throws Exceptio
headers.put(Constants.REQUEST_HEADERS.AUTHORIZATION, Constants.SUBSCRIPTION_BASIC_AUTH_TOKEN);

HttpResponse httpResponse = httpClient.doPost(Utils.getTokenEndpointURL(), headers,
"grant_type=client_credentials&scope=" + scopes,
Constants.CONTENT_TYPES.APPLICATION_X_WWW_FORM_URLENCODED);
"grant_type=client_credentials&scope=" + scopes,
Constants.CONTENT_TYPES.APPLICATION_X_WWW_FORM_URLENCODED);
sharedContext.setAccessToken(Utils.extractToken(httpResponse));
sharedContext.addStoreValue(Constants.ACCESS_TOKEN, sharedContext.getAccessToken());
}

@Given("I have a valid subscription with groups")
public void iHaveValidSubscriptionWithGroups(DataTable dataTable) throws Exception {
List<List<String>> rows = dataTable.asLists(String.class);
String groups = Constants.EMPTY_STRING;
for (List<String> row : rows) {
String group = row.get(0);
groups += group + Constants.SPACE_STRING;
}
Map<String, String> headers = new HashMap<>();
headers.put(Constants.REQUEST_HEADERS.HOST, Constants.DEFAULT_IDP_HOST);
headers.put(Constants.REQUEST_HEADERS.AUTHORIZATION, Constants.SUBSCRIPTION_BASIC_AUTH_TOKEN);

HttpResponse httpResponse = httpClient.doPost(Utils.getTokenEndpointURL(), headers,
"grant_type=client_credentials&scope=" + Constants.API_VIEW_SCOPE + "&groups=" + groups,
Constants.CONTENT_TYPES.APPLICATION_X_WWW_FORM_URLENCODED);
sharedContext.setAccessToken(Utils.extractToken(httpResponse));
sharedContext.addStoreValue("accessToken", sharedContext.getAccessToken());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ public class Constants {
public static final String DEFAULT_TOKEN_EP = "oauth2/token";
public static final String DEFAULT_API_CONFIGURATOR = "api/configurator/1.0.0/";
public static final String DEFAULT_API_DEPLOYER = "api/deployer/1.0.0/";
public static final String DEFAULT_BACKOFFICE = "/api/backoffice/1.0.0/";
public static final String ACCESS_TOKEN = "accessToken";
public static final String EMPTY_STRING = "";
public static final String API_CREATE_SCOPE = "apk:api_create";
public static final String API_VIEW_SCOPE = "apk:api_view";
public static final String SPACE_STRING = " ";
public static final String SUBSCRIPTION_BASIC_AUTH_TOKEN =
"Basic NDVmMWM1YzgtYTkyZS0xMWVkLWFmYTEtMDI0MmFjMTIwMDAyOjRmYmQ2MmVjLWE5MmUtMTFlZC1hZmExLTAyNDJhYzEyMDAwMg==";
public static final String SUBSCRIPTION_BASIC_AUTH_TOKEN = "Basic NDVmMWM1YzgtYTkyZS0xMWVkLWFmYTEtMDI0MmFjMTIwMDAyOjRmYmQ2MmVjLWE5MmUtMTFlZC1hZmExLTAyNDJhYzEyMDAwMg==";

public class REQUEST_HEADERS {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ public static String getAPIDeployerURL() {
+ Constants.DEFAULT_API_DEPLOYER + "apis/deploy";
}

public static String getBackOfficeAPIURL() {

return "https://" + Constants.DEFAULT_API_HOST + ":" + Constants.DEFAULT_GW_PORT + "/"
+ Constants.DEFAULT_BACKOFFICE + "apis";
}

public static String getAPIUnDeployerURL() {

return "https://" + Constants.DEFAULT_API_HOST + ":" + Constants.DEFAULT_GW_PORT + "/"
Expand Down
Loading
Loading