Битва за FS (Active Directory Federation Services)

Передісторія

Проект починався як портал на основі SP 2007, а пізніше на основі 2010 SP. Спочатку всі користувачі були в Active Directory. Був тільки один тип користувачів. Зв'язки між ними були досить простими. З'являлися нові типи користувачів, які складним чином ставали пов'язані один з одним. Також поступово проект обростав різними пов'язаними підсистемами, частина з яких працювала всередині порталу, частину поза його. І це все ускладнювало схему авторизації.



Які проблеми?

До якогось моменту використання NTLM задовольняло всі потреби. Єдина проблема була в тому, що зіткнулися з тим, що при переході на пов'язаний сервіс, в тому випадку якщо url-адреса відрізнявся від адреси порталу, доводилося заново вводити логін і пароль. В принципі таке завдання могла б бути вирішена за допомогою продукту web application proxy. Проте пізніше з'явилися модулі на Java і, таким чином, середовище стала в значній мірі гетерогенною. Також на горизонті замаячила необхідність надання доступу користувачам з сторонніх доменів. Для вирішення цих завдань стала зрозумілою необхідність Single Sign On (SSO). Було прийнято рішення про впровадження ADFS і перекладі порталу та всіх служб на цю технологію.

Ми обговорили наше бачення з замовником та намітили День X, коли все має запрацювати на ADFS.

Хід подій:



День X – 1 рік
Першим ділом ми вирішили переключити на ADFS один з модулів.
Ідея була в тому, щоб включивши ADFS на порталі, набити всі шишки, які можна набити, обпектися там, де можна обпектися і т. п. Поимев певну кількість проблем, ми успішно провели це перемикання, про що вже писали habrahabr.ru/company/eastbanctech/blog/209834.

День X – 3 місяці

Перше, з чого почали — рефакторинг коду, щоб він працював коректно під Claims Authentication. Пара прикладів з того, що ми змінили:

1. Роздача дозволів на елементи списків SharePoint для користувачів і груп Active Directory. Так як дозволи повинні були тепер лунати на клэймы, то довелося переписувати всі ці місця. Добре, що майже всі такі місця використовували одну нашу бібліотеку по роботі з AD (а ті, що не використовували, ми змінили таким чином, щоб вони теж працювали з нашою бібліотекою).

Замість роздачі дозволів користувачам виду «domain\user», дозволу стали видаватися клэймам «i:0e.t/ADFS/user@domain». Для «domain\group» — на клэймы виду «c:0-.t/ADFS/group» відповідно. Нам також довелося відокремити випадки, коли дозволи видаються користувачу, від випадків, коли вони видаються групі, оскільки без використання claims доменні групи виглядають в MS SharePoint однаково. Таким чином метод GetPrincipalName, визначає, повне ім'я principal я перетворився в 2:

public static string GetGroupPrincipalName(string group)
{
if (string.IsNullOrEmpty(TrustedIdentityProviderName))
{
return string.Format(CultureInfo.InvariantCulture, "{0}\\{1}", CurrentDomain, GetPrincipalNameWithoutDomain(group));
}
return string.Concat("c:0-.t|", TrustedIdentityProviderName, "|", GetPrincipalNameWithoutDomain(@group));
}

public static string GetUserPrincipalName(string user)
{
if (string.IsNullOrEmpty(TrustedIdentityProviderName))
{
return string.Format(CultureInfo.InvariantCulture, "{0}\\{1}", CurrentDomain, GetPrincipalNameWithoutDomain(user.ToLower(CultureInfo.InvariantCulture)));
} 
return string.Concat("i:0e.t|", TrustedIdentityProviderName, "|", GetPrincipalNameWithoutDomain(user), TrustedIdentityProviderDomain);
}


2. Перевірки на членство в групах
Перевірки на членство в групах також проходили через цю бібліотеку, тому особливо міняти їх не довелося. Поміняли тільки функціонал за визначенням користувача. Тепер на вхід можна стало подавати як доменних користувачів, так і клэймовых:

public static string GetPrincipalNameWithoutDomain(string principal)
{
if (string.IsNullOrEmpty(principal))
return string.Empty;
return
Regex.Match(principal.Trim(),
@"(i:)?0e.t.*\/(?<userName>[\d\w_&\.\s-]+)@[\w\d\._]/(c:)?0-.t.*\/(?<userName>[\d\w_&\.\s-]+)$/^(?<userName>[\d\w_&\.\s-]+)@[\w\d\._]/^(?<userName>[\d\w_&\.\s-]+)$/[\w]+\\(?<userName>[\d\w_&\.\s-]+)$")
.Groups["userName"].Value;
}


Навіщо це було зроблено? У якийсь момент у нас вийшло, що частина команди працює вже на серверах, переведених на ADFS, а решта на серверах з NTLM аутентифікацією. Оскільки вдалося домогтися щоб наша бібліотека працювала однаково і для NTLM і для ADFS, це допомогло не зупиняти процес розробки нових модулів.

День X – 2 місяці

Через місяць-інший портал ожив, заробили базові речі, модуль новин, бізнес-процеси на основі Sharepoint Workflows і багато іншого.

Після цього мігрували портал на тестовому оточенні, склали чек-лист за нашим безкрайніх модулів, призначили відповідальних за кожен модуль і полетіли за схемою:

• модуль зробили
• викотили на тестування
• знайшли купу багів, поправили
• модуль готовий

По ходу справи нам зустрілося пара цікавих проблем
1. Перша проблема, з якою ми зіткнулися, – це виклик SOAP сервісів
Більшість wcf-сервісів, які «живуть» в папці _vti_bin і використовуються скриптами з браузера і через webHttpBinding. З решти частина сервісів використовується для міжмодульної взаємодії, а решта потрібні іншим системам клієнта, з якими налаштована інтеграція. Природно, більшість цих взаємодій зроблені на основі SOAP (так зручніше) і зав'язані на NTLM-аутентифікації. Для початку ми спробували зрозуміти, наскільки проблемно буде перевести всіх клієнтів (хоча б наших, що працюють на WCF) на ADFS. Спробували, жахнулися кількістю необхідних рухів і складності конфігурації клієнта і забули цю думку. Витратили трохи часу на спробу змусити SharePoint працювати одночасно з двома схемами аутентифікації для сервісів (так так, щоб користувачі не помітили). Не вийшло, NTLM вперто відмовлявся працювати (про причини трохи нижче).

Таким чином, нам було необхідно терміново повернути «справжній» NTLM для сервісів. Тому звернулися до такої можливості SharePoint, як розширення (extend) web-додатків. Розсудили так, що на основну адресу залишаться REST-сервіси для браузера, аутентифікацією яких займеться браузер користувача, а всі службові сервіси переїдуть на адресу розширеного сайту, який буде працювати з NTLM. Здавалося б, все просто… Почали.

Ми розширили портал, уже перекладений на ADFS, з допомогою стандартних коштів SharePoint (Extend Web application), вибравши в якості Authentication Provider для розширеного сайту NTLM. Очікуваний результат: усі користувачі WCF-сервісів з службової директорії «ISAPI» в SharePoint працюють з ними, як і раніше, максимум — правлять адреса викликається сервісу в секції client у файлі конфігурації «Web.config». Розширення основного сайту, вже перекладеного ADFS, відразу не вирішило проблем викликів WCF-сервісів для користувачів через аутентифікацію NTLM — ми невблаганно отримували відповідь на кожен виклик WCF-сервісу: «The HTTP request is unauthorized with client authentication scheme 'Ntlm'. The authentication header received from the server was 'Negotiate,NTLM'». Справжньою причиною проблеми стало те, що при розширенні головного сайту в основний файл конфігурації «Web.config» Sharepoint скопіював неправильні аутентифікаційні модулі в секції «modules»:

<add name="FederatedAuthentication" type="Microsoft.SharePoint.IdentityModel.SPFederationAuthenticationModule ..." />
<add name="SessionAuthentication" type="Microsoft.SharePoint.IdentityModel.SPSessionAuthenticationModule ..." />
<add name="SPWindowsClaimsAuthentication" type="Microsoft.SharePoint.IdentityModel.SPWindowsClaimsAuthenticationHttpmodule ..." />

Тоді як правильним модулем для NTLM-сайту є:

<add name="Session" type="System.Web.SessionState.SessionStateModule" />

Після довгого обговорення вирішили діяти в наступній послідовності:
— створити розширення головного сайту до його перекладу на ADFS, тоді залишиться колишній правильний NTLM-модуль аутентифікації;
— перевести головний сайт порталу Sharepoint на ADFS
У випадку якщо у вас з'являться одні і ті ж WCF-сервіси в ISAPI, які мають одночасно endpoint'и як для доступу через NTLM, так і через ADFS, то вони будуть вимагати одночасної підтримки сайтом IIS «Forms Authentication», так і «Windows authentication». У нашому випадку ми маємо головний сайт Sharepoint і його розширення, які навмисно одночасно не підтримують обидва способи аутентифікації. Для вирішення цієї проблеми використовували:

— Створити дві підпапки в ISAPI «ModuleServiceAdfs», «ModuleServiceNtlm»

— скопіювати в обидві папки svc-файл сервісу WCF

— створити в кожній з папок свій файл конфігурації «Web.config» — у першій для ADFS, у другій — для NTLM.

2. Друга проблема – старіння saml-токена

Більшість запитів, які виконувалися до сервера, працювали через ajax допомогою jquery. При цьому періодично виникають ситуації, коли saml-токен стає невалидным (коли токен застарів, коли був перезапущений пул sharepoint, коли був перезапущений пул FS). Стандартний механізм перенаправлення на сторінку аутентифікації з автообновлением сертифіката або введення логіна/пароля і подальшого повернення на вихідну сторінку у випадку з jquery.ajax не працює. Та й сама ситуація, коли користувача доводиться відправляти на сторінку аутентифікації тільки за тим, щоб автоматично повернути його на вихідну сторінку, але з втраченими результатами роботи, ентузіазму не викликала. Побіжний пошук на просторах інтернету привів нас до рішення. Для тих, кому ліньки ходити по посиланню, – суть рішення в закладі preauth-сторінки і оборачивании всіх ajax-запитів обробником, який, на випадок відповіді сервера 401, завантажує цю сторінку через iFrame, після чого повторює вихідний запит. Створення єдиної точки виконання ajax-запитів на наш випадок проблемою не було, оскільки додаток і був написаний у такій манері (Ми вже розповідали про наш підхід до цієї і цієї статтях).

Ми розвинули знайдене рішення рішення, зробивши деякі доповнення:

1. На випадок, якщо було зроблено кілька запитів поспіль, то, замість того, щоб створювати для кожного з iFrame-при отриманні 401, ми для другого і наступних запитів повертаємо той же deferred, що був створений для першого запиту.

2. Даний підхід працює на випадок, коли токен застарілий або коли його «забув» sharepoint. Але він не працював на випадок, коли нас «забув» ADFS, – в цьому випадку потрібно повторне введення логіна/пароля. Підхід, описаний у статті, на такі випадки приводив до нескінченного циклу завантаження preauth-сторінки через iFrame без якого-небудь результату. Прямий редирект на сторінку аутентифікації теж був не дуже приємний, оскільки це означало втрату результатів роботи для користувача. Рішенням стало відображення модальника для введення логіна/пароля на випадок, якщо завантаження через iFrame не допомогла, і ми знову отримали 401. Модальник, в свою чергу, робить виклик кастомного сервісу, який вже виконує аутентифікацію в ADFS. Після виконання аутентифікації – дублюємо вихідний ajax-запит/запити.
Доповнений код виглядає наступним чином:

refreshToken: function ()
{
if (wcfDispatcherDef.frameLoadPromise === undefined)
{
return jquery.Deferred(function (d)
{
wcfDispatcherDef.frameLoadPromise = d;
var iFrame = jquery('<iframe></iframe>');
iFrame.hide();
iFrame.appendTo('body');
iFrame.attr('src', wcfDispatcherDef.PreauthUrl);
iFrame.load(function ()
{
setTimeout(function ()
{
wcfDispatcherDef.frameLoadPromise = undefined;
d.resolve();
iFrame.remove();
}, 100);
});
});
} else
{
return wcfDispatcherDef.frameLoadPromise;
}
},
makeServiceCall: function (settings, initialPromise)
{
var self = this;
var d = initialPromise || jquery.Deferred();
var promise = jquery.ajax(settings)
.done(function ()
{
d.resolveWith(self.requestContext || self, jquery.makeArray(arguments));
}).fail(function (error)
{
if (error.status * 1 === ETR.HttpStatusCode.Unauthorized && wcfDispatcherDef.HandleUnauthorizedError === true)
{
if (initialPromise)
{
wcfDispatcherDef.AuthDialog.show().done(function (result)
{
if (result === true)
{
self.makeServiceCall.call(self, settings, d).done(function ()
{
d.resolveWith(self.requestContext || self, jquery.makeArray(arguments));
});
} else
{
router.navigate('#forbidden');
}
});
} else
{
self.refreshToken().then(function ()
{
self.makeServiceCall.call(self, settings, d).done(function ()
{
d.resolveWith(self.requestContext || self, jquery.makeArray(arguments));
});
});
}
} else
{
d.rejectWith(self.requestContext || self, jquery.makeArray(arguments));
}
});

return d;
},


Крім цього, ми вирішили безліч цікавих завдань, які сюди не входить, так як їх опис тягне на цілу книгу.

День X – 1 місяць


Коли закінчили на тестовому оточенні, допрацювали інструкцію, зібрали величезний пакет для поновлення і провели тестову міграцію на предрелизном сервері з реальними даними. Три дні воювали з виникаючими дрібницями (як без них, тестове оточення не ідеально), і зарядили тестування з самого початку, після чого призначили День X.

День X


День Х був призначений на суботу, ми збираємося в офісі в 9-00. В офісі клієнта – їх адміністратор, який, власне, і виробляє розгортання. Коли деплоишь не сам, а хтось інший з твоєї інструкції, завжди страшно, тому весь день стежили за кожним його кроком через расшаренный екран Lync. В 15-00 міграція закінчена. Все перевіряємо, знаходимо, що не злетіло, допиливаем напилком. В 18-00 все інше запрацювало, розходимося задоволені.

Перший робочий день після Дня X


Настає понеділок, перший робочий день. Для нас починається ПЕКЛО. З основного, з'ясовується, що:
a. Токен стає тухлою набагато частіше, ніж ми думали;
b. Портал гальмує сильніше звичайного;
c. Користувачів постійно викидає на сторінку логіна, що дуже-дуже заважає працювати;
d. Є проблема не тільки з ajax запитами. Якщо користувач заповнює стандартну форму нового елемента списку півгодини, то з імовірністю 90% він свої зміни при збереженні втрачає.

Отримуємо доступ до робочого сервера, аналізуємо логи, намагаємося зрозуміти, що відбувається:

Перший" сюрприз". У нас є кастомный uploader файлів з превьюшки, який зберігає тимчасові файли на диску (для випадків, коли об'єкт і файли створюються в одній формі і аттачить файли у момент поки нікуди, а показати в preview треба). Так ось uploader зберігає тимчасові файли у піддиректорії додатки, типу C:\inetpub\Sharepoint\Files при цьому створюючи там свої піддиректорії, а потім їх видаляють. Видалення ці приводили до періодичного recycle пулу застосунків. А так як Sharepoint Logon Easy Cache живе просто в пам'яті, то з усіма сесіями можна було попрощатися. Uploader це, треба зізнатися, живе у нас вже рік, і пул швидше за все він запруджував і раніше, але до цього ніхто особливо не помічав, з Windows аутентифікацією це не призводило до повторного запитом облікових даних). У результаті оперативно змінили цільову папку uploader і вакханалія стала менше.

Другий" сюрприз". Іноді процес починав з'їдати всю пам'ять, що змушувало пул перевантажуватися. Відловити, що конкретно вантажить портал, коли на ньому працюють сотні людей, задача не проста і не швидка. Аналізуємо запити до бази, логи, знаходимо ті самі критичні місця, які все періодично завалюють, розуміємо, як їх зробити по-іншому, переписуємо, накочується… Дихати стає легше. Знову ж таки, проблема цієї неоптимальності була завжди, але на періодичні гальма раніше закривали очі. Мало що гальмує, а з відсутністю прямого доступу до прод сервера оперативна діагностика набагато ускладнюється, тому й ігнорували цю проблему.

Портал працює спритніше, кожні 5 хвилин вже не викидає, але раз в півгодини-годину все одно відбувається перелогин. І тут ми відкриваємо для себе базові речі, які мали для себе відкрити в самому початку, — управління часом життя Logon Token для тих, кому цікаві подробиці msdn.microsoft.com/ru-ru/library/office/hh147183(v=office.14).aspx (цю тему пропустили, так як при тестуванні це особливо не виникало. Перевірка окремого тест-кейсу легко укладається в 5 хвилин, а від кейса до кейсу тестувальник змінює користувачів, і сесія оновлюється. Тривалу роботу з 9 до 6 не эмулировали. Налаштували тривалості сесії 1 день і начебто все повинно було стати добре, але…

Черговий сюрприз. Користувачів продовжує викидати. Вже не так регулярно, але є випадки, причому масові. Судячи по логам, пул не перевантажується в цей час. У чому ж справа? Читаємо, розбираємося, як воно все працює, знаходимо корисну статтю про кеші blogs.msdn.com/b/besidethepoint/archive/2013/03/27/appfabric-caching-and-sharepoint-1.aspx Розуміємо, що за замовчуванням розмір кеша — тільки для 250 токенів, а коли кеш переповнюється, — всім привіт :) Збільшуємо розмір кеша — настає ейфорія… Потік негативу і гнівних листів спадає.

далі

В принципі на цьому могли б і закінчити, але цікавість бере вгору. Є ще один момент, коли сесії «гаснуть». Специфіка бізнесу і темпу розробки така, що рідкісний день обходиться без «хотфикса». У момент хотфикса доводиться перевантажити пул додатки, ось тут-то все і побігли перелогиниваться.

Почали дивитися, читати, вивчати, що можна в цій ситуації зробити. Літератури, докладно розкриває механізми роботи SharePoint модулів, як зазвичай, тямущої не знайшлося, тому пішли важкою артилерією — аналіз модулів, що підключаються SharePoint-му, дизасемблирование збірок, перегляд того, як це все працює зсередини. День досліджень і знаходимо лиходія:

<add type="Microsoft.SharePoint.IdentityModel.SPTokenCache, Microsoft.SharePoint.IdentityModel, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />

А в ньому SPSecurityTokenCache private KeyValuePair<string, SecurityToken>[] m_StrongCache; — ось як реалізований всередині той самий StrongCache.

Заради інтересу вирішили спробувати написати свій TokenCache, проте спроба зробити це за 5 хвилин успіхом не увінчалися. Покопавшись глибше у всьому SP-конвеєрі, виявили, що зв'язність компонентів там досить висока. У підсумку все ж написали в тестових цілях свою версію TokenCache, хоча подекуди довелося все-таки використовувати Reflection.

Висновок

Які ж висновки можна зробити, озираючись назад. Подібні зміни пов'язані з заміною цегли в підставі піраміди, який утримує на собі всю конструкцію. Звичайно, можна говорити про те, що так робити не потрібно, що систему потрібно надбудовувати, а не перебудовувати. Однак у реальному житті обов'язково настане момент, коли фундаментальні зміни стануть необхідні. І тоді, по нашому розумінню, потрібно:
a. Досконально вивчити внутрішні механізми того, що зазнає змін, скільки б постачальники не говорили про" добре документовану чорну коробку";
b. Як можна краще змоделювати роботу системи, включаючи сценарії тривалого використання, високого навантаження і т. п.;
c. Спробувати спрогнозувати наслідки. Опрацювати сценарії відкату, якщо зміни доведеться все-таки прибирати;
d. Домовитися з клієнтом, про те, що можливі збої;
e. І приготуватися…

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.