Skip to content

Commit

Permalink
Merge pull request #188 from Sunbird-RC/create_attach_entity_role_to_…
Browse files Browse the repository at this point in the history
…user

Create entity role to user
  • Loading branch information
tejash-jl authored Dec 1, 2022
2 parents a8c233b + 06fb772 commit 98f17a2
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 96 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ java/registry/target/registry.jar: $(SOURCES)

test: build
@docker-compose down
@rm -rf db-data* || echo "no permission to delete"
# test with ES & standard definition manager
@RELEASE_VERSION=latest KEYCLOAK_IMPORT_DIR=java/apitest/src/test/resources KEYCLOAK_SECRET=a52c5f4a-89fd-40b9-aea2-3f711f14c889 DB_DIR=db-data-1 docker-compose up -d
@echo "Starting the test" && sh build/wait_for_port.sh 8080
Expand Down
85 changes: 3 additions & 82 deletions imports/realm-export.json
Original file line number Diff line number Diff line change
Expand Up @@ -468,86 +468,6 @@
"realmRoles": [],
"clientRoles": {},
"subGroups": []
},
{
"id": "5ea4e456-bf79-4a16-8903-ce6b95d9f328",
"name": "controller",
"path": "/controller",
"attributes": {},
"realmRoles": [],
"clientRoles": {},
"subGroups": []
},
{
"id": "860560da-911e-4db1-92f2-0cbbaad8f7b3",
"name": "facility admin",
"path": "/facility admin",
"attributes": {},
"realmRoles": [],
"clientRoles": {
"realm-management": [
"manage-users"
]
},
"subGroups": []
},
{
"id": "84682840-4c21-428d-b2d5-06e6298be1cc",
"name": "facility staff",
"path": "/facility staff",
"attributes": {},
"realmRoles": [],
"clientRoles": {
"realm-management": [
"view-users"
]
},
"subGroups": []
},
{
"id": "ee4cbc2d-f6a0-4424-9fcd-5dd876c2580c",
"name": "monitoring",
"path": "/monitoring",
"attributes": {},
"realmRoles": [],
"clientRoles": {},
"subGroups": []
},
{
"id": "7a859744-fae3-4d24-9f1c-4aeed92ba383",
"name": "recipient",
"path": "/recipient",
"attributes": {},
"realmRoles": [],
"clientRoles": {
"certificate-login": [
"recipient"
]
},
"subGroups": []
},
{
"id": "691ef8ed-110d-4855-84a6-7c68171e9579",
"name": "system admin",
"path": "/system admin",
"attributes": {},
"realmRoles": [],
"clientRoles": {
"account": [
"manage-account",
"view-profile"
]
},
"subGroups": []
},
{
"id": "dd163e2a-7bc6-4509-be7d-c18055bf3049",
"name": "test",
"path": "/test",
"attributes": {},
"realmRoles": [],
"clientRoles": {},
"subGroups": []
}
],
"defaultRole": {
Expand Down Expand Up @@ -612,7 +532,8 @@
],
"clientRoles": {
"realm-management": [
"manage-users"
"manage-users",
"manage-realm"
],
"admin-api": [
"api"
Expand Down Expand Up @@ -2987,4 +2908,4 @@
"clientPolicies": {
"policies": []
}
}
}
22 changes: 22 additions & 0 deletions java/apitest/src/test/java/e2e/registry/inviteFlow.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"boardSchema": {
"name": "Board",
"schema": "{\n \"$schema\": \"http://json-schema.org/draft-07/schema\",\n \"type\": \"object\",\n \"properties\": {\n \"Board\": {\n \"$ref\": \"#/definitions/Board\"\n }\n },\n \"required\": [\n \"Board\"\n ],\n \"title\":\"Board\",\n \"definitions\": {\n \"Board\": {\n \"$id\": \"#/properties/Board\",\n \"type\": \"object\",\n \"title\": \"The Board Schema\",\n \"required\": [\n\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"email\": {\n \"type\": \"string\"\n },\n \"mobile\": {\n \"type\": \"string\"\n }\n }\n }\n },\n \"_osConfig\": {\n \"osComment\": [\"This section contains the OpenSABER specific configuration information\",\n \"privateFields: Optional; list of field names to be encrypted and stored in database\",\n \"signedFields: Optional; list of field names that must be pre-signed\",\n \"indexFields: Optional; list of field names used for creating index. Enclose within braces to indicate it is a composite index. In this definition, (serialNum, studentCode) is a composite index and studentName is a single column index.\",\n \"uniqueIndexFields: Optional; list of field names used for creating unique index. Field names must be different from index field name\",\n \"systemFields: Optional; list of fields names used for system standard information like created, updated timestamps and userid\"],\n\n \"privateFields\": [\n\n ],\n \"signedFields\": [],\n \"indexFields\": [],\n \"uniqueIndexFields\": [],\n \"systemFields\": [\n \"_osCreatedAt\",\n \"_osUpdatedAt\",\n \"_osCreatedBy\",\n \"_osUpdatedBy\",\n \"_osAttestedData\",\n \"_osClaimId\",\n \"_osState\"\n ],\n \"inviteRoles\":[\"anonymous\"],\n \"enableLogin\": true,\n \"ownershipAttributes\": [\n {\n \"email\": \"/email\",\n \"mobile\": \"/mobile\",\n \"userId\": \"/mobile\"\n }\n ]\n }\n}",
"status": "PUBLISHED"
},
"boardInviteRequest": {
"name": "cbse_board",
"email": "t@mail.com",
"mobile": "123"
},
"instituteSchema": {
"name": "Institute",
"schema": "{\n \"$schema\": \"http://json-schema.org/draft-07/schema\",\n \"type\": \"object\",\n \"properties\": {\n \"Institute\": {\n \"$ref\": \"#/definitions/Institute\"\n }\n },\n \"required\": [\n \"Institute\"\n ],\n \"title\":\"Institute\",\n \"definitions\": {\n \"Institute\": {\n \"$id\": \"#/properties/Institute\",\n \"type\": \"object\",\n \"title\": \"The Institute Schema\",\n \"required\": [\n\n ],\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"email\": {\n \"type\": \"string\"\n },\n \"mobile\": {\n \"type\": \"string\"\n }\n }\n }\n },\n \"_osConfig\": {\n \"osComment\": [\"This section contains the OpenSABER specific configuration information\",\n \"privateFields: Optional; list of field names to be encrypted and stored in database\",\n \"signedFields: Optional; list of field names that must be pre-signed\",\n \"indexFields: Optional; list of field names used for creating index. Enclose within braces to indicate it is a composite index. In this definition, (serialNum, studentCode) is a composite index and studentName is a single column index.\",\n \"uniqueIndexFields: Optional; list of field names used for creating unique index. Field names must be different from index field name\",\n \"systemFields: Optional; list of fields names used for system standard information like created, updated timestamps and userid\"],\n\n \"privateFields\": [\n\n ],\n \"signedFields\": [],\n \"indexFields\": [],\n \"uniqueIndexFields\": [],\n \"systemFields\": [\n \"_osCreatedAt\",\n \"_osUpdatedAt\",\n \"_osCreatedBy\",\n \"_osUpdatedBy\",\n \"_osAttestedData\",\n \"_osClaimId\",\n \"_osState\"\n ],\n \"inviteRoles\":[\"Board\"],\n \"enableLogin\": true,\n \"ownershipAttributes\": [\n {\n \"email\": \"/email\",\n \"mobile\": \"/mobile\",\n \"userId\": \"/mobile\"\n }\n ]\n }\n}",
"status": "PUBLISHED"
},
"instituteRequest": {
"name": "test_institute",
"email": "test_institute@mail.com",
"mobile": "456"
}
}
98 changes: 96 additions & 2 deletions java/apitest/src/test/java/e2e/registry/registry.feature
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,8 @@ Feature: Registry api tests
When method get
Then status 404

