diff --git a/.env b/.env index 060496679..bd676a9b5 100644 --- a/.env +++ b/.env @@ -1,3 +1 @@ RELEASE_VERSION=v0.0.14 -VIEW_DIR=java/apitest/src/test/resources/views -SCHEMA_DIR=java/apitest/src/test/resources/schemas diff --git a/Makefile b/Makefile index 3fb895d95..bc60c3ab2 100644 --- a/Makefile +++ b/Makefile @@ -50,6 +50,19 @@ test: build @cd java/apitest && MODE=async ../mvnw -Pe2e test || echo 'Tests failed' @docker-compose down @rm -rf db-data-2 || echo "no permission to delete" + # test with fusionauth + @RELEASE_VERSION=latest DB_DIR=db-data-7 SEARCH_PROVIDER_NAME=dev.sunbirdrc.registry.service.NativeSearchService FUSION_WRAPPER_BUILD=services/sample-fusionauth-service/ FUSIONAUTH_ISSUER_URL=http://fusionauth:9011/ oauth2_resource_uri=http://fusionauth:9011/ oauth2_resource_roles_path=roles identity_provider=dev.sunbirdrc.auth.genericiam.AuthProviderImpl sunbird_sso_url=http://fusionauthwrapper:3990/fusionauth/api/v1/user IMPORTS_DIR=services/sample-fusionauth-service/imports docker-compose -f docker-compose.yml -f services/sample-fusionauth-service/docker-compose.yml up -d db es fusionauth fusionauthwrapper + sleep 20 + @echo "Starting the test" && sh build/wait_for_port.sh 9011 + @echo "Starting the test" && sh build/wait_for_port.sh 3990 + sleep 20 + @RELEASE_VERSION=latest DB_DIR=db-data-7 SEARCH_PROVIDER_NAME=dev.sunbirdrc.registry.service.NativeSearchService FUSION_WRAPPER_BUILD=services/sample-fusionauth-service/ FUSIONAUTH_ISSUER_URL=http://fusionauth:9011/ oauth2_resource_uri=http://fusionauth:9011/ oauth2_resource_roles_path=roles identity_provider=dev.sunbirdrc.auth.genericiam.AuthProviderImpl sunbird_sso_url=http://fusionauthwrapper:3990/fusionauth/api/v1/user IMPORTS_DIR=services/sample-fusionauth-service/imports docker-compose -f docker-compose.yml -f services/sample-fusionauth-service/docker-compose.yml up -d --no-deps registry + @echo "Starting the test" && sh build/wait_for_port.sh 8081 + @docker-compose -f docker-compose.yml -f services/sample-fusionauth-service/docker-compose.yml ps + @curl -v http://localhost:8081/health + @cd java/apitest && MODE=fusionauth ../mvnw -Pe2e test || echo 'Tests failed' + @docker-compose -f docker-compose.yml -f services/sample-fusionauth-service/docker-compose.yml down + @rm -rf db-data-7 || echo "no permission to delete" make -C services/certificate-signer test make -C services/public-key-service test make -C services/context-proxy-service test diff --git a/docker-compose.yml b/docker-compose.yml index 8b49b3986..934e4cce5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -50,7 +50,10 @@ services: - elastic_search_password=${ELASTIC_SEARCH_PASSWORD} - search_providerName=${SEARCH_PROVIDER_NAME-dev.sunbirdrc.registry.service.ElasticSearchService} - sunbird_sso_realm=${KEYCLOAK_REALM-sunbird-rc} - - sunbird_sso_url=http://keycloak:8080/auth + - sunbird_sso_url=${sunbird_sso_url-http://keycloak:8080/auth} + - oauth2_resource_uri=${oauth2_resource_uri-http://keycloak:8080/auth/realms/sunbird-rc} + - oauth2_resource_roles_path=${oauth2_resource_roles_path-realm_access.roles} + - identity_provider=${identity_provider-dev.sunbirdrc.auth.keycloak.KeycloakProviderImpl} - sunbird_sso_admin_client_id=${KEYCLOAK_ADMIN_CLIENT_ID-admin-api} - sunbird_sso_client_id=${KEYCLOAK_CLIENT_ID-registry-frontend} - sunbird_sso_admin_client_secret=${KEYCLOAK_SECRET} @@ -89,6 +92,8 @@ services: condition: service_healthy db: condition: service_healthy + keycloak: + condition: service_healthy healthcheck: test: [ "CMD-SHELL", "wget -nv -t1 --spider http://localhost:8081/health || exit 1" ] interval: 30s diff --git a/java/apitest/src/test/java/e2e/registry/StudentWithPasswordRequest.json b/java/apitest/src/test/java/e2e/registry/StudentWithPasswordRequest.json index a41374e92..2af9e514f 100644 --- a/java/apitest/src/test/java/e2e/registry/StudentWithPasswordRequest.json +++ b/java/apitest/src/test/java/e2e/registry/StudentWithPasswordRequest.json @@ -1,9 +1,9 @@ { "userDetails": { - "passkey": "abcd" + "passkey": "abcd@12345" }, "contactDetails": { "mobile": "9876543210", "email": "abcd@gmail.com" } -} \ No newline at end of file +} diff --git a/java/apitest/src/test/java/e2e/registry/fusionauth-registry.feature b/java/apitest/src/test/java/e2e/registry/fusionauth-registry.feature new file mode 100644 index 000000000..1bea1f399 --- /dev/null +++ b/java/apitest/src/test/java/e2e/registry/fusionauth-registry.feature @@ -0,0 +1,60 @@ +Feature: Registry api tests + Background: + * string registryUrl = "http://localhost:8081" + * string authUrl = "http://localhost:9011" + * url registryUrl + * def admin_token = "" + * def client_secret = 'a52c5f4a-89fd-40b9-aea2-3f711f14c889' + * def sleep = function(millis){ java.lang.Thread.sleep(millis) } + @env=fusionauth + Scenario: Create student with password schema and verify if password is set + # get admin token + * url authUrl + * path '/oauth2/token' + * header Content-Type = 'application/x-www-form-urlencoded; charset=utf-8' + * header Host = 'fusionauth:9011' + * form field grant_type = 'password' + * form field client_id = '85a03867-dccf-4882-adde-1a79aeec50df' + * form field username = 'admin@sunbirdrc.dev' + * form field password = 'admin@12345' + * method post + Then status 200 + And print response.access_token + * def admin_token = 'Bearer ' + response.access_token + # create student schema + Given url registryUrl + And path 'api/v1/Schema' + And header Authorization = admin_token + And request read('StudentWithPasswordSchemaRequest.json') + When method post + Then status 200 + And response.params.status == "SUCCESSFUL" + # invite entity for student + Given url registryUrl + And path 'api/v1/StudentWithPassword/invite' + * def studentRequest = read('StudentWithPasswordRequest.json') + And request studentRequest + When method post + Then status 200 + * def studentOsid = response.result.StudentWithPassword.osid + # get student token + * url authUrl + * path '/oauth2/token' + * header Content-Type = 'application/x-www-form-urlencoded; charset=utf-8' + * header Host = 'fusionauth:9011' + * form field grant_type = 'password' + * form field client_id = '85a03867-dccf-4882-adde-1a79aeec50df' + * form field username = studentRequest.contactDetails.mobile + * form field password = studentRequest.userDetails.passkey + * method post + Then status 200 + And print response.access_token + * def student_token = 'Bearer ' + response.access_token + * sleep(3000) + # get student info + Given url registryUrl + And path 'api/v1/StudentWithPassword/' + And header Authorization = student_token + When method get + Then status 200 + And response[0].osid.length > 0 diff --git a/java/middleware/pom.xml b/java/middleware/pom.xml index c0b099501..2edfd5f10 100644 --- a/java/middleware/pom.xml +++ b/java/middleware/pom.xml @@ -1,4 +1,6 @@ - + 4.0.0 diff --git a/java/middleware/registry-middleware/auth0/pom.xml b/java/middleware/registry-middleware/auth0/pom.xml new file mode 100644 index 000000000..b36dd6ff2 --- /dev/null +++ b/java/middleware/registry-middleware/auth0/pom.xml @@ -0,0 +1,76 @@ + + + + registry-middleware + dev.sunbirdrc + 2.0.3 + + 4.0.0 + + auth0 + + + + + + + + + com.auth0 + auth0 + 2.3.0 + + + org.springframework + spring-context + 5.0.2.RELEASE + + + org.springframework + spring-web + 5.0.2.RELEASE + + + org.slf4j + slf4j-api + 1.7.32 + compile + + + org.springframework.boot + spring-boot-starter-test + test + 2.5.0 + + + junit + junit + 4.12 + test + + + org.springframework + spring-test + 5.3.9 + test + + + dev.sunbirdrc + pojos + 2.0.3 + + + dev.sunbirdrc + middleware-commons + 2.0.3 + + + dev.sunbirdrc + identity-provider + 2.0.3 + + + + diff --git a/java/middleware/registry-middleware/auth0/src/main/java/dev/sunbirdrc/auth/auth0/Auth0AdminUtil.java b/java/middleware/registry-middleware/auth0/src/main/java/dev/sunbirdrc/auth/auth0/Auth0AdminUtil.java new file mode 100644 index 000000000..83bfee83e --- /dev/null +++ b/java/middleware/registry-middleware/auth0/src/main/java/dev/sunbirdrc/auth/auth0/Auth0AdminUtil.java @@ -0,0 +1,200 @@ +package dev.sunbirdrc.auth.auth0; + +import com.auth0.client.auth.AuthAPI; +import com.auth0.client.mgmt.ManagementAPI; +import com.auth0.client.mgmt.filter.RolesFilter; +import com.auth0.client.mgmt.filter.UserFilter; +import com.auth0.exception.Auth0Exception; +import com.auth0.json.auth.TokenHolder; +import com.auth0.json.mgmt.permissions.Permission; +import com.auth0.json.mgmt.resourceserver.ResourceServer; +import com.auth0.json.mgmt.resourceserver.Scope; +import com.auth0.json.mgmt.roles.Role; +import com.auth0.json.mgmt.roles.RolesPage; +import com.auth0.json.mgmt.users.User; +import com.auth0.json.mgmt.users.UsersPage; +import com.auth0.net.TokenRequest; +import dev.sunbirdrc.pojos.ComponentHealthInfo; +import dev.sunbirdrc.registry.identity_providers.pojos.IdentityProviderConfiguration; +import org.apache.logging.log4j.util.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import dev.sunbirdrc.registry.identity_providers.pojos.CreateUserRequest; +import dev.sunbirdrc.registry.identity_providers.pojos.IdentityException; +import dev.sunbirdrc.registry.identity_providers.pojos.IdentityManager; +import org.springframework.beans.factory.annotation.Value; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import static dev.sunbirdrc.registry.middleware.util.Constants.CONNECTION_FAILURE; + + +public class Auth0AdminUtil implements IdentityManager { + private static final Logger logger = LoggerFactory.getLogger(Auth0AdminUtil.class); + private static final String EMAIL = "email_id"; + private static final String ENTITY = "entity"; + private static final String MOBILE_NUMBER = "mobile_number"; + private static final String PASSWORD = "password"; + public static final String SUNBIRD_AUTH0_SERVICE_NAME = "sunbird.auth0.service"; + private final IdentityProviderConfiguration identityProviderConfiguration; + + @Value("${auth0.resource.server.id}") + private String resourceServerId; + + @Value("${auth0.resource.user.connection:Username-Password-Authentication}") + private String userConnection; + + + public Auth0AdminUtil(IdentityProviderConfiguration identityProviderConfiguration) { + this.identityProviderConfiguration = identityProviderConfiguration; + } + + + @Override + public String createUser(CreateUserRequest createUserRequest) throws IdentityException { + logger.info("Creating user with mobile_number : " + createUserRequest.getUserName()); + try { + String token = getToken(); + ManagementAPI mgmt = ManagementAPI.newBuilder(identityProviderConfiguration.getRealm(), token).build(); + Optional roleOptional = createOrGetRole(mgmt, createUserRequest); + if (roleOptional.isPresent()) { + Optional userOptional = createOrGetUser(mgmt, createUserRequest, roleOptional.get()); + if (userOptional.isPresent()) { + return userOptional.get(); + } else { + throw new IdentityException("Creating user failed"); + } + } + } catch (Exception e) { + logger.error("Error creating user in auth0", e); + throw new IdentityException(String.format("Auth0 user creation error %s", e.getMessage())); + } + return ""; + } + + private Optional createOrGetUser(ManagementAPI mgmt, CreateUserRequest createUserRequest, String roleId) throws Auth0Exception { + try { + User userObject = createUserObject(createUserRequest); + User userRepresentation = mgmt.users().create(userObject).execute().getBody(); + assignUserRole(mgmt, roleId, userRepresentation.getId()); + return Optional.ofNullable(userRepresentation.getId()); + } catch (Exception e) { + logger.error("User creation exception, {}", e.getMessage()); + } + UsersPage usersPage = searchUser(mgmt, createUserRequest); + if (usersPage.getItems().size() > 0) { + User existingUser = usersPage.getItems().get(0); + assignUserRole(mgmt, roleId, existingUser.getId()); + return Optional.ofNullable(existingUser.getId()); + } + return Optional.empty(); + } + + private void assignUserRole(ManagementAPI mgmt, String roleId, String userId) throws Auth0Exception { + mgmt.roles().assignUsers(roleId, Collections.singletonList(userId)).execute(); + } + + private UsersPage searchUser(ManagementAPI mgmt, CreateUserRequest createUserRequest) throws Auth0Exception { + UserFilter userFilter = new UserFilter(); + userFilter.withQuery(createUserRequest.getUserName()); + return mgmt.users().list(userFilter).execute().getBody(); + } + + private Optional createOrGetRole(ManagementAPI mgmt, CreateUserRequest createUserRequest) throws Auth0Exception { + String role = createUserRequest.getEntity(); + try { + Role roleRepresentation = createRole(mgmt, role); + addPermission(mgmt, role, roleRepresentation); + return Optional.ofNullable(roleRepresentation.getId()); + } catch (Exception e) { + logger.error("Role creation exception, {}", e.getMessage()); + } + RolesPage rolesPage = searchExistingRoles(mgmt, role); + if (rolesPage.getItems().size() > 0) { + Role existingRoleRepresentation = rolesPage.getItems().get(0); + addPermission(mgmt, role, existingRoleRepresentation); + return Optional.ofNullable(existingRoleRepresentation.getId()); + } + return Optional.empty(); + } + + private void addScopeToResourceServer(ManagementAPI mgmt, String role) { + try { + ResourceServer resourceServer = new ResourceServer(); + resourceServer.setScopes(Collections.singletonList(new Scope(role))); + mgmt.resourceServers().update(resourceServerId, resourceServer).execute(); + } catch (Exception e) { + logger.error("Error adding scopes to resource server, {}", e.getMessage()); + } + } + + private RolesPage searchExistingRoles(ManagementAPI mgmt, String role) throws Auth0Exception { + RolesFilter filter = new RolesFilter(); + filter.withName(role); + return mgmt.roles().list(filter).execute().getBody(); + } + + private Role createRole(ManagementAPI mgmt, String role) throws Auth0Exception { + Role roleRepresentation = new Role(); + roleRepresentation.setName(role); + roleRepresentation = mgmt.roles().create(roleRepresentation).execute().getBody(); + return roleRepresentation; + } + + private void addPermission(ManagementAPI mgmt, String role, Role roleRepresentation) throws Auth0Exception { + addScopeToResourceServer(mgmt, role); + Permission permission = new Permission(); + permission.setName(role); + permission.setResourceServerId(resourceServerId); + mgmt.roles().addPermissions(roleRepresentation.getId(), Collections.singletonList(permission)).execute(); + } + + private User createUserObject(CreateUserRequest createUserRequest) { + User user = new User(); + user.setEmail(createUserRequest.getEmail()); +// user.setPhoneNumber(createUserRequest.getMobile()); + user.setName(createUserRequest.getUserName()); + Map userMetadata = new HashMap<>(); + userMetadata.put(ENTITY, Collections.singletonList(createUserRequest.getEntity())); + userMetadata.put(EMAIL, createUserRequest.getEmail()); + userMetadata.put(MOBILE_NUMBER, createUserRequest.getMobile()); + user.setUserMetadata(userMetadata); + user.setConnection("Username-Password-Authentication"); + if (!Strings.isBlank(createUserRequest.getPassword())) { + user.setPassword(createUserRequest.getPassword().toCharArray()); + } + return user; + } + + //TODO: cache token + public String getToken() throws Auth0Exception { + AuthAPI authAPI = AuthAPI.newBuilder(identityProviderConfiguration.getRealm(), + identityProviderConfiguration.getClientId(), identityProviderConfiguration.getClientSecret()).build(); + TokenRequest tokenRequest = authAPI.requestToken(identityProviderConfiguration.getUrl()); + TokenHolder holder = tokenRequest.execute().getBody(); + return holder.getAccessToken(); + + } + + @Override + public String getServiceName() { + return SUNBIRD_AUTH0_SERVICE_NAME; + } + + @Override + public ComponentHealthInfo getHealthInfo() { + if (identityProviderConfiguration.isAuthenticationEnabled()) { + try { + //TODO: check auth0 status + return new ComponentHealthInfo(getServiceName(), true); + } catch (Exception e) { + return new ComponentHealthInfo(getServiceName(), false, CONNECTION_FAILURE, e.getMessage()); + } + } else { + return new ComponentHealthInfo(getServiceName(), true, "AUTHENTICATION_ENABLED", "false"); + } + } +} diff --git a/java/middleware/registry-middleware/auth0/src/main/java/dev/sunbirdrc/auth/auth0/Auth0ProviderImpl.java b/java/middleware/registry-middleware/auth0/src/main/java/dev/sunbirdrc/auth/auth0/Auth0ProviderImpl.java new file mode 100644 index 000000000..dda5d5d2c --- /dev/null +++ b/java/middleware/registry-middleware/auth0/src/main/java/dev/sunbirdrc/auth/auth0/Auth0ProviderImpl.java @@ -0,0 +1,12 @@ +package dev.sunbirdrc.auth.auth0; + +import dev.sunbirdrc.registry.identity_providers.pojos.IdentityManager; +import dev.sunbirdrc.registry.identity_providers.pojos.IdentityProviderConfiguration; +import dev.sunbirdrc.registry.identity_providers.providers.IdentityProvider; + +public class Auth0ProviderImpl implements IdentityProvider { + @Override + public IdentityManager createManager(IdentityProviderConfiguration identityProviderConfiguration) { + return new Auth0AdminUtil(identityProviderConfiguration); + } +} diff --git a/java/middleware/registry-middleware/auth0/src/main/resources/META-INF/services/dev.sunbirdrc.registry.identity_providers.providers.IdentityProvider b/java/middleware/registry-middleware/auth0/src/main/resources/META-INF/services/dev.sunbirdrc.registry.identity_providers.providers.IdentityProvider new file mode 100644 index 000000000..0baa94cb6 --- /dev/null +++ b/java/middleware/registry-middleware/auth0/src/main/resources/META-INF/services/dev.sunbirdrc.registry.identity_providers.providers.IdentityProvider @@ -0,0 +1 @@ +dev.sunbirdrc.auth.auth0.Auth0ProviderImpl diff --git a/java/middleware/registry-middleware/authorization/pom.xml b/java/middleware/registry-middleware/authorization/pom.xml index 6273ffb35..65431a960 100644 --- a/java/middleware/registry-middleware/authorization/pom.xml +++ b/java/middleware/registry-middleware/authorization/pom.xml @@ -20,12 +20,12 @@ org.springframework.boot spring-boot-starter-web - 2.0.1.RELEASE + 2.3.12.RELEASE - org.springframework.security - spring-security-core - 5.0.12.RELEASE + org.springframework.boot + spring-boot-starter-security + 2.3.12.RELEASE io.jsonwebtoken @@ -96,59 +96,37 @@ 2.0.0.RELEASE test + + org.springframework.security.oauth.boot + spring-security-oauth2-autoconfigure + 2.3.1.RELEASE + + + org.springframework.boot + spring-boot-configuration-processor + 2.3.12.RELEASE + true + + + org.springframework.security + spring-security-oauth2-resource-server + 5.3.9.RELEASE + + + org.springframework.security + spring-security-oauth2-jose + 5.3.9.RELEASE + + + jakarta.validation + jakarta.validation-api + 2.0.2 + - - org.jacoco - jacoco-maven-plugin - ${jacoco.version} - - - prepare-agent - - prepare-agent - - - - report - prepare-package - - report - - - - post-unit-test - test - - report - - - - target/jacoco.exec - - target/jacoco-ut - - - - - - target/jacoco.exec - - - - - org.apache.maven.plugins - maven-surefire-plugin - - - ${runSuite} - - 0 - - - \ No newline at end of file + diff --git a/java/middleware/registry-middleware/authorization/src/main/java/dev/sunbirdrc/registry/authorization/CustomJwtAuthenticationConverter.java b/java/middleware/registry-middleware/authorization/src/main/java/dev/sunbirdrc/registry/authorization/CustomJwtAuthenticationConverter.java new file mode 100644 index 000000000..9f53e7b35 --- /dev/null +++ b/java/middleware/registry-middleware/authorization/src/main/java/dev/sunbirdrc/registry/authorization/CustomJwtAuthenticationConverter.java @@ -0,0 +1,65 @@ +package dev.sunbirdrc.registry.authorization; + +import com.jayway.jsonpath.DocumentContext; +import com.jayway.jsonpath.JsonPath; +import dev.sunbirdrc.registry.authorization.pojos.UserToken; +import dev.sunbirdrc.registry.authorization.pojos.OAuth2Properties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.*; +import java.util.stream.Collectors; + +class CustomJwtAuthenticationConverter implements Converter { + private static final Logger logger = LoggerFactory.getLogger(CustomJwtAuthenticationConverter.class); + + private final OAuth2Properties oAuth2Properties; + + public CustomJwtAuthenticationConverter(OAuth2Properties oAuth2Properties) { + this.oAuth2Properties = oAuth2Properties; + } + + @Override + public AbstractAuthenticationToken convert(Jwt source) { + try { + DocumentContext documentContext = JsonPath.parse(source.getClaims()); + List roles = getValue(documentContext, oAuth2Properties.getRolesPath(), ArrayList.class); + String email = getValue(documentContext, oAuth2Properties.getEmailPath(), String.class); + Map consentFields = getValue(documentContext, oAuth2Properties.getConsentPath(), Map.class); + List entities = getValue(documentContext, oAuth2Properties.getEntityPath(), ArrayList.class); + String userId = getValue(documentContext, oAuth2Properties.getUserIdPath(), String.class); + return new UserToken(source, userId, email, consentFields, entities, + roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList())); + } catch (Exception e) { + logger.error("Error while extracting claim", e); + return new UserToken(source, "", "", Collections.emptyMap(), Collections.emptyList(), Collections.emptyList()); + } + + } + + private T getValue(DocumentContext documentContext, String path, Class type) { + try { + T value = documentContext.read(path, type); + return type.cast(value); + } catch (Exception e) { + logger.debug("Fetching {} from token claims failed", path, e); + } + return getDefaultValue(type); + } + + private T getDefaultValue(Class type) { + if (String.class.equals(type)) { + return (T) ""; + } else if (Map.class.equals(type)) { + return (T) Collections.emptyMap(); + } else if (ArrayList.class.equals(type)) { + return (T) Collections.emptyList(); + } + return null; + } + +} diff --git a/java/middleware/registry-middleware/authorization/src/main/java/dev/sunbirdrc/registry/authorization/SchemaAuthFilter.java b/java/middleware/registry-middleware/authorization/src/main/java/dev/sunbirdrc/registry/authorization/SchemaAuthFilter.java new file mode 100644 index 000000000..beb58966f --- /dev/null +++ b/java/middleware/registry-middleware/authorization/src/main/java/dev/sunbirdrc/registry/authorization/SchemaAuthFilter.java @@ -0,0 +1,60 @@ +package dev.sunbirdrc.registry.authorization; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class SchemaAuthFilter implements Filter { + private static final Logger logger = LoggerFactory.getLogger(SchemaAuthFilter.class); + private static final String INVITE_URL_ENDPOINT = "/invite"; + + private final Set anonymousInviteSchemas = new HashSet<>(); + private final Set anonymousSchemas = new HashSet<>(); + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws ServletException, IOException { + HttpServletRequest request = (HttpServletRequest) servletRequest; + try { + if (request.getRequestURI().contains(INVITE_URL_ENDPOINT) && + anonymousInviteSchemas.stream().anyMatch(request.getRequestURI()::contains)) { + servletRequest.getRequestDispatcher(((HttpServletRequest) servletRequest).getServletPath()).forward(servletRequest, servletResponse); + return; + } else if (!request.getRequestURI().contains(INVITE_URL_ENDPOINT) && anonymousSchemas.stream().anyMatch(request.getRequestURI()::contains)) { + servletRequest.getRequestDispatcher(((HttpServletRequest) servletRequest).getServletPath()).forward(servletRequest, servletResponse); + return; + } + filterChain.doFilter(servletRequest, servletResponse); + } catch (Exception e) { + logger.error("Exception while applying security filters: ", e); + throw e; + } + } + + public void appendAnonymousInviteSchema(String schema) { + anonymousInviteSchemas.add(schema); + } + + public void appendAnonymousSchema(String schema) { + anonymousSchemas.add(schema); + } + + + public void appendAnonymousInviteSchema(List entitiesWithAnonymousInviteRoles) { + anonymousInviteSchemas.addAll(entitiesWithAnonymousInviteRoles); + } + + public void appendAnonymousSchema(List entitiesWithAnonymousManageRoles) { + anonymousSchemas.addAll(entitiesWithAnonymousManageRoles); + } + + public void removeSchema(String schema) { + anonymousSchemas.remove(schema); + anonymousInviteSchemas.remove(schema); + } +} diff --git a/java/middleware/registry-middleware/authorization/src/main/java/dev/sunbirdrc/registry/authorization/SecurityConfig.java b/java/middleware/registry-middleware/authorization/src/main/java/dev/sunbirdrc/registry/authorization/SecurityConfig.java new file mode 100644 index 000000000..121a79d79 --- /dev/null +++ b/java/middleware/registry-middleware/authorization/src/main/java/dev/sunbirdrc/registry/authorization/SecurityConfig.java @@ -0,0 +1,71 @@ +package dev.sunbirdrc.registry.authorization; + +import dev.sunbirdrc.registry.authorization.pojos.OAuth2Configuration; +import dev.sunbirdrc.registry.authorization.pojos.OAuth2Resources; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.oauth2.jwt.JwtDecoders; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; +import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver; +import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +@EnableWebSecurity +@ConditionalOnProperty(name = "authentication.enabled",havingValue = "true",matchIfMissing = false) +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Value("${authentication.enabled:true}") + boolean authenticationEnabled; + + @Autowired + private OAuth2Configuration oAuth2Configuration; + + @Autowired + private SchemaAuthFilter schemaAuthFilter; + + @Override + protected void configure(HttpSecurity http) throws Exception { + HttpSecurity httpConfig = http.csrf().disable(); + if (authenticationEnabled) { + Map authenticationManagers = new HashMap<>(); + this.oAuth2Configuration.getResources().forEach(issuer -> addManager(authenticationManagers, issuer)); + httpConfig + .addFilterBefore(schemaAuthFilter, WebAsyncManagerIntegrationFilter.class) + .authorizeRequests(auth -> auth + .antMatchers("/**/invite", "/health", "/error", + "/_schemas/**", "/**/templates/**", "/**/*.json", "/**/verify", + "/swagger-ui", "/**/search", "/**/attestation/**", + "/api/docs/swagger.json", "/api/docs/*.json", "/plugin/**", "/swagger-ui.html") + .permitAll() + ) + .authorizeRequests(auth -> auth + .anyRequest() + .authenticated()) + .oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer + .authenticationManagerResolver(new JwtIssuerAuthenticationManagerResolver(authenticationManagers::get))); + } else { + httpConfig.authorizeRequests(auth -> auth + .anyRequest() + .permitAll() + ); + } + + } + + private void addManager(Map authenticationManagers, OAuth2Resources issuer) { + JwtAuthenticationProvider authenticationProvider = new JwtAuthenticationProvider(JwtDecoders.fromOidcIssuerLocation(issuer.getUri())); + authenticationProvider.setJwtAuthenticationConverter(new CustomJwtAuthenticationConverter(issuer.getProperties())); + authenticationManagers.put(issuer.getUri(), authenticationProvider::authenticate); + } + +} + diff --git a/java/middleware/registry-middleware/authorization/src/main/java/dev/sunbirdrc/registry/authorization/pojos/OAuth2Configuration.java b/java/middleware/registry-middleware/authorization/src/main/java/dev/sunbirdrc/registry/authorization/pojos/OAuth2Configuration.java new file mode 100644 index 000000000..4bdca4aa4 --- /dev/null +++ b/java/middleware/registry-middleware/authorization/src/main/java/dev/sunbirdrc/registry/authorization/pojos/OAuth2Configuration.java @@ -0,0 +1,19 @@ +package dev.sunbirdrc.registry.authorization.pojos; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +import java.util.List; + +@Validated +@Configuration +@ConfigurationProperties(prefix = "oauth2") +@Component +@Data +public class OAuth2Configuration { + List resources; + +} diff --git a/java/middleware/registry-middleware/authorization/src/main/java/dev/sunbirdrc/registry/authorization/pojos/OAuth2Properties.java b/java/middleware/registry-middleware/authorization/src/main/java/dev/sunbirdrc/registry/authorization/pojos/OAuth2Properties.java new file mode 100644 index 000000000..cca41a580 --- /dev/null +++ b/java/middleware/registry-middleware/authorization/src/main/java/dev/sunbirdrc/registry/authorization/pojos/OAuth2Properties.java @@ -0,0 +1,13 @@ +package dev.sunbirdrc.registry.authorization.pojos; + + +import lombok.Data; + +@Data +public class OAuth2Properties { + private String rolesPath = "realm_access.roles"; + private String emailPath = "email"; + private String consentPath = "consent"; + private String entityPath = "entity"; + private String userIdPath = "sub"; +} diff --git a/java/middleware/registry-middleware/authorization/src/main/java/dev/sunbirdrc/registry/authorization/pojos/OAuth2Resources.java b/java/middleware/registry-middleware/authorization/src/main/java/dev/sunbirdrc/registry/authorization/pojos/OAuth2Resources.java new file mode 100644 index 000000000..5ba009ebe --- /dev/null +++ b/java/middleware/registry-middleware/authorization/src/main/java/dev/sunbirdrc/registry/authorization/pojos/OAuth2Resources.java @@ -0,0 +1,13 @@ +package dev.sunbirdrc.registry.authorization.pojos; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +@Data +public class OAuth2Resources { + @NotBlank + private String uri; + private OAuth2Properties properties = new OAuth2Properties(); + +} diff --git a/java/middleware/registry-middleware/authorization/src/main/java/dev/sunbirdrc/registry/authorization/pojos/UserToken.java b/java/middleware/registry-middleware/authorization/src/main/java/dev/sunbirdrc/registry/authorization/pojos/UserToken.java new file mode 100644 index 000000000..7a8591a96 --- /dev/null +++ b/java/middleware/registry-middleware/authorization/src/main/java/dev/sunbirdrc/registry/authorization/pojos/UserToken.java @@ -0,0 +1,40 @@ +package dev.sunbirdrc.registry.authorization.pojos; + +import lombok.Getter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.security.Principal; +import java.util.List; +import java.util.Map; + +@Getter +public class UserToken extends AbstractAuthenticationToken { + private final List entities; + private final Jwt source; + private String userId; + private final String email; + + private final Map consentFields; + + public UserToken(Jwt source, String userId, String email, Map consentFields, List entities, List authorities) { + super(authorities); + this.source = source; + this.userId = userId; + this.email = email; + this.consentFields = consentFields; + this.entities = entities; + this.setAuthenticated(true); + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return (Principal) this.source::getId; + } +} diff --git a/java/middleware/registry-middleware/authorization/src/test/java/dev/sunbirdrc/registry/authorization/CustomJwtAuthenticationConverterTest.java b/java/middleware/registry-middleware/authorization/src/test/java/dev/sunbirdrc/registry/authorization/CustomJwtAuthenticationConverterTest.java new file mode 100644 index 000000000..9e18849f5 --- /dev/null +++ b/java/middleware/registry-middleware/authorization/src/test/java/dev/sunbirdrc/registry/authorization/CustomJwtAuthenticationConverterTest.java @@ -0,0 +1,77 @@ +package dev.sunbirdrc.registry.authorization; + +import dev.sunbirdrc.registry.authorization.pojos.OAuth2Properties; +import dev.sunbirdrc.registry.authorization.pojos.UserToken; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.oauth2.jwt.Jwt; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class CustomJwtAuthenticationConverterTest { + @Test + public void shouldExtractValuesFromJWTClaim() { + OAuth2Properties oAuth2Properties = new OAuth2Properties(); + oAuth2Properties.setUserIdPath("sub"); + oAuth2Properties.setRolesPath("user.roles"); + oAuth2Properties.setConsentPath("user.consent"); + oAuth2Properties.setEmailPath("user.email"); + oAuth2Properties.setEntityPath("entity"); + CustomJwtAuthenticationConverter converter = new CustomJwtAuthenticationConverter(oAuth2Properties); + Jwt jwt = mock(Jwt.class); + Map claims = new HashMap<>(); + claims.put("sub", "1234"); + Map userMap = new HashMap<>(); + userMap.put("roles", Collections.singletonList("admin")); + Map consentMap = new HashMap<>(); + consentMap.put("name", 1); + userMap.put("consent", consentMap); + userMap.put("email", "1234@mail.com"); + claims.put("user", userMap); + claims.put("entity", Arrays.asList("Student", "Teacher")); + when(jwt.getClaims()).thenReturn(claims); + AbstractAuthenticationToken authenticationToken = converter.convert(jwt); + assert authenticationToken != null; + Assert.assertEquals("1234", ((UserToken) authenticationToken).getUserId()); + Assert.assertEquals("{name=1}", ((UserToken) authenticationToken).getConsentFields().toString()); + Assert.assertEquals("1234@mail.com", ((UserToken) authenticationToken).getEmail()); + Assert.assertEquals("[Student, Teacher]", ((UserToken) authenticationToken).getEntities().toString()); + Assert.assertEquals("[admin]", authenticationToken.getAuthorities().toString()); + } + + @Test + public void shouldHandleInvalidValuesWhileExtractionWithDefaultValues() { + OAuth2Properties oAuth2Properties = new OAuth2Properties(); + oAuth2Properties.setUserIdPath("sub"); + oAuth2Properties.setRolesPath("user.roles"); + oAuth2Properties.setConsentPath("user.consent"); + oAuth2Properties.setEmailPath("user.email"); + oAuth2Properties.setEntityPath("entity"); + CustomJwtAuthenticationConverter converter = new CustomJwtAuthenticationConverter(oAuth2Properties); + Jwt jwt = mock(Jwt.class); + Map claims = new HashMap<>(); + claims.put("sub", "1234"); + Map userMap = new HashMap<>(); + userMap.put("roles", "admin"); + userMap.put("email", "1234@mail.com"); + claims.put("user", userMap); + when(jwt.getClaims()).thenReturn(claims); + AbstractAuthenticationToken authenticationToken = converter.convert(jwt); + assert authenticationToken != null; + Assert.assertEquals("1234", ((UserToken) authenticationToken).getUserId()); + Assert.assertEquals("{}", ((UserToken) authenticationToken).getConsentFields().toString()); + Assert.assertEquals("1234@mail.com", ((UserToken) authenticationToken).getEmail()); + Assert.assertEquals("[]", ((UserToken) authenticationToken).getEntities().toString()); + Assert.assertEquals("[]", authenticationToken.getAuthorities().toString()); + } +} diff --git a/java/middleware/registry-middleware/generic-iam/pom.xml b/java/middleware/registry-middleware/generic-iam/pom.xml new file mode 100644 index 000000000..e52c4e6bf --- /dev/null +++ b/java/middleware/registry-middleware/generic-iam/pom.xml @@ -0,0 +1,72 @@ + + + + registry-middleware + dev.sunbirdrc + 2.0.3 + + 4.0.0 + + generic-iam + + + + + + + + + + org.springframework + spring-context + 5.0.2.RELEASE + + + org.springframework + spring-web + 5.0.2.RELEASE + + + org.slf4j + slf4j-api + 1.7.32 + compile + + + org.springframework.boot + spring-boot-starter-test + test + 2.5.0 + + + junit + junit + 4.12 + test + + + org.springframework + spring-test + 5.3.9 + test + + + dev.sunbirdrc + pojos + 2.0.3 + + + dev.sunbirdrc + middleware-commons + 2.0.3 + + + dev.sunbirdrc + identity-provider + 2.0.3 + + + + diff --git a/java/middleware/registry-middleware/generic-iam/src/main/java/dev/sunbirdrc/auth/genericiam/AdminUtil.java b/java/middleware/registry-middleware/generic-iam/src/main/java/dev/sunbirdrc/auth/genericiam/AdminUtil.java new file mode 100644 index 000000000..0310fe652 --- /dev/null +++ b/java/middleware/registry-middleware/generic-iam/src/main/java/dev/sunbirdrc/auth/genericiam/AdminUtil.java @@ -0,0 +1,67 @@ +package dev.sunbirdrc.auth.genericiam; + +import dev.sunbirdrc.pojos.ComponentHealthInfo; +import dev.sunbirdrc.registry.identity_providers.pojos.*; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import java.util.Collections; +import java.util.Objects; + +import static dev.sunbirdrc.registry.middleware.util.Constants.CONNECTION_FAILURE; + + +public class AdminUtil implements IdentityManager { + public static final String SUNBIRD_GENERIC_IAM_SERVICE_NAME = "sunbird.generic-iam.service"; + private static final Logger logger = LoggerFactory.getLogger(AdminUtil.class); + private final IdentityProviderConfiguration identityProviderConfiguration; + + private final RestTemplate restTemplate; + + private final String iamServiceURL; + + public AdminUtil(IdentityProviderConfiguration identityProviderConfiguration) { + this.identityProviderConfiguration = identityProviderConfiguration; + this.restTemplate = new RestTemplate(); + this.iamServiceURL = System.getenv() + .getOrDefault("sunbird_sso_url", "http://localhost:3990/fusionauth/api/v1/user"); + } + + + @Override + public String createUser(CreateUserRequest createUserRequest) throws IdentityException { + logger.info("Creating user with mobile_number : " + createUserRequest.getUserName()); + ResponseEntity response = this.restTemplate.postForEntity(this.iamServiceURL, + createUserRequest, CreateUserResponse.class); + if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null && + !StringUtils.isEmpty(response.getBody().getUserId())) { + return response.getBody().getUserId(); + } + logger.error("Error creating user in IAM service"); + throw new IdentityException(String.format("User creation error %s", createUserRequest)); + } + + + @Override + public String getServiceName() { + return SUNBIRD_GENERIC_IAM_SERVICE_NAME; + } + + @Override + public ComponentHealthInfo getHealthInfo() { + if (identityProviderConfiguration.isAuthenticationEnabled()) { + try { + //TODO: check auth0 status + return new ComponentHealthInfo(getServiceName(), true); + } catch (Exception e) { + return new ComponentHealthInfo(getServiceName(), false, CONNECTION_FAILURE, e.getMessage()); + } + } else { + return new ComponentHealthInfo(getServiceName(), true, "AUTHENTICATION_ENABLED", "false"); + } + } +} diff --git a/java/middleware/registry-middleware/generic-iam/src/main/java/dev/sunbirdrc/auth/genericiam/AuthProviderImpl.java b/java/middleware/registry-middleware/generic-iam/src/main/java/dev/sunbirdrc/auth/genericiam/AuthProviderImpl.java new file mode 100644 index 000000000..443e87693 --- /dev/null +++ b/java/middleware/registry-middleware/generic-iam/src/main/java/dev/sunbirdrc/auth/genericiam/AuthProviderImpl.java @@ -0,0 +1,12 @@ +package dev.sunbirdrc.auth.genericiam; + +import dev.sunbirdrc.registry.identity_providers.pojos.IdentityManager; +import dev.sunbirdrc.registry.identity_providers.pojos.IdentityProviderConfiguration; +import dev.sunbirdrc.registry.identity_providers.providers.IdentityProvider; + +public class AuthProviderImpl implements IdentityProvider { + @Override + public IdentityManager createManager(IdentityProviderConfiguration identityProviderConfiguration) { + return new AdminUtil(identityProviderConfiguration); + } +} diff --git a/java/middleware/registry-middleware/generic-iam/src/main/resources/META-INF/services/dev.sunbirdrc.registry.identity_providers.providers.IdentityProvider b/java/middleware/registry-middleware/generic-iam/src/main/resources/META-INF/services/dev.sunbirdrc.registry.identity_providers.providers.IdentityProvider new file mode 100644 index 000000000..ac8797b7a --- /dev/null +++ b/java/middleware/registry-middleware/generic-iam/src/main/resources/META-INF/services/dev.sunbirdrc.registry.identity_providers.providers.IdentityProvider @@ -0,0 +1 @@ +dev.sunbirdrc.auth.genericiam.AuthProviderImpl diff --git a/java/middleware/registry-middleware/identity-provider/pom.xml b/java/middleware/registry-middleware/identity-provider/pom.xml new file mode 100644 index 000000000..16ef44352 --- /dev/null +++ b/java/middleware/registry-middleware/identity-provider/pom.xml @@ -0,0 +1,30 @@ + + 4.0.0 + + identity-provider + 2.0.3 + Identity Provider + + + dev.sunbirdrc + registry-middleware + 2.0.3 + + + + + + dev.sunbirdrc + pojos + 2.0.3 + compile + + + + + + + + + diff --git a/java/middleware/registry-middleware/identity-provider/src/main/java/dev/sunbirdrc/registry/identity_providers/pojos/CreateUserRequest.java b/java/middleware/registry-middleware/identity-provider/src/main/java/dev/sunbirdrc/registry/identity_providers/pojos/CreateUserRequest.java new file mode 100644 index 000000000..7b90ad67d --- /dev/null +++ b/java/middleware/registry-middleware/identity-provider/src/main/java/dev/sunbirdrc/registry/identity_providers/pojos/CreateUserRequest.java @@ -0,0 +1,22 @@ +package dev.sunbirdrc.registry.identity_providers.pojos; + + +import lombok.Data; + +@Data +public class CreateUserRequest { + private final String userName; + private final String email; + private final String mobile; + private final String entity; + + private final String password; + public CreateUserRequest(String entity, String userName, String email, String mobile, String password) { + this.userName = userName; + this.email = email; + this.mobile = mobile; + this.entity = entity; + this.password = password; + } + +} diff --git a/java/middleware/registry-middleware/identity-provider/src/main/java/dev/sunbirdrc/registry/identity_providers/pojos/CreateUserResponse.java b/java/middleware/registry-middleware/identity-provider/src/main/java/dev/sunbirdrc/registry/identity_providers/pojos/CreateUserResponse.java new file mode 100644 index 000000000..bf714df9b --- /dev/null +++ b/java/middleware/registry-middleware/identity-provider/src/main/java/dev/sunbirdrc/registry/identity_providers/pojos/CreateUserResponse.java @@ -0,0 +1,16 @@ +package dev.sunbirdrc.registry.identity_providers.pojos; + + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class CreateUserResponse { + private String userId; + private String status; + private String message; + +} diff --git a/java/middleware/registry-middleware/identity-provider/src/main/java/dev/sunbirdrc/registry/identity_providers/pojos/IdentityException.java b/java/middleware/registry-middleware/identity-provider/src/main/java/dev/sunbirdrc/registry/identity_providers/pojos/IdentityException.java new file mode 100644 index 000000000..e8e1a967b --- /dev/null +++ b/java/middleware/registry-middleware/identity-provider/src/main/java/dev/sunbirdrc/registry/identity_providers/pojos/IdentityException.java @@ -0,0 +1,8 @@ +package dev.sunbirdrc.registry.identity_providers.pojos; + +public class IdentityException extends Exception { + + public IdentityException(String message) { + super(message); + } +} diff --git a/java/middleware/registry-middleware/identity-provider/src/main/java/dev/sunbirdrc/registry/identity_providers/pojos/IdentityManager.java b/java/middleware/registry-middleware/identity-provider/src/main/java/dev/sunbirdrc/registry/identity_providers/pojos/IdentityManager.java new file mode 100644 index 000000000..2a38a9057 --- /dev/null +++ b/java/middleware/registry-middleware/identity-provider/src/main/java/dev/sunbirdrc/registry/identity_providers/pojos/IdentityManager.java @@ -0,0 +1,7 @@ +package dev.sunbirdrc.registry.identity_providers.pojos; + +import dev.sunbirdrc.pojos.HealthIndicator; + +public interface IdentityManager extends HealthIndicator { + String createUser(CreateUserRequest createUserRequest) throws IdentityException; +} diff --git a/java/middleware/registry-middleware/identity-provider/src/main/java/dev/sunbirdrc/registry/identity_providers/pojos/IdentityProviderConfiguration.java b/java/middleware/registry-middleware/identity-provider/src/main/java/dev/sunbirdrc/registry/identity_providers/pojos/IdentityProviderConfiguration.java new file mode 100644 index 000000000..5456e303f --- /dev/null +++ b/java/middleware/registry-middleware/identity-provider/src/main/java/dev/sunbirdrc/registry/identity_providers/pojos/IdentityProviderConfiguration.java @@ -0,0 +1,38 @@ +package dev.sunbirdrc.registry.identity_providers.pojos; + +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Getter +public class IdentityProviderConfiguration { + @Value("${identity.provider}") + private String provider; + @Value("${identity.url}") + private String url; + @Value("${identity.realm}") + private String realm; + @Value("${identity.client_id}") + private String clientId; + @Value("${identity.client_secret}") + private String clientSecret; + @Value("${identity.set_default_password}") + private Boolean setDefaultPassword; + @Value("${identity.default_password}") + private String defaultPassword; + @Value("${identity.user_actions}") + private List userActions; + + @Value("${httpConnection.maxConnections:5}") + private int httpMaxConnections; + + @Value("${authentication.enabled:true}") + boolean authenticationEnabled; + + public Boolean setDefaultPassword() { + return setDefaultPassword; + } +} diff --git a/java/middleware/registry-middleware/keycloak/src/main/java/dev/sunbirdrc/keycloak/OwnerCreationException.java b/java/middleware/registry-middleware/identity-provider/src/main/java/dev/sunbirdrc/registry/identity_providers/pojos/OwnerCreationException.java similarity index 77% rename from java/middleware/registry-middleware/keycloak/src/main/java/dev/sunbirdrc/keycloak/OwnerCreationException.java rename to java/middleware/registry-middleware/identity-provider/src/main/java/dev/sunbirdrc/registry/identity_providers/pojos/OwnerCreationException.java index 8d3ad623c..f296d4969 100644 --- a/java/middleware/registry-middleware/keycloak/src/main/java/dev/sunbirdrc/keycloak/OwnerCreationException.java +++ b/java/middleware/registry-middleware/identity-provider/src/main/java/dev/sunbirdrc/registry/identity_providers/pojos/OwnerCreationException.java @@ -1,9 +1,9 @@ -package dev.sunbirdrc.keycloak; +package dev.sunbirdrc.registry.identity_providers.pojos; public class OwnerCreationException extends Exception { /** - * + * */ private static final long serialVersionUID = 8690094725383702979L; diff --git a/java/middleware/registry-middleware/identity-provider/src/main/java/dev/sunbirdrc/registry/identity_providers/providers/IdentityProvider.java b/java/middleware/registry-middleware/identity-provider/src/main/java/dev/sunbirdrc/registry/identity_providers/providers/IdentityProvider.java new file mode 100644 index 000000000..c9855d33f --- /dev/null +++ b/java/middleware/registry-middleware/identity-provider/src/main/java/dev/sunbirdrc/registry/identity_providers/providers/IdentityProvider.java @@ -0,0 +1,8 @@ +package dev.sunbirdrc.registry.identity_providers.providers; + +import dev.sunbirdrc.registry.identity_providers.pojos.IdentityManager; +import dev.sunbirdrc.registry.identity_providers.pojos.IdentityProviderConfiguration; + +public interface IdentityProvider { + IdentityManager createManager(IdentityProviderConfiguration identityProviderConfiguration); +} diff --git a/java/middleware/registry-middleware/keycloak/pom.xml b/java/middleware/registry-middleware/keycloak/pom.xml index 84868de9e..f134c6ce4 100644 --- a/java/middleware/registry-middleware/keycloak/pom.xml +++ b/java/middleware/registry-middleware/keycloak/pom.xml @@ -13,13 +13,7 @@ - - org.keycloak.bom - keycloak-adapter-bom - 11.0.2 - pom - import - + @@ -72,5 +66,11 @@ middleware-commons 2.0.3 + + dev.sunbirdrc + identity-provider + 2.0.3 + + - \ No newline at end of file + diff --git a/java/middleware/registry-middleware/keycloak/src/main/java/dev/sunbirdrc/keycloak/KeycloakAdminUtil.java b/java/middleware/registry-middleware/keycloak/src/main/java/dev/sunbirdrc/auth/keycloak/KeycloakAdminUtil.java similarity index 57% rename from java/middleware/registry-middleware/keycloak/src/main/java/dev/sunbirdrc/keycloak/KeycloakAdminUtil.java rename to java/middleware/registry-middleware/keycloak/src/main/java/dev/sunbirdrc/auth/keycloak/KeycloakAdminUtil.java index da0a3e2ae..60f5236fe 100644 --- a/java/middleware/registry-middleware/keycloak/src/main/java/dev/sunbirdrc/keycloak/KeycloakAdminUtil.java +++ b/java/middleware/registry-middleware/keycloak/src/main/java/dev/sunbirdrc/auth/keycloak/KeycloakAdminUtil.java @@ -1,6 +1,7 @@ -package dev.sunbirdrc.keycloak; +package dev.sunbirdrc.auth.keycloak; import dev.sunbirdrc.pojos.ComponentHealthInfo; +import dev.sunbirdrc.registry.identity_providers.pojos.*; import dev.sunbirdrc.pojos.HealthIndicator; import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; import org.keycloak.OAuth2Constants; @@ -13,10 +14,6 @@ import org.keycloak.representations.idm.UserRepresentation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.PropertySource; -import org.springframework.stereotype.Component; import java.util.*; import javax.ws.rs.core.Response; @@ -25,91 +22,64 @@ import static dev.sunbirdrc.registry.middleware.util.Constants.SUNBIRD_KEYCLOAK_SERVICE_NAME; -@Component -@PropertySource(value = "classpath:application.yml", ignoreResourceNotFound = true) -public class KeycloakAdminUtil implements HealthIndicator { +public class KeycloakAdminUtil implements IdentityManager { private static final Logger logger = LoggerFactory.getLogger(KeycloakAdminUtil.class); private static final String EMAIL = "email_id"; private static final String ENTITY = "entity"; private static final String MOBILE_NUMBER = "mobile_number"; private static final String PASSWORD = "password"; - - - private String realm; - private String adminClientSecret; - private String adminClientId; - private String authURL; - private String defaultPassword; - private boolean setDefaultPassword; - private List emailActions; private final Keycloak keycloak; - private boolean authenticationEnabled; - - @Autowired - public KeycloakAdminUtil( - @Value("${authentication.enabled}") - boolean authenticationEnabled, - @Value("${keycloak.realm:}") String realm, - @Value("${keycloak-admin.client-secret:}") String adminClientSecret, - @Value("${keycloak-admin.client-id:}") String adminClientId, - @Value("${keycloak-user.default-password:}") String defaultPassword, - @Value("${keycloak-user.set-default-password:false}") boolean setDefaultPassword, - @Value("${keycloak.auth-server-url:}") String authURL, - @Value("${keycloak-user.email-actions:}") List emailActions, - @Value("${httpConnection.maxConnections:5}") int httpMaxConnections) { - this.authenticationEnabled = authenticationEnabled; - this.realm = realm; - this.adminClientSecret = adminClientSecret; - this.adminClientId = adminClientId; - this.authURL = authURL; - this.defaultPassword = defaultPassword; - this.setDefaultPassword = setDefaultPassword; - this.emailActions = emailActions; - this.keycloak = buildKeycloak(httpMaxConnections); + + private final IdentityProviderConfiguration providerConfiguration; + public KeycloakAdminUtil(IdentityProviderConfiguration identityProviderConfiguration) { + this.providerConfiguration = identityProviderConfiguration; + this.keycloak = buildKeycloak(identityProviderConfiguration); } - private Keycloak buildKeycloak(int httpMaxConnections) { + private Keycloak buildKeycloak(IdentityProviderConfiguration configuration) { return KeycloakBuilder.builder() - .serverUrl(authURL) - .realm(realm) + .serverUrl(configuration.getUrl()) + .realm(configuration.getRealm()) .grantType(OAuth2Constants.CLIENT_CREDENTIALS) - .clientId(adminClientId) - .clientSecret(adminClientSecret) + .clientId(configuration.getClientId()) + .clientSecret(configuration.getClientSecret()) .resteasyClient( new ResteasyClientBuilder() - .connectionPoolSize(httpMaxConnections).build() + .connectionPoolSize(configuration.getHttpMaxConnections()).build() ) .build(); } - public String createUser(String entityName, String userName, String email, String mobile, String password) throws OwnerCreationException { - logger.info("Creating user with mobile_number : " + userName); - String groupId = createOrUpdateRealmGroup(entityName); - UserRepresentation newUser = createUserRepresentation(entityName, userName, email, mobile, password); - UsersResource usersResource = keycloak.realm(realm).users(); + @Override + public String createUser(CreateUserRequest createUserRequest) throws IdentityException { + logger.info("Creating user with mobile_number : " + createUserRequest.getUserName()); + String groupId = createOrUpdateRealmGroup(createUserRequest.getEntity()); + UserRepresentation newUser = createUserRepresentation(createUserRequest); + UsersResource usersResource = keycloak.realm(providerConfiguration.getRealm()).users(); try (Response response = usersResource.create(newUser)) { if (response.getStatus() == 201) { logger.info("Response | Status: {} | Status Info: {}", response.getStatus(), response.getStatusInfo()); logger.info("User ID path" + response.getLocation().getPath()); String userID = response.getLocation().getPath().replaceAll(".*/([^/]+)$", "$1"); logger.info("User ID : " + userID); - if (!emailActions.isEmpty()) - usersResource.get(userID).executeActionsEmail(emailActions); + if (!providerConfiguration.getUserActions().isEmpty()) { + usersResource.get(userID).executeActionsEmail(providerConfiguration.getUserActions()); + } return userID; } else if (response.getStatus() == 409) { - logger.info("UserID: {} exists", userName); - return updateExistingUserAttributes(entityName, userName, email, mobile, groupId); + logger.info("UserID: {} exists", createUserRequest.getUserName()); + return updateExistingUserAttributes(createUserRequest, groupId); } else if (response.getStatus() == 500) { - throw new OwnerCreationException("Keycloak user creation error"); + throw new IdentityException("Keycloak user creation error"); } else { - throw new OwnerCreationException("Username already invited / registered"); + throw new IdentityException("Username already invited / registered"); } } } private String createOrUpdateRealmGroup(String entityName) { RoleRepresentation roleRepresentation = createOrGetRealmRole(entityName); - GroupsResource groupsResource = keycloak.realm(realm).groups(); + GroupsResource groupsResource = keycloak.realm(providerConfiguration.getRealm()).groups(); GroupRepresentation groupRepresentation = new GroupRepresentation(); groupRepresentation.setName(entityName); Response groupAddResponse = groupsResource.add(groupRepresentation); @@ -128,7 +98,7 @@ private String createOrUpdateRealmGroup(String entityName) { } private RoleRepresentation createOrGetRealmRole(String entityName) { - RolesResource rolesResource = keycloak.realm(realm).roles(); + RolesResource rolesResource = keycloak.realm(providerConfiguration.getRealm()).roles(); try { RoleRepresentation roleRepresentation = new RoleRepresentation(); roleRepresentation.setName(entityName); @@ -139,63 +109,65 @@ private RoleRepresentation createOrGetRealmRole(String entityName) { return rolesResource.get(entityName).toRepresentation(); } - private String updateExistingUserAttributes(String entityName, String userName, String email, String mobile, - String groupId) throws OwnerCreationException { - Optional userRepresentationOptional = getUserByUsername(userName); +// private String updateExistingUserAttributes(String entityName, String userName, String email, String mobile, + private String updateExistingUserAttributes(CreateUserRequest createUserRequest, String groupId) throws IdentityException { + Optional userRepresentationOptional = getUserByUsername(createUserRequest.getUserName()); if (userRepresentationOptional.isPresent()) { UserResource userResource = userRepresentationOptional.get(); UserRepresentation userRepresentation = userResource.toRepresentation(); - updateUserAttributes(entityName, email, mobile, userRepresentation); + updateUserAttributes(createUserRequest, userRepresentation); List groups = userRepresentation.getGroups(); if (groups == null) { groups = new ArrayList<>(); } - if (!groups.contains(entityName)) { - groups.add(entityName); + if (!groups.contains(createUserRequest.getEntity())) { + groups.add(createUserRequest.getEntity()); userRepresentation.setGroups(groups); } userResource.update(userRepresentation); userResource.joinGroup(groupId); return userRepresentation.getId(); } else { - logger.error("Failed fetching user by username: {}", userName); - throw new OwnerCreationException("Creating user failed"); + logger.error("Failed fetching user by username: {}", createUserRequest.getUserName()); + throw new IdentityException("Creating user failed"); } } - private UserRepresentation createUserRepresentation(String entityName, String userName, String email, String mobile, String password) { + private UserRepresentation createUserRepresentation(CreateUserRequest createUserRequest) { UserRepresentation newUser = new UserRepresentation(); newUser.setEnabled(true); - newUser.setUsername(userName); - if(Objects.equals(password, "") && this.setDefaultPassword) { - password = this.defaultPassword; - } - if (!Objects.equals(password, "")) { + newUser.setUsername(createUserRequest.getUserName()); + if (!Objects.equals(createUserRequest.getPassword(), "") || providerConfiguration.setDefaultPassword()) { CredentialRepresentation passwordCredential = new CredentialRepresentation(); - passwordCredential.setValue(password); + if (!Objects.equals(createUserRequest.getPassword(), "")) { + passwordCredential.setValue(createUserRequest.getPassword()); + passwordCredential.setTemporary(false); + } else { + passwordCredential.setValue(providerConfiguration.getDefaultPassword()); + passwordCredential.setTemporary(true); + } passwordCredential.setType(PASSWORD); - passwordCredential.setTemporary(false); newUser.setCredentials(Collections.singletonList(passwordCredential)); } - newUser.setGroups(Collections.singletonList(entityName)); - newUser.setEmail(email); - newUser.singleAttribute(MOBILE_NUMBER, mobile); - newUser.singleAttribute(EMAIL, email); - newUser.singleAttribute(ENTITY, entityName); + newUser.setGroups(Collections.singletonList(createUserRequest.getEntity())); + newUser.setEmail(createUserRequest.getEmail()); + newUser.singleAttribute(MOBILE_NUMBER, createUserRequest.getMobile()); + newUser.singleAttribute(EMAIL, createUserRequest.getEmail()); + newUser.singleAttribute(ENTITY, createUserRequest.getEntity()); return newUser; } - private void updateUserAttributes(String entityName, String email, String mobile, UserRepresentation userRepresentation) { + private void updateUserAttributes(CreateUserRequest createUserRequest, UserRepresentation userRepresentation) { if (userRepresentation.getAttributes() == null || !userRepresentation.getAttributes().containsKey(ENTITY)) { - userRepresentation.singleAttribute(ENTITY, entityName); + userRepresentation.singleAttribute(ENTITY, createUserRequest.getEntity()); } else { List entities = userRepresentation.getAttributes().getOrDefault(ENTITY, Collections.emptyList()); - if (!entities.contains(entityName)) { - entities.add(entityName); + if (!entities.contains(createUserRequest.getEntity())) { + entities.add(createUserRequest.getEntity()); } } - addAttributeIfNotExists(userRepresentation, EMAIL, email); - addAttributeIfNotExists(userRepresentation, MOBILE_NUMBER, mobile); + addAttributeIfNotExists(userRepresentation, EMAIL, createUserRequest.getEmail()); + addAttributeIfNotExists(userRepresentation, MOBILE_NUMBER, createUserRequest.getMobile()); } private void addAttributeIfNotExists(UserRepresentation userRepresentation, String key, String value) { @@ -212,17 +184,17 @@ private void checkIfUserRegisteredForEntity(String entityName, UserRepresentatio } private Optional getUserByUsername(String username) { - List users = keycloak.realm(realm).users().search(username); + List users = keycloak.realm(providerConfiguration.getRealm()).users().search(username); if (users.size() > 0) { - return Optional.of(keycloak.realm(realm).users().get(users.get(0).getId())); + return Optional.of(keycloak.realm(providerConfiguration.getRealm()).users().get(users.get(0).getId())); } return Optional.empty(); } private void addUserToGroup(String groupName, UserRepresentation user) { - keycloak.realm(realm).groups().groups().stream() + keycloak.realm(providerConfiguration.getRealm()).groups().groups().stream() .filter(g -> g.getName().equals(groupName)).findFirst() - .ifPresent(g -> keycloak.realm(realm).users().get(user.getId()).joinGroup(g.getId())); + .ifPresent(g -> keycloak.realm(providerConfiguration.getRealm()).users().get(user.getId()).joinGroup(g.getId())); } @Override @@ -232,7 +204,7 @@ public String getServiceName() { @Override public ComponentHealthInfo getHealthInfo() { - if (authenticationEnabled) { + if (providerConfiguration.isAuthenticationEnabled()) { try { return new ComponentHealthInfo(getServiceName(), keycloak.serverInfo().getInfo().getSystemInfo().getUptimeMillis() > 0); } catch (Exception e) { diff --git a/java/middleware/registry-middleware/keycloak/src/main/java/dev/sunbirdrc/auth/keycloak/KeycloakProviderImpl.java b/java/middleware/registry-middleware/keycloak/src/main/java/dev/sunbirdrc/auth/keycloak/KeycloakProviderImpl.java new file mode 100644 index 000000000..023920dde --- /dev/null +++ b/java/middleware/registry-middleware/keycloak/src/main/java/dev/sunbirdrc/auth/keycloak/KeycloakProviderImpl.java @@ -0,0 +1,12 @@ +package dev.sunbirdrc.auth.keycloak; + +import dev.sunbirdrc.registry.identity_providers.pojos.IdentityManager; +import dev.sunbirdrc.registry.identity_providers.pojos.IdentityProviderConfiguration; +import dev.sunbirdrc.registry.identity_providers.providers.IdentityProvider; + +public class KeycloakProviderImpl implements IdentityProvider { + @Override + public IdentityManager createManager(IdentityProviderConfiguration identityProviderConfiguration) { + return new KeycloakAdminUtil(identityProviderConfiguration); + } +} diff --git a/java/middleware/registry-middleware/keycloak/src/main/resources/META-INF/services/dev.sunbirdrc.registry.identity_providers.providers.IdentityProvider b/java/middleware/registry-middleware/keycloak/src/main/resources/META-INF/services/dev.sunbirdrc.registry.identity_providers.providers.IdentityProvider new file mode 100644 index 000000000..a04ad1bd4 --- /dev/null +++ b/java/middleware/registry-middleware/keycloak/src/main/resources/META-INF/services/dev.sunbirdrc.registry.identity_providers.providers.IdentityProvider @@ -0,0 +1 @@ +dev.sunbirdrc.auth.keycloak.KeycloakProviderImpl diff --git a/java/middleware/registry-middleware/pom.xml b/java/middleware/registry-middleware/pom.xml index 06bcdbd98..2f1b5b26b 100644 --- a/java/middleware/registry-middleware/pom.xml +++ b/java/middleware/registry-middleware/pom.xml @@ -11,11 +11,20 @@ 2.0.3 middleware-bom - + + + dev.sunbirdrc + pojos + 2.0.3 + + authorization validation keycloak workflow + identity-provider + auth0 + generic-iam diff --git a/java/middleware/registry-middleware/validation/src/main/java/dev/sunbirdrc/validators/IValidate.java b/java/middleware/registry-middleware/validation/src/main/java/dev/sunbirdrc/validators/IValidate.java index 6395be8bf..adddff028 100644 --- a/java/middleware/registry-middleware/validation/src/main/java/dev/sunbirdrc/validators/IValidate.java +++ b/java/middleware/registry-middleware/validation/src/main/java/dev/sunbirdrc/validators/IValidate.java @@ -21,4 +21,5 @@ public interface IValidate { void addDefinitions(JsonNode schema) throws IOException; void removeDefinition(JsonNode jsonNode); + void removeDefinition(String schema); } diff --git a/java/middleware/registry-middleware/workflow/pom.xml b/java/middleware/registry-middleware/workflow/pom.xml index 3831e7ab7..b18e5dad5 100644 --- a/java/middleware/registry-middleware/workflow/pom.xml +++ b/java/middleware/registry-middleware/workflow/pom.xml @@ -24,11 +24,6 @@ middleware-commons 2.0.3 - - dev.sunbirdrc - keycloak - 2.0.3 - junit junit @@ -50,6 +45,12 @@ drools-decisiontables ${drools.version} + + dev.sunbirdrc + identity-provider + 2.0.3 + compile + - \ No newline at end of file + diff --git a/java/middleware/registry-middleware/workflow/src/main/java/dev/sunbirdrc/workflow/RuleEngineService.java b/java/middleware/registry-middleware/workflow/src/main/java/dev/sunbirdrc/workflow/RuleEngineService.java index 0339d5111..23a1c2e3e 100644 --- a/java/middleware/registry-middleware/workflow/src/main/java/dev/sunbirdrc/workflow/RuleEngineService.java +++ b/java/middleware/registry-middleware/workflow/src/main/java/dev/sunbirdrc/workflow/RuleEngineService.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import dev.sunbirdrc.keycloak.KeycloakAdminUtil; import dev.sunbirdrc.pojos.OwnershipsAttributes; import dev.sunbirdrc.registry.middleware.util.JSONUtil; import dev.sunbirdrc.registry.middleware.util.OSSystemFields; @@ -10,6 +9,7 @@ import org.kie.api.runtime.StatelessKieSession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import dev.sunbirdrc.registry.identity_providers.pojos.IdentityManager; import java.util.List; @@ -18,25 +18,25 @@ @Service public class RuleEngineService { private final KieContainer kieContainer; - private final KeycloakAdminUtil keycloakAdminUtil; + private final IdentityManager identityManager; private static final String PATH = "path"; @Autowired - public RuleEngineService(KieContainer kieContainer, KeycloakAdminUtil keycloakAdminUtil) { + public RuleEngineService(KieContainer kieContainer, IdentityManager identityManager) { this.kieContainer = kieContainer; - this.keycloakAdminUtil = keycloakAdminUtil; + this.identityManager = identityManager; } public void doTransition(List stateContexts) { StatelessKieSession kieSession = kieContainer.newStatelessKieSession(); - kieSession.setGlobal("keycloakAdminUtil", keycloakAdminUtil); + kieSession.setGlobal("identityManager", identityManager); kieSession.setGlobal("ruleEngineService", this); kieSession.execute(stateContexts); } public void doTransition(StateContext stateContext) { StatelessKieSession kieSession = kieContainer.newStatelessKieSession(); - kieSession.setGlobal("keycloakAdminUtil", keycloakAdminUtil); + kieSession.setGlobal("identityManager", identityManager); kieSession.setGlobal("ruleEngineService", this); kieSession.execute(stateContext); } diff --git a/java/middleware/registry-middleware/workflow/src/main/resources/workflow/statetransitions.drl b/java/middleware/registry-middleware/workflow/src/main/resources/workflow/statetransitions.drl index af42af88e..98a9237b6 100644 --- a/java/middleware/registry-middleware/workflow/src/main/resources/workflow/statetransitions.drl +++ b/java/middleware/registry-middleware/workflow/src/main/resources/workflow/statetransitions.drl @@ -1,9 +1,10 @@ package dev.sunbirdrc.workflow; import dev.sunbirdrc.workflow.StateContext; import dev.sunbirdrc.pojos.attestation.States; +import dev.sunbirdrc.registry.identity_providers.pojos.CreateUserRequest; dialect "mvel" -global dev.sunbirdrc.keycloak.KeycloakAdminUtil keycloakAdminUtil; +global dev.sunbirdrc.registry.identity_providers.pojos.IdentityManager identityManager; global dev.sunbirdrc.workflow.RuleEngineService ruleEngineService; rule "Set state as draft if there is a change" @@ -40,9 +41,10 @@ rule "Create entity owner for newly added owner fields" when stateDefinition:StateContext(isOwnershipProperty() && isOwnerNewlyAdded() && isLoginEnabled()); then - String owner = keycloakAdminUtil.createUser(stateDefinition.getEntityName(), stateDefinition.getUpdated().get("userId").textValue(), - stateDefinition.getUpdated().get("email").textValue(), stateDefinition.getUpdated().get("mobile").textValue(), - stateDefinition.getUpdated().get("password").textValue()); + CreateUserRequest createUserRequest = new CreateUserRequest(stateDefinition.getEntityName(), + stateDefinition.getUpdated().get("userId").textValue(), stateDefinition.getUpdated().get("email").textValue(), + stateDefinition.getUpdated().get("mobile").textValue(), stateDefinition.getUpdated().get("password").textValue()); + String owner = identityManager.createUser(createUserRequest); stateDefinition.addOwner(owner); end @@ -58,4 +60,4 @@ rule "Revert if any modifications to system fields" stateDefinition:StateContext(revertSystemFieldsChangedEnabled()); then ruleEngineService.revertSystemFields(stateDefinition); -end \ No newline at end of file +end diff --git a/java/pojos/src/main/java/dev/sunbirdrc/pojos/APIMessage.java b/java/pojos/src/main/java/dev/sunbirdrc/pojos/APIMessage.java index 4df2e7ae3..019b60be4 100644 --- a/java/pojos/src/main/java/dev/sunbirdrc/pojos/APIMessage.java +++ b/java/pojos/src/main/java/dev/sunbirdrc/pojos/APIMessage.java @@ -41,7 +41,8 @@ public APIMessage(HttpServletRequest servletRequest) { try { request = new ObjectMapper().readValue(body, Request.class); } catch (IOException jpe) { - logger.error("Can't read request body", jpe); + logger.error("Can't read request body, {}", jpe.getMessage()); + logger.debug("Exception: ", jpe); request = null; } } diff --git a/java/registry-interceptor/pom.xml b/java/registry-interceptor/pom.xml index 2f4b97f1c..19b48a8f6 100644 --- a/java/registry-interceptor/pom.xml +++ b/java/registry-interceptor/pom.xml @@ -8,7 +8,6 @@ dev.sunbirdrc 2.0.3 - dev.sunbirdrc registry-interceptor 2.0.3 jar @@ -24,7 +23,7 @@ 2.0.3 pom import - + @@ -48,7 +47,7 @@ pojos 2.0.3 - + org.springframework spring-context diff --git a/java/registry/pom.xml b/java/registry/pom.xml index 3a1dc4b06..a7531967a 100644 --- a/java/registry/pom.xml +++ b/java/registry/pom.xml @@ -43,13 +43,7 @@ pom import - - org.keycloak.bom - keycloak-adapter-bom - 11.0.2 - pom - import - + @@ -88,10 +82,6 @@ handlebars 4.3.1 - - org.keycloak - keycloak-spring-boot-starter - org.jboss.resteasy resteasy-client @@ -112,11 +102,7 @@ registry-interceptor ${revision} - - dev.sunbirdrc - keycloak - 2.0.3 - + dev.sunbirdrc workflow @@ -432,7 +418,26 @@ redis.clients jedis - + + dev.sunbirdrc + identity-provider + 2.0.3 + + + dev.sunbirdrc + keycloak + 2.0.3 + + + dev.sunbirdrc + auth0 + 2.0.3 + + + dev.sunbirdrc + generic-iam + 2.0.3 + @@ -481,6 +486,14 @@ org.springframework.boot spring-boot-maven-plugin + + + + org.springframework.boot + spring-boot-configuration-processor + + + org.jacoco @@ -565,4 +578,4 @@ - \ No newline at end of file + diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/app/SunbirdRCApplication.java b/java/registry/src/main/java/dev/sunbirdrc/registry/app/SunbirdRCApplication.java index bf28e1cb2..ecefd1d53 100644 --- a/java/registry/src/main/java/dev/sunbirdrc/registry/app/SunbirdRCApplication.java +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/app/SunbirdRCApplication.java @@ -22,7 +22,7 @@ import java.time.Duration; @SpringBootApplication(exclude={SecurityAutoConfiguration.class}) -@ComponentScan(basePackages = {"dev.sunbirdrc.registry", "dev.sunbirdrc.pojos", "dev.sunbirdrc.keycloak", "dev.sunbirdrc.workflow", "dev.sunbirdrc.plugin"}) +@ComponentScan(basePackages = {"dev.sunbirdrc.registry", "dev.sunbirdrc.pojos", "dev.sunbirdrc.auth", "dev.sunbirdrc.workflow", "dev.sunbirdrc.plugin"}) public class SunbirdRCApplication { private static ApplicationContext context; private static SpringApplication application = new SpringApplication(SunbirdRCApplication.class); diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/config/GenericConfiguration.java b/java/registry/src/main/java/dev/sunbirdrc/registry/config/GenericConfiguration.java index d3b143e4d..45ad906c4 100644 --- a/java/registry/src/main/java/dev/sunbirdrc/registry/config/GenericConfiguration.java +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/config/GenericConfiguration.java @@ -11,9 +11,11 @@ import dev.sunbirdrc.pojos.AuditRecord; import dev.sunbirdrc.pojos.Response; import dev.sunbirdrc.pojos.SunbirdRCInstrumentation; +import dev.sunbirdrc.registry.authorization.SchemaAuthFilter; import dev.sunbirdrc.registry.exception.CustomException; import dev.sunbirdrc.registry.exception.CustomExceptionHandler; import dev.sunbirdrc.registry.frame.FrameContext; +import dev.sunbirdrc.registry.identity_providers.pojos.IdentityProviderConfiguration; import dev.sunbirdrc.registry.interceptor.RequestIdValidationInterceptor; import dev.sunbirdrc.registry.interceptor.ValidationInterceptor; import dev.sunbirdrc.registry.middleware.util.Constants; @@ -56,11 +58,11 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.resource.PathResourceResolver; import org.sunbird.akka.core.SunbirdActorFactory; +import dev.sunbirdrc.registry.identity_providers.pojos.IdentityManager; +import dev.sunbirdrc.registry.identity_providers.providers.IdentityProvider; import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; +import java.util.*; @Configuration @EnableRetry @@ -151,6 +153,8 @@ public class GenericConfiguration implements WebMvcConfigurer { private String scheme; @Autowired private DBConnectionInfoMgr dbConnectionInfoMgr; + @Autowired + private IdentityProviderConfiguration identityProviderConfiguration; @Bean public ObjectMapper objectMapper() { @@ -252,6 +256,15 @@ public IValidate validationServiceImpl() throws IOException, CustomException { return null; } + @Bean + public SchemaAuthFilter schemaAuthFilter() { + SchemaAuthFilter schemaAuthFilter = new SchemaAuthFilter(); + schemaAuthFilter.appendAnonymousInviteSchema(iDefinitionsManager.getEntitiesWithAnonymousInviteRoles()); + schemaAuthFilter.appendAnonymousSchema(iDefinitionsManager.getEntitiesWithAnonymousManageRoles()); + logger.info("Added anonymous schema to auth filters"); + return schemaAuthFilter; + } + @Bean @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS) public AuditRecord auditRecord() { @@ -447,4 +460,15 @@ public RegistryService registryService() { // IAuditService auditService = new AuditProviderFactory().getAuditService(auditFrameStore); // return auditService; // } + + @Bean + public IdentityManager identityManager() { + ServiceLoader loader = ServiceLoader.load(IdentityProvider.class); + for (IdentityProvider provider : loader) { + if (identityProviderConfiguration.getProvider().equals(provider.getClass().getName())) { + return provider.createManager(identityProviderConfiguration); + } + } + throw new RuntimeException("Identity provider " + identityProviderConfiguration.getProvider() + " not found"); + } } diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/config/SchemaFilter.java b/java/registry/src/main/java/dev/sunbirdrc/registry/config/SchemaFilter.java deleted file mode 100644 index 7bb07d446..000000000 --- a/java/registry/src/main/java/dev/sunbirdrc/registry/config/SchemaFilter.java +++ /dev/null @@ -1,38 +0,0 @@ -package dev.sunbirdrc.registry.config; - -import dev.sunbirdrc.registry.util.IDefinitionsManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import javax.servlet.*; -import javax.servlet.http.HttpServletRequest; -import java.io.IOException; - -@Component -public class SchemaFilter implements Filter { - private static final Logger logger = LoggerFactory.getLogger(SchemaFilter.class); - private static final String INVITE_URL_ENDPOINT = "/invite"; - @Autowired - private IDefinitionsManager definitionsManager; - - @Override - public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws ServletException, IOException { - HttpServletRequest request = (HttpServletRequest) servletRequest; - try { - if (request.getRequestURI().contains(INVITE_URL_ENDPOINT) && - definitionsManager.getEntitiesWithAnonymousInviteRoles().stream().anyMatch(request.getRequestURI()::contains)) { - servletRequest.getRequestDispatcher(((HttpServletRequest) servletRequest).getServletPath()).forward(servletRequest, servletResponse); - return; - } else if (definitionsManager.getEntitiesWithAnonymousManageRoles().stream().anyMatch(request.getRequestURI()::contains)) { - servletRequest.getRequestDispatcher(((HttpServletRequest) servletRequest).getServletPath()).forward(servletRequest, servletResponse); - return; - } - filterChain.doFilter(servletRequest, servletResponse); - } catch (Exception e) { - logger.error("Exception while applying security filters: ", e); - throw e; - } - } -} diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/config/SecurityConfig.java b/java/registry/src/main/java/dev/sunbirdrc/registry/config/SecurityConfig.java deleted file mode 100644 index a93880853..000000000 --- a/java/registry/src/main/java/dev/sunbirdrc/registry/config/SecurityConfig.java +++ /dev/null @@ -1,78 +0,0 @@ -package dev.sunbirdrc.registry.config; - -import dev.sunbirdrc.registry.util.DefinitionsManager; -import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver; -import org.keycloak.adapters.springsecurity.KeycloakSecurityComponents; -import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider; -import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -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.configuration.EnableWebSecurity; -import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper; -import org.springframework.security.core.session.SessionRegistryImpl; -import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy; -import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; -import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter; - -@Configuration -@EnableWebSecurity -@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class) -@ConditionalOnProperty(name = "authentication.enabled",havingValue = "true",matchIfMissing = false) -public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter { - - @Value("${authentication.enabled:true}") boolean authenticationEnabled; - - @Autowired - private SchemaFilter schemaFilter; - @Autowired - public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { - - KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider(); - keycloakAuthenticationProvider.setGrantedAuthoritiesMapper( - new SimpleAuthorityMapper()); - auth.authenticationProvider(keycloakAuthenticationProvider); - } - - @Bean - public KeycloakSpringBootConfigResolver KeycloakConfigResolver() { - return new KeycloakSpringBootConfigResolver(); - } - - @Bean - @Override - protected SessionAuthenticationStrategy sessionAuthenticationStrategy() { - return new RegisterSessionAuthenticationStrategy( - new SessionRegistryImpl()); - } - - //TODO: verify all paths - @Override - protected void configure(HttpSecurity http) throws Exception { - super.configure(http); - - HttpSecurity httpConfig = http.csrf().disable(); - if (authenticationEnabled) { - httpConfig.authorizeRequests() - .antMatchers("/**/invite", "/health", "/error", - "/_schemas/**", "/**/templates/**", "/**/*.json", "/**/verify", - "/swagger-ui", "/**/search", "/**/attestation/**", - "/api/docs/swagger.json","/api/docs/*.json", "/plugin/**", "/swagger-ui.html") - .permitAll() - .and() - .addFilterBefore(schemaFilter, WebAsyncManagerIntegrationFilter.class) - .authorizeRequests() - .anyRequest() - .authenticated(); - } else { - httpConfig.authorizeRequests() - .anyRequest() - .permitAll(); - } - } -} diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryAttestationPolicyController.java b/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryAttestationPolicyController.java index d515d4714..fa1e9cd51 100644 --- a/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryAttestationPolicyController.java +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryAttestationPolicyController.java @@ -31,7 +31,7 @@ public ResponseEntity createAttestationPolicy(@PathVariable String entityName, @ List userEntities = registryHelper.getUserEntities(request); if (createAttestationEntities.containsAll(userEntities) && definitionsManager.isValidEntityName(entityName) && !registryHelper.isAttestationPolicyNameAlreadyUsed(entityName, attestationPolicy.getName())) { - String userId = registryHelper.getUserId(request, entityName); + String userId = registryHelper.getUserId(entityName); attestationPolicy.setCreatedBy(userId); logger.info("Creating attestation policy for entity: {} - {}", entityName, attestationPolicy); attestationPolicy.setEntity(entityName); @@ -54,7 +54,7 @@ public ResponseEntity getAttestationPolicies(@PathVariable String entityName, Ht Response response = new Response(Response.API_ID.READ, "OK", responseParams); try { if (definitionsManager.isValidEntityName(entityName)) { - String userId = registryHelper.getUserId(request, entityName); + String userId = registryHelper.getUserId(entityName); List userEntities = registryHelper.getUserEntities(request); if (userEntities.contains(entityName)) { logger.info("Retrieving attestation policies for entity: {}", entityName); @@ -82,7 +82,7 @@ public ResponseEntity updateAttestationPolicy(@PathVariable String entityName, @ Response response = new Response(Response.API_ID.UPDATE, "OK", responseParams); try { if (definitionsManager.isValidEntityName(entityName)) { - String userId = registryHelper.getUserId(request, entityName); + String userId = registryHelper.getUserId(entityName); final Optional attestationPolicyOptional = registryHelper.findAttestationPolicyById(userId, policyOSID); if (attestationPolicyOptional.isPresent() && attestationPolicyOptional.get().getCreatedBy().equals(userId)) { logger.info("Updating attestation policies id: {}", policyOSID); @@ -105,7 +105,7 @@ public ResponseEntity updateAttestationPolicyStatus(@PathVariable String entityN Response response = new Response(Response.API_ID.UPDATE, "OK", responseParams); try { if (definitionsManager.isValidEntityName(entityName)) { - String userId = registryHelper.getUserId(request, entityName); + String userId = registryHelper.getUserId(entityName); Optional attestationPolicyOptional = registryHelper.findAttestationPolicyById(userId, policyId); if (attestationPolicyOptional.isPresent() && attestationPolicyOptional.get().getCreatedBy().equals(userId)) { logger.info("Updating attestation policy status of id: {}", policyId); @@ -130,7 +130,7 @@ public ResponseEntity deleteAttestationPolicy(@PathVariable String entityName, @ Response response = new Response(Response.API_ID.UPDATE, "OK", responseParams); try { if (definitionsManager.isValidEntityName(entityName)) { - String userId = registryHelper.getUserId(request, entityName); + String userId = registryHelper.getUserId(entityName); Optional attestationPolicyOptional = registryHelper.findAttestationPolicyById(userId, policyId); if (attestationPolicyOptional.isPresent() && attestationPolicyOptional.get().getCreatedBy().equals(userId)) { logger.info("Updating attestation policy status of id: {}", policyId); diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryClaimsController.java b/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryClaimsController.java index edad915bf..6cb3a6b2e 100644 --- a/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryClaimsController.java +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryClaimsController.java @@ -26,8 +26,6 @@ import javax.servlet.http.HttpServletRequest; import java.util.Collections; -import static dev.sunbirdrc.registry.middleware.util.Constants.USER_ID; - @RestController public class RegistryClaimsController extends AbstractController{ private static final Logger logger = LoggerFactory.getLogger(RegistryClaimsController.class); @@ -91,7 +89,7 @@ public ResponseEntity attestClaim( pluginRequestMessage.setAttestorPlugin(attestorPlugin); pluginRequestMessage.setAdditionalInputs(additionalInputs); pluginRequestMessage.setStatus(action.asText()); - pluginRequestMessage.setUserId(registryHelper.getKeycloakUserId(request)); + pluginRequestMessage.setUserId(registryHelper.getPrincipalUserId()); PluginRouter.route(pluginRequestMessage); responseParams.setStatus(Response.Status.SUCCESSFUL); @@ -132,7 +130,7 @@ public ResponseEntity riseAttestation(HttpServletRequest request, @Reque try { // Generate property Data - String userId = registryHelper.getUserId(request, attestationRequest.getEntityName()); + String userId = registryHelper.getUserId(attestationRequest.getEntityName()); String emailId = registryHelper.fetchEmailIdFromToken(request, attestationRequest.getEntityName()); JsonNode entityNode = registryHelper.readEntity(userId, attestationRequest.getEntityName(), attestationRequest.getEntityId(), false, null, false) diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryEntityController.java b/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryEntityController.java index 5b29c2af9..430b4bdf9 100644 --- a/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryEntityController.java +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/controller/RegistryEntityController.java @@ -5,16 +5,17 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; -import dev.sunbirdrc.keycloak.OwnerCreationException; import dev.sunbirdrc.pojos.AsyncRequest; import dev.sunbirdrc.pojos.PluginResponseMessage; import dev.sunbirdrc.pojos.Response; import dev.sunbirdrc.pojos.ResponseParams; import dev.sunbirdrc.registry.entities.AttestationPolicy; +import dev.sunbirdrc.registry.authorization.pojos.UserToken; import dev.sunbirdrc.registry.exception.AttestationNotFoundException; import dev.sunbirdrc.registry.exception.ErrorMessages; import dev.sunbirdrc.registry.exception.RecordNotFoundException; import dev.sunbirdrc.registry.exception.UnAuthorizedException; +import dev.sunbirdrc.registry.identity_providers.pojos.IdentityException; import dev.sunbirdrc.registry.middleware.MiddlewareHaltException; import dev.sunbirdrc.registry.middleware.util.Constants; import dev.sunbirdrc.registry.middleware.util.JSONUtil; @@ -31,8 +32,6 @@ import org.apache.tinkerpop.gremlin.structure.Vertex; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -41,6 +40,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; @@ -104,7 +104,7 @@ public ResponseEntity invite( createSchemaNotFoundResponse(e.getMessage(), responseParams); response = new Response(Response.API_ID.INVITE, "ERROR", responseParams); return new ResponseEntity<>(response, HttpStatus.NOT_FOUND); - } catch (MiddlewareHaltException | ValidationException | OwnerCreationException e) { + } catch (MiddlewareHaltException | ValidationException | IdentityException e) { return badRequestException(responseParams, response, e.getMessage()); } catch (UnAuthorizedException unAuthorizedException) { return createUnauthorizedExceptionResponse(unAuthorizedException); @@ -112,7 +112,7 @@ public ResponseEntity invite( if (e.getCause() != null && e.getCause().getCause() != null && e.getCause().getCause() instanceof InvocationTargetException) { Throwable targetException = ((InvocationTargetException) (e.getCause().getCause())).getTargetException(); - if (targetException instanceof OwnerCreationException) { + if (targetException instanceof IdentityException) { return badRequestException(responseParams, response, targetException.getMessage()); } } @@ -479,13 +479,11 @@ private ObjectNode copyWhiteListedFields(ArrayList fields, JsonNode data } private ArrayList getConsentFields(HttpServletRequest request) { + UserToken userToken = (UserToken) SecurityContextHolder.getContext().getAuthentication(); ArrayList fields = new ArrayList<>(); - KeycloakAuthenticationToken principal = (KeycloakAuthenticationToken) request.getUserPrincipal(); try { - Map otherClaims = ((KeycloakPrincipal) principal.getPrincipal()).getKeycloakSecurityContext().getToken().getOtherClaims(); - if (otherClaims.keySet().contains(dev.sunbirdrc.registry.Constants.KEY_CONSENT) && otherClaims.get(dev.sunbirdrc.registry.Constants.KEY_CONSENT) instanceof Map) { - Map consentFields = (Map) otherClaims.get(dev.sunbirdrc.registry.Constants.KEY_CONSENT); - for (Object key : consentFields.keySet()) { + if (userToken != null && userToken.getConsentFields().size() > 0) { + for (Object key : userToken.getConsentFields().keySet()) { fields.add(key.toString()); } } @@ -617,7 +615,7 @@ public ResponseEntity getEntity( } private String getUserId(String entityName, HttpServletRequest request) throws Exception { - return registryHelper.getUserId(request, entityName); + return registryHelper.getUserId(entityName); } private void addJsonLDSpec(JsonNode node) { diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/helper/EntityStateHelper.java b/java/registry/src/main/java/dev/sunbirdrc/registry/helper/EntityStateHelper.java index c3d171d28..5702d7f97 100644 --- a/java/registry/src/main/java/dev/sunbirdrc/registry/helper/EntityStateHelper.java +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/helper/EntityStateHelper.java @@ -17,6 +17,7 @@ import dev.sunbirdrc.workflow.RuleEngineService; import dev.sunbirdrc.workflow.StateContext; import org.apache.commons.math3.util.Pair; +import org.apache.logging.log4j.util.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -46,6 +47,11 @@ public class EntityStateHelper { private final ClaimRequestClient claimRequestClient; + @Value("${identity.set_default_password}") + private Boolean setDefaultPassword; + @Value("${identity.default_password}") + private String defaultPassword; + @Autowired public EntityStateHelper(IDefinitionsManager definitionsManager, RuleEngineService ruleEngineService, ConditionResolverService conditionResolverService, ClaimRequestClient claimRequestClient) { @@ -131,7 +137,13 @@ private ObjectNode createOwnershipNode(JsonNode entityNode, String entityName, O objectNode.put(MOBILE, entityNode.at(String.format("/%s%s", entityName, mobilePath)).asText("")); objectNode.put(EMAIL, entityNode.at(String.format("/%s%s", entityName, emailPath)).asText("")); objectNode.put(USER_ID, entityNode.at(String.format("/%s%s", entityName, userIdPath)).asText("")); - objectNode.put(PASSWORD, entityNode.at(String.format("/%s%s", entityName, passwordPath)).asText("")); + String password = entityNode.at(String.format("/%s%s", entityName, passwordPath)).asText(""); + if (Strings.isBlank(password)) { + if (setDefaultPassword && !Strings.isBlank(defaultPassword)) { + password = defaultPassword; + } + } + objectNode.put(PASSWORD, password); return objectNode; } diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/helper/RegistryHelper.java b/java/registry/src/main/java/dev/sunbirdrc/registry/helper/RegistryHelper.java index 51327ae9b..10dff1858 100644 --- a/java/registry/src/main/java/dev/sunbirdrc/registry/helper/RegistryHelper.java +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/helper/RegistryHelper.java @@ -17,10 +17,8 @@ import dev.sunbirdrc.pojos.attestation.States; import dev.sunbirdrc.pojos.attestation.exception.PolicyNotFoundException; import dev.sunbirdrc.registry.dao.VertexReader; -import dev.sunbirdrc.registry.entities.AttestationPolicy; -import dev.sunbirdrc.registry.entities.AttestationType; -import dev.sunbirdrc.registry.entities.FlowType; -import dev.sunbirdrc.registry.entities.RevokedCredential; +import dev.sunbirdrc.registry.authorization.pojos.UserToken; +import dev.sunbirdrc.registry.entities.*; import dev.sunbirdrc.registry.exception.SignatureException; import dev.sunbirdrc.registry.exception.UnAuthorizedException; import dev.sunbirdrc.registry.middleware.MiddlewareHaltException; @@ -48,7 +46,6 @@ import org.apache.commons.lang3.StringUtils; import org.apache.tinkerpop.gremlin.structure.Vertex; import org.jetbrains.annotations.NotNull; -import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -56,6 +53,8 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.lang.Nullable; import org.springframework.scheduling.annotation.Async; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.util.DigestUtils; import org.springframework.web.bind.annotation.PathVariable; @@ -71,7 +70,8 @@ import static dev.sunbirdrc.registry.Constants.*; import static dev.sunbirdrc.registry.exception.ErrorMessages.*; import static dev.sunbirdrc.registry.middleware.util.Constants.*; -import static dev.sunbirdrc.registry.middleware.util.OSSystemFields.*; +import static dev.sunbirdrc.registry.middleware.util.OSSystemFields._osState; +import static dev.sunbirdrc.registry.middleware.util.OSSystemFields.osOwner; /** * This is helper class, user-service calls this class in-order to access registry functionality @@ -161,6 +161,8 @@ public class RegistryHelper { @Value("${workflow.enabled:true}") private boolean workflowEnabled; + @Value("${attestationPolicy.search_enabled:false}") + private boolean attestationPolicySearchEnabled; @Value("${view_template.decrypt_private_fields:false}") private boolean viewTemplateDecryptPrivateFields; @@ -714,43 +716,41 @@ public boolean doesEntityContainOwnershipAttributes(@PathVariable String entityN } } - public String getUserId(HttpServletRequest request, String entityName) throws Exception { + public String getUserId(String entityName) throws Exception { if (doesEntityOperationRequireAuthorization(entityName)) { - return fetchUserIdFromToken(request); + return fetchUserIdFromToken(); } else { return dev.sunbirdrc.registry.Constants.USER_ANONYMOUS; } } - private String fetchUserIdFromToken(HttpServletRequest request) throws Exception { + private String fetchUserIdFromToken() throws Exception { if(!securityEnabled){ return DEFAULT_USER; } - return getKeycloakUserId(request); + return getPrincipalUserId(); } - public String getKeycloakUserId(HttpServletRequest request) throws Exception { - KeycloakAuthenticationToken principal = (KeycloakAuthenticationToken) request.getUserPrincipal(); - if (principal != null) { - return principal.getAccount().getPrincipal().getName(); + public String getPrincipalUserId() throws Exception { + UserToken userToken = (UserToken) SecurityContextHolder.getContext().getAuthentication(); + if (userToken != null) { + return userToken.getUserId(); } throw new Exception("Forbidden"); } public String fetchEmailIdFromToken(HttpServletRequest request, String entityName) throws Exception { if (doesEntityContainOwnershipAttributes(entityName) || getManageRoles(entityName).size() > 0) { - KeycloakAuthenticationToken principal = (KeycloakAuthenticationToken) request.getUserPrincipal(); + UserToken principal = (UserToken) request.getUserPrincipal(); if (principal != null) { try{ - return principal.getAccount().getKeycloakSecurityContext().getToken().getEmail(); + return principal.getEmail(); }catch (Exception exception){ - return principal.getAccount().getPrincipal().getName(); + return principal.getName(); } } - return USER_ANONYMOUS; - } else { - return dev.sunbirdrc.registry.Constants.USER_ANONYMOUS; } + return USER_ANONYMOUS; } public JsonNode getRequestedUserDetails(HttpServletRequest request, String entityName) throws Exception { @@ -778,7 +778,7 @@ private JsonNode getUserInfoFromKeyCloak(HttpServletRequest request, String enti } private JsonNode getUserInfoFromRegistry(HttpServletRequest request, String entityName) throws Exception { - String userId = getUserId(request,entityName); + String userId = getUserId(entityName); if (userId != null) { ObjectNode payload = getSearchByOwnerQuery(entityName, userId); @@ -801,7 +801,7 @@ private ObjectNode getSearchByOwnerQuery(String entityName, String userId) { } public String authorize(String entityName, String entityId, HttpServletRequest request) throws Exception { - String userIdFromRequest = getUserId(request, entityName); + String userIdFromRequest = getUserId(entityName); if (getManageRoles(entityName).size() > 0) { try { return authorizeManageEntity(request, entityName); @@ -886,7 +886,7 @@ public String authorizeDeleteEntity(HttpServletRequest request, String entityNam return entityName; } Set userRoles = getUserRolesFromRequest(request); - String userIdFromRequest = getUserId(request, entityName); + String userIdFromRequest = getUserId(entityName); JsonNode response = readEntity(userIdFromRequest, entityName, entityId, false, null, false); JsonNode entityFromDB = response.get(entityName); final boolean hasNoValidRole = !deleteRoles.isEmpty() && deleteRoles.stream().noneMatch(userRoles::contains); @@ -906,7 +906,7 @@ public String authorizeManageEntity(HttpServletRequest request, String entityNam } Set userRoles = getUserRolesFromRequest(request); authorizeUserRole(userRoles, managingRoles); - return fetchUserIdFromToken(request); + return fetchUserIdFromToken(); } else { return ROLE_ANONYMOUS; } @@ -924,8 +924,12 @@ private List getManageRoles(String entityName) { } private Set getUserRolesFromRequest(HttpServletRequest request) { - KeycloakAuthenticationToken userPrincipal = (KeycloakAuthenticationToken) request.getUserPrincipal(); - return userPrincipal!=null ? userPrincipal.getAccount().getRoles():Collections.emptySet(); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + Set roles = new HashSet<>(); + if (authentication != null) { + authentication.getAuthorities().forEach(authority -> roles.add(authority.getAuthority())); + } + return roles; } private void authorizeUserRole(Set userRoles, List allowedRoles) throws Exception { @@ -935,23 +939,18 @@ private void authorizeUserRole(Set userRoles, List allowedRoles) } public void authorizeAttestor(String entity, HttpServletRequest request) throws Exception { - List keyCloakEntities = getUserEntities(request); + List userEntities = getUserEntities(request); Set allTheAttestorEntities = definitionsManager.getDefinition(entity) .getOsSchemaConfiguration() .getAllTheAttestorEntities(); - if (keyCloakEntities.stream().noneMatch(allTheAttestorEntities::contains)) { + if (userEntities.stream().noneMatch(allTheAttestorEntities::contains)) { throw new Exception(UNAUTHORIZED_EXCEPTION_MESSAGE); } } public List getUserEntities(HttpServletRequest request) { - KeycloakAuthenticationToken principal = (KeycloakAuthenticationToken) request.getUserPrincipal(); - Object customAttributes = principal.getAccount() - .getKeycloakSecurityContext() - .getToken() - .getOtherClaims() - .get("entity"); - return (List) customAttributes; + UserToken principal = (UserToken) request.getUserPrincipal(); + return principal.getEntities(); } @Async @@ -1048,21 +1047,25 @@ public List getAttestationPolicies(String entityName) { } private List getAttestationsFromRegistry(String entityName) { - try { - JsonNode searchRequest = objectMapper.readTree("{\n" + - " \"entityType\": [\n" + - " \"" + ATTESTATION_POLICY + "\"\n" + - " ],\n" + - " \"filters\": {\n" + - " \"entity\": {\n" + - " \"eq\": \"" + entityName + "\"\n" + - " }\n" + - " }\n" + - "}"); - JsonNode searchResponse = searchEntity(searchRequest); - return convertJsonNodeToAttestationList(searchResponse); - } catch (Exception e) { - logger.error("Error fetching attestation policy", e); + if (attestationPolicySearchEnabled) { + try { + JsonNode searchRequest = objectMapper.readTree("{\n" + + " \"entityType\": [\n" + + " \"" + ATTESTATION_POLICY + "\"\n" + + " ],\n" + + " \"filters\": {\n" + + " \"entity\": {\n" + + " \"eq\": \"" + entityName + "\"\n" + + " }\n" + + " }\n" + + "}"); + JsonNode searchResponse = searchEntity(searchRequest); + return convertJsonNodeToAttestationList(searchResponse); + } catch (Exception e) { + logger.error("Error fetching attestation policy", e); + return Collections.emptyList(); + } + } else { return Collections.emptyList(); } } diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/service/SchemaService.java b/java/registry/src/main/java/dev/sunbirdrc/registry/service/SchemaService.java index 746e14105..987962198 100644 --- a/java/registry/src/main/java/dev/sunbirdrc/registry/service/SchemaService.java +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/service/SchemaService.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; +import dev.sunbirdrc.registry.authorization.SchemaAuthFilter; import dev.sunbirdrc.registry.entities.SchemaStatus; import dev.sunbirdrc.registry.exception.SchemaException; import dev.sunbirdrc.registry.middleware.util.JSONUtil; @@ -17,8 +18,8 @@ import java.io.IOException; -import static dev.sunbirdrc.registry.Constants.PATH; -import static dev.sunbirdrc.registry.Constants.Schema; +import static dev.sunbirdrc.registry.Constants.*; +import static dev.sunbirdrc.registry.helper.RegistryHelper.ROLE_ANONYMOUS; import static dev.sunbirdrc.registry.exception.ErrorMessages.NOT_ALLOWED_FOR_PUBLISHED_SCHEMA; @Service @@ -33,13 +34,28 @@ public class SchemaService { @Autowired private IValidate validator; + @Autowired + private SchemaAuthFilter schemaAuthFilter; + public void deleteSchemaIfExists(Vertex vertex) throws SchemaException { if (vertex.property(STATUS) != null && vertex.property(STATUS).value().equals(SchemaStatus.PUBLISHED.toString())) { throw new SchemaException(NOT_ALLOWED_FOR_PUBLISHED_SCHEMA); } JsonNode schema = JsonNodeFactory.instance.textNode(vertex.property(Schema.toLowerCase()).value().toString()); - definitionsManager.removeDefinition(schema); - validator.removeDefinition(schema); + try { + String schemaName = getSchemaName(schema); + definitionsManager.removeDefinition(schemaName); + validator.removeDefinition(schemaName); + schemaAuthFilter.removeSchema(schemaName); + } catch (JsonProcessingException e) { + throw new SchemaException("Removing schemas from resources failed"); + } + } + + private String getSchemaName(JsonNode jsonNode) throws JsonProcessingException { + String schemaAsText = jsonNode.asText("{}"); + JsonNode schemaJsonNode = new ObjectMapper().readTree(schemaAsText); + return schemaJsonNode.get(TITLE).asText(); } @@ -53,17 +69,28 @@ public void addSchema(JsonNode schemaNode) throws IOException, SchemaException { if (definitionsManager.getDefinition(definition.getTitle()) == null) { definitionsManager.appendNewDefinition(definition); validator.addDefinitions(schema); + addAnonymousSchemaToFilter(definition); } else { throw new SchemaException("Duplicate Error: Schema already exists"); } } } + private void addAnonymousSchemaToFilter(Definition definition) { + if (definition.getOsSchemaConfiguration().getInviteRoles().contains(ROLE_ANONYMOUS)) { + schemaAuthFilter.appendAnonymousInviteSchema(definition.getTitle()); + } + if (definition.getOsSchemaConfiguration().getRoles().contains(ROLE_ANONYMOUS)) { + schemaAuthFilter.appendAnonymousSchema(definition.getTitle()); + } + } + public void updateSchema(JsonNode updatedSchema) throws IOException { JsonNode schemaNode = updatedSchema.get(Schema); if (schemaNode.get(STATUS) != null && schemaNode.get(STATUS).textValue().equals(SchemaStatus.PUBLISHED.toString())) { JsonNode schema = schemaNode.get(Schema.toLowerCase()); - definitionsManager.appendNewDefinition(schema); + Definition definition = definitionsManager.appendNewDefinition(schema); + addAnonymousSchemaToFilter(definition); validator.addDefinitions(schema); } } diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/service/impl/RegistryAsyncServiceImpl.java b/java/registry/src/main/java/dev/sunbirdrc/registry/service/impl/RegistryAsyncServiceImpl.java index 9c4bc2fe9..2b6faec6f 100644 --- a/java/registry/src/main/java/dev/sunbirdrc/registry/service/impl/RegistryAsyncServiceImpl.java +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/service/impl/RegistryAsyncServiceImpl.java @@ -3,11 +3,11 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import dev.sunbirdrc.pojos.AsyncRequest; +import dev.sunbirdrc.registry.authorization.pojos.UserToken; import dev.sunbirdrc.registry.model.dto.CreateEntityMessage; import dev.sunbirdrc.registry.service.RegistryService; import dev.sunbirdrc.registry.sink.shard.Shard; import org.jetbrains.annotations.NotNull; -import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -38,10 +38,10 @@ public class RegistryAsyncServiceImpl extends RegistryServiceImpl implements Reg @Override public String addEntity(Shard shard, String userId, JsonNode inputJson, boolean skipSignature) throws Exception { - KeycloakAuthenticationToken keycloakAuthenticationToken = (KeycloakAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + UserToken authenticationToken = (UserToken) SecurityContextHolder.getContext().getAuthentication(); CreateEntityMessage createEntityMessage = CreateEntityMessage.builder().userId(userId).inputJson(inputJson) .skipSignature(skipSignature).webhookUrl(asyncRequest.getWebhookUrl()) - .emailId(keycloakAuthenticationToken == null ? "" : keycloakAuthenticationToken.getAccount().getKeycloakSecurityContext().getToken().getEmail()) + .emailId(authenticationToken == null ? "" : authenticationToken.getEmail()) .build(); String message = objectMapper.writeValueAsString(createEntityMessage); String transactionId = UUID.randomUUID().toString(); diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/util/DefinitionsManager.java b/java/registry/src/main/java/dev/sunbirdrc/registry/util/DefinitionsManager.java index 2eba53a0a..3a5d7cfc3 100644 --- a/java/registry/src/main/java/dev/sunbirdrc/registry/util/DefinitionsManager.java +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/util/DefinitionsManager.java @@ -134,11 +134,14 @@ public boolean isValidEntityName(String entityName) { } @Override - public void appendNewDefinition(JsonNode jsonNode) { + public Definition appendNewDefinition(JsonNode jsonNode) { try { - appendNewDefinition(Definition.toDefinition(jsonNode)); + Definition definition = Definition.toDefinition(jsonNode); + appendNewDefinition(definition); + return definition; } catch (Exception e) { logger.error("Failed loading schema from DB", e); + throw new RuntimeException(e); } } @@ -150,12 +153,22 @@ public void appendNewDefinition(Definition definition) { definitionMap.put(definition.getTitle(), definition); } + @Override public void removeDefinition(JsonNode jsonNode) { try { String schemaAsText = jsonNode.asText("{}"); JsonNode schemaJsonNode = objectMapper.readTree(schemaAsText); String schemaTitle = schemaJsonNode.get(TITLE).asText(); - definitionMap.remove(schemaTitle); + removeDefinition(schemaTitle); + } catch (Exception e) { + logger.error("Failed removing schema from definition manager", e); + } + } + + @Override + public void removeDefinition(String schema) { + try { + definitionMap.remove(schema); } catch (Exception e) { logger.error("Failed removing schema from definition manager", e); } diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/util/DistributedDefinitionsManager.java b/java/registry/src/main/java/dev/sunbirdrc/registry/util/DistributedDefinitionsManager.java index 6deece463..1d477bbf7 100644 --- a/java/registry/src/main/java/dev/sunbirdrc/registry/util/DistributedDefinitionsManager.java +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/util/DistributedDefinitionsManager.java @@ -153,9 +153,11 @@ public boolean isValidEntityName(String entityName) { } @Override - public void appendNewDefinition(JsonNode jsonNode) { + public Definition appendNewDefinition(JsonNode jsonNode) { try { - appendNewDefinition(Definition.toDefinition(jsonNode)); + Definition definition = Definition.toDefinition(jsonNode); + appendNewDefinition(definition); + return definition; } catch (Exception e) { throw new RuntimeException(e); } @@ -170,14 +172,23 @@ public void appendNewDefinition(Definition definition) { @Override public void removeDefinition(JsonNode jsonNode) { - try(Jedis jedis = jedisPool.getResource()) { + try{ String schemaAsText = jsonNode.asText("{}"); JsonNode schemaJsonNode = objectMapper.readTree(schemaAsText); String schemaTitle = SCHEMA + schemaJsonNode.get(TITLE).asText(); - jedis.del(schemaTitle); + removeDefinition(schemaTitle); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } + + @Override + public void removeDefinition(String schema) { + try(Jedis jedis = jedisPool.getResource()) { + jedis.del(schema); + } catch (Exception e) { + throw new RuntimeException(e); + } + } } diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/util/IDefinitionsManager.java b/java/registry/src/main/java/dev/sunbirdrc/registry/util/IDefinitionsManager.java index 55717256c..2e268b797 100644 --- a/java/registry/src/main/java/dev/sunbirdrc/registry/util/IDefinitionsManager.java +++ b/java/registry/src/main/java/dev/sunbirdrc/registry/util/IDefinitionsManager.java @@ -80,9 +80,10 @@ default Map getCertificateTemplates(String entityName) { return getDefinition(entityName).getOsSchemaConfiguration().getCertificateTemplates(); } boolean isValidEntityName(String entityName); - void appendNewDefinition(JsonNode jsonNode); + Definition appendNewDefinition(JsonNode jsonNode); void appendNewDefinition(Definition definition); void removeDefinition(JsonNode jsonNode); + void removeDefinition(String schema); default List getEntitiesWithAnonymousInviteRoles() { List anonymousEntities = new ArrayList<>(); diff --git a/java/registry/src/main/resources/application.yml b/java/registry/src/main/resources/application.yml index aefd99c47..c3512d63b 100644 --- a/java/registry/src/main/resources/application.yml +++ b/java/registry/src/main/resources/application.yml @@ -14,6 +14,19 @@ spring: ansi: enabled: ALWAYS + +oauth2: + resources: + - uri: ${oauth2_resource_uri:http://localhost:8080/auth/realms/sunbird-rc} + properties: + emailPath: ${oauth2_resource_email_path:email} + consentPath: ${oauth2_resource_consent_path:consent} + rolesPath: ${oauth2_resource_roles_path:realm_access.roles} + entityPath: ${oauth2_resource_entity_path:entity} + userIdPath: ${oauth2_resource_user_id_path:sub} + + + name: default-yaml environment: default @@ -59,6 +72,7 @@ registry: schema: url: ${registry_schema_url:http://localhost:8081/_schemas/} attestationPolicy: + search_enabled: ${search_attestation_db:false} createAccess: ${create_attestation_access_entities:User} manager: type: ${manager_type:DefinitionsManager} @@ -230,6 +244,16 @@ audit: suffix: ${audit_suffix:Audit} suffixSeparator: ${audit_suffixSeparator:_} +identity: + provider: ${identity_provider:dev.sunbirdrc.auth.keycloak.KeycloakProviderImpl} + url: ${sunbird_sso_url:http://localhost:8080/auth/} + realm: ${sunbird_sso_realm:sunbird-rc} + client_id: ${sunbird_sso_admin_client_id:admin-api} + client_secret: ${sunbird_sso_admin_client_secret:xxx} + set_default_password: ${sunbird_keycloak_user_set_password:false} + default_password: ${sunbird_keycloak_user_password:abcd@1234} + user_actions: ${identity_user_actions:} + keycloak: #publicKey: ${sunbird_sso_publickey:pk} auth-server-url: ${sunbird_sso_url:http://localhost:8080/auth} diff --git a/java/registry/src/main/resources/internal/_schemas/Schema.json b/java/registry/src/main/resources/internal/_schemas/Schema.json index c250426e5..6ef6c3c5b 100644 --- a/java/registry/src/main/resources/internal/_schemas/Schema.json +++ b/java/registry/src/main/resources/internal/_schemas/Schema.json @@ -52,4 +52,4 @@ "inviteRoles": ["admin"], "ownershipAttributes": [] } -} \ No newline at end of file +} diff --git a/java/registry/src/test/java/dev/sunbirdrc/registry/controller/RegistryEntityControllerTest.java b/java/registry/src/test/java/dev/sunbirdrc/registry/controller/RegistryEntityControllerTest.java index 200043d46..981b2b44d 100644 --- a/java/registry/src/test/java/dev/sunbirdrc/registry/controller/RegistryEntityControllerTest.java +++ b/java/registry/src/test/java/dev/sunbirdrc/registry/controller/RegistryEntityControllerTest.java @@ -98,7 +98,7 @@ public void testGetAttestationCertificate_success() throws Exception { mockHttpServletRequest.addHeader("accept", "application/pdf"); mockHttpServletRequest.addHeader("template", "http://dummy.com"); try { - Mockito.when(registryHelper.getUserId(mockHttpServletRequest, "Institute")).thenReturn("anonymous"); + Mockito.when(registryHelper.getUserId("Institute")).thenReturn("anonymous"); } catch (Exception e) { throw new RuntimeException(e); } @@ -120,7 +120,7 @@ public void testGetAttestationCertificate_failureWhenClaimNotAttested() throws E .get("/api/v1/Institute/123/attestation/instituteAffiliation/456") .with(mockHttpServletRequest -> { try { - Mockito.when(registryHelper.getUserId(mockHttpServletRequest, "Institute")).thenReturn("anonymous"); + Mockito.when(registryHelper.getUserId("Institute")).thenReturn("anonymous"); } catch (Exception e) { throw new RuntimeException(e); } @@ -142,7 +142,7 @@ public void testGetAttestationCertificate_failureWhenUserNotAllowed() throws Exc .get("/api/v1/Institute/123/attestation/instituteAffiliation/456") .with(mockHttpServletRequest -> { try { - Mockito.when(registryHelper.getUserId(mockHttpServletRequest, "Institute")).thenThrow(new Exception("Forbidden")); + Mockito.when(registryHelper.getUserId("Institute")).thenThrow(new Exception("Forbidden")); } catch (Exception e) { throw new RuntimeException(e); } @@ -168,7 +168,7 @@ public void testGetAttestationCertificate_failureIfRecordNotFound() throws Excep mockHttpServletRequest.addHeader("accept", "application/pdf"); mockHttpServletRequest.addHeader("template", "http://dummy.com"); try { - Mockito.when(registryHelper.getUserId(mockHttpServletRequest, "Institute")).thenThrow(new RecordNotFoundException("Invalid id")); + Mockito.when(registryHelper.getUserId("Institute")).thenThrow(new RecordNotFoundException("Invalid id")); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/java/registry/src/test/java/dev/sunbirdrc/registry/helper/EntityStateHelperTest.java b/java/registry/src/test/java/dev/sunbirdrc/registry/helper/EntityStateHelperTest.java index dfad4b1de..b35962812 100644 --- a/java/registry/src/test/java/dev/sunbirdrc/registry/helper/EntityStateHelperTest.java +++ b/java/registry/src/test/java/dev/sunbirdrc/registry/helper/EntityStateHelperTest.java @@ -2,9 +2,9 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import dev.sunbirdrc.keycloak.KeycloakAdminUtil; -import dev.sunbirdrc.keycloak.OwnerCreationException; import dev.sunbirdrc.registry.entities.AttestationPolicy; +import dev.sunbirdrc.registry.identity_providers.pojos.IdentityException; +import dev.sunbirdrc.registry.identity_providers.pojos.OwnerCreationException; import dev.sunbirdrc.registry.util.Definition; import dev.sunbirdrc.workflow.KieConfiguration; import dev.sunbirdrc.registry.exception.DuplicateRecordException; @@ -20,7 +20,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.kie.api.runtime.KieContainer; -import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.springframework.beans.factory.annotation.Autowired; @@ -29,6 +28,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.util.ReflectionTestUtils; +import dev.sunbirdrc.registry.identity_providers.pojos.IdentityManager; import java.io.File; import java.io.IOException; @@ -39,6 +39,7 @@ 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.when; @@ -56,7 +57,7 @@ public class EntityStateHelperTest { ClaimRequestClient claimRequestClient; @Mock - KeycloakAdminUtil keycloakAdminUtil; + IdentityManager identityManager; DefinitionsManager definitionsManager; @@ -81,9 +82,10 @@ public void initMocks() throws IOException { } private void runTest(JsonNode existing, JsonNode updated, JsonNode expected, List attestationPolicies) throws IOException { - RuleEngineService ruleEngineService = new RuleEngineService(kieContainer, keycloakAdminUtil); + RuleEngineService ruleEngineService = new RuleEngineService(kieContainer, identityManager); EntityStateHelper entityStateHelper = new EntityStateHelper(definitionsManager, ruleEngineService, conditionResolverService, claimRequestClient); ReflectionTestUtils.setField(entityStateHelper, "uuidPropertyName", "osid"); + ReflectionTestUtils.setField(entityStateHelper, "setDefaultPassword", false); updated = entityStateHelper.applyWorkflowTransitions(existing, updated, attestationPolicies); assertEquals(expected, updated); } @@ -112,22 +114,22 @@ public void shouldBeNoStateChangeIfTheDataDidNotChange() throws IOException { } @Test - public void shouldCreateNewOwnersForNewlyAddedOwnerFields() throws IOException, DuplicateRecordException, EntityCreationException, OwnerCreationException { - when(keycloakAdminUtil.createUser(anyString(), anyString(), anyString(), anyString(), anyString())).thenReturn("456"); + public void shouldCreateNewOwnersForNewlyAddedOwnerFields() throws IOException, DuplicateRecordException, EntityCreationException, IdentityException { + when(identityManager.createUser(any())).thenReturn("456"); JsonNode test = m.readTree(new File(getBaseDir() + "shouldAddNewOwner.json")); runTest(test.get("existing"), test.get("updated"), test.get("expected"), Collections.emptyList()); } @Test - public void shouldNotCreateNewOwners() throws IOException, DuplicateRecordException, EntityCreationException, OwnerCreationException { - when(keycloakAdminUtil.createUser(anyString(), anyString(), anyString(), anyString(), anyString())).thenReturn("456"); + public void shouldNotCreateNewOwners() throws IOException, DuplicateRecordException, EntityCreationException, IdentityException { + when(identityManager.createUser(any())).thenReturn("456"); JsonNode test = m.readTree(new File(getBaseDir() + "shouldNotAddNewOwner.json")); runTest(test.get("existing"), test.get("updated"), test.get("expected"), Collections.emptyList()); } @Test - public void shouldNotModifyExistingOwners() throws IOException, DuplicateRecordException, EntityCreationException, OwnerCreationException { - when(keycloakAdminUtil.createUser(anyString(), anyString(), anyString(), anyString(), anyString())).thenReturn("456"); + public void shouldNotModifyExistingOwners() throws IOException, DuplicateRecordException, EntityCreationException, IdentityException { + when(identityManager.createUser(any())).thenReturn("456"); JsonNode test = m.readTree(new File(getBaseDir() + "shouldNotModifyExistingOwner.json")); runTest(test.get("existing"), test.get("updated"), test.get("expected"), Collections.emptyList()); } @@ -145,10 +147,10 @@ public void shouldNotAllowUserModifyingSystemFields() throws IOException, Duplic } @Test - public void shouldRemovePasswordOwnershipFields() throws IOException, OwnerCreationException { - when(keycloakAdminUtil.createUser(anyString(), anyString(), anyString(), anyString(), anyString())).thenReturn("456"); + public void shouldRemovePasswordOwnershipFields() throws IOException, OwnerCreationException, IdentityException { + when(identityManager.createUser(any())).thenReturn("456"); JsonNode test = m.readTree(new File(getBaseDir() + "shouldRemovePasswordOwnershipFields.json")); runTest(test.get("existing"), test.get("updated"), test.get("expected"), Collections.emptyList()); } -} \ No newline at end of file +} diff --git a/java/registry/src/test/java/dev/sunbirdrc/registry/helper/RegistryHelperTest.java b/java/registry/src/test/java/dev/sunbirdrc/registry/helper/RegistryHelperTest.java index 5dcd4c904..e8e510e02 100644 --- a/java/registry/src/test/java/dev/sunbirdrc/registry/helper/RegistryHelperTest.java +++ b/java/registry/src/test/java/dev/sunbirdrc/registry/helper/RegistryHelperTest.java @@ -8,11 +8,11 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; -import dev.sunbirdrc.keycloak.KeycloakAdminUtil; import dev.sunbirdrc.pojos.AsyncRequest; import dev.sunbirdrc.pojos.PluginResponseMessage; import dev.sunbirdrc.pojos.SunbirdRCInstrumentation; import dev.sunbirdrc.registry.entities.AttestationPolicy; +import dev.sunbirdrc.registry.identity_providers.pojos.IdentityManager; import dev.sunbirdrc.registry.middleware.MiddlewareHaltException; import dev.sunbirdrc.registry.middleware.service.ConditionResolverService; import dev.sunbirdrc.registry.middleware.util.Constants; @@ -111,7 +111,7 @@ private String getBaseDir() { @Mock - private KeycloakAdminUtil keycloakAdminUtil; + private IdentityManager identityManager; @Mock private IValidate validationService; @@ -137,8 +137,9 @@ public void initMocks() { registryHelper.setObjectMapper(objectMapper); MockitoAnnotations.initMocks(this); registryHelper.uuidPropertyName = "osid"; - RuleEngineService ruleEngineService = new RuleEngineService(kieContainer, keycloakAdminUtil); + RuleEngineService ruleEngineService = new RuleEngineService(kieContainer, identityManager); registryHelper.entityStateHelper = new EntityStateHelper(definitionsManager, ruleEngineService, conditionResolverService, claimRequestClient); + ReflectionTestUtils.setField(registryHelper.entityStateHelper, "setDefaultPassword", false); registryHelper.setDefinitionsManager(definitionsManager); } @@ -345,7 +346,7 @@ public void shouldCreateOwnersForInvite() throws Exception { JsonNode inviteJson = new ObjectMapper().readTree("{\"Institute\":{\"email\":\"gecasu.ihises@tovinit.com\",\"instituteName\":\"gecasu\"}}"); mockDefinitionManager(); String testUserId = "be6d30e9-7c62-4a05-b4c8-ee28364da8e4"; - when(keycloakAdminUtil.createUser(any(), any(), any(), any(), any())).thenReturn(testUserId); + when(identityManager.createUser(any())).thenReturn(testUserId); when(registryService.addEntity(any(), any(), any(), anyBoolean())).thenReturn(UUID.randomUUID().toString()); when(shardManager.getShard(any())).thenReturn(new Shard()); ReflectionTestUtils.setField(registryHelper, "workflowEnabled", true); @@ -361,7 +362,7 @@ public void shouldSendInviteInvitationsAfterCreatingOwners() throws Exception { JsonNode inviteJson = new ObjectMapper().readTree("{\"Institute\":{\"email\":\"gecasu.ihises@tovinit.com\",\"instituteName\":\"gecasu\"}}"); mockDefinitionManager(); String testUserId = "be6d30e9-7c62-4a05-b4c8-ee28364da8e4"; - when(keycloakAdminUtil.createUser(any(), any(), any(), any(), any())).thenReturn(testUserId); + when(identityManager.createUser(any())).thenReturn(testUserId); when(registryService.addEntity(any(), any(), any(), anyBoolean())).thenReturn(UUID.randomUUID().toString()); when(shardManager.getShard(any())).thenReturn(new Shard()); ReflectionTestUtils.setField(registryHelper, "notificationEnabled", true); @@ -390,7 +391,7 @@ public void shouldSendMultipleInviteInvitationsAfterCreatingOwners() throws Exce " \"adminMobile\": \"1234\"\n" + "}}"); String testUserId = "be6d30e9-7c62-4a05-b4c8-ee28364da8e4"; - when(keycloakAdminUtil.createUser(any(), any(), any(), any(), any())).thenReturn(testUserId); + when(identityManager.createUser(any())).thenReturn(testUserId); when(registryService.addEntity(any(), any(), any(), anyBoolean())).thenReturn(UUID.randomUUID().toString()); when(shardManager.getShard(any())).thenReturn(new Shard()); mockDefinitionManager(); @@ -502,6 +503,7 @@ public void shouldTriggerNextAttestationFlow() throws Exception { Config config = ConfigFactory.parseResources("sunbirdrc-actors.conf"); SunbirdActorFactory sunbirdActorFactory = new SunbirdActorFactory(config, "dev.sunbirdrc.actors"); sunbirdActorFactory.init("sunbirdrc-actors"); + ReflectionTestUtils.setField(registryHelper, "attestationPolicySearchEnabled", true); registryHelper.updateState(pluginResponseMessage); verify(registryHelper, times(1)).triggerAttestation(any(), any()); verify(notificationHelper, times(1)).sendNotification(any(), any()); @@ -533,6 +535,7 @@ public void shouldNotTriggerNextAttestationFlowIfOnCompleteIsNotPresent() throws Config config = ConfigFactory.parseResources("sunbirdrc-actors.conf"); SunbirdActorFactory sunbirdActorFactory = new SunbirdActorFactory(config, "dev.sunbirdrc.actors"); sunbirdActorFactory.init("sunbirdrc-actors"); + ReflectionTestUtils.setField(registryHelper, "attestationPolicySearchEnabled", true); registryHelper.updateState(pluginResponseMessage); verify(registryHelper, times(0)).triggerAttestation(any(), any()); } @@ -573,6 +576,7 @@ public void shouldTriggerConcatFunctionOnAttestationCompleted() throws Exception Config config = ConfigFactory.parseResources("sunbirdrc-actors.conf"); SunbirdActorFactory sunbirdActorFactory = new SunbirdActorFactory(config, "dev.sunbirdrc.actors"); sunbirdActorFactory.init("sunbirdrc-actors"); + ReflectionTestUtils.setField(registryHelper, "attestationPolicySearchEnabled", true); registryHelper.updateState(pluginResponseMessage); verify(functionExecutorMock, times(1)).execute(any(), any(), any()); } @@ -614,15 +618,16 @@ public void shouldTriggerProviderFunctionOnAttestationCompleted() throws Excepti Config config = ConfigFactory.parseResources("sunbirdrc-actors.conf"); SunbirdActorFactory sunbirdActorFactory = new SunbirdActorFactory(config, "dev.sunbirdrc.actors"); sunbirdActorFactory.init("sunbirdrc-actors"); + ReflectionTestUtils.setField(registryHelper, "attestationPolicySearchEnabled", true); registryHelper.updateState(pluginResponseMessage); verify(functionExecutorMock, times(1)).execute(any(), any(), any()); } @Test - public void shouldReturnTrueIfEntityContainsOwnershipAttributes() throws IOException { + public void shouldReturnFalseIfEntityContainsOwnershipAttributes() throws IOException { mockDefinitionManager(); String entity = "Student"; - Assert.assertTrue(registryHelper.doesEntityOperationRequireAuthorization(entity)); + Assert.assertFalse(registryHelper.doesEntityOperationRequireAuthorization(entity)); } @Test @@ -642,13 +647,6 @@ public void shouldReturnFalseIfEntityDoesContainRolesAndOwnership() throws IOExc assertFalse(registryHelper.doesEntityOperationRequireAuthorization(entity)); } - @Test - public void shouldDeleteReturnTrueIfEntityContainsOwnershipAttributes() throws IOException { - mockDefinitionManager(); - String entity = "Student"; - Assert.assertTrue(registryHelper.doesEntityOperationRequireAuthorization(entity)); - } - @Test public void shouldDeleteReturnTrueIfEntityContainsManageRoles() throws IOException { mockDefinitionManager(); @@ -783,6 +781,7 @@ public void shouldRaiseClaimIfAttestationTypeIsAutomated() throws Exception { objectNode.set("gender", JsonNodeFactory.instance.textNode("Male")); ReflectionTestUtils.setField(registryHelper, "workflowEnabled", true); doNothing().when(notificationHelper).sendNotification(any(), any()); + ReflectionTestUtils.setField(registryHelper, "attestationPolicySearchEnabled", true); registryHelper.autoRaiseClaim("Student", "12345", "556302c9-d8b4-4f60-9ac1-c16c8839a9f3", null, requestBody, ""); verify(conditionResolverService, times(1)).resolve(objectNode, REQUESTER, null, Collections.emptyList()); verify(registryHelper, times(1)).triggerAttestation(any(), any()); @@ -871,7 +870,7 @@ public void shouldRaiseRequiredExceptions() throws Exception { mockDefinitionManager(); mockValidationService(); String testUserId = "be6d30e9-7c62-4a05-b4c8-ee28364da8e4"; - when(keycloakAdminUtil.createUser(any(), any(), any(), any(), any())).thenReturn(testUserId); + when(identityManager.createUser(any())).thenReturn(testUserId); when(registryService.addEntity(any(), any(), any(), anyBoolean())).thenReturn(UUID.randomUUID().toString()); when(shardManager.getShard(any())).thenReturn(new Shard()); ReflectionTestUtils.setField(registryHelper, "workflowEnabled", true); @@ -889,7 +888,7 @@ public void shouldNotRaiseRequiredExceptionsIFFlagDisabled() throws Exception { mockDefinitionManager(); mockValidationService(); String testUserId = "be6d30e9-7c62-4a05-b4c8-ee28364da8e4"; - when(keycloakAdminUtil.createUser(any(), any(), any(), any(), any())).thenReturn(testUserId); + when(identityManager.createUser(any())).thenReturn(testUserId); when(registryService.addEntity(any(), any(), any(), anyBoolean())).thenReturn(UUID.randomUUID().toString()); when(shardManager.getShard(any())).thenReturn(new Shard()); ReflectionTestUtils.setField(registryHelper, "workflowEnabled", true); @@ -916,4 +915,46 @@ public void shouldUpdateEntityAndSendNotificationToOwners() throws Exception { verify(registryService, times(1)).updateEntity(any(), any(), any(), any(), anyBoolean()); verify(notificationHelper, times(1)).sendNotification(any(), any()); } + + @Test + public void shouldNotFetchAttestationPolicyFromDBIfDisabled() throws Exception { + mockDefinitionManager(); + ObjectNode attestationPolicyObject = JsonNodeFactory.instance.objectNode(); + ArrayNode attestationArrayNodes = JsonNodeFactory.instance.arrayNode(); + ObjectNode mockAttestationPolicy = JsonNodeFactory.instance.objectNode(); + mockAttestationPolicy.set("onComplete", JsonNodeFactory.instance.textNode("attestation:nextAttestationPolicy")); + mockAttestationPolicy.set("name", JsonNodeFactory.instance.textNode("testAttestationPolicy")); + attestationArrayNodes.add(mockAttestationPolicy); + ObjectNode mockAttestationPolicy2 = JsonNodeFactory.instance.objectNode(); + mockAttestationPolicy2.set("name", JsonNodeFactory.instance.textNode("nextAttestationPolicy")); + mockAttestationPolicy2.set("attestorPlugin", JsonNodeFactory.instance.textNode("did:internal:ClaimPluginActor?entity=board-cbse")); + attestationArrayNodes.add(mockAttestationPolicy2); + attestationPolicyObject.set(ATTESTATION_POLICY, attestationArrayNodes); + when(searchService.search(any())).thenReturn(attestationPolicyObject); + ReflectionTestUtils.setField(registryHelper, "attestationPolicySearchEnabled", false); + List policies = registryHelper.getAttestationPolicies("Student"); + assertEquals(1, policies.size()); + verify(searchService, never()).search(any()); + } + + @Test + public void shouldFetchAttestationPolicyFromDBIfEnabled() throws Exception { + mockDefinitionManager(); + ObjectNode attestationPolicyObject = JsonNodeFactory.instance.objectNode(); + ArrayNode attestationArrayNodes = JsonNodeFactory.instance.arrayNode(); + ObjectNode mockAttestationPolicy = JsonNodeFactory.instance.objectNode(); + mockAttestationPolicy.set("onComplete", JsonNodeFactory.instance.textNode("attestation:nextAttestationPolicy")); + mockAttestationPolicy.set("name", JsonNodeFactory.instance.textNode("testAttestationPolicy")); + attestationArrayNodes.add(mockAttestationPolicy); + ObjectNode mockAttestationPolicy2 = JsonNodeFactory.instance.objectNode(); + mockAttestationPolicy2.set("name", JsonNodeFactory.instance.textNode("nextAttestationPolicy")); + mockAttestationPolicy2.set("attestorPlugin", JsonNodeFactory.instance.textNode("did:internal:ClaimPluginActor?entity=board-cbse")); + attestationArrayNodes.add(mockAttestationPolicy2); + attestationPolicyObject.set(ATTESTATION_POLICY, attestationArrayNodes); + when(searchService.search(any())).thenReturn(attestationPolicyObject); + ReflectionTestUtils.setField(registryHelper, "attestationPolicySearchEnabled", true); + List policies = registryHelper.getAttestationPolicies("Student"); + assertEquals(3, policies.size()); + verify(searchService, atMostOnce()).search(any()); + } } diff --git a/java/registry/src/test/java/dev/sunbirdrc/registry/service/SchemaServiceTest.java b/java/registry/src/test/java/dev/sunbirdrc/registry/service/SchemaServiceTest.java index 711013073..8741c6e17 100644 --- a/java/registry/src/test/java/dev/sunbirdrc/registry/service/SchemaServiceTest.java +++ b/java/registry/src/test/java/dev/sunbirdrc/registry/service/SchemaServiceTest.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; +import dev.sunbirdrc.registry.authorization.SchemaAuthFilter; import dev.sunbirdrc.registry.entities.SchemaStatus; import dev.sunbirdrc.registry.exception.SchemaException; import dev.sunbirdrc.registry.middleware.util.Constants; @@ -20,12 +21,14 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; +import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.util.ReflectionTestUtils; import java.io.IOException; import java.nio.charset.Charset; +import java.util.ArrayList; import java.util.HashMap; import java.util.Map; @@ -44,6 +47,9 @@ public class SchemaServiceTest { @Mock IValidate validator; + + @Spy + SchemaAuthFilter schemaAuthFilter; @InjectMocks SchemaService schemaService; @@ -515,4 +521,95 @@ public void shouldNotAddSchemaToDefinitionManagerWithInternalSchemaNames() throw assertEquals("Duplicate Error: Internal schema \"Schema\" already exists", e.getMessage()); } } + + @Test + public void shouldAddSchemaToSchemFilter() throws IOException, SchemaException { + String schema = IOUtils.toString(this.getClass().getClassLoader().getResourceAsStream("Student.json"), Charset.defaultCharset()); + ObjectNode schemaNode = JsonNodeFactory.instance.objectNode(); + ObjectNode object = JsonNodeFactory.instance.objectNode(); + object.put(Schema.toLowerCase(), schema); + object.put("status", SchemaStatus.PUBLISHED.toString()); + schemaNode.set(Schema, object); + schemaService.addSchema(schemaNode); + verify(schemaAuthFilter, times(1)).appendAnonymousInviteSchema(anyString()); + } + + @Test + public void shouldAddSchemaToSchemFilterInviteAnonymous() throws IOException, SchemaException { + String schema = IOUtils.toString(this.getClass().getClassLoader().getResourceAsStream("Student.json"), Charset.defaultCharset()); + ObjectNode schemaNode = JsonNodeFactory.instance.objectNode(); + ObjectNode object = JsonNodeFactory.instance.objectNode(); + object.put(Schema.toLowerCase(), schema); + object.put("status", SchemaStatus.PUBLISHED.toString()); + schemaNode.set(Schema, object); + schemaService.addSchema(schemaNode); + verify(schemaAuthFilter, times(1)).appendAnonymousSchema(anyString()); + } + + @Test + public void shouldAddSchemaToAnonymousSchemaFilterOnUpdate() throws SchemaException, IOException { + JsonNode existingDefinition = objectMapper.readTree(definitionsManager.getDefinition(TRAINING_CERTIFICATE).getContent()); + + assertNull(existingDefinition.get("_osConfig").get("certificateTemplates")); + JsonNode updatedSchema = objectMapper.readTree("{\n" + + " \"Schema\": {\n" + + " \"schema\": \"{\\n \\\"$schema\\\": \\\"http://json-schema.org/draft-07/schema\\\",\\n \\\"type\\\": \\\"object\\\",\\n \\\"properties\\\": {\\n \\\"TrainingCertificate\\\": {\\n \\\"$ref\\\": \\\"#/definitions/TrainingCertificate\\\"\\n }\\n },\\n \\\"required\\\": [\\n \\\"TrainingCertificate\\\"\\n ],\\n \\\"title\\\": \\\"TrainingCertificate\\\",\\n \\\"definitions\\\": {\\n \\\"TrainingCertificate\\\": {\\n \\\"$id\\\": \\\"#/properties/TrainingCertificate\\\",\\n \\\"type\\\": \\\"object\\\",\\n \\\"title\\\": \\\"The TrainingCertificate Schema\\\",\\n \\\"required\\\": [\\n \\\"name\\\",\\n \\\"contact\\\"\\n ],\\n \\\"properties\\\": {\\n \\\"name\\\": {\\n \\\"type\\\": \\\"string\\\"\\n },\\n \\\"trainingTitle\\\": {\\n \\\"type\\\": \\\"string\\\"\\n },\\n \\\"contact\\\": {\\n \\\"type\\\": \\\"string\\\"\\n },\\n \\\"date\\\": {\\n \\\"type\\\": \\\"string\\\",\\n \\\"format\\\": \\\"date\\\"\\n },\\n \\\"note\\\": {\\n \\\"type\\\": \\\"string\\\"\\n }\\n }\\n }\\n },\\n \\\"_osConfig\\\": {\\n \\\"uniqueIndexFields\\\": [\\n \\\"contact\\\"\\n ],\\n \\\"ownershipAttributes\\\": [],\\n \\\"roles\\\": [\\\"anonymous\\\"],\\n \\\"inviteRoles\\\": [\\n \\\"anonymous\\\"\\n ],\\n \\\"enableLogin\\\": false,\\n \\\"credentialTemplate\\\": {\\n \\\"@context\\\": [\\n \\\"https://www.w3.org/2018/credentials/v1\\\",\\n \\\"https://gist.githubusercontent.com/dileepbapat/eb932596a70f75016411cc871113a789/raw/498e5af1d94784f114b32c1ab827f951a8a24def/skill\\\"\\n ],\\n \\\"type\\\": [\\n \\\"VerifiableCredential\\\"\\n ],\\n \\\"issuanceDate\\\": \\\"2021-08-27T10:57:57.237Z\\\",\\n \\\"credentialSubject\\\": {\\n \\\"type\\\": \\\"Person\\\",\\n \\\"name\\\": \\\"{{name}}\\\",\\n \\\"trainedOn\\\": \\\"{{trainingTitle}}\\\"\\n },\\n \\\"issuer\\\": \\\"did:web:sunbirdrc.dev/vc/skill\\\"\\n },\\n \\\"certificateTemplates\\\": {\\n \\\"html\\\": \\\"https://raw.githubusercontent.com/dileepbapat/ref-sunbirdrc-certificate/main/schemas/templates/TrainingCertificate.html\\\"\\n }\\n }\\n}\",\n" + + " \"osUpdatedAt\": \"2022-09-14T05:38:41.909Z\",\n" + + " \"osCreatedAt\": \"2022-09-14T05:34:04.862Z\",\n" + + " \"osUpdatedBy\": \"anonymous\",\n" + + " \"@type\": \"Schema\",\n" + + " \"name\": \"schema_new\",\n" + + " \"osCreatedBy\": \"anonymous\",\n" + + " \"osid\": \"756cea4b-93a0-44d5-affd-bb605cf30abd\",\n" + + " \"osOwner\": [\n" + + " \"anonymous\"\n" + + " ],\n" + + " \"status\": \"PUBLISHED\"\n" + + " }\n" + + "}"); + schemaService.updateSchema(updatedSchema); + JsonNode updatedDefinition = objectMapper.readTree(definitionsManager.getDefinition(TRAINING_CERTIFICATE).getContent()); + assertNotNull(updatedDefinition.get("_osConfig").get("certificateTemplates")); + assertEquals(1, updatedDefinition.get("_osConfig").get("certificateTemplates").size()); + verify(schemaAuthFilter, times(1)).appendAnonymousSchema(anyString()); + } + + @Test + public void shouldAddSchemaToAnonymousInviteSchemaFilterOnUpdate() throws SchemaException, IOException { + JsonNode existingDefinition = objectMapper.readTree(definitionsManager.getDefinition(TRAINING_CERTIFICATE).getContent()); + + assertNull(existingDefinition.get("_osConfig").get("certificateTemplates")); + JsonNode updatedSchema = objectMapper.readTree("{\n" + + " \"Schema\": {\n" + + " \"schema\": \"{\\n \\\"$schema\\\": \\\"http://json-schema.org/draft-07/schema\\\",\\n \\\"type\\\": \\\"object\\\",\\n \\\"properties\\\": {\\n \\\"TrainingCertificate\\\": {\\n \\\"$ref\\\": \\\"#/definitions/TrainingCertificate\\\"\\n }\\n },\\n \\\"required\\\": [\\n \\\"TrainingCertificate\\\"\\n ],\\n \\\"title\\\": \\\"TrainingCertificate\\\",\\n \\\"definitions\\\": {\\n \\\"TrainingCertificate\\\": {\\n \\\"$id\\\": \\\"#/properties/TrainingCertificate\\\",\\n \\\"type\\\": \\\"object\\\",\\n \\\"title\\\": \\\"The TrainingCertificate Schema\\\",\\n \\\"required\\\": [\\n \\\"name\\\",\\n \\\"contact\\\"\\n ],\\n \\\"properties\\\": {\\n \\\"name\\\": {\\n \\\"type\\\": \\\"string\\\"\\n },\\n \\\"trainingTitle\\\": {\\n \\\"type\\\": \\\"string\\\"\\n },\\n \\\"contact\\\": {\\n \\\"type\\\": \\\"string\\\"\\n },\\n \\\"date\\\": {\\n \\\"type\\\": \\\"string\\\",\\n \\\"format\\\": \\\"date\\\"\\n },\\n \\\"note\\\": {\\n \\\"type\\\": \\\"string\\\"\\n }\\n }\\n }\\n },\\n \\\"_osConfig\\\": {\\n \\\"uniqueIndexFields\\\": [\\n \\\"contact\\\"\\n ],\\n \\\"ownershipAttributes\\\": [],\\n \\\"roles\\\": [\\\"anonymous\\\"],\\n \\\"inviteRoles\\\": [\\n \\\"anonymous\\\"\\n ],\\n \\\"enableLogin\\\": false,\\n \\\"credentialTemplate\\\": {\\n \\\"@context\\\": [\\n \\\"https://www.w3.org/2018/credentials/v1\\\",\\n \\\"https://gist.githubusercontent.com/dileepbapat/eb932596a70f75016411cc871113a789/raw/498e5af1d94784f114b32c1ab827f951a8a24def/skill\\\"\\n ],\\n \\\"type\\\": [\\n \\\"VerifiableCredential\\\"\\n ],\\n \\\"issuanceDate\\\": \\\"2021-08-27T10:57:57.237Z\\\",\\n \\\"credentialSubject\\\": {\\n \\\"type\\\": \\\"Person\\\",\\n \\\"name\\\": \\\"{{name}}\\\",\\n \\\"trainedOn\\\": \\\"{{trainingTitle}}\\\"\\n },\\n \\\"issuer\\\": \\\"did:web:sunbirdrc.dev/vc/skill\\\"\\n },\\n \\\"certificateTemplates\\\": {\\n \\\"html\\\": \\\"https://raw.githubusercontent.com/dileepbapat/ref-sunbirdrc-certificate/main/schemas/templates/TrainingCertificate.html\\\"\\n }\\n }\\n}\",\n" + + " \"osUpdatedAt\": \"2022-09-14T05:38:41.909Z\",\n" + + " \"osCreatedAt\": \"2022-09-14T05:34:04.862Z\",\n" + + " \"osUpdatedBy\": \"anonymous\",\n" + + " \"@type\": \"Schema\",\n" + + " \"name\": \"schema_new\",\n" + + " \"osCreatedBy\": \"anonymous\",\n" + + " \"osid\": \"756cea4b-93a0-44d5-affd-bb605cf30abd\",\n" + + " \"osOwner\": [\n" + + " \"anonymous\"\n" + + " ],\n" + + " \"status\": \"PUBLISHED\"\n" + + " }\n" + + "}"); + schemaService.updateSchema(updatedSchema); + JsonNode updatedDefinition = objectMapper.readTree(definitionsManager.getDefinition(TRAINING_CERTIFICATE).getContent()); + assertNotNull(updatedDefinition.get("_osConfig").get("certificateTemplates")); + assertEquals(1, updatedDefinition.get("_osConfig").get("certificateTemplates").size()); + verify(schemaAuthFilter, times(1)).appendAnonymousInviteSchema(anyString()); + } + + @Test + public void shouldDeleteSchemaFromSchemaFilter() throws SchemaException { + assertEquals(1, definitionsManager.getAllKnownDefinitions().size()); + Vertex vertex = mock(Vertex.class); + VertexProperty vertexProperty = mock(VertexProperty.class); + Mockito.when(vertex.property(Schema.toLowerCase())).thenReturn(vertexProperty); + Mockito.when(vertexProperty.value()).thenReturn(trainingCertificateSchema); + schemaService.deleteSchemaIfExists(vertex); + verify(schemaAuthFilter, times(1)).removeSchema(anyString()); + } } diff --git a/java/registry/src/test/java/dev/sunbirdrc/registry/service/impl/RegistryServiceImplTest.java b/java/registry/src/test/java/dev/sunbirdrc/registry/service/impl/RegistryServiceImplTest.java index a7b9f4505..eaac60455 100644 --- a/java/registry/src/test/java/dev/sunbirdrc/registry/service/impl/RegistryServiceImplTest.java +++ b/java/registry/src/test/java/dev/sunbirdrc/registry/service/impl/RegistryServiceImplTest.java @@ -11,6 +11,7 @@ import dev.sunbirdrc.pojos.ComponentHealthInfo; import dev.sunbirdrc.pojos.HealthCheckResponse; import dev.sunbirdrc.pojos.HealthIndicator; +import dev.sunbirdrc.registry.authorization.SchemaAuthFilter; import dev.sunbirdrc.registry.dao.IRegistryDao; import dev.sunbirdrc.registry.dao.VertexReader; import dev.sunbirdrc.registry.dao.VertexWriter; @@ -144,6 +145,9 @@ public Constants.SchemaType getValidationType() throws IllegalArgumentException @Mock private IAuditService auditService; + @Mock + private SchemaAuthFilter schemaAuthFilter; + public void setup() throws IOException { MockitoAnnotations.initMocks(this); ReflectionTestUtils.setField(encryptionService, "encryptionServiceHealthCheckUri", "encHealthCheckUri"); @@ -153,6 +157,7 @@ public void setup() throws IOException { ReflectionTestUtils.setField(registryService, "definitionsManager", definitionsManager); ReflectionTestUtils.setField(schemaService, "definitionsManager", definitionsManager); ReflectionTestUtils.setField(schemaService, "validator", jsonValidationService); + ReflectionTestUtils.setField(schemaService, "schemaAuthFilter", schemaAuthFilter); ReflectionTestUtils.setField(registryService, "schemaService", schemaService); ReflectionTestUtils.setField(registryService, "objectMapper", objectMapper); ReflectionTestUtils.setField(registryService, "eventService", eventService); diff --git a/java/registry/src/test/resources/Student.json b/java/registry/src/test/resources/Student.json index de4f66861..6d47912ff 100644 --- a/java/registry/src/test/resources/Student.json +++ b/java/registry/src/test/resources/Student.json @@ -161,6 +161,8 @@ "mobile": "/contactDetails/mobile", "userId": "/contactDetails/mobile" } - ] + ], + "inviteRoles": ["anonymous"], + "roles": ["anonymous"] } -} \ No newline at end of file +} diff --git a/java/validators/json/jsonschema/src/main/java/dev/sunbirdrc/validators/json/jsonschema/JsonValidationServiceImpl.java b/java/validators/json/jsonschema/src/main/java/dev/sunbirdrc/validators/json/jsonschema/JsonValidationServiceImpl.java index 5421f8dda..b6381b7db 100644 --- a/java/validators/json/jsonschema/src/main/java/dev/sunbirdrc/validators/json/jsonschema/JsonValidationServiceImpl.java +++ b/java/validators/json/jsonschema/src/main/java/dev/sunbirdrc/validators/json/jsonschema/JsonValidationServiceImpl.java @@ -126,7 +126,16 @@ public void removeDefinition(JsonNode jsonNode) { String schemaAsText = jsonNode.asText("{}"); JsonNode schemaJsonNode = objectMapper.readTree(schemaAsText); String schemaTitle = schemaJsonNode.get(TITLE).asText(); - definitionMap.remove(schemaTitle); + removeDefinition(schemaTitle); + } catch (Exception e) { + logger.error("Failed removing schema from definition manager", e); + } + } + + @Override + public void removeDefinition(String schema) { + try { + definitionMap.remove(schema); } catch (Exception e) { logger.error("Failed removing schema from definition manager", e); } diff --git a/services/certificate-api/main.js b/services/certificate-api/main.js index 86a4927f6..051bcba46 100644 --- a/services/certificate-api/main.js +++ b/services/certificate-api/main.js @@ -1,33 +1,56 @@ -const certificateController = require("./src/routes/certificate_controller"); -const http = require('http'); -const port = process.env.PORT || 4321; - -const server = http.createServer(async (req, res) => { - const label = `${req.url}-${new Date().getTime()}`; - console.time(label) - console.log(`API ${req.method} ${req.url} called`); - try { - if (req.method === 'GET' && req.url.startsWith("/health")) { - res.end("OK") - } else if (req.method === 'POST' && req.url.startsWith("/api/v1/certificate") && ["application/pdf"].includes(req.headers.accept)) { - const data = await certificateController.getCertificatePDF(req, res); - res.end(data) - } else if (req.method === 'POST' && req.url.startsWith("/api/v1/certificate") && ["text/html", "image/svg+xml"].includes(req.headers.accept)) { - const data = await certificateController.getCertificate(req, res); - res.end(data) - } else { - res.statusCode = 404; - res.end("Not found"); - } - } finally { - if (!res.writableEnded) { - res.statusCode = 500; - res.end("Error occurred"); +const axios = require('axios'); +let token = ''; +let osids = [""]; +osids.forEach(osid => { + + + let config = { + method: 'get', + maxBodyLength: Infinity, + url: 'http://localhost:8081/api/v1/ProofOfAchievement/'+osid, + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token, } - } - console.timeEnd(label) -}); + }; + + axios.request(config) + .then((response) => { + const getResp = response.data; + delete getResp["osUpdatedAt"] + delete getResp["osUpdatedBy"] + delete getResp["_osSignedData"] + delete getResp["osOwner"] + delete getResp["osCreatedAt"] + delete getResp["osCreatedBy"] + getResp["achievementTitle"] = "Participation" + console.log(getResp) + let config = { + method: 'put', + maxBodyLength: Infinity, + url: 'http://localhost:8081/api/v1/ProofOfAchievement/' + getResp["osid"], + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token, + }, + data : getResp + }; + + axios.request(config) + .then((response) => { + console.log(JSON.stringify(response.data)); + }) + .catch((error) => { + console.log(error); + }); + + + }) + .catch((error) => { + console.log(error); + }); +}) + + + -server.listen(port, async () => { - console.log(`Server listening on port ${port}`); -}); diff --git a/services/sample-fusionauth-service/.env b/services/sample-fusionauth-service/.env new file mode 100644 index 000000000..cedf30fde --- /dev/null +++ b/services/sample-fusionauth-service/.env @@ -0,0 +1,6 @@ +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +DATABASE_USERNAME=fusionauth +DATABASE_PASSWORD=hkaLBM3RVnyYeYeqE3WI1w2e4Avpy0Wd5O3s3 +ES_JAVA_OPTS="-Xms512m -Xmx512m" +FUSIONAUTH_APP_MEMORY=512M diff --git a/services/sample-fusionauth-service/Dockerfile b/services/sample-fusionauth-service/Dockerfile new file mode 100644 index 000000000..702f5ca3c --- /dev/null +++ b/services/sample-fusionauth-service/Dockerfile @@ -0,0 +1,7 @@ +FROM node:lts-alpine +WORKDIR /usr/app +COPY package*.json ./ +RUN npm install +COPY . . +EXPOSE 3990 +CMD ["npm", "start"] diff --git a/services/sample-fusionauth-service/api-spec.yml b/services/sample-fusionauth-service/api-spec.yml new file mode 100644 index 000000000..25a9915ed --- /dev/null +++ b/services/sample-fusionauth-service/api-spec.yml @@ -0,0 +1,58 @@ +openapi: 3.0.1 +info: + title: User API + version: 1.0.0 + description: API for creating users +servers: + - url: http://api.example.com/v1 +paths: + /api/v1/user: + post: + summary: Create a user + operationId: createUser + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + userName: + type: string + email: + type: string + format: email + password: + type: string + mobile: + type: string + role: + type: string + enum: + - admin + - user + required: + - userName + - email + - mobile + - role + responses: + '200': + description: User created successfully + content: + application/json: + schema: + type: object + properties: + userId: + type: string + status: + type: string + message: + type: string + required: + - userId + '400': + description: Invalid request payload + '500': + description: Internal server error diff --git a/services/sample-fusionauth-service/app.js b/services/sample-fusionauth-service/app.js new file mode 100644 index 000000000..2615c78ad --- /dev/null +++ b/services/sample-fusionauth-service/app.js @@ -0,0 +1,36 @@ +const createError = require('http-errors'); +const express = require('express'); +const path = require('path'); +const cookieParser = require('cookie-parser'); +const logger = require('morgan'); + +const indexRouter = require('./routes/index'); +const fusionAuthRouter = require('./routes/fusionauth'); + +const app = express(); + +// view engine setup +app.set('views', path.join(__dirname, 'views')); + +// app.use(logger('dev')); +app.use(express.json()); +app.use(express.urlencoded({ extended: false })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +app.use('/', indexRouter); +app.use('/fusionauth', fusionAuthRouter); + +// catch 404 and forward to error handler +app.use(function(req, res, next) { + next(createError(404)); +}); + +// error handler +app.use(function(err, req, res, next) { + // set locals, only providing error in development + res.status(err.status || 500); + res.json({err}); +}); + +module.exports = app; diff --git a/services/sample-fusionauth-service/app/www b/services/sample-fusionauth-service/app/www new file mode 100644 index 000000000..6a8707096 --- /dev/null +++ b/services/sample-fusionauth-service/app/www @@ -0,0 +1,87 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ +const app = require('../app'); +const debug = require('debug')('sample-fusionauth-service:server'); +const http = require('http'); + +/** + * Get port from environment and store in Express. + */ +const port = normalizePort(process.env.PORT || '3000'); +app.set('port', port); + +/** + * Create HTTP server. + */ +const server = http.createServer(app); + +/** + * Listen on provided port, on all network interfaces. + */ + +server.listen(port); +server.on('error', onError); +server.on('listening', onListening); + +/** + * Normalize a port into a number, string, or false. + */ + +function normalizePort(val) { + const port = parseInt(val, 10); + + if (isNaN(port)) { + // named pipe + return val; + } + + if (port >= 0) { + // port number + return port; + } + + return false; +} + +/** + * Event listener for HTTP server "error" event. + */ + +function onError(error) { + if (error.syscall !== 'listen') { + throw error; + } + + const bind = typeof port === 'string' + ? 'Pipe ' + port + : 'Port ' + port; + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +} + +/** + * Event listener for HTTP server "listening" event. + */ + +function onListening() { + const addr = server.address(); + const bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr.port; + console.log('Listening on ' + bind); +} diff --git a/services/sample-fusionauth-service/config/index.js b/services/sample-fusionauth-service/config/index.js new file mode 100644 index 000000000..40a3db1bb --- /dev/null +++ b/services/sample-fusionauth-service/config/index.js @@ -0,0 +1,5 @@ +module.exports = { + FUSIONAUTH_BASE_URL: process.env.FUSIONAUTH_BASE_URL || "http://localhost:9011", + FUSIONAUTH_APPLICATION_ID: process.env.FUSIONAUTH_APPLICATION_ID || "xxx", + FUSIONAUTH_API_KEY: process.env.FUSIONAUTH_API_KEY || "xxx" +}; diff --git a/services/sample-fusionauth-service/docker-compose.yml b/services/sample-fusionauth-service/docker-compose.yml new file mode 100644 index 000000000..b2e148a99 --- /dev/null +++ b/services/sample-fusionauth-service/docker-compose.yml @@ -0,0 +1,65 @@ +version: '3' + +services: + db: + image: postgres:12.9 + environment: + PGDATA: /var/lib/postgresql/data/pgdata + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U postgres" ] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + es: + image: docker.elastic.co/elasticsearch/elasticsearch:7.17.0 + environment: + cluster.name: fusionauth + bootstrap.memory_lock: "true" + discovery.type: single-node + ES_JAVA_OPTS: "-Xms512m -Xmx512m" + healthcheck: + test: [ "CMD", "curl", "--fail" ,"--write-out", "'HTTP %{http_code}'", "--silent", "--output", "/dev/null", "http://localhost:9200/" ] + interval: 30s + timeout: 10s + retries: 4 + restart: unless-stopped + ulimits: + memlock: + soft: -1 + hard: -1 + fusionauth: + image: fusionauth/fusionauth-app:latest + depends_on: + db: + condition: service_healthy + es: + condition: service_healthy + environment: + DATABASE_URL: jdbc:postgresql://db:5432/fusionauth + DATABASE_ROOT_USERNAME: postgres + DATABASE_ROOT_PASSWORD: postgres + DATABASE_USERNAME: postgres + DATABASE_PASSWORD: postgres + FUSIONAUTH_APP_MEMORY: 512M + FUSIONAUTH_APP_RUNTIME_MODE: development + FUSIONAUTH_APP_URL: http://fusionauth:9011 + SEARCH_SERVERS: http://es:9200 + SEARCH_TYPE: elasticsearch + FUSIONAUTH_APP_KICKSTART_FILE: /home/kickstart.json + FUSIONAUTH_ISSUER_URL: ${FUSIONAUTH_ISSUER_URL-http://localhost:9011/} + restart: unless-stopped + ports: + - 9011:9011 + volumes: + - ./${IMPORTS_DIR-imports}/:/home/ + fusionauthwrapper: + build: ${FUSION_WRAPPER_BUILD-.} + ports: + - 3990:3990 + environment: + FUSIONAUTH_API_KEY: bf69486b-4733-4470-a592-f1bfce7af580 + FUSIONAUTH_APPLICATION_ID: 85a03867-dccf-4882-adde-1a79aeec50df + FUSIONAUTH_BASE_URL: http://fusionauth:9011 diff --git a/services/sample-fusionauth-service/imports/kickstart.json b/services/sample-fusionauth-service/imports/kickstart.json new file mode 100644 index 000000000..59f9e2b89 --- /dev/null +++ b/services/sample-fusionauth-service/imports/kickstart.json @@ -0,0 +1,106 @@ +{ + "variables": { + "adminEmail": "admin@fusionauth.io", + "password": "password", + "defaultTenantId": "30663132-6464-6665-3032-326466613934", + "keyID": "#{UUID()}", + "issuerURL": "#{ENV.FUSIONAUTH_ISSUER_URL}" + }, + "apiKeys": [ + { + "key": "bf69486b-4733-4470-a592-f1bfce7af580", + "description": "Standard development API key" + } + ], + "requests": [ + { + "method": "POST", + "url": "/api/user/registration/00000000-0000-0000-0000-000000000001", + "body": { + "user": { + "birthDate": "1981-06-04", + "email": "#{adminEmail}", + "firstName": "Erlich", + "lastName": "Bachman", + "password": "#{password}", + "data": { + "Company": "PiedPiper", + "PreviousCompany": "Aviato", + "user_type": "iconclast" + } + }, + "registration": { + "applicationId": "#{FUSIONAUTH_APPLICATION_ID}", + "roles": [ + "admin" + ] + } + } + }, + { + "method": "POST", + "url": "/api/application/85a03867-dccf-4882-adde-1a79aeec50df", + "body": { + "application": { + "name": "SunbirdRC", + "roles": [ + { + "name": "admin" + } + ], + "oauthConfiguration": { + "clientAuthenticationPolicy": "NotRequired", + "enabledGrants": [ + "password" + ] + } + } + } + }, + { + "method": "POST", + "url": "/api/user/registration/00000000-0000-0000-0000-000000000002", + "body": { + "user": { + "birthDate": "1981-06-04", + "email": "admin@sunbirdrc.dev", + "firstName": "admin", + "lastName": "sunbirdrc", + "password": "admin@12345", + "data": { + } + }, + "registration": { + "applicationId": "85a03867-dccf-4882-adde-1a79aeec50df", + "roles": [ + "admin" + ] + } + } + }, + { + "method": "POST", + "url": "/api/key/generate/#{keyID}", + "body": { + "key": { + "algorithm": "RS256", + "issuer": "#{issuerURL}", + "name": "#{issuerURL}", + "length": 2048 + } + } + }, + { + "method": "PATCH", + "url": "/api/tenant/#{defaultTenantId}", + "body": { + "tenant": { + "issuer": "#{issuerURL}", + "jwtConfiguration": { + "accessTokenKeyId": "#{keyID}" + } + } + } + } + ] +} diff --git a/services/sample-fusionauth-service/package-lock.json b/services/sample-fusionauth-service/package-lock.json new file mode 100644 index 000000000..b676cc3de --- /dev/null +++ b/services/sample-fusionauth-service/package-lock.json @@ -0,0 +1,1485 @@ +{ + "name": "sample-fusionauth-service", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sample-fusionauth-service", + "version": "0.0.0", + "dependencies": { + "axios": "^1.4.0", + "cookie-parser": "~1.4.4", + "debug": "~2.6.9", + "express": "~4.16.1", + "http-errors": "~1.6.3", + "jade": "~1.11.0", + "morgan": "~1.9.1", + "request": "^2.88.2" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", + "integrity": "sha512-pXK8ez/pVjqFdAgBkF1YPVRacuLQ9EXBKaKWaeh58WNfMkCmZhOZzu+NtKSPD5PHmCCHheQ5cD29qM1K4QTxIg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-1.0.9.tgz", + "integrity": "sha512-j3/4pkfih8W4NK22gxVSXcEonTpAHOHh0hu5BoZrKcOsW/4oBPxTi4Yk3SAj+FhC1f3+bRTkXdm4019gw1vg9g==", + "license": "MIT", + "dependencies": { + "acorn": "^2.1.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha512-GrTZLRpmp6wIC2ztrWW9MjjTgSKccffgFagbNDOX95/dcjEcYZibYTeaOntySQLcdw1ztBoFkviiUvTMbb9MYg==", + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==", + "license": "BSD-3-Clause OR MIT", + "engines": { + "node": ">=0.4.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/asap/-/asap-1.0.0.tgz", + "integrity": "sha512-Ej9qjcXY+8Tuy1cNqiwNMwFRXOy9UwgTeMA8LxreodygIPV48lx8PU1ecFxb5ZeU1DpMKxiq6vGLTxcitWZPbA==" + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" + }, + "node_modules/axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/body-parser": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + "integrity": "sha512-YQyoqQG3sO8iCmf8+hyVpgHHOv0/hCEFiS4zTGUwTA1HjAFX66wRcNQrVCeJq9pgESMRvUAOvSil5MJlmccuKQ==", + "license": "MIT", + "dependencies": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "~1.6.3", + "iconv-lite": "0.4.23", + "on-finished": "~2.3.0", + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "~1.6.16" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha512-wzLkDa4K/mzI1OSITC+DUyjgIl/ETNHE9QvYgy6J6Jvqyyz4C0Xfd+lQhb19sX2jMpZV4IssUn0VDVmglV+s4g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + }, + "node_modules/center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha512-Baz3aNe2gd2LP2qk5U+sDk/m4oSuwSDcBfayTCTBoWpfIGO5XFxPmjILQII4NGiZjD6DoDI6kf7gKaxkf7s3VQ==", + "license": "MIT", + "dependencies": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/character-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-1.2.1.tgz", + "integrity": "sha512-6OEBVBlf/y8LaAphnbAnt743O3zMhlBer+FO5D40H6wqAdU9B1TvuApkejgLW0cvv0tEZNLktv1AnRI+C87ueQ==", + "license": "MIT" + }, + "node_modules/clean-css": { + "version": "3.4.28", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-3.4.28.tgz", + "integrity": "sha512-aTWyttSdI2mYi07kWqHi24NUU9YlELFKGOAgFzZjDN1064DMAOy2FBuoyGmkKRlXkbpXd0EVHmiVkbKhKoirTw==", + "license": "MIT", + "dependencies": { + "commander": "2.8.x", + "source-map": "0.4.x" + }, + "bin": { + "cleancss": "bin/cleancss" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clean-css/node_modules/commander": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", + "integrity": "sha512-+pJLBFVk+9ZZdlAOB5WuIElVPPth47hILFkmGym57aq8kwxsowvByvB0DHs1vQAhyMZzdcpTtF0VDKGkSDR4ZQ==", + "license": "MIT", + "dependencies": { + "graceful-readlink": ">= 1.0.0" + }, + "engines": { + "node": ">= 0.6.x" + } + }, + "node_modules/cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha512-GIOYRizG+TGoc7Wgc1LiOTLare95R3mzKgoln+Q/lE4ceiYH19gUpl0l0Ffq4lJDEf3FxujMe6IBfOCs7pfqNA==", + "license": "ISC", + "dependencies": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.6.0.tgz", + "integrity": "sha512-PhbTMT+ilDXZKqH8xbvuUY2ZEQNef0Q7DKxgoEKb4ccytsdvVVJmYqR0sGbi96nxU6oGrwEIQnclpK2NBZuQlg==", + "license": "MIT", + "engines": { + "node": ">= 0.6.x" + } + }, + "node_modules/constantinople": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.0.2.tgz", + "integrity": "sha512-UnEggAQrmhxuTxlb7n1OsTtagNXWUv2CRlOogZhWOU4jLK4EJEbF8UDSNxuGu+jVtWNtO2j51ab2H1wlBIzF/w==", + "license": "MIT", + "dependencies": { + "acorn": "^2.1.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "license": "MIT", + "dependencies": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + }, + "node_modules/css": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/css/-/css-1.0.8.tgz", + "integrity": "sha512-qmTYWhHk910nQWnGqMAiWWPQlB6tESiWgNebQJmiozOAGcBAQ1+U/UzUOkhdrcshlkSRRiKWodwmVvO0OmnIGg==", + "dependencies": { + "css-parse": "1.0.4", + "css-stringify": "1.0.5" + } + }, + "node_modules/css-parse": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-1.0.4.tgz", + "integrity": "sha512-pfstzKVRZiHprDXdsmtfH1HYUEw22lzjuHdnpe1hscwoQvgW2C5zDQIBE0RKoALEReTn9W1ECdY8uaT/kO4VfA==" + }, + "node_modules/css-stringify": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/css-stringify/-/css-stringify-1.0.5.tgz", + "integrity": "sha512-aIThpcErhG5EyHorGqNlTh0TduNBqLrrXLO3x5rku3ZKBxuVfY+T7noyM2G2X/01iQANqJUb6d3+FLoa+N7Xwg==" + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==", + "license": "MIT" + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", + "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.5", + "array-flatten": "1.1.1", + "body-parser": "1.18.3", + "content-disposition": "0.5.2", + "content-type": "~1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.1.1", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.4", + "qs": "6.5.2", + "range-parser": "~1.2.0", + "safe-buffer": "5.1.2", + "send": "0.16.2", + "serve-static": "1.13.2", + "setprototypeof": "1.1.0", + "statuses": "~1.4.0", + "type-is": "~1.6.16", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/finalhandler": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.4.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==", + "license": "MIT" + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "license": "MIT" + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" + }, + "node_modules/jade": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/jade/-/jade-1.11.0.tgz", + "integrity": "sha512-J76sbGKeLtu7uwW97Ntzb1UvGnpKTDplYa9ROr2gNRhM+SxvlBSG0Ees3TQ8+7ya2UVkzMEeFxhRhEpN68s7Tg==", + "license": "MIT", + "dependencies": { + "character-parser": "1.2.1", + "clean-css": "^3.1.9", + "commander": "~2.6.0", + "constantinople": "~3.0.1", + "jstransformer": "0.0.2", + "mkdirp": "~0.5.0", + "transformers": "2.1.0", + "uglify-js": "^2.4.19", + "void-elements": "~2.0.1", + "with": "~4.0.0" + }, + "bin": { + "jade": "bin/jade.js" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/jstransformer": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-0.0.2.tgz", + "integrity": "sha512-b7tmf91j1ChMuYhwbPBnNgB62dmHuqiHpOdd6QLKzde8HydZqm+ud3qWreGWecSxPBFFNOf1Ozjx0xo2plFdHA==", + "license": "MIT", + "dependencies": { + "is-promise": "^2.0.0", + "promise": "^6.0.1" + } + }, + "node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha512-k+yt5n3l48JU4k8ftnKG6V7u32wyH2NfKzeMto9F/QRE0amxy/LayxwlvjjkZEIzqR+19IrtFO8p5kB9QaYUFg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", + "license": "MIT", + "bin": { + "mime": "cli.js" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/morgan": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz", + "integrity": "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.0", + "debug": "2.6.9", + "depd": "~1.1.2", + "on-finished": "~2.3.0", + "on-headers": "~1.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "engines": { + "node": "*" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/optimist": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", + "integrity": "sha512-TCx0dXQzVtSCg2OgY/bO9hjM9cV4XYx09TVK+s3+FhkjT6LovsLe+pPMzpWf+6yXK/hUizs2gUoTw3jHM0VaTQ==", + "license": "MIT/X11", + "dependencies": { + "wordwrap": "~0.0.2" + } + }, + "node_modules/optimist/node_modules/wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "license": "MIT" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + }, + "node_modules/promise": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-6.1.0.tgz", + "integrity": "sha512-O+uwGKreKNKkshzZv2P7N64lk6EP17iXBn0PbUnNQhk+Q0AHLstiTrjkx3v5YBd3cxUe7Sq6KyRhl/A0xUjk7Q==", + "license": "MIT", + "dependencies": { + "asap": "~1.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "license": "MIT", + "dependencies": { + "bytes": "3.0.0", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha512-yqINtL/G7vs2v+dFIZmFUDbnVyFUJFKd6gK22Kgo6R4jfJGFtisKyncWDDULgjfqf4ASQuIQyjJ7XZ+3aWpsAg==", + "license": "MIT", + "dependencies": { + "align-text": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", + "send": "0.16.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha512-Y8nIfcb1s/7DcobUz1yOO1GSp7gyL+D9zLHDehT7iRESqGSxjJ448Sg7rvfgsRJCnKLdSl11uGf0s9X80cH0/A==", + "license": "BSD-3-Clause", + "dependencies": { + "amdefine": ">=0.0.4" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/sshpk": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", + "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/transformers": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/transformers/-/transformers-2.1.0.tgz", + "integrity": "sha512-zJf5m2EIOngmBbDe2fhTPpCjzM2qkZVqrFJZc2jaln+KBeEaYKhS2QMOIkfVrNUyoOwqgbTwOHATzr3jZRQDyg==", + "license": "MIT", + "dependencies": { + "css": "~1.0.8", + "promise": "~2.0", + "uglify-js": "~2.2.5" + } + }, + "node_modules/transformers/node_modules/is-promise": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-1.0.1.tgz", + "integrity": "sha512-mjWH5XxnhMA8cFnDchr6qRP9S/kLntKuEfIYku+PaN1CnS8v+OG9O/BKpRCVRJvpIkgAZm0Pf5Is3iSSOILlcg==", + "license": "MIT" + }, + "node_modules/transformers/node_modules/promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-2.0.0.tgz", + "integrity": "sha512-OgMc+sxI3zWF8D5BJGtA0z7/IsrDy1/0cPaDv6HPpqa2fSTo7AdON5U10NbZCUeF+zCAj3PtfPE50Hf02386aA==", + "license": "MIT", + "dependencies": { + "is-promise": "~1" + } + }, + "node_modules/transformers/node_modules/source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha512-VtCvB9SIQhk3aF6h+N85EaqIaBFIAfZ9Cu+NJHHVvc8BbEcnvDcFw6sqQ2dQrT6SlOrZq3tIvyD9+EGq/lJryQ==", + "dependencies": { + "amdefine": ">=0.0.4" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/transformers/node_modules/uglify-js": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.2.5.tgz", + "integrity": "sha512-viLk+/8G0zm2aKt1JJAVcz5J/5ytdiNaIsKgrre3yvSUjwVG6ZUujGH7E2TiPigZUwLYCe7eaIUEP2Zka2VJPA==", + "dependencies": { + "optimist": "~0.3.5", + "source-map": "~0.1.7" + }, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha512-qLq/4y2pjcU3vhlhseXGGJ7VbFO4pBANu0kwl8VCa9KEI0V8VfZIx2Fy3w01iSTA/pGwKZSmu/+I4etLNDdt5w==", + "license": "BSD-2-Clause", + "dependencies": { + "source-map": "~0.5.1", + "yargs": "~3.10.0" + }, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + }, + "optionalDependencies": { + "uglify-to-browserify": "~1.0.0" + } + }, + "node_modules/uglify-js/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha512-vb2s1lYx2xBtUgy+ta+b2J/GLVUR+wmpINwHePmPRhOsIVCG2wDzKJ0n14GslH1BifsqVzSOwQhRaCAsZ/nI4Q==", + "license": "MIT", + "optional": true + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha512-1pTPQDKTdd61ozlKGNCjhNRd+KPmgLSGa3mZTHoOliaGcESD8G1PXhh7c1fgiPjVbNVfgy2Faw4BI8/m0cC8Mg==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/with": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/with/-/with-4.0.3.tgz", + "integrity": "sha512-mJZFpyEc1JTAdxhi/vhVeAM2S7vsltEKDiexDDo1HuAzlYKhcVUU6cwY8cHrFYdt82ZNkfKCeyhA3IYFegI0Kg==", + "license": "MIT", + "dependencies": { + "acorn": "^1.0.1", + "acorn-globals": "^1.0.3" + } + }, + "node_modules/with/node_modules/acorn": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-1.2.2.tgz", + "integrity": "sha512-FsqWmApWGMGLKKNpHt12PMc5AK7BaZee0WRh04fCysmTzHe+rrKOa2MKjORhnzfpe4r0JnfdqHn02iDA9Dqj2A==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q==", + "license": "MIT/X11", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha512-QFzUah88GAGy9lyDKGBqZdkYApt63rCXYBGYnEP4xDJPXNqXXnBDACnbrXnViV6jRSqAePwrATi2i8mfYm4L1A==", + "license": "MIT", + "dependencies": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + } + } +} diff --git a/services/sample-fusionauth-service/package.json b/services/sample-fusionauth-service/package.json new file mode 100644 index 000000000..e42033c67 --- /dev/null +++ b/services/sample-fusionauth-service/package.json @@ -0,0 +1,16 @@ +{ + "name": "sample-fusionauth-service", + "version": "0.0.0", + "private": true, + "scripts": { + "start": "PORT=3990 node app/www" + }, + "dependencies": { + "axios": "^1.4.0", + "cookie-parser": "~1.4.4", + "debug": "~2.6.9", + "express": "~4.16.1", + "http-errors": "~1.6.3", + "morgan": "~1.9.1" + } +} diff --git a/services/sample-fusionauth-service/routes/fusionauth.js b/services/sample-fusionauth-service/routes/fusionauth.js new file mode 100644 index 000000000..fbc022f82 --- /dev/null +++ b/services/sample-fusionauth-service/routes/fusionauth.js @@ -0,0 +1,172 @@ +const express = require('express'); +const router = express.Router(); +const {FUSIONAUTH_BASE_URL, FUSIONAUTH_APPLICATION_ID, FUSIONAUTH_API_KEY} = require("../config"); +const axios = require("axios"); + +/* GET users listing. */ +router.post('/api/v1/user', async function (req, res, next) { + try { + const {userName, email, mobile, entity, password} = req.body; + await createRole(entity) + try { + const newUser = await createUser(userName, email, mobile, entity, password); + res.json({ + userId: newUser.user.id, + status: "SUCCESSFUL" + }) + return; + } catch (e) { + if (e.response.data.fieldErrors["user.username"][0].code === "[duplicate]user.username") { + console.log("User already exists") + const existingUser = await getUser(userName) + await updateUserEntity(existingUser.user.id, existingUser, entity); + await updateUserRole(existingUser.user.id, entity) + res.json({ + userId: existingUser.user.id, + status: "SUCCESSFUL" + }) + return; + } + res.status(500) + res.json({ + userId: "", + status: "UNSUCCESSFUL", + message: e.message + }) + } + } catch (e) { + console.log("Error while creating user", e) + } + res.status(500) + res.json({ + userId: "", + status: "UNSUCCESSFUL", + message: "Internal server error" + }) +}); + +const updateUserRole = async (userId, role) => { + const options = { + 'method': 'PATCH', + 'url': `${FUSIONAUTH_BASE_URL}/api/user/${userId}/${FUSIONAUTH_APPLICATION_ID}`, + 'headers': { + 'Authorization': FUSIONAUTH_API_KEY, + 'Content-Type': 'application/json' + }, + data: JSON.stringify({ + "registration": { + "applicationId": FUSIONAUTH_APPLICATION_ID, + "roles": [ + role + ] + } + }) + + }; + await axios.request(options).then((response) => { + return response.data; + }); + +} + +const updateUserEntity = async (userId, existingUserDetails, entity) => { + existingUserDetails.user.data.entity.push(entity) + const options = { + 'method': 'PUT', + 'url': `${FUSIONAUTH_BASE_URL}/api/user/${userId}`, + 'headers': { + 'Authorization': FUSIONAUTH_API_KEY, + 'Content-Type': 'application/json' + }, + data: JSON.stringify(existingUserDetails) + + }; + await axios.request(options).then((response) => { + return response.data; + }); + +} + +const getUser = async (userName) => { + const options = { + 'method': 'GET', + 'url': `${FUSIONAUTH_BASE_URL}/api/user?username=${userName}`, + 'headers': { + 'Authorization': FUSIONAUTH_API_KEY + } + }; + return await axios.request(options).then((response) => { + return response.data; + }); +} + +const createUser = async (userName, email, mobile, role, password="") => { + const options = { + 'method': 'POST', + 'url': `${FUSIONAUTH_BASE_URL}/api/user/registration`, + 'headers': { + 'Authorization': FUSIONAUTH_API_KEY, + 'Content-Type': 'application/json' + }, + data: JSON.stringify({ + "user": { + "username": userName, + "email": email, + "password": password, + "data": { + "entity": [ + role + ] + }, + "mobilePhone": mobile + }, + "registration": { + "applicationId": FUSIONAUTH_APPLICATION_ID, + "roles": [ + role + ] + } + }) + + }; + return await axios.request(options).then((response) => { + return response.data; + }).catch((e) => { + console.log(e.response.data.fieldErrors); + throw e; + }); +} + +const createRole = async (role) => { + try { + let data = JSON.stringify({ + "role": { + "description": "", + "name": role, + "isDefault": false + } + }); + + let config = { + method: 'post', + maxBodyLength: Infinity, + url: `${FUSIONAUTH_BASE_URL}/api/application/${FUSIONAUTH_APPLICATION_ID}/role`, + headers: { + 'Authorization': FUSIONAUTH_API_KEY, + 'Content-Type': 'application/json' + }, + data: data + }; + + await axios.request(config) + .then((response) => { + return response.data; + }) + .catch((error) => { + console.log(error.response.data.fieldErrors); + }); + } catch (e) { + console.log(e) + } +} +module.exports = router; diff --git a/services/sample-fusionauth-service/routes/index.js b/services/sample-fusionauth-service/routes/index.js new file mode 100644 index 000000000..f9cf49b1f --- /dev/null +++ b/services/sample-fusionauth-service/routes/index.js @@ -0,0 +1,8 @@ +var express = require('express'); +var router = express.Router(); + +router.get('/health', function (req, res, next) { + res.json({status: "UP"}); +}); + +module.exports = router; diff --git a/services/sample-fusionauth-service/yarn.lock b/services/sample-fusionauth-service/yarn.lock new file mode 100644 index 000000000..bee303e2b --- /dev/null +++ b/services/sample-fusionauth-service/yarn.lock @@ -0,0 +1,1002 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +accepts@~1.3.5: + version "1.3.8" + resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-globals@^1.0.3: + version "1.0.9" + resolved "https://registry.npmjs.org/acorn-globals/-/acorn-globals-1.0.9.tgz" + integrity sha512-j3/4pkfih8W4NK22gxVSXcEonTpAHOHh0hu5BoZrKcOsW/4oBPxTi4Yk3SAj+FhC1f3+bRTkXdm4019gw1vg9g== + dependencies: + acorn "^2.1.0" + +acorn@^1.0.1: + version "1.2.2" + resolved "https://registry.npmjs.org/acorn/-/acorn-1.2.2.tgz" + integrity sha512-FsqWmApWGMGLKKNpHt12PMc5AK7BaZee0WRh04fCysmTzHe+rrKOa2MKjORhnzfpe4r0JnfdqHn02iDA9Dqj2A== + +acorn@^2.1.0: + version "2.7.0" + resolved "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz" + integrity sha512-pXK8ez/pVjqFdAgBkF1YPVRacuLQ9EXBKaKWaeh58WNfMkCmZhOZzu+NtKSPD5PHmCCHheQ5cD29qM1K4QTxIg== + +ajv@^6.12.3: + version "6.12.6" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +align-text@^0.1.1, align-text@^0.1.3: + version "0.1.4" + resolved "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz" + integrity sha512-GrTZLRpmp6wIC2ztrWW9MjjTgSKccffgFagbNDOX95/dcjEcYZibYTeaOntySQLcdw1ztBoFkviiUvTMbb9MYg== + dependencies: + kind-of "^3.0.2" + longest "^1.0.1" + repeat-string "^1.5.2" + +amdefine@>=0.0.4: + version "1.0.1" + resolved "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz" + integrity sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +asap@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/asap/-/asap-1.0.0.tgz" + integrity sha512-Ej9qjcXY+8Tuy1cNqiwNMwFRXOy9UwgTeMA8LxreodygIPV48lx8PU1ecFxb5ZeU1DpMKxiq6vGLTxcitWZPbA== + +asn1@~0.2.3: + version "0.2.6" + resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@^1.0.0, assert-plus@1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" + integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz" + integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== + +aws4@^1.8.0: + version "1.12.0" + resolved "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz" + integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg== + +axios@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz" + integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +basic-auth@~2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz" + integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== + dependencies: + tweetnacl "^0.14.3" + +body-parser@1.18.3: + version "1.18.3" + resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz" + integrity sha512-YQyoqQG3sO8iCmf8+hyVpgHHOv0/hCEFiS4zTGUwTA1HjAFX66wRcNQrVCeJq9pgESMRvUAOvSil5MJlmccuKQ== + dependencies: + bytes "3.0.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "~1.6.3" + iconv-lite "0.4.23" + on-finished "~2.3.0" + qs "6.5.2" + raw-body "2.3.3" + type-is "~1.6.16" + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz" + integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== + +camelcase@^1.0.2: + version "1.2.1" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz" + integrity sha512-wzLkDa4K/mzI1OSITC+DUyjgIl/ETNHE9QvYgy6J6Jvqyyz4C0Xfd+lQhb19sX2jMpZV4IssUn0VDVmglV+s4g== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz" + integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== + +center-align@^0.1.1: + version "0.1.3" + resolved "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz" + integrity sha512-Baz3aNe2gd2LP2qk5U+sDk/m4oSuwSDcBfayTCTBoWpfIGO5XFxPmjILQII4NGiZjD6DoDI6kf7gKaxkf7s3VQ== + dependencies: + align-text "^0.1.3" + lazy-cache "^1.0.3" + +character-parser@1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/character-parser/-/character-parser-1.2.1.tgz" + integrity sha512-6OEBVBlf/y8LaAphnbAnt743O3zMhlBer+FO5D40H6wqAdU9B1TvuApkejgLW0cvv0tEZNLktv1AnRI+C87ueQ== + +clean-css@^3.1.9: + version "3.4.28" + resolved "https://registry.npmjs.org/clean-css/-/clean-css-3.4.28.tgz" + integrity sha512-aTWyttSdI2mYi07kWqHi24NUU9YlELFKGOAgFzZjDN1064DMAOy2FBuoyGmkKRlXkbpXd0EVHmiVkbKhKoirTw== + dependencies: + commander "2.8.x" + source-map "0.4.x" + +cliui@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz" + integrity sha512-GIOYRizG+TGoc7Wgc1LiOTLare95R3mzKgoln+Q/lE4ceiYH19gUpl0l0Ffq4lJDEf3FxujMe6IBfOCs7pfqNA== + dependencies: + center-align "^0.1.1" + right-align "^0.1.1" + wordwrap "0.0.2" + +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@~2.6.0: + version "2.6.0" + resolved "https://registry.npmjs.org/commander/-/commander-2.6.0.tgz" + integrity sha512-PhbTMT+ilDXZKqH8xbvuUY2ZEQNef0Q7DKxgoEKb4ccytsdvVVJmYqR0sGbi96nxU6oGrwEIQnclpK2NBZuQlg== + +commander@2.8.x: + version "2.8.1" + resolved "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz" + integrity sha512-+pJLBFVk+9ZZdlAOB5WuIElVPPth47hILFkmGym57aq8kwxsowvByvB0DHs1vQAhyMZzdcpTtF0VDKGkSDR4ZQ== + dependencies: + graceful-readlink ">= 1.0.0" + +constantinople@~3.0.1: + version "3.0.2" + resolved "https://registry.npmjs.org/constantinople/-/constantinople-3.0.2.tgz" + integrity sha512-UnEggAQrmhxuTxlb7n1OsTtagNXWUv2CRlOogZhWOU4jLK4EJEbF8UDSNxuGu+jVtWNtO2j51ab2H1wlBIzF/w== + dependencies: + acorn "^2.1.0" + +content-disposition@0.5.2: + version "0.5.2" + resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz" + integrity sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA== + +content-type@~1.0.4: + version "1.0.5" + resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-parser@~1.4.4: + version "1.4.6" + resolved "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz" + integrity sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA== + dependencies: + cookie "0.4.1" + cookie-signature "1.0.6" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.3.1: + version "0.3.1" + resolved "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz" + integrity sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw== + +cookie@0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz" + integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== + +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" + integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== + +css-parse@1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/css-parse/-/css-parse-1.0.4.tgz" + integrity sha512-pfstzKVRZiHprDXdsmtfH1HYUEw22lzjuHdnpe1hscwoQvgW2C5zDQIBE0RKoALEReTn9W1ECdY8uaT/kO4VfA== + +css-stringify@1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/css-stringify/-/css-stringify-1.0.5.tgz" + integrity sha512-aIThpcErhG5EyHorGqNlTh0TduNBqLrrXLO3x5rku3ZKBxuVfY+T7noyM2G2X/01iQANqJUb6d3+FLoa+N7Xwg== + +css@~1.0.8: + version "1.0.8" + resolved "https://registry.npmjs.org/css/-/css-1.0.8.tgz" + integrity sha512-qmTYWhHk910nQWnGqMAiWWPQlB6tESiWgNebQJmiozOAGcBAQ1+U/UzUOkhdrcshlkSRRiKWodwmVvO0OmnIGg== + dependencies: + css-parse "1.0.4" + css-stringify "1.0.5" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz" + integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== + dependencies: + assert-plus "^1.0.0" + +debug@~2.6.9, debug@2.6.9: + version "2.6.9" + resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +decamelize@^1.0.0: + version "1.2.0" + resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz" + integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz" + integrity sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg== + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz" + integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +express@~4.16.1: + version "4.16.4" + resolved "https://registry.npmjs.org/express/-/express-4.16.4.tgz" + integrity sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg== + dependencies: + accepts "~1.3.5" + array-flatten "1.1.1" + body-parser "1.18.3" + content-disposition "0.5.2" + content-type "~1.0.4" + cookie "0.3.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.1.1" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.2" + path-to-regexp "0.1.7" + proxy-addr "~2.0.4" + qs "6.5.2" + range-parser "~1.2.0" + safe-buffer "5.1.2" + send "0.16.2" + serve-static "1.13.2" + setprototypeof "1.1.0" + statuses "~1.4.0" + type-is "~1.6.16" + utils-merge "1.0.1" + vary "~1.1.2" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extsprintf@^1.2.0, extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz" + integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +finalhandler@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz" + integrity sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.2" + statuses "~1.4.0" + unpipe "~1.0.0" + +follow-redirects@^1.15.0: + version "1.15.2" + resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" + integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz" + integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== + dependencies: + assert-plus "^1.0.0" + +"graceful-readlink@>= 1.0.0": + version "1.0.1" + resolved "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz" + integrity sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w== + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz" + integrity sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q== + +har-validator@~5.1.3: + version "5.1.5" + resolved "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz" + integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== + dependencies: + ajv "^6.12.3" + har-schema "^2.0.0" + +http-errors@~1.6.2, http-errors@~1.6.3, http-errors@1.6.3: + version "1.6.3" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" + integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz" + integrity sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ== + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +iconv-lite@0.4.23: + version "0.4.23" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz" + integrity sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-promise@^2.0.0: + version "2.2.2" + resolved "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz" + integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== + +is-promise@~1: + version "1.0.1" + resolved "https://registry.npmjs.org/is-promise/-/is-promise-1.0.1.tgz" + integrity sha512-mjWH5XxnhMA8cFnDchr6qRP9S/kLntKuEfIYku+PaN1CnS8v+OG9O/BKpRCVRJvpIkgAZm0Pf5Is3iSSOILlcg== + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" + integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" + integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== + +jade@~1.11.0: + version "1.11.0" + resolved "https://registry.npmjs.org/jade/-/jade-1.11.0.tgz" + integrity sha512-J76sbGKeLtu7uwW97Ntzb1UvGnpKTDplYa9ROr2gNRhM+SxvlBSG0Ees3TQ8+7ya2UVkzMEeFxhRhEpN68s7Tg== + dependencies: + character-parser "1.2.1" + clean-css "^3.1.9" + commander "~2.6.0" + constantinople "~3.0.1" + jstransformer "0.0.2" + mkdirp "~0.5.0" + transformers "2.1.0" + uglify-js "^2.4.19" + void-elements "~2.0.1" + with "~4.0.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz" + integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" + integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== + +jsprim@^1.2.2: + version "1.4.2" + resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz" + integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.4.0" + verror "1.10.0" + +jstransformer@0.0.2: + version "0.0.2" + resolved "https://registry.npmjs.org/jstransformer/-/jstransformer-0.0.2.tgz" + integrity sha512-b7tmf91j1ChMuYhwbPBnNgB62dmHuqiHpOdd6QLKzde8HydZqm+ud3qWreGWecSxPBFFNOf1Ozjx0xo2plFdHA== + dependencies: + is-promise "^2.0.0" + promise "^6.0.1" + +kind-of@^3.0.2: + version "3.2.2" + resolved "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz" + integrity sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ== + dependencies: + is-buffer "^1.1.5" + +lazy-cache@^1.0.3: + version "1.0.4" + resolved "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz" + integrity sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ== + +longest@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz" + integrity sha512-k+yt5n3l48JU4k8ftnKG6V7u32wyH2NfKzeMto9F/QRE0amxy/LayxwlvjjkZEIzqR+19IrtFO8p5kB9QaYUFg== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.4.1: + version "1.4.1" + resolved "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz" + integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ== + +minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mkdirp@~0.5.0: + version "0.5.6" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +morgan@~1.9.1: + version "1.9.1" + resolved "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz" + integrity sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA== + dependencies: + basic-auth "~2.0.0" + debug "2.6.9" + depd "~1.1.2" + on-finished "~2.3.0" + on-headers "~1.0.1" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +optimist@~0.3.5: + version "0.3.7" + resolved "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz" + integrity sha512-TCx0dXQzVtSCg2OgY/bO9hjM9cV4XYx09TVK+s3+FhkjT6LovsLe+pPMzpWf+6yXK/hUizs2gUoTw3jHM0VaTQ== + dependencies: + wordwrap "~0.0.2" + +parseurl@~1.3.2: + version "1.3.3" + resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" + integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== + +promise@^6.0.1: + version "6.1.0" + resolved "https://registry.npmjs.org/promise/-/promise-6.1.0.tgz" + integrity sha512-O+uwGKreKNKkshzZv2P7N64lk6EP17iXBn0PbUnNQhk+Q0AHLstiTrjkx3v5YBd3cxUe7Sq6KyRhl/A0xUjk7Q== + dependencies: + asap "~1.0.0" + +promise@~2.0: + version "2.0.0" + resolved "https://registry.npmjs.org/promise/-/promise-2.0.0.tgz" + integrity sha512-OgMc+sxI3zWF8D5BJGtA0z7/IsrDy1/0cPaDv6HPpqa2fSTo7AdON5U10NbZCUeF+zCAj3PtfPE50Hf02386aA== + dependencies: + is-promise "~1" + +proxy-addr@~2.0.4: + version "2.0.7" + resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +psl@^1.1.28: + version "1.9.0" + resolved "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz" + integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== + +punycode@^2.1.0, punycode@^2.1.1: + version "2.3.0" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz" + integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + +qs@~6.5.2, qs@6.5.2: + version "6.5.2" + resolved "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +range-parser@~1.2.0: + version "1.2.1" + resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.3.3: + version "2.3.3" + resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz" + integrity sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw== + dependencies: + bytes "3.0.0" + http-errors "1.6.3" + iconv-lite "0.4.23" + unpipe "1.0.0" + +repeat-string@^1.5.2: + version "1.6.1" + resolved "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz" + integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== + +request@^2.88.2: + version "2.88.2" + resolved "https://registry.npmjs.org/request/-/request-2.88.2.tgz" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +right-align@^0.1.1: + version "0.1.3" + resolved "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz" + integrity sha512-yqINtL/G7vs2v+dFIZmFUDbnVyFUJFKd6gK22Kgo6R4jfJGFtisKyncWDDULgjfqf4ASQuIQyjJ7XZ+3aWpsAg== + dependencies: + align-text "^0.1.1" + +safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safer-buffer@^2.0.2, safer-buffer@^2.1.0, "safer-buffer@>= 2.1.2 < 3", safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +send@0.16.2: + version "0.16.2" + resolved "https://registry.npmjs.org/send/-/send-0.16.2.tgz" + integrity sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.6.2" + mime "1.4.1" + ms "2.0.0" + on-finished "~2.3.0" + range-parser "~1.2.0" + statuses "~1.4.0" + +serve-static@1.13.2: + version "1.13.2" + resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz" + integrity sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.2" + send "0.16.2" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +source-map@~0.1.7: + version "0.1.43" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz" + integrity sha512-VtCvB9SIQhk3aF6h+N85EaqIaBFIAfZ9Cu+NJHHVvc8BbEcnvDcFw6sqQ2dQrT6SlOrZq3tIvyD9+EGq/lJryQ== + dependencies: + amdefine ">=0.0.4" + +source-map@~0.5.1: + version "0.5.7" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz" + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== + +source-map@0.4.x: + version "0.4.4" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz" + integrity sha512-Y8nIfcb1s/7DcobUz1yOO1GSp7gyL+D9zLHDehT7iRESqGSxjJ448Sg7rvfgsRJCnKLdSl11uGf0s9X80cH0/A== + dependencies: + amdefine ">=0.0.4" + +sshpk@^1.7.0: + version "1.17.0" + resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz" + integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +statuses@~1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz" + integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew== + +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +transformers@2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/transformers/-/transformers-2.1.0.tgz" + integrity sha512-zJf5m2EIOngmBbDe2fhTPpCjzM2qkZVqrFJZc2jaln+KBeEaYKhS2QMOIkfVrNUyoOwqgbTwOHATzr3jZRQDyg== + dependencies: + css "~1.0.8" + promise "~2.0" + uglify-js "~2.2.5" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz" + integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" + integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== + +type-is@~1.6.16: + version "1.6.18" + resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +uglify-js@^2.4.19: + version "2.8.29" + resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz" + integrity sha512-qLq/4y2pjcU3vhlhseXGGJ7VbFO4pBANu0kwl8VCa9KEI0V8VfZIx2Fy3w01iSTA/pGwKZSmu/+I4etLNDdt5w== + dependencies: + source-map "~0.5.1" + yargs "~3.10.0" + optionalDependencies: + uglify-to-browserify "~1.0.0" + +uglify-js@~2.2.5: + version "2.2.5" + resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-2.2.5.tgz" + integrity sha512-viLk+/8G0zm2aKt1JJAVcz5J/5ytdiNaIsKgrre3yvSUjwVG6ZUujGH7E2TiPigZUwLYCe7eaIUEP2Zka2VJPA== + dependencies: + optimist "~0.3.5" + source-map "~0.1.7" + +uglify-to-browserify@~1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz" + integrity sha512-vb2s1lYx2xBtUgy+ta+b2J/GLVUR+wmpINwHePmPRhOsIVCG2wDzKJ0n14GslH1BifsqVzSOwQhRaCAsZ/nI4Q== + +unpipe@~1.0.0, unpipe@1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^3.3.2: + version "3.4.0" + resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz" + integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +void-elements@~2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz" + integrity sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung== + +window-size@0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz" + integrity sha512-1pTPQDKTdd61ozlKGNCjhNRd+KPmgLSGa3mZTHoOliaGcESD8G1PXhh7c1fgiPjVbNVfgy2Faw4BI8/m0cC8Mg== + +with@~4.0.0: + version "4.0.3" + resolved "https://registry.npmjs.org/with/-/with-4.0.3.tgz" + integrity sha512-mJZFpyEc1JTAdxhi/vhVeAM2S7vsltEKDiexDDo1HuAzlYKhcVUU6cwY8cHrFYdt82ZNkfKCeyhA3IYFegI0Kg== + dependencies: + acorn "^1.0.1" + acorn-globals "^1.0.3" + +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz" + integrity sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw== + +wordwrap@0.0.2: + version "0.0.2" + resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz" + integrity sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q== + +yargs@~3.10.0: + version "3.10.0" + resolved "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz" + integrity sha512-QFzUah88GAGy9lyDKGBqZdkYApt63rCXYBGYnEP4xDJPXNqXXnBDACnbrXnViV6jRSqAePwrATi2i8mfYm4L1A== + dependencies: + camelcase "^1.0.2" + cliui "^2.1.0" + decamelize "^1.0.0" + window-size "0.1.0"