webui: authentication module

General purpose authentication interface and state. See doc of 'freeipa/auth' module.

https://fedorahosted.org/freeipa/ticket/3903

Reviewed-By: Adam Misnyovszki <amisnyov@redhat.com>
This commit is contained in:
Petr Vobornik
2014-02-14 19:04:15 +01:00
parent 7c068f036f
commit 2ec5d969a2
5 changed files with 318 additions and 55 deletions

View File

@@ -18,6 +18,8 @@
"classes": [
"phases",
"_base.Phase_controller*",
"auth",
"auth.Auth",
"Application_controller",
"app",
"plugin_loader",

View File

@@ -102,13 +102,13 @@ define([
on(this.app_widget, 'logout-click', lang.hitch(this, this.on_logout));
on(this.app_widget, 'password-reset-click', lang.hitch(this, this.on_password_reset));
on(this.app_widget, 'about-click', lang.hitch(this, this.on_about));
on(this.menu, 'selected', lang.hitch(this, this.on_menu_select));
on(this.router, 'facet-show', lang.hitch(this, this.on_facet_show));
on(this.router, 'facet-change', lang.hitch(this, this.on_facet_change));
on(this.router, 'facet-change-canceled', lang.hitch(this, this.on_facet_canceled));
on(this.router, 'error', lang.hitch(this, this.on_router_error));
topic.subscribe('phase-error', lang.hitch(this, this.on_phase_error));
topic.subscribe('authenticate', lang.hitch(this, this.on_authenticate));
this.app_widget.render();
this.app_widget.hide();
@@ -263,6 +263,8 @@ define([
var new_facet = event.facet;
var current_facet = this.current_facet;
if (current_facet === new_facet) return;
if (current_facet && !current_facet.can_leave()) {
var permit_clb = lang.hitch(this, function() {
// Some facet's might not call reset before this call but after
@@ -417,29 +419,45 @@ define([
},
/**
* Watches menu changes and adjusts facet space when there is
* a need for larger menu space.
*
* Show extended menu space when:
* * there is 3+ levels of menu
*
* Don't show when:
* * all items of levels 3+ are hidden
* Starts authentication process in authentication UI
* @returns {undefined}
*/
on_menu_select: function(select_state) {
on_authenticate: function() {
var visible_levels = 0;
var levels = select_state.new_selection.length;
for (var i=0; i< levels; i++) {
var item = select_state.new_selection[i];
if(!item.hidden) visible_levels++;
var self = this;
if (this.auth_ui === 'dialog') {
var dummy_command = {
execute: function() {
topic.publish('auth-successful');
}
};
var dialog = IPA.unauthorized_dialog({
close_on_escape: false,
error_thrown: { name: '', message: ''},
command: dummy_command
});
dialog.open();
} else {
var facet = this.current_facet;
// we don't want the load facet to be displayed after successful auth
if (facet && facet.name === 'load') {
facet = null;
}
var login_facet = reg.facet.get('login');
on.once(login_facet, "logged_in", function() {
if (facet) {
self.show_facet(facet);
}
topic.publish('auth-successful');
});
this.show_facet(login_facet);
}
var three_levels = visible_levels >= 3;
dom_class.toggle(this.app_widget.content_node,
'nav-space-3',
three_levels);
}
});

View File

@@ -0,0 +1,252 @@
/* Authors:
* Petr Vobornik <pvoborni@redhat.com>
*
* Copyright (C) 2014 Red Hat
* see file 'COPYING' for use and warranty information
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
define([
'dojo/_base/declare',
'dojo/_base/lang',
'dojo/Deferred',
'dojo/Evented',
'dojo/Stateful',
'dojo/topic',
'dojo/when'
],
function(declare, lang, Deferred, Evented, Stateful, topic, when) {
/**
* Authentication module
* @class auth
* @singleton
*/
var auth = {
/**
* Current authentication state
* @property {auth.Auth}
*/
current: null
};
/**
* Authentication interface and state.
*
* Can be used for checking whether user is authenticated, by what method or
* what methods can be used for authentication. Actual authentication is
* done by separate object - authentication provider.
*
* Communication with authentication providers is done through global messages
* (`dojo/topic`).
*
* Some component can initiate the authentication process by calling:
*
* var auth_promise = auth.current.authenticate();
*
* `auth_promise` is a promise which is resolve on auth success and rejected
* on auth failure.
*
* Logout works in similar fashion:
*
* var logout_promise = auth.current.logout();
*
* The communication with authentication providers works as follows:
*
* 1. `auth.current.authenticate();` publishes `authenticate` topic
* 2. provider starts the authentication process
* 3. if it finishes with a success provider publishes `auth-successful`, if not
* it publishes `auth-failed`
* 4. the promise is resolved or rejected
*
* Logout works in similar fashion, only the topic names are `log-out`,
* `logout-successful` and `logout-failed`.
*
* New `authenticate` or `log-out` topics are not published if there is
* already authentication or logout in progress. The promises from subsequent
* `authenticate()` or `logout()` calls are resolved as expected.
*
* `login`, `principal`, `whoami`, `fullname` properties are supposed to be
* set by authentication providers.
*
* @class
*/
auth.Auth = declare([Stateful, Evented], {
/**
* Raw User information
*
* @property {Object}
*/
whoami: {},
/**
* User is authenticated
*
* Use `set_authenticated(state, method)` for setting it.
*
* @property {boolean}
* @readonly
*/
authenticated: false,
/**
* Method used for authentication
* @property {string}
*/
authenticated_by: "",
/**
* Enabled auth methods
* @property {string[]}
*/
auth_methods: ['kerberos', 'password'],
/**
* Authenticated user's Kerberos principal
* @property {string}
*/
principal: "",
/**
* Authenticated user's login
* @property {string}
*/
login: "",
/**
* Authenticated user's fullname
* @property {string}
*/
fullname: "",
/**
* Authentication is in progress
* @property {boolean}
*/
authenticating: false,
/**
* Logging out is in progress
* @property {boolean}
*/
logging_out: false,
/**
* Indicates whether user was previously authenticated
* @property {boolean}
*/
expired: false,
/**
* Update authenticated state
* @param {boolean} state User is authenticated
* @param {string} method used for authentication
*/
set_authenticated: function(state, method) {
if (this.authenticated && !state) {
this.set('expired', true);
}
this.set('authenticated', state);
this.set('authenticated_by', method);
if (this.authenticated) {
this.set('expired', false);
}
},
/**
* Initiate authentication process (if not already initiated)
*
* Returns promise which is fulfilled when user is authenticated. It's
* rejected when authentication is canceled.
* @returns {Promise}
*/
authenticate: function() {
var authenticated = new Deferred();
var ok_handler = topic.subscribe('auth-successful', function(info) {
authenticated.resolve(true);
ok_handler.remove();
fail_handler.remove();
});
var fail_handler = topic.subscribe('auth-failed', function(info) {
authenticated.reject();
ok_handler.remove();
fail_handler.remove();
});
if (!this.authenticating) {
topic.publish('authenticate', this);
}
return authenticated.promise;
},
/**
* Initiate logout process (if not already initiated)
*
* Returns promise which is fulfilled when user is logged-out. It's
* rejected when logout failed.
* @returns {Promise}
*/
logout: function() {
var loggedout = new Deferred();
var ok_handler = topic.subscribe('logout-successful', function(info) {
loggedout.resolve(true);
ok_handler.remove();
fail_handler.remove();
});
var fail_handler = topic.subscribe('logout-failed', function(info) {
loggedout.reject();
ok_handler.remove();
fail_handler.remove();
});
if (!this.logging_out) {
topic.publish('log-out', this);
}
return loggedout.promise;
},
/**
* Initializes instance
*
* @private
*/
postscript: function() {
var self = this;
var auth_true = function() {
self.set('authenticating', true);
};
var auth_false = function() {
self.set('authenticating', false);
};
var out_true = function() {
self.set('logging_out', true);
};
var out_false = function() {
self.set('logging_out', false);
};
topic.subscribe('auth-successful', auth_false);
topic.subscribe('auth-failed', auth_false);
topic.subscribe('authenticate', auth_true);
topic.subscribe('logout-successful', out_true);
topic.subscribe('logout-failed', out_true);
topic.subscribe('log-out', out_false);
}
});
auth.current = new auth.Auth();
return auth;
});

View File

@@ -28,6 +28,7 @@ define([
'./jquery',
'./json2',
'./_base/i18n',
'./auth',
'./datetime',
'./metadata',
'./builder',
@@ -35,7 +36,7 @@ define([
'./rpc',
'./text',
'exports'
], function(keys, topic, $, JSON, i18n, datetime, metadata_provider,
], function(keys, topic, $, JSON, i18n, auth, datetime, metadata_provider,
builder, reg, rpc, text, exports) {
/**
@@ -107,9 +108,6 @@ var IPA = function () {
* - metadata
* - user information
* - server configuration
* @property {boolean} logged_kerberos - User authenticated by
* Kerberos negotiation
* @property {boolean} logged_password - User authenticated by password
*/
that.ui = {};
@@ -362,7 +360,7 @@ IPA.object = function(s) {
/**
* Make request on Kerberos authentication url to initialize Kerberos negotiation.
*
* Set result to IPA.ui.logged_kerberos.
* Set result to auth module.
*
* @member IPA
*/
@@ -371,12 +369,11 @@ IPA.get_credentials = function() {
function error_handler(xhr, text_status, error_thrown) {
status = xhr.status;
IPA.ui.logged_kerberos = false;
}
function success_handler(data, text_status, xhr) {
status = xhr.status;
IPA.ui.logged_kerberos = true;
auth.current.set_authenticated(true, 'kerberos');
}
var request = {
@@ -397,7 +394,7 @@ IPA.get_credentials = function() {
* Logout
*
* - terminate the session.
* - redirect to logout landing page on success
* - reloads UI
*
* @member IPA
*/
@@ -412,21 +409,22 @@ IPA.logout = function() {
dialog.open();
}
function redirect () {
window.location = 'logout.html';
function reload () {
var l = window.location;
l.assign(l.href.split('#')[0]);
}
function success_handler(data, text_status, xhr) {
if (data && data.error) {
show_error(data.error.message);
} else {
redirect();
reload();
}
}
function error_handler(xhr, text_status, error_thrown) {
if (xhr.status === 401) {
redirect();
reload();
} else {
show_error(text_status);
}
@@ -461,7 +459,7 @@ IPA.login_password = function(username, password) {
function success_handler(data, text_status, xhr) {
result = 'success';
IPA.ui.logged_password = true;
auth.current.set_authenticated(true, 'password');
}
function error_handler(xhr, text_status, error_thrown) {
@@ -475,8 +473,6 @@ IPA.login_password = function(username, password) {
result = reason;
}
}
IPA.ui.logged_password = false;
}
var data = {
@@ -825,7 +821,7 @@ IPA.error_dialog = function(spec) {
IPA.confirm_mixin().apply(that);
/** @property {XMLHttpRequest} xhr Command's xhr */
that.xhr = spec.xhr || {};
that.xhr = spec.xhr || null;
/** @property {string} text_status Command's text status */
that.text_status = spec.text_status || '';
/** @property {{name:string,message:string}} error_thrown Command's error */

View File

@@ -23,12 +23,13 @@
*/
define([
'dojo/_base/lang',
'dojo/_base/lang',
'./auth',
'./ipa',
'./text',
'exports'
],
function(lang, IPA, text, rpc /*exports*/) {
function(lang, auth, IPA, text, rpc /*exports*/) {
/**
* Call an IPA command over JSON-RPC.
@@ -206,19 +207,12 @@ rpc.command = function(spec) {
dialog.open();
}
function auth_dialog_open(xhr, text_status, error_thrown) {
function error_handler_auth(xhr, text_status, error_thrown) {
var ajax = this;
var dialog = IPA.unauthorized_dialog({
xhr: xhr,
text_status: text_status,
error_thrown: error_thrown,
close_on_escape: false,
command: that
auth.current.set_authenticated(false, '');
auth.current.authenticate().then(function() {
that.execute();
});
dialog.open();
}
/*
@@ -259,7 +253,7 @@ rpc.command = function(spec) {
IPA.hide_activity_icon();
if (xhr.status === 401) {
auth_dialog_open(xhr, text_status, error_thrown);
error_handler_auth(xhr, text_status, error_thrown);
return;
} else if (!error_thrown) {
error_thrown = {
@@ -281,13 +275,14 @@ rpc.command = function(spec) {
error_thrown.message = error_msg;
}
// global specical cases error handlers section
// global special cases error handlers section
// With trusts, user from trusted domain can use his ticket but he
// doesn't have rights for LDAP modify. It will throw internal errror.
// doesn't have rights for LDAP modify. It will throw internal error.
// We should offer form base login.
if (xhr.status === 500 && IPA.ui.logged_kerberos && !IPA.ui.initialized) {
auth_dialog_open(xhr, text_status, error_thrown);
if (xhr.status === 500 && auth.authenticated_by === 'kerberos' &&
!IPA.ui.initialized) {
error_handler_auth(xhr, text_status, error_thrown);
return;
}