@env=async
Scenario: Create a teacher schema and create teacher entity asynchronously
@env=async
Scenario: Create a teacher schema and create teacher entity asynchronously
# get admin token
* url authUrl
* path 'auth/realms/sunbird-rc/protocol/openid-connect/token'
Expand Down Expand Up @@ -220,3 +220,97 @@ Scenario: Create a teacher schema and create teacher entity asynchronously
Then status 200
* print response
And response.length == 1

Scenario: Create Board and invite institutes
# get admin token
* url authUrl
* path 'auth/realms/sunbird-rc/protocol/openid-connect/token'
* header Content-Type = 'application/x-www-form-urlencoded; charset=utf-8'
* header Host = 'keycloak:8080'
* form field grant_type = 'client_credentials'
* form field client_id = 'admin-api'
* form field client_secret = 'a52c5f4a-89fd-40b9-aea2-3f711f14c889'
* method post
* def sample = read('inviteFlow.json')
Then status 200
And print response.access_token
* def admin_token = 'Bearer ' + response.access_token
# create board schema
Given url registryUrl
And path 'api/v1/Schema'
And header Authorization = admin_token
And request sample.boardSchema
When method post
Then status 200
And response.params.status == "SUCCESSFUL"
# create institute schema
Given url registryUrl
And path 'api/v1/Schema'
And header Authorization = admin_token
And request sample.instituteSchema
When method post
Then status 200
And response.params.status == "SUCCESSFUL"
# invite institute without token should fail
Given url registryUrl
And path 'api/v1/Institute/invite'
And request sample.instituteRequest
When method post
Then status 401
# invite board
Given url registryUrl
And path 'api/v1/Board/invite'
And request sample.boardInviteRequest
When method post
Then status 200
# get board token
* url authUrl
* path 'auth/realms/sunbird-rc/protocol/openid-connect/token'
* header Content-Type = 'application/x-www-form-urlencoded; charset=utf-8'
* header Host = 'keycloak:8080'
* form field grant_type = 'password'
* form field client_id = 'registry-frontend'
* form field username = sample.boardInviteRequest.mobile
* form field password = 'abcd@123'
* method post
Then status 200
And print response.access_token
* def board_token = 'Bearer ' + response.access_token
* sleep(3000)
# get board info
Given url registryUrl
And path 'api/v1/Board'
And header Authorization = board_token
When method get
Then status 200
And response[0].osid.length > 0

# invite institute with token
Given url registryUrl
And path 'api/v1/Institute/invite'
And request sample.instituteRequest
And header Authorization = board_token
When method post
Then status 200
# get institute token
* url authUrl
* path 'auth/realms/sunbird-rc/protocol/openid-connect/token'
* header Content-Type = 'application/x-www-form-urlencoded; charset=utf-8'
* header Host = 'keycloak:8080'
* form field grant_type = 'password'
* form field client_id = 'registry-frontend'
* form field username = sample.instituteRequest.mobile
* form field password = 'abcd@123'
* method post
Then status 200
And print response.access_token
* def institute_token = 'Bearer ' + response.access_token
* sleep(3000)
# get institute info
Given url registryUrl
And path 'api/v1/Institute'
And header Authorization = institute_token
When method get
Then status 200
And response[0].osid.length > 0

5 changes: 3 additions & 2 deletions java/apitest/src/test/resources/realm-export.json
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,8 @@
],
"clientRoles": {
"realm-management": [
"manage-users"
"manage-users",
"manage-realm"
],
"admin-api": [
"api"
Expand Down Expand Up @@ -2996,4 +2997,4 @@
"clientPolicies": {
"policies": []
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@

import dev.sunbirdrc.pojos.ComponentHealthInfo;
import dev.sunbirdrc.pojos.HealthIndicator;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient43Engine;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.KeycloakBuilder;
import org.keycloak.admin.client.resource.*;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -18,6 +23,7 @@
import org.springframework.stereotype.Component;

import java.util.*;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.Response;

import static dev.sunbirdrc.registry.middleware.util.Constants.CONNECTION_FAILURE;
Expand Down Expand Up @@ -83,9 +89,8 @@ private Keycloak buildKeycloak(int httpMaxConnections) {

public String createUser(String entityName, String userName, String email, String mobile) throws OwnerCreationException {
logger.info("Creating user with mobile_number : " + userName);
String groupId = createOrUpdateRealmGroup(entityName);
UserRepresentation newUser = createUserRepresentation(entityName, userName, email, mobile);
GroupRepresentation entityGroup = createGroupRepresentation(entityName);
keycloak.realm(realm).groups().add(entityGroup);
UsersResource usersResource = keycloak.realm(realm).users();
try (Response response = usersResource.create(newUser)) {
if (response.getStatus() == 201) {
Expand All @@ -98,7 +103,7 @@ public String createUser(String entityName, String userName, String email, Strin
return userID;
} else if (response.getStatus() == 409) {
logger.info("UserID: {} exists", userName);
return updateExistingUserAttributes(entityName, userName, email, mobile);
return updateExistingUserAttributes(entityName, userName, email, mobile, groupId);
} else if (response.getStatus() == 500) {
throw new OwnerCreationException("Keycloak user creation error");
} else {
Expand All @@ -107,13 +112,40 @@ public String createUser(String entityName, String userName, String email, Strin
}
}

private GroupRepresentation createGroupRepresentation(String entityName) {
private String createOrUpdateRealmGroup(String entityName) {
RoleRepresentation roleRepresentation = createOrGetRealmRole(entityName);
GroupsResource groupsResource = keycloak.realm(realm).groups();
GroupRepresentation groupRepresentation = new GroupRepresentation();
groupRepresentation.setName(entityName);
return groupRepresentation;
Response groupAddResponse = groupsResource.add(groupRepresentation);
String groupId = "";
if (groupAddResponse.getStatus() == 409) {
Optional<GroupRepresentation> groupRepresentationOptional = groupsResource.groups().stream().filter(gp -> gp.getName().equalsIgnoreCase(entityName)).findFirst();
if (groupRepresentationOptional.isPresent()) {
groupId = groupRepresentationOptional.get().getId();
}
} else {
groupId = groupAddResponse.getLocation().getPath().replaceAll(".*/([^/]+)$", "$1");
}
groupsResource.group(groupId)
.roles().realmLevel().add(Collections.singletonList(roleRepresentation));
return groupId;
}

private String updateExistingUserAttributes(String entityName, String userName, String email, String mobile) throws OwnerCreationException {
private RoleRepresentation createOrGetRealmRole(String entityName) {
RolesResource rolesResource = keycloak.realm(realm).roles();
try {
RoleRepresentation roleRepresentation = new RoleRepresentation();
roleRepresentation.setName(entityName);
rolesResource.create(roleRepresentation);
} catch (Exception e){
logger.error("Role creation exception", e);
}
return rolesResource.get(entityName).toRepresentation();
}

private String updateExistingUserAttributes(String entityName, String userName, String email, String mobile,
String groupId) throws OwnerCreationException {
Optional<UserResource> userRepresentationOptional = getUserByUsername(userName);
if (userRepresentationOptional.isPresent()) {
UserResource userResource = userRepresentationOptional.get();
Expand All @@ -128,6 +160,7 @@ private String updateExistingUserAttributes(String entityName, String userName,
userRepresentation.setGroups(groups);
}
userResource.update(userRepresentation);
userResource.joinGroup(groupId);
return userRepresentation.getId();
} else {
logger.error("Failed fetching user by username: {}", userName);
Expand All @@ -154,16 +187,20 @@ private UserRepresentation createUserRepresentation(String entityName, String us
}

private void updateUserAttributes(String entityName, String email, String mobile, UserRepresentation userRepresentation) {
List<String> entities = userRepresentation.getAttributes().getOrDefault(ENTITY, Collections.emptyList());
if (!entities.contains(entityName)) {
entities.add(entityName);
if (userRepresentation.getAttributes() == null || !userRepresentation.getAttributes().containsKey(ENTITY)) {
userRepresentation.singleAttribute(ENTITY, entityName);
} else {
List<String> entities = userRepresentation.getAttributes().getOrDefault(ENTITY, Collections.emptyList());
if (!entities.contains(entityName)) {
entities.add(entityName);
}
}
addAttributeIfNotExists(userRepresentation, EMAIL, email);
addAttributeIfNotExists(userRepresentation, MOBILE_NUMBER, mobile);
}

private void addAttributeIfNotExists(UserRepresentation userRepresentation, String key, String value) {
if (!userRepresentation.getAttributes().containsKey(key)) {
if (userRepresentation.getAttributes() == null || !userRepresentation.getAttributes().containsKey(key)) {
userRepresentation.singleAttribute(key, value);
}
}
Expand Down

0 comments on commit 98f17a2

Please sign in to comment.