|
|
ac7d03 |
From 18e9bc2399af788399e66c3f4b28e6a7f0378b78 Mon Sep 17 00:00:00 2001
|
|
|
ac7d03 |
From: Pavel Vomacka <pvomacka@redhat.com>
|
|
|
ac7d03 |
Date: Wed, 22 Mar 2017 16:54:33 +0100
|
|
|
ac7d03 |
Subject: [PATCH] WebUI: Add support for login for AD users
|
|
|
ac7d03 |
|
|
|
ac7d03 |
After login, method user-find --whoami was called which cannot be
|
|
|
ac7d03 |
called for AD users. That method was replaced by ipa whoami command
|
|
|
ac7d03 |
and sequential command according to result of ipa whoami. AD user
|
|
|
ac7d03 |
can now be logged in.
|
|
|
ac7d03 |
|
|
|
ac7d03 |
AD users have new menu definition which contains only list of IPA
|
|
|
ac7d03 |
users and profile page of AD user - "User ID Override".
|
|
|
ac7d03 |
|
|
|
ac7d03 |
This commit also fixes several places where IPA.whoami object was
|
|
|
ac7d03 |
used, because its structure was also changed. It now contains two
|
|
|
ac7d03 |
objects. First one is stored in 'metadata' property and stores
|
|
|
ac7d03 |
result from ipa whoami (type of object, command which should be
|
|
|
ac7d03 |
called for showing detailed data about currently logged entity, etc).
|
|
|
ac7d03 |
The second one is stored in 'data' property which stores result of
|
|
|
ac7d03 |
_show command for currently logged entity.
|
|
|
ac7d03 |
|
|
|
ac7d03 |
https://pagure.io/freeipa/issue/3242
|
|
|
ac7d03 |
|
|
|
ac7d03 |
Reviewed-By: Petr Vobornik <pvoborni@redhat.com>
|
|
|
ac7d03 |
Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
|
|
|
ac7d03 |
---
|
|
|
ac7d03 |
install/ui/src/freeipa/Application_controller.js | 52 +++++++++++++++++++-----
|
|
|
ac7d03 |
install/ui/src/freeipa/idviews.js | 21 +++++++++-
|
|
|
ac7d03 |
install/ui/src/freeipa/ipa.js | 47 +++++++++++++--------
|
|
|
ac7d03 |
install/ui/src/freeipa/navigation/menu_spec.js | 10 +++++
|
|
|
ac7d03 |
install/ui/src/freeipa/otptoken.js | 2 +-
|
|
|
ac7d03 |
install/ui/src/freeipa/user.js | 5 ++-
|
|
|
ac7d03 |
ipaserver/plugins/internal.py | 1 +
|
|
|
ac7d03 |
7 files changed, 108 insertions(+), 30 deletions(-)
|
|
|
ac7d03 |
|
|
|
ac7d03 |
diff --git a/install/ui/src/freeipa/Application_controller.js b/install/ui/src/freeipa/Application_controller.js
|
|
|
ac7d03 |
index d809c1f2662609e390609270ef3ddc42f0727936..5eb4e7a5104b780761b9a5179dbfd1501a8d1478 100644
|
|
|
ac7d03 |
--- a/install/ui/src/freeipa/Application_controller.js
|
|
|
ac7d03 |
+++ b/install/ui/src/freeipa/Application_controller.js
|
|
|
ac7d03 |
@@ -31,6 +31,7 @@ define([
|
|
|
ac7d03 |
'./widgets/App',
|
|
|
ac7d03 |
'./widgets/FacetContainer',
|
|
|
ac7d03 |
'./ipa',
|
|
|
ac7d03 |
+ './rpc',
|
|
|
ac7d03 |
'./reg',
|
|
|
ac7d03 |
'./config',
|
|
|
ac7d03 |
'./widget',
|
|
|
ac7d03 |
@@ -41,7 +42,7 @@ define([
|
|
|
ac7d03 |
'./plugins/load_page'
|
|
|
ac7d03 |
],
|
|
|
ac7d03 |
function(declare, array, Deferred, on, topic, query, dom_class, auth,
|
|
|
ac7d03 |
- JSON, App_widget, FacetContainer, IPA, reg, config, widget_mod,
|
|
|
ac7d03 |
+ JSON, App_widget, FacetContainer, IPA, rpc, reg, config, widget_mod,
|
|
|
ac7d03 |
Menu, Router, routing, menu_spec) {
|
|
|
ac7d03 |
|
|
|
ac7d03 |
/**
|
|
|
ac7d03 |
@@ -156,7 +157,7 @@ define([
|
|
|
ac7d03 |
/**
|
|
|
ac7d03 |
* Turns off one item in user dropdown menu and remove its listener.
|
|
|
ac7d03 |
* @param {string} name of the user menu item which should be disabled
|
|
|
ac7d03 |
- * @param {Object} listener disable this listener
|
|
|
ac7d03 |
+ * @param {Object} listener disable di
|
|
|
ac7d03 |
*/
|
|
|
ac7d03 |
disable_user_menu_item: function(name, listener) {
|
|
|
ac7d03 |
this.app_widget.disable_user_menu_item(name);
|
|
|
ac7d03 |
@@ -179,16 +180,22 @@ define([
|
|
|
ac7d03 |
*/
|
|
|
ac7d03 |
choose_profile: function() {
|
|
|
ac7d03 |
|
|
|
ac7d03 |
- // TODO: change IPA.whoami.cn[0] to something readable
|
|
|
ac7d03 |
- this.update_logged_in(true, IPA.whoami.cn[0]);
|
|
|
ac7d03 |
+ this.update_logged_in(true);
|
|
|
ac7d03 |
var selfservice = this.is_selfservice();
|
|
|
ac7d03 |
|
|
|
ac7d03 |
|
|
|
ac7d03 |
this.app_widget.menu_widget.ignore_changes = true;
|
|
|
ac7d03 |
|
|
|
ac7d03 |
if (selfservice) {
|
|
|
ac7d03 |
- this.menu.name = menu_spec.self_service.name;
|
|
|
ac7d03 |
- this.menu.add_items(menu_spec.self_service.items);
|
|
|
ac7d03 |
+ if (this.is_aduser_selfservice()) {
|
|
|
ac7d03 |
+ this.menu.name = menu_spec.ad_self_service.name;
|
|
|
ac7d03 |
+ this.menu.add_items(menu_spec.ad_self_service.items);
|
|
|
ac7d03 |
+ this.disable_user_menu_item('password_reset',
|
|
|
ac7d03 |
+ this.on_passwd_reset_listener);
|
|
|
ac7d03 |
+ } else {
|
|
|
ac7d03 |
+ this.menu.name = menu_spec.self_service.name;
|
|
|
ac7d03 |
+ this.menu.add_items(menu_spec.self_service.items);
|
|
|
ac7d03 |
+ }
|
|
|
ac7d03 |
} else {
|
|
|
ac7d03 |
this.menu.name = menu_spec.admin.name;
|
|
|
ac7d03 |
this.menu.add_items(menu_spec.admin.items);
|
|
|
ac7d03 |
@@ -232,10 +239,9 @@ define([
|
|
|
ac7d03 |
},
|
|
|
ac7d03 |
|
|
|
ac7d03 |
is_selfservice: function() {
|
|
|
ac7d03 |
- var whoami = IPA.whoami;
|
|
|
ac7d03 |
+ var whoami = IPA.whoami.data;
|
|
|
ac7d03 |
var self_service = true;
|
|
|
ac7d03 |
|
|
|
ac7d03 |
-
|
|
|
ac7d03 |
if (whoami.hasOwnProperty('memberof_group') &&
|
|
|
ac7d03 |
whoami.memberof_group.indexOf('admins') !== -1) {
|
|
|
ac7d03 |
self_service = false;
|
|
|
ac7d03 |
@@ -255,13 +261,39 @@ define([
|
|
|
ac7d03 |
return self_service;
|
|
|
ac7d03 |
},
|
|
|
ac7d03 |
|
|
|
ac7d03 |
- update_logged_in: function(logged_in, fullname) {
|
|
|
ac7d03 |
+ is_aduser_selfservice: function() {
|
|
|
ac7d03 |
+ var selfservice = IPA.whoami.metadata.object === 'idoverrideuser';
|
|
|
ac7d03 |
+ // quite ugly, needed for users and iduseroverride to hide breadcrumb
|
|
|
ac7d03 |
+ IPA.is_aduser_selfservice = selfservice;
|
|
|
ac7d03 |
+
|
|
|
ac7d03 |
+ return selfservice;
|
|
|
ac7d03 |
+ },
|
|
|
ac7d03 |
+
|
|
|
ac7d03 |
+ update_logged_in: function(logged_in) {
|
|
|
ac7d03 |
this.app_widget.set('logged', logged_in);
|
|
|
ac7d03 |
+
|
|
|
ac7d03 |
+ var whoami = IPA.whoami;
|
|
|
ac7d03 |
+ var fullname = '';
|
|
|
ac7d03 |
+ var entity = whoami.metadata.object;
|
|
|
ac7d03 |
+
|
|
|
ac7d03 |
+ if (whoami.data.cn) {
|
|
|
ac7d03 |
+ fullname = whoami.data.cn[0];
|
|
|
ac7d03 |
+ } else if (whoami.data.displayname) {
|
|
|
ac7d03 |
+ fullname = whoami.data.displayname[0];
|
|
|
ac7d03 |
+ } else if (whoami.data.gecos) {
|
|
|
ac7d03 |
+ fullname = whoami.data.gecos[0];
|
|
|
ac7d03 |
+ } else if (whoami.data.krbprincipalname) {
|
|
|
ac7d03 |
+ fullname = whoami.data.krbprincipalname[0];
|
|
|
ac7d03 |
+ } else if (whoami.data.ipaoriginaluid) {
|
|
|
ac7d03 |
+ fullname = whoami.data.ipaoriginaluid[0];
|
|
|
ac7d03 |
+ }
|
|
|
ac7d03 |
+
|
|
|
ac7d03 |
this.app_widget.set('fullname', fullname);
|
|
|
ac7d03 |
},
|
|
|
ac7d03 |
|
|
|
ac7d03 |
on_profile: function() {
|
|
|
ac7d03 |
- routing.navigate(['entity', 'user', 'details', [IPA.whoami.uid[0]]]);
|
|
|
ac7d03 |
+ routing.navigate(['entity', IPA.whoami.metadata.object, 'details',
|
|
|
ac7d03 |
+ IPA.whoami.metadata.arguments]);
|
|
|
ac7d03 |
},
|
|
|
ac7d03 |
|
|
|
ac7d03 |
on_logout: function(event) {
|
|
|
ac7d03 |
diff --git a/install/ui/src/freeipa/idviews.js b/install/ui/src/freeipa/idviews.js
|
|
|
ac7d03 |
index f383ab3be4c4ed997fb209da2da4d04835236d8a..d9133a13c2ed8970e7919bb28d06f818d832170a 100644
|
|
|
ac7d03 |
--- a/install/ui/src/freeipa/idviews.js
|
|
|
ac7d03 |
+++ b/install/ui/src/freeipa/idviews.js
|
|
|
ac7d03 |
@@ -452,6 +452,21 @@ idviews.id_override_user_details_facet = function(spec) {
|
|
|
ac7d03 |
return that;
|
|
|
ac7d03 |
};
|
|
|
ac7d03 |
|
|
|
ac7d03 |
+
|
|
|
ac7d03 |
+idviews.aduser_idoverrideuser_pre_op = function(spec, context) {
|
|
|
ac7d03 |
+ spec = spec || [];
|
|
|
ac7d03 |
+
|
|
|
ac7d03 |
+ if (!IPA.is_aduser_selfservice) return spec;
|
|
|
ac7d03 |
+
|
|
|
ac7d03 |
+ var facet = spec.facets[0];
|
|
|
ac7d03 |
+ facet.label = '@i18n:objects.idoverrideuser.profile';
|
|
|
ac7d03 |
+ facet.actions = [];
|
|
|
ac7d03 |
+ facet.header_actions = [];
|
|
|
ac7d03 |
+ facet.disable_breadcrumb = true;
|
|
|
ac7d03 |
+
|
|
|
ac7d03 |
+ return spec;
|
|
|
ac7d03 |
+};
|
|
|
ac7d03 |
+
|
|
|
ac7d03 |
/**
|
|
|
ac7d03 |
* @extends IPA.cert.certs_widget
|
|
|
ac7d03 |
*/
|
|
|
ac7d03 |
@@ -948,7 +963,11 @@ idviews.register = function() {
|
|
|
ac7d03 |
var w = reg.widget;
|
|
|
ac7d03 |
|
|
|
ac7d03 |
e.register({type: 'idview', spec: idviews.spec});
|
|
|
ac7d03 |
- e.register({type: 'idoverrideuser', spec: idviews.idoverrideuser_spec});
|
|
|
ac7d03 |
+ e.register({
|
|
|
ac7d03 |
+ type: 'idoverrideuser',
|
|
|
ac7d03 |
+ spec: idviews.idoverrideuser_spec,
|
|
|
ac7d03 |
+ pre_ops: [idviews.aduser_idoverrideuser_pre_op]
|
|
|
ac7d03 |
+ });
|
|
|
ac7d03 |
e.register({type: 'idoverridegroup', spec: idviews.idoverridegroup_spec});
|
|
|
ac7d03 |
f.copy('attribute', 'idview_appliedtohosts', {
|
|
|
ac7d03 |
factory: idviews.appliedtohosts_facet
|
|
|
ac7d03 |
diff --git a/install/ui/src/freeipa/ipa.js b/install/ui/src/freeipa/ipa.js
|
|
|
ac7d03 |
index 0ddbd0744d699cacddb3970e5ec7cb72b9dbf4f4..2538001c94141b823d634ca63327a66fd148129f 100644
|
|
|
ac7d03 |
--- a/install/ui/src/freeipa/ipa.js
|
|
|
ac7d03 |
+++ b/install/ui/src/freeipa/ipa.js
|
|
|
ac7d03 |
@@ -86,7 +86,8 @@ var IPA = function () {
|
|
|
ac7d03 |
/**
|
|
|
ac7d03 |
* User information
|
|
|
ac7d03 |
*
|
|
|
ac7d03 |
- * - output of ipa user-find --whoami
|
|
|
ac7d03 |
+ * - output of ipa whoami in that.whoami.metadata and then object_show method
|
|
|
ac7d03 |
+ * in that.whoami.data
|
|
|
ac7d03 |
*/
|
|
|
ac7d03 |
that.whoami = {};
|
|
|
ac7d03 |
|
|
|
ac7d03 |
@@ -263,19 +264,33 @@ var IPA = function () {
|
|
|
ac7d03 |
*/
|
|
|
ac7d03 |
that.get_whoami_command = function(batch) {
|
|
|
ac7d03 |
return rpc.command({
|
|
|
ac7d03 |
- entity: 'user',
|
|
|
ac7d03 |
- method: 'find',
|
|
|
ac7d03 |
- options: {
|
|
|
ac7d03 |
- whoami: true,
|
|
|
ac7d03 |
- all: true
|
|
|
ac7d03 |
- },
|
|
|
ac7d03 |
+ method: 'whoami',
|
|
|
ac7d03 |
on_success: function(data, text_status, xhr) {
|
|
|
ac7d03 |
- that.whoami = batch ? data.result[0] : data.result.result[0];
|
|
|
ac7d03 |
- var cn = that.whoami.krbcanonicalname;
|
|
|
ac7d03 |
- if (cn) that.principal = cn[0];
|
|
|
ac7d03 |
- if (!that.principal) {
|
|
|
ac7d03 |
- that.principal = that.whoami.krbprincipalname[0];
|
|
|
ac7d03 |
- }
|
|
|
ac7d03 |
+ that.whoami.metadata = data;
|
|
|
ac7d03 |
+
|
|
|
ac7d03 |
+ rpc.command({
|
|
|
ac7d03 |
+ method: data.details || data.command,
|
|
|
ac7d03 |
+ args: data.arguments,
|
|
|
ac7d03 |
+ options: function() {
|
|
|
ac7d03 |
+ var options = data.options || [];
|
|
|
ac7d03 |
+ $.extend(options, {all: true});
|
|
|
ac7d03 |
+ return options;
|
|
|
ac7d03 |
+ }(),
|
|
|
ac7d03 |
+ on_success: function(data, text_status, xhr) {
|
|
|
ac7d03 |
+ that.whoami.data = false ? data.result[0] : data.result.result;
|
|
|
ac7d03 |
+ var entity = that.whoami.metadata.object;
|
|
|
ac7d03 |
+
|
|
|
ac7d03 |
+ if (entity === 'user') {
|
|
|
ac7d03 |
+ var cn = that.whoami.data.krbcanonicalname;
|
|
|
ac7d03 |
+ if (cn) that.principal = cn[0];
|
|
|
ac7d03 |
+ if (!that.principal) {
|
|
|
ac7d03 |
+ that.principal = that.whoami.data.krbprincipalname[0];
|
|
|
ac7d03 |
+ }
|
|
|
ac7d03 |
+ } else if (entity === 'idoverrideuser') {
|
|
|
ac7d03 |
+ that.principal = that.whoami.data.ipaoriginaluid[0];
|
|
|
ac7d03 |
+ }
|
|
|
ac7d03 |
+ }
|
|
|
ac7d03 |
+ }).execute();
|
|
|
ac7d03 |
}
|
|
|
ac7d03 |
});
|
|
|
ac7d03 |
};
|
|
|
ac7d03 |
@@ -616,7 +631,7 @@ IPA.update_password_expiration = function() {
|
|
|
ac7d03 |
|
|
|
ac7d03 |
var now, expires, notify_days, diff, message, container, notify;
|
|
|
ac7d03 |
|
|
|
ac7d03 |
- expires = rpc.extract_objects(IPA.whoami.krbpasswordexpiration);
|
|
|
ac7d03 |
+ expires = rpc.extract_objects(IPA.whoami.data.krbpasswordexpiration);
|
|
|
ac7d03 |
expires = expires ? datetime.parse(expires[0]) : null;
|
|
|
ac7d03 |
|
|
|
ac7d03 |
notify_days = IPA.server_config.ipapwdexpadvnotify;
|
|
|
ac7d03 |
@@ -650,13 +665,13 @@ IPA.update_password_expiration = function() {
|
|
|
ac7d03 |
IPA.password_selfservice = function() {
|
|
|
ac7d03 |
var reset_dialog = builder.build('dialog', {
|
|
|
ac7d03 |
$type: 'user_password',
|
|
|
ac7d03 |
- args: [IPA.whoami.uid[0]]
|
|
|
ac7d03 |
+ args: [IPA.whoami.data.uid[0]]
|
|
|
ac7d03 |
});
|
|
|
ac7d03 |
reset_dialog.succeeded.attach(function() {
|
|
|
ac7d03 |
var command = IPA.get_whoami_command();
|
|
|
ac7d03 |
var orig_on_success = command.on_success;
|
|
|
ac7d03 |
command.on_success = function(data, text_status, xhr) {
|
|
|
ac7d03 |
- orig_on_success.call(this, data, text_status, xhr);
|
|
|
ac7d03 |
+ orig_on_success.call(this, data.result, text_status, xhr);
|
|
|
ac7d03 |
IPA.update_password_expiration();
|
|
|
ac7d03 |
};
|
|
|
ac7d03 |
command.execute();
|
|
|
ac7d03 |
diff --git a/install/ui/src/freeipa/navigation/menu_spec.js b/install/ui/src/freeipa/navigation/menu_spec.js
|
|
|
ac7d03 |
index 4f78e4bf9ba25bf2f7585e38086b1cbc6db34026..9329694c14a47cbe1ec244554327b40743044d7b 100644
|
|
|
ac7d03 |
--- a/install/ui/src/freeipa/navigation/menu_spec.js
|
|
|
ac7d03 |
+++ b/install/ui/src/freeipa/navigation/menu_spec.js
|
|
|
ac7d03 |
@@ -353,5 +353,15 @@ nav.self_service = {
|
|
|
ac7d03 |
]
|
|
|
ac7d03 |
};
|
|
|
ac7d03 |
|
|
|
ac7d03 |
+nav.ad_self_service = {
|
|
|
ac7d03 |
+ name: 'ad_self_service',
|
|
|
ac7d03 |
+ items: [
|
|
|
ac7d03 |
+ {
|
|
|
ac7d03 |
+ entity: 'idoverrideuser',
|
|
|
ac7d03 |
+ label: 'Profile'
|
|
|
ac7d03 |
+ }
|
|
|
ac7d03 |
+ ]
|
|
|
ac7d03 |
+};
|
|
|
ac7d03 |
+
|
|
|
ac7d03 |
return nav;
|
|
|
ac7d03 |
});
|
|
|
ac7d03 |
diff --git a/install/ui/src/freeipa/otptoken.js b/install/ui/src/freeipa/otptoken.js
|
|
|
ac7d03 |
index caa7a85523d6e63db629a3a518e8611d511f7952..1f6f20d801042a5424ecf5894658df9411723bcc 100644
|
|
|
ac7d03 |
--- a/install/ui/src/freeipa/otptoken.js
|
|
|
ac7d03 |
+++ b/install/ui/src/freeipa/otptoken.js
|
|
|
ac7d03 |
@@ -361,7 +361,7 @@ otptoken.adder_dialog = function(spec) {
|
|
|
ac7d03 |
|
|
|
ac7d03 |
var command = that.entity_adder_dialog_create_add_command(record);
|
|
|
ac7d03 |
if (that.self_service) {
|
|
|
ac7d03 |
- command.set_option('ipatokenowner', IPA.whoami.uid[0]);
|
|
|
ac7d03 |
+ command.set_option('ipatokenowner', IPA.whoami.data.uid[0]);
|
|
|
ac7d03 |
}
|
|
|
ac7d03 |
return command;
|
|
|
ac7d03 |
};
|
|
|
ac7d03 |
diff --git a/install/ui/src/freeipa/user.js b/install/ui/src/freeipa/user.js
|
|
|
ac7d03 |
index 4bb04488b51dd43a437ab3759eb3f530afe62550..6b2bf196c31e7891d3389eb2e2774f56d88ac2ba 100644
|
|
|
ac7d03 |
--- a/install/ui/src/freeipa/user.js
|
|
|
ac7d03 |
+++ b/install/ui/src/freeipa/user.js
|
|
|
ac7d03 |
@@ -735,7 +735,7 @@ IPA.user.password_dialog = function(spec) {
|
|
|
ac7d03 |
var that = dialogs.command_dialog(spec);
|
|
|
ac7d03 |
|
|
|
ac7d03 |
that.is_self_service = function() {
|
|
|
ac7d03 |
- var self_service = that.args[0] === IPA.whoami.uid[0];
|
|
|
ac7d03 |
+ var self_service = that.args[0] === IPA.whoami.data.uid[0];
|
|
|
ac7d03 |
return self_service;
|
|
|
ac7d03 |
};
|
|
|
ac7d03 |
|
|
|
ac7d03 |
@@ -895,7 +895,8 @@ IPA.user.self_service_other_user_evaluator = function(spec) {
|
|
|
ac7d03 |
that.state = [];
|
|
|
ac7d03 |
|
|
|
ac7d03 |
var value = that.adapter.load(data);
|
|
|
ac7d03 |
- if (IPA.is_selfservice && IPA.whoami.uid[0] !== value[0]) {
|
|
|
ac7d03 |
+ if (IPA.is_aduser_selfservice ||
|
|
|
ac7d03 |
+ (IPA.is_selfservice && IPA.whoami.data.uid[0] !== value[0])) {
|
|
|
ac7d03 |
that.state.push('self-service-other');
|
|
|
ac7d03 |
}
|
|
|
ac7d03 |
|
|
|
ac7d03 |
diff --git a/ipaserver/plugins/internal.py b/ipaserver/plugins/internal.py
|
|
|
ac7d03 |
index 9fa1b6de857cf7e21210f557befabff32da0d4ff..6feefa5941506f38f01b8016a22cad14a831e3fc 100644
|
|
|
ac7d03 |
--- a/ipaserver/plugins/internal.py
|
|
|
ac7d03 |
+++ b/ipaserver/plugins/internal.py
|
|
|
ac7d03 |
@@ -625,6 +625,7 @@ class i18n_messages(Command):
|
|
|
ac7d03 |
"anchor_label": _("User to override"),
|
|
|
ac7d03 |
"anchor_tooltip": _("Enter trusted or IPA user login. Note: search doesn't list users from trusted domains."),
|
|
|
ac7d03 |
"anchor_tooltip_ad": _("Enter trusted user login."),
|
|
|
ac7d03 |
+ "profile": _("Profile"),
|
|
|
ac7d03 |
},
|
|
|
ac7d03 |
"idoverridegroup": {
|
|
|
ac7d03 |
"anchor_label": _("Group to override"),
|
|
|
ac7d03 |
--
|
|
|
ac7d03 |
2.12.1
|
|
|
ac7d03 |
|