diff --git a/src/openlmis-login/login-controller.js b/src/openlmis-login/login-controller.js index cca02abd..d926e338 100644 --- a/src/openlmis-login/login-controller.js +++ b/src/openlmis-login/login-controller.js @@ -28,10 +28,10 @@ .controller('LoginController', LoginController); LoginController.$inject = [ - 'loginService', 'modalDeferred', 'loadingModalService', '$rootScope' + 'loginService', 'modalDeferred', 'loadingModalService', '$rootScope', 'supersetOAuthService' ]; - function LoginController(loginService, modalDeferred, loadingModalService, $rootScope) { + function LoginController(loginService, modalDeferred, loadingModalService, $rootScope, supersetOAuthService) { var vm = this; @@ -53,6 +53,7 @@ .then(function() { $rootScope.$emit('openlmis-auth.login'); modalDeferred.resolve(); + loginToSuperset(); }) .catch(function(error) { vm.loginError = error; @@ -61,5 +62,18 @@ .finally(loadingModalService.close); } + function loginToSuperset() { + supersetOAuthService.checkAuthorizationInSuperset() + .then(function(data) { + vm.supersetOAuthState = data.state; + if (data.isAuthorized === false) { + supersetOAuthService.authorizeInSuperset(vm.username, vm.password, vm.supersetOAuthState) + .then(function() { + $rootScope.$emit('openlmis-auth.authorized-in-superset'); + }); + } + }); + } + } }()); diff --git a/src/openlmis-login/login-controller.spec.js b/src/openlmis-login/login-controller.spec.js index cf337d6e..553470ce 100644 --- a/src/openlmis-login/login-controller.spec.js +++ b/src/openlmis-login/login-controller.spec.js @@ -23,6 +23,7 @@ describe('LoginController', function() { return {}; }); }); + module('openlmis-superset'); inject(function($injector) { this.$q = $injector.get('$q'); @@ -30,6 +31,11 @@ describe('LoginController', function() { this.$controller = $injector.get('$controller'); this.loginService = $injector.get('loginService'); this.loadingModalService = $injector.get('loadingModalService'); + this.supersetOAuthService = $injector.get('supersetOAuthService'); + + spyOn(this.supersetOAuthService, 'checkAuthorizationInSuperset') + .andReturn(this.$q.resolve(this.isAuthorizedResponse)); + spyOn(this.supersetOAuthService, 'authorizeInSuperset').andReturn(this.$q.resolve()); }); this.modalDeferred = this.$q.defer(); @@ -37,6 +43,13 @@ describe('LoginController', function() { this.vm = this.$controller('LoginController', { modalDeferred: this.modalDeferred }); + this.isAuthorizedResponse = { + isAuthorized: true + }; + this.isNotAuthorizedResponse = { + isAuthorized: false, + state: 'test_state' + }; }); describe('doLogin', function() { @@ -174,5 +187,50 @@ describe('LoginController', function() { expect(success).toBe(true); }); + + it('should check Superset authorization and not send authorize request after successful login', function() { + this.loginService.login.andReturn(this.$q.resolve()); + this.supersetOAuthService.checkAuthorizationInSuperset + .andReturn(this.$q.resolve(this.isAuthorizedResponse)); + + this.vm.doLogin(); + this.$rootScope.$apply(); + + expect(this.supersetOAuthService.checkAuthorizationInSuperset).toHaveBeenCalled(); + expect(this.supersetOAuthService.authorizeInSuperset).not.toHaveBeenCalled(); + }); + + it('should check Superset authorization and send authorize request after successful login', function() { + var success = false; + this.$rootScope.$on('openlmis-auth.authorized-in-superset', function() { + success = true; + }); + + this.loginService.login.andReturn(this.$q.resolve()); + this.supersetOAuthService.checkAuthorizationInSuperset + .andReturn(this.$q.resolve(this.isNotAuthorizedResponse)); + + this.vm.doLogin(); + this.$rootScope.$apply(); + + expect(this.supersetOAuthService.checkAuthorizationInSuperset).toHaveBeenCalled(); + expect(this.supersetOAuthService.authorizeInSuperset).toHaveBeenCalled(); + expect(success).toBe(true); + }); + + it('should not check authorization in Superset after failed login', function() { + var success = false; + this.$rootScope.$on('openlmis-auth.authorized-in-superset', function() { + success = true; + }); + this.loginService.login.andReturn(this.$q.reject()); + + this.vm.doLogin(); + this.$rootScope.$apply(); + + expect(this.supersetOAuthService.checkAuthorizationInSuperset).not.toHaveBeenCalled(); + expect(this.supersetOAuthService.authorizeInSuperset).not.toHaveBeenCalled(); + expect(success).toBe(false); + }); }); }); diff --git a/src/openlmis-login/openlmis-login.module.js b/src/openlmis-login/openlmis-login.module.js index 8ce0426f..a2beff48 100644 --- a/src/openlmis-login/openlmis-login.module.js +++ b/src/openlmis-login/openlmis-login.module.js @@ -28,7 +28,8 @@ 'openlmis-offline', 'openlmis-locale', 'openlmis-modal', - 'ui.router' + 'ui.router', + 'openlmis-superset' ]); })(); diff --git a/src/openlmis-superset/messages_en.json b/src/openlmis-superset/messages_en.json new file mode 100644 index 00000000..650523ab --- /dev/null +++ b/src/openlmis-superset/messages_en.json @@ -0,0 +1,11 @@ +{ + "superset.auth.pageRequiresAuthorization": "This page requires authorization in the reporting stack. If nothing shows up, you still can authorize manually by", + "superset.auth.clickingHere": "clicking here", + "superset.oAuthLogin.header": "Allow Superset to get access to OpenLMIS", + "superset.oAuthLogin.authorize": "Authorize", + "superset.oAuthLogin.cancel": "Cancel", + "superset.oAuthLogin.username": "Username", + "superset.oAuthLogin.password": "Password", + "superset.oAuthLogin.invalidCredentialsOrOAuthRequest": "Invalid credentials or unsuccessful OAuth request", + "superset.oAuthLogin.unsuccessfulApprovingPermissions": "Unsuccessful approving permissions for Superset" +} diff --git a/src/openlmis-superset/modal-cancelled.constant.js b/src/openlmis-superset/modal-cancelled.constant.js new file mode 100644 index 00000000..d49d7e15 --- /dev/null +++ b/src/openlmis-superset/modal-cancelled.constant.js @@ -0,0 +1,30 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +(function() { + 'use strict'; + + /** + * @ngdoc object + * @name openlmis-superset.MODAL_CANCELLED + * + * @description + * This is the constant describing the rejection of promise in case of cancellation of a modal. + */ + angular + .module('openlmis-superset') + .constant('MODAL_CANCELLED', 'MODAL_CANCELLED'); + +})(); diff --git a/src/openlmis-superset/superset-locale.service.js b/src/openlmis-superset/superset-locale.service.js new file mode 100644 index 00000000..24f120ed --- /dev/null +++ b/src/openlmis-superset/superset-locale.service.js @@ -0,0 +1,61 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +(function() { + 'use strict'; + + /** + * @ngdoc service + * @name superset:supersetLocaleService + * @description + * The service that allows modifing Superset locales. + */ + angular + .module('openlmis-superset') + .service('supersetLocaleService', supersetLocaleService); + + supersetLocaleService.$inject = ['SUPERSET_URL', 'SUPERSET_LOCALES', 'DEFAULT_LANGUAGE', '$http']; + + function supersetLocaleService(SUPERSET_URL, SUPERSET_LOCALES, DEFAULT_LANGUAGE, $http) { + + this.changeLocale = changeLocale; + + /** + * @ngdoc method + * @methodOf superset:supersetLocaleService + * @name changeLocale + * + * @description + * The method that changes Superset locales. + * + * @param {String} locale (optional) locale to populate + * @return {Promise} The promise of Superset locale change request. + */ + function changeLocale(locale) { + return $http({ + method: 'GET', + url: SUPERSET_URL + '/lang/change/' + getValidLocale(locale), + withCredentials: true + }); + } + + function getValidLocale(locale) { + if (SUPERSET_LOCALES.indexOf(locale) === -1) { + locale = DEFAULT_LANGUAGE; + } + return locale; + } + } +}()); diff --git a/src/openlmis-superset/superset-locale.service.spec.js b/src/openlmis-superset/superset-locale.service.spec.js new file mode 100644 index 00000000..58d576f8 --- /dev/null +++ b/src/openlmis-superset/superset-locale.service.spec.js @@ -0,0 +1,58 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +describe('supersetLocaleService', function() { + + var that = this; + + beforeEach(function() { + that.SUPERSET_URL = 'http://localhost/superset'; + that.DEFAULT_LANGUAGE = 'en'; + + module('openlmis-superset', function($provide) { + $provide.constant('SUPERSET_URL', that.SUPERSET_URL); + $provide.constant('DEFAULT_LANGUAGE', that.DEFAULT_LANGUAGE); + }); + + inject(function($injector) { + that.supersetLocaleService = $injector.get('supersetLocaleService'); + that.$httpBackend = $injector.get('$httpBackend'); + }); + + }); + + describe('changeLocale', function() { + + // eslint-disable-next-line jasmine/missing-expect + it('should send change language request', function() { + that.$httpBackend.expectGET(that.SUPERSET_URL + '/lang/change/' + that.DEFAULT_LANGUAGE); + + that.supersetLocaleService.changeLocale(that.DEFAULT_LANGUAGE); + }); + + // eslint-disable-next-line jasmine/missing-expect + it('should change the language to default if a not known locale provided', function() { + that.$httpBackend.expectGET(that.SUPERSET_URL + '/lang/change/' + that.DEFAULT_LANGUAGE); + + that.supersetLocaleService.changeLocale('not_known_locale'); + }); + + afterEach(function() { + that.$httpBackend.verifyNoOutstandingExpectation(); + that.$httpBackend.verifyNoOutstandingRequest(); + }); + }); + +}); diff --git a/src/openlmis-superset/superset-locales.constant.js b/src/openlmis-superset/superset-locales.constant.js new file mode 100644 index 00000000..749dca1d --- /dev/null +++ b/src/openlmis-superset/superset-locales.constant.js @@ -0,0 +1,33 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +(function() { + 'use strict'; + + /** + * @ngdoc object + * @name superset.SUPERSET_LOCALES + * + * @description + * This is the constant defining a list of locales available in Superset. + */ + angular + .module('openlmis-superset') + .constant('SUPERSET_LOCALES', locales()); + + function locales() { + return ['en', 'fr', 'pt']; + } +})(); diff --git a/src/openlmis-superset/superset-oauth-login.controller.js b/src/openlmis-superset/superset-oauth-login.controller.js new file mode 100755 index 00000000..0cee1a16 --- /dev/null +++ b/src/openlmis-superset/superset-oauth-login.controller.js @@ -0,0 +1,135 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +(function() { + 'use strict'; + + /** + * @ngdoc controller + * @name superset:SupersetOAuthLoginController + * @description + * Controller that drives the Superset OAuth login form. + */ + angular + .module('openlmis-superset') + .controller('SupersetOAuthLoginController', SupersetOAuthLoginController); + + SupersetOAuthLoginController.$inject = [ + 'modalDeferred', 'authorizationService', 'loadingModalService', + 'supersetOAuthService', 'MODAL_CANCELLED' + ]; + + function SupersetOAuthLoginController(modalDeferred, authorizationService, loadingModalService, + supersetOAuthService, MODAL_CANCELLED) { + var vm = this; + vm.$onInit = onInit; + + vm.cancel = cancel; + vm.doLogin = doLogin; + + /** + * @ngdoc property + * @propertyOf superset:SupersetOAuthLoginController + * @name username + * @type {String} + * + * @description + * The username of the currently signed-in user. + */ + vm.username = undefined; + + /** + * @ngdoc property + * @propertyOf superset:SupersetOAuthLoginController + * @name supersetOAuthState + * @type {String} + * + * @description + * The Superset state which should be passed to Superset during next authorizing requests. + */ + vm.supersetOAuthState = undefined; + + /** + * @ngdoc property + * @propertyOf superset:SupersetOAuthLoginController + * @name loginError + * @type {String} + * + * @description + * The message key of error. If error not occurs, it should be set to undefined. + */ + vm.loginError = undefined; + + /** + * @ngdoc method + * @methodOf superset:SupersetOAuthLoginController + * @name $onInit + * + * @description + * The method that is executed on initiating SupersetOAuthLoginController. + * It checks whatever the user is already authorized in Superset. + */ + function onInit() { + loadingModalService.open(); + vm.username = authorizationService.getUser().username; + + supersetOAuthService.checkAuthorizationInSuperset() + .then(function(data) { + vm.supersetOAuthState = data.state; + if (data.isAuthorized === true) { + modalDeferred.resolve(); + } + }) + .catch(function() { + modalDeferred.reject('Superset is not available'); + }) + .finally(loadingModalService.close); + } + + /** + * @ngdoc method + * @methodOf superset:SupersetOAuthLoginController + * @name cancel + * + * @description + * The method that is invoked when user clicks the cancel button. It rejects the modal. + */ + function cancel() { + modalDeferred.reject(MODAL_CANCELLED); + } + + /** + * @ngdoc method + * @methodOf superset:SupersetOAuthLoginController + * @name doLogin + * + * @description + * The method that is invoked when the user clicks the authorize button. + * It starts the authorization process to Superset via Superset OAuth service. + */ + function doLogin() { + loadingModalService.open(); + vm.loginError = undefined; + supersetOAuthService.authorizeInSuperset(vm.username, vm.password, vm.supersetOAuthState) + .then(function() { + modalDeferred.resolve(); + }) + .catch(function(errorMessage) { + vm.loginError = errorMessage; + }) + .finally(loadingModalService.close); + } + } +}()); diff --git a/src/openlmis-superset/superset-oauth-login.controller.spec.js b/src/openlmis-superset/superset-oauth-login.controller.spec.js new file mode 100644 index 00000000..18729938 --- /dev/null +++ b/src/openlmis-superset/superset-oauth-login.controller.spec.js @@ -0,0 +1,190 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +describe('SupersetOAuthLoginController', function() { + + var vm, $controller, $q, $rootScope, modalDeferred, + authorizationService, loadingModalService, supersetOAuthService, + MODAL_CANCELLED, user, isAuthorizedResponse, isNotAuthorizedResponse; + + beforeEach(function() { + module('openlmis-superset'); + + inject(function($injector) { + $controller = $injector.get('$controller'); + $q = $injector.get('$q'); + $rootScope = $injector.get('$rootScope'); + + authorizationService = $injector.get('authorizationService'); + loadingModalService = $injector.get('loadingModalService'); + supersetOAuthService = $injector.get('supersetOAuthService'); + + MODAL_CANCELLED = $injector.get('MODAL_CANCELLED'); + + user = { + id: 'user-id-1', + username: 'user-1', + firstName: 'Jack', + lastName: 'Smith', + email: 'jack.smith@opelmis.com', + jobTitle: 'Junior Tester', + phoneNumber: '000-000-000', + timezone: 'UTC', + homeFacilityId: 'facility-id' + }; + + spyOn(authorizationService, 'getUser').andReturn(user); + spyOn(loadingModalService, 'open'); + spyOn(loadingModalService, 'close'); + spyOn(supersetOAuthService, 'authorizeInSuperset').andReturn($q.resolve()); + spyOn(supersetOAuthService, 'checkAuthorizationInSuperset') + .andReturn($q.resolve(isNotAuthorizedResponse)); + }); + + modalDeferred = $q.defer(); + isAuthorizedResponse = { + isAuthorized: true + }; + isNotAuthorizedResponse = { + isAuthorized: false, + state: 'test_state' + }; + + spyOn(modalDeferred, 'resolve'); + spyOn(modalDeferred, 'reject'); + + vm = $controller('SupersetOAuthLoginController', { + modalDeferred: modalDeferred + }); + }); + + describe('onInit', function() { + + it('should expose cancel method', function() { + vm.$onInit(); + + expect(angular.isFunction(vm.cancel)).toBe(true); + }); + + it('should expose doLogin method', function() { + vm.$onInit(); + + expect(angular.isFunction(vm.doLogin)).toBe(true); + }); + + it('should expose username', function() { + vm.$onInit(); + + expect(vm.username).toEqual(user.username); + }); + + it('should open loading modal', function() { + vm.$onInit(); + + expect(loadingModalService.open).toHaveBeenCalled(); + }); + + it('should skip modal if the user is already authorized', function() { + supersetOAuthService.checkAuthorizationInSuperset + .andReturn($q.resolve(isAuthorizedResponse)); + + vm.$onInit(); + $rootScope.$apply(); + + expect(modalDeferred.resolve).toHaveBeenCalled(); + }); + + it('should not skip modal and set state if the user is not authorized', function() { + vm.$onInit(); + $rootScope.$apply(); + + expect(vm.supersetOAuthState).toEqual(isNotAuthorizedResponse.state); + expect(modalDeferred.resolve).not.toHaveBeenCalled(); + expect(modalDeferred.reject).not.toHaveBeenCalled(); + }); + + it('should reject modal if cannot fetch response from Supserset', function() { + supersetOAuthService.checkAuthorizationInSuperset + .andReturn($q.reject()); + + vm.$onInit(); + $rootScope.$apply(); + + expect(modalDeferred.reject).toHaveBeenCalled(); + }); + }); + + describe('cancel', function() { + + beforeEach(function() { + vm.$onInit(); + vm.cancel(); + }); + + it('should reject modal with cancelled status', function() { + expect(modalDeferred.reject).toHaveBeenCalledWith(MODAL_CANCELLED); + }); + }); + + describe('doLogin', function() { + + beforeEach(function() { + vm.username = 'test_username'; + vm.password = 'test_password'; + vm.supersetOAuthState = isNotAuthorizedResponse.state; + + vm.$onInit(); + $rootScope.$apply(); + }); + + it('should open loading modal', function() { + vm.doLogin(); + + expect(loadingModalService.open).toHaveBeenCalled(); + }); + + it('should close loading modal after processing', function() { + vm.doLogin(); + $rootScope.$apply(); + + expect(loadingModalService.close).toHaveBeenCalled(); + }); + + it('should call authorizeInSuperset with modal properties', function() { + vm.doLogin(); + + expect(supersetOAuthService.authorizeInSuperset) + .toHaveBeenCalledWith(vm.username, vm.password, vm.supersetOAuthState); + }); + + it('should not close the modal if credentials are not correct', function() { + supersetOAuthService.authorizeInSuperset.andReturn($q.reject('openlmisLogin.invalidCredentials')); + + vm.doLogin(); + $rootScope.$apply(); + + expect(vm.loginError).toEqual('openlmisLogin.invalidCredentials'); + expect(modalDeferred.resolve).not.toHaveBeenCalled(); + expect(modalDeferred.reject).not.toHaveBeenCalled(); + }); + + it('should resolve the modal after approving', function() { + vm.doLogin(); + $rootScope.$apply(); + + expect(modalDeferred.resolve).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/openlmis-superset/superset-oauth-login.html b/src/openlmis-superset/superset-oauth-login.html new file mode 100755 index 00000000..0c6ca054 --- /dev/null +++ b/src/openlmis-superset/superset-oauth-login.html @@ -0,0 +1,33 @@ + diff --git a/src/openlmis-superset/superset-oauth.service.js b/src/openlmis-superset/superset-oauth.service.js new file mode 100755 index 00000000..b5de2c19 --- /dev/null +++ b/src/openlmis-superset/superset-oauth.service.js @@ -0,0 +1,163 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +(function() { + 'use strict'; + + /** + * @ngdoc service + * @name superset:supersetOAuthService + * @description + * The service that allows authorizing the Superset application from the OpenLMIS side. + * The functionality exposed by this service makes it possible to omit the default OAuth Approve page. + */ + angular + .module('openlmis-superset') + .service('supersetOAuthService', supersetOAuthService); + + supersetOAuthService.$inject = ['supersetUrlFactory', '$q', '$http', '$httpParamSerializer', 'authUrl']; + + function supersetOAuthService(supersetUrlFactory, $q, $http, $httpParamSerializer, authUrl) { + + this.checkAuthorizationInSuperset = checkAuthorizationInSuperset; + this.authorizeInSuperset = authorizeInSuperset; + + /** + * @ngdoc method + * @methodOf superset:supersetOAuthService + * @name checkAuthorizationInSuperset + * + * @description + * The method that checks the status of authorization in Supserset. + * If a user needs to be authorized the method returns state required for OAuth requests. + * + * @return {Object} Response from Superset with autorization status and state + */ + function checkAuthorizationInSuperset() { + return $http({ + method: 'GET', + url: supersetUrlFactory.buildCheckSupersetAuthorizationUrl(), + withCredentials: true + }) + .then(function(response) { + return $q.resolve(response.data); + }); + } + + /** + * @ngdoc method + * @methodOf superset:supersetOAuthService + * @name authorizeInSuperset + * + * @description + * The method that authorizes the user in Superset. + * + * @param {String} username The username of the person trying to login + * @param {String} password The password of the person is trying to login with + * @param {String} supersetOAuthState The OAuth state received from Superset + * @return {Promise} The promise for the authorization request + */ + function authorizeInSuperset(username, password, supersetOAuthState) { + return checkCredentials(username, password) + .then(function() { + return sendOAuthRequest(username, password, supersetOAuthState); + }) + .then(function() { + return approveSupersetIfNeeded(username, password); + }); + } + + function checkCredentials(username, password) { + return $http({ + method: 'POST', + url: authUrl('/api/oauth/token?grant_type=password'), + data: 'username=' + username + '&password=' + password, + headers: { + Authorization: uiAuthorizationHeader(), + 'Content-Type': 'application/x-www-form-urlencoded' + } + }) + .catch(function(response) { + var errorMessage; + if (response.status === 400) { + errorMessage = 'openlmisLogin.invalidCredentials'; + } else if (response.status === -1) { + errorMessage = 'openlmisLogin.cannotConnectToServer'; + } else { + errorMessage = 'openlmisLogin.unknownServerError'; + } + return $q.reject(errorMessage); + }); + } + + function sendOAuthRequest(username, password, supersetOAuthState) { + return $http({ + method: 'GET', + headers: { + 'Access-Control-Allow-Credentials': 'false', + Authorization: authorizationHeader(username, password) + }, + url: supersetUrlFactory.buildSupersetOAuthRequestUrl(supersetOAuthState), + withCredentials: true, + ignoreAuthModule: true + }) + .catch(function() { + return $q.reject('superset.oAuthLogin.invalidCredentialsOrOAuthRequest'); + }); + } + + function approveSupersetIfNeeded(username, password) { + return checkAuthorizationInSuperset() + .then(function(data) { + if (data.isAuthorized !== true) { + return approveSuperset(username, password); + } + }); + } + + function approveSuperset(username, password) { + return $http({ + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: authorizationHeader(username, password) + }, + url: supersetUrlFactory.buildApproveSupersetUrl(), + data: $httpParamSerializer({ + authorize: 'Authorize', + // eslint-disable-next-line camelcase + user_oauth_approval: 'true', + 'scope.read': 'true', + 'scope.write': 'true' + }), + withCredentials: true, + ignoreAuthModule: true + }) + .catch(function() { + return $q.reject('superset.oAuthLogin.unsuccessfulApprovingPermissions'); + }); + } + + function authorizationHeader(username, password) { + var data = btoa(username + ':' + password); + return 'Basic ' + data; + } + + function uiAuthorizationHeader() { + var data = btoa('@@AUTH_SERVER_CLIENT_ID' + ':' + '@@AUTH_SERVER_CLIENT_SECRET'); + return 'Basic ' + data; + } + } +}()); diff --git a/src/openlmis-superset/superset-oauth.service.spec.js b/src/openlmis-superset/superset-oauth.service.spec.js new file mode 100644 index 00000000..486e58a2 --- /dev/null +++ b/src/openlmis-superset/superset-oauth.service.spec.js @@ -0,0 +1,211 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +describe('supersetOAuthService', function() { + + var supersetOAuthService, $httpBackend, authUrl, supersetUrlFactory, + isAuthorizedResponse, isNotAuthorizedResponse, CHECK_SUPERSET_AUTORIZATION_URL; + + beforeEach(function() { + module('openlmis-superset'); + + inject(function($injector) { + supersetOAuthService = $injector.get('supersetOAuthService'); + $httpBackend = $injector.get('$httpBackend'); + + authUrl = $injector.get('authUrl'); + supersetUrlFactory = $injector.get('supersetUrlFactory'); + + CHECK_SUPERSET_AUTORIZATION_URL = supersetUrlFactory.buildCheckSupersetAuthorizationUrl(); + }); + + isAuthorizedResponse = { + isAuthorized: true + }; + isNotAuthorizedResponse = { + isAuthorized: false, + state: 'test_state' + }; + }); + + describe('checkAuthorizationInSuperset', function() { + + it('should return proper response if a user is already authorized', function() { + $httpBackend.expectGET(CHECK_SUPERSET_AUTORIZATION_URL) + .respond(200, isAuthorizedResponse); + + var result; + supersetOAuthService.checkAuthorizationInSuperset() + .then(function(response) { + result = response; + }); + $httpBackend.flush(); + + expect(result.isAuthorized).toBe(true); + }); + + it('should return proper response with state if the user is not authorized', function() { + $httpBackend.whenGET(CHECK_SUPERSET_AUTORIZATION_URL) + .respond(200, isNotAuthorizedResponse); + + var result; + supersetOAuthService.checkAuthorizationInSuperset() + .then(function(response) { + result = response; + }); + $httpBackend.flush(); + + expect(result.isAuthorized).toBe(false); + expect(result.state).toEqual('test_state'); + }); + }); + + describe('authorizeInSuperset', function() { + + var checkCredentialsEndpointMock, checkAuthorizationEndpointMock, state, username, password; + + beforeEach(function() { + checkCredentialsEndpointMock = $httpBackend + .whenPOST(authUrl('/api/oauth/token?grant_type=password')) + .respond(200); + + checkAuthorizationEndpointMock = $httpBackend + .whenGET(CHECK_SUPERSET_AUTORIZATION_URL) + .respond(200, isNotAuthorizedResponse); + + username = 'test_username'; + password = 'test_password'; + state = isNotAuthorizedResponse.state; + }); + + it('should send check credentials request with authorization header to OpenLMIS', function() { + $httpBackend.expectPOST(authUrl('/api/oauth/token?grant_type=password'), + function(data) { + return isString(data) && data.indexOf('username') !== -1 && data.indexOf('password') !== -1; + }, + function(headers) { + var authorizationHeader = headers['Authorization']; + return isString(authorizationHeader) && authorizationHeader.startsWith('Basic'); + }); + + supersetOAuthService.authorizeInSuperset(username, password, state); + $httpBackend.flush(); + }); + + it('should not send OAuth request if credentials are not correct', function() { + var oauthRequestUrl = supersetUrlFactory.buildSupersetOAuthRequestUrl(state); + var forbiddenCallTriggered = false; + + checkCredentialsEndpointMock.respond(400); + $httpBackend.whenGET(oauthRequestUrl).respond(function() { + forbiddenCallTriggered = true; + return [400, '']; + }); + + var error; + supersetOAuthService.authorizeInSuperset(username, password, state) + .catch(function(response) { + error = response; + }); + $httpBackend.flush(); + + expect(error).not.toBeUndefined(); + expect(forbiddenCallTriggered).toBe(false); + }); + + it('should send OAuth request with authorization header to OpenLMIS', function() { + var oauthRequestUrl = supersetUrlFactory.buildSupersetOAuthRequestUrl(state); + + $httpBackend.expectGET(oauthRequestUrl, function(headers) { + var authorizationHeader = headers['Authorization']; + return isString(authorizationHeader) && authorizationHeader.startsWith('Basic'); + }); + + supersetOAuthService.authorizeInSuperset(username, password, state); + $httpBackend.flush(); + }); + + it('should check whatever the user is already approved after OAuth request', function() { + var oauthRequestUrl = supersetUrlFactory.buildSupersetOAuthRequestUrl(state); + + $httpBackend.whenGET(oauthRequestUrl).respond(200); + $httpBackend.expectGET(CHECK_SUPERSET_AUTORIZATION_URL); + + supersetOAuthService.authorizeInSuperset(username, password, state); + $httpBackend.flush(); + }); + + it('should not approve OAuth request if the application is already approved', function() { + var oauthRequestUrl = supersetUrlFactory.buildSupersetOAuthRequestUrl(state); + var approveOAuthRequestUrl = supersetUrlFactory.buildApproveSupersetUrl(); + var forbiddenCallTriggered = false; + + $httpBackend.whenGET(oauthRequestUrl).respond(200); + checkAuthorizationEndpointMock.respond(200, isAuthorizedResponse); + $httpBackend.whenPOST(approveOAuthRequestUrl).respond(function() { + forbiddenCallTriggered = true; + return [400, '']; + }); + + supersetOAuthService.authorizeInSuperset(username, password, state); + $httpBackend.flush(); + + expect(forbiddenCallTriggered).toBe(false); + }); + + it('should send proper approve OAuth request if the application is not approved', function() { + var oauthRequestUrl = supersetUrlFactory.buildSupersetOAuthRequestUrl(state); + var approveOAuthRequestUrl = supersetUrlFactory.buildApproveSupersetUrl(); + + $httpBackend.whenGET(oauthRequestUrl).respond(200); + $httpBackend.expectPOST(approveOAuthRequestUrl, + 'authorize=Authorize&scope.read=true&scope.write=true&user_oauth_approval=true', + function(headers) { + var authorizationHeader = headers['Authorization']; + return isString(headers['Authorization']) && authorizationHeader.startsWith('Basic'); + }); + + supersetOAuthService.authorizeInSuperset(username, password, state); + $httpBackend.flush(); + }); + + it('should resolve the modal after approving', function() { + var state = isNotAuthorizedResponse.state; + var oauthRequestUrl = supersetUrlFactory.buildSupersetOAuthRequestUrl(state); + var approveOAuthRequestUrl = supersetUrlFactory.buildApproveSupersetUrl(); + + $httpBackend.whenGET(oauthRequestUrl).respond(200); + $httpBackend.whenPOST(approveOAuthRequestUrl).respond(200); + + var error; + supersetOAuthService.authorizeInSuperset(username, password, state) + .catch(function(response) { + error = response; + }); + $httpBackend.flush(); + + expect(error).toBeUndefined(); + }); + + afterEach(function() { + $httpBackend.verifyNoOutstandingExpectation(); + $httpBackend.verifyNoOutstandingRequest(); + }); + }); + + function isString(value) { + return value && (typeof value === 'string' || value instanceof String); + } +}); diff --git a/src/openlmis-superset/superset-url.constant.js b/src/openlmis-superset/superset-url.constant.js new file mode 100644 index 00000000..f8b07d4d --- /dev/null +++ b/src/openlmis-superset/superset-url.constant.js @@ -0,0 +1,30 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +(function() { + 'use strict'; + + /** + * @ngdoc object + * @name superset.SUPERSET_URL + * + * @description + * This is constant defining superset URL. + */ + angular + .module('openlmis-superset') + .constant('SUPERSET_URL', 'https://superset-uat.openlmis.org'); + +})(); \ No newline at end of file diff --git a/src/openlmis-superset/superset-url.factory.js b/src/openlmis-superset/superset-url.factory.js new file mode 100644 index 00000000..8ea6afc0 --- /dev/null +++ b/src/openlmis-superset/superset-url.factory.js @@ -0,0 +1,94 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +(function() { + + 'use strict'; + + /** + * @ngdoc service + * @name superset.supersetUrlFactory + * + * @description + * A factory that prepends URL required to get access to Superset. + */ + angular + .module('openlmis-superset') + .factory('supersetUrlFactory', supersetUrlFactory); + + supersetUrlFactory.$inject = ['authUrl', 'SUPERSET_URL']; + + function supersetUrlFactory(authUrl, SUPERSET_URL) { + var redirectUrl = SUPERSET_URL + '/oauth-authorized/openlmis', + factory = { + buildSupersetOAuthRequestUrl: buildSupersetOAuthRequestUrl, + buildApproveSupersetUrl: buildApproveSupersetUrl, + buildCheckSupersetAuthorizationUrl: buildCheckSupersetAuthorizationUrl + }; + + return factory; + + /** + * @ngdoc method + * @methodOf superset.supersetUrlFactory + * @name buildSupersetOAuthRequestUrl + * + * @description + * Prepares an URL for creating Superset OAuth request in OpenLMIS. + + * @param {String} supersetOAuthState the state from Superset to append as an url parameter + * @return {String} url that is directed towards the OpenLMIS Auth + */ + function buildSupersetOAuthRequestUrl(supersetOAuthState) { + var url = '/api/oauth/authorize?response_type=code&client_id=superset' + + '&redirect_uri=' + redirectUrl + + '&scope=read+write&state=' + supersetOAuthState; + return authUrl(url); + } + + /** + * @ngdoc method + * @methodOf superset.supersetUrlFactory + * @name buildApproveSupersetUrl + * + * @description + * Prepares URL which allows approving the Superset application in OpenLMIS. + + * @return {String} url that is directed towards the OpenLMIS Auth + */ + function buildApproveSupersetUrl() { + var redirectUrl = SUPERSET_URL + '/oauth-authorized/openlmis'; + var url = '/api/oauth/authorize?response_type=code&client_id=superset' + + '&redirect_uri=' + redirectUrl; + return authUrl(url); + } + + /** + * @ngdoc method + * @methodOf superset.supersetUrlFactory + * @name buildCheckSupersetAuthorizationUrl + * + * @description + * Prepares URL which allows to check user authorization and init OAuth Request in Superset. + + * @return {String} url that is directed towards Superset + */ + function buildCheckSupersetAuthorizationUrl() { + var redirectUrl = SUPERSET_URL + '/oauth-authorized/openlmis'; + return SUPERSET_URL + '/oauth-init/openlmis?redirect_url=' + redirectUrl; + } + } + +})(); diff --git a/src/openlmis-superset/superset.module.js b/src/openlmis-superset/superset.module.js new file mode 100644 index 00000000..0ca11f74 --- /dev/null +++ b/src/openlmis-superset/superset.module.js @@ -0,0 +1,34 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + *   + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  + */ + +(function() { + + 'use strict'; + + /** + * @module openlmis-superset + * + * @description + * Provides openlmis-superset login pages + */ + angular.module('openlmis-superset', [ + 'ngResource', + 'openlmis-auth', + 'openlmis-i18n', + 'openlmis-urls', + 'ui.router' + ]); + +})(); \ No newline at end of file