Chapter 9: Kontrola dostępu

Kontrola dostępu

Auth
kontrola dostępu
RBAC
DAC
MAC

Platforma web2py zawiera zaawansowany i łatwy do dostosowania mechanizm kontroli dostępu oparty na rolach (Role Based Access Control - RBAC).

Oto defincja z Wikipedia:

"RBAC (ang. Role-based Access Control - kontrola dostępu oparta na rolach) – mechanizm kontroli dostępu w systemach komputerowych. Dla podkreślenia różnicy w stosunku do DAC nazywany również non-discretionary access control.

...

Cechą charakterystyczną RBAC jest określanie ról i uprawnień w taki sposób, aby odzwierciedlały one rzeczywiste funkcje w organizacji.

RBAC sprawdza się szczególnie dobrze tam, gdzie ważne jest stosowanie zasady rozdziału obowiązków (ang. separation of duties). Przykładowo, gdy w celu zapobiegania nadużyciom niektóre operacje wymagają akceptacji dwu niezależnych użytkowników."

RBAC jest technologią neutralnej i eleastycznej polityki kontroli dostępu, wystarczająco silną, aby symulować mechanizmy DAC i MAC. Odwrotnie, MAC może symulować RBAC, jeśli graf ról jest ograniczony do drzewa, a nie jest częściowo uporządkowanym zbiorem.

Przed opracowaniem RBAC, mechanizmy MAC i DAC były uważane za jedyne znane modele kontroli dostępu: jeśli model nie był MAC, to był uważany za model DAC i vice versa. Badania w latach 90-tych ubiegłego wieku wykazały, że RBAC nie pasuje do żadnej z tych kategorii.

W organizacji, role są tworzone dla różnych funkcji zadaniowych. Uprawnienia do wykonywnia pewnych czynności są przypisywane do określonych ról. Członkowie zespołu (lub inni użytkownicy systemowi) są przypisywani do właściwych ról i przez to nabywają uprawnień do wykonywania określonych funkcji systemowych. W przeciwieństwie do kontekstowej kontroli dostępu (CBAC), RBAC nie uwzględnia kontektu komunikatów (takiego jak żródło połączenia).

Ponieważ uprawnienia nie są bezpośrednio przypisywane użytkownikom, ale nabywają oni te uprawnienia tylko poprzez rolę (lub role), zarządzanie indywidualnymi prawami użytkownika sprowadza się do prostego przypisania mu odpowiednich ról - upraszcza to typowe operacje, takie jak dodawanie użytkowników lub zmienianie działu organizacji dla użytkownika.

RBAC różni się od mechanizmu list kotroli dostępu (ACL), stosowanym w tradycyjnych uznaniowych systemach kontroli dostępu, ponieważ przyspisuje uprawnienia do określonych operacji, w znaczeniu organizacyjnym, a nie do obiektów na niskim poziomie. Na przykład, lista kontroli dostępu może być wykorzystana do udzielania lub odmowy dostępu do zapisu określonego pliku systemowego, ale nie podaje ona, jak ten plik może być zmieniany.

Klasa web2py implemetująca RBAC nazywa się Auth.

Auth potrzebuje (i definiuje) następujące tabele:

  • auth_user przechowuje nazwę, adres email, hasło i status użytkownika.
  • auth_group przechowuje grupy lub role użytkowników w strukturze wiele-do-wielu. Domyślnie, każdy użytkownik znajduje się we własnej grupie, ale użytkownik może należeć do wielu grup a każda grupa może się składać z wielu użytkowników. Grupa jest identyfikowana przez rolę i opis.
  • auth_membership łaczy użytkowników i grupy z strukturę wiele-do-wielu.
  • auth_permission łaczy grupy i uprawnienia. Uprawnienia są identyfikowane przez nazwę i opcjonalnie przez tabelę i rekord. Na przykład, członkowie określonej grupy mogą mieć uprawnienia "update" dla określonego rekordu określonej tabeli.
  • auth_event rejestruje zmiany w innych tabelach i udany dostęp poprzez CRUD do obiektów kontrolowanych przez RBAC.
  • auth_cas wykorzystywana jest przez zcentralizowaną usługę uwierzytelniania (Central Authentication Service - CAS). Każda aplikacja web2py jest dostawcą CAS i może opcjonalnie być konsumentem CAS.

Schemat graficzny jest przedstawiony na poniższym obrazku:

image

W zasadzie, nie ma ograniczeń co do nazw ról i nazw uprawnień. Programista może tworzyć je zgodnie z nazwami ról i uprawanień w organizacji. Po ich utworzeniu, web2py udostępnia API do sprawdzania, czy użytkownik jest zalogowany i czy jest członkiem odpowiedniej grupy i ewentualnie, czy użytkownik jest członkiem każdej grupy, która ma wymagane uprawnienia.

Platforma web2py udostępnia również dekoratory dla ograniczania dostępu do dowolnej funkcji na podstawie loginu, członkowstwa i uprawnień.

W web2py rozróżnia się również kilka szczególnych uprawnień, czyli takich, które mają nazwę odpowiadającą metodom CRUD (create, read, update, delete) i można egzekwować je automatycznie, bez potrzeby używania dekoratorów.

W tym rozdziale omawiamy różne części mechanizmu RBAC, jedną po drugiej.

Uwierzytelnianie

uwierzytelnianie

Użycia RBAC wymaga zidentyfikowania użytkownika. Oznacza to, że użytkownicy muszą zostać zarejestrowni (lub być zarejestrowanymi) i zalogować się.

Klasa Auth dostarcza wiele metod logowania. Domyślna metoda polega na identyfikacji użytkowników w oparciu o lokalną tabelę auth_user. Alternatywnie, można logować użytkowników w zewnętrznych systemach uwierzytelniania, z wykorzystaniem podpisu takich dostawców jak Google, PAM, LDAP, Facebook, LinkedIn, Dropbox, OpenID, OAuth itd.

Dla rozpoczęcia korzystania z Auth, trzeba przynajmniej umieścić w pliku modelu poniższy kod. Znajduje się on również w aplikacji "welcome" aweb2py i pobiera obiekt połączenia db:

1
2
3
from gluon.tools import Auth
auth = Auth(db)
auth.define_tables(username=False,signature=False)

Domyślnie, web2py stosuje adres email jako login. Jeśli zamiast tego chce się stosować nazwę użytkownika, trzeba ustawić to w auth.define_tables(username=True)

Ustawienie signature=True dodaje użytkowników i znacznik daty do tabel uwierzytelniania, w celu śledzenia zmian.

Klasa Auth ma opcjonalny argument secure=True, który wymusza, aby strony uwierzytelniania działały w protokole HTTPS.

https

Domyślnie Auth zabezpiecza dane logowania przed fałszowaniem żądań CSRF. Jest to faktycznie realizowane przez standardową ochronę CSRF web2py, za każdym razem, gdy w sesji generowane są formularze. Jednak w pewnych okolicznościach, narzut na tworzenie sesji dla loginu, żądania hasła i prób resetowania może być niepożądany. Ataki DOS są teoretycznie możliwe. Ochrona CSRF może zostać wyłączona dla formularzy uwierzytelniania (począwszy od wersji 2.6):

1
Auth = Auth(..., csrf_prevention = False)

Należy mieć na uwadze, że robienie tego w celu uniknięcia przeciążenia sesji na witrynie o dużym ruchu nie jest zalecane ze względów bezpieczeństwa. Zamiast tego lepiej zastosować technikę omówioną w rozdziale 13 dla wdrożenia redukcji obciażenia sesji.

Pole password tabeli db.auth_user domyślnie używa walidatora CRYPT, który wymaga hmac_key. W starszych aplikacjach web2py można znależć dodatkowy argument przekazywany do konstruktora Auth: hmac_key = Auth.get_or_create_key(). Ten ostatni jest funkcją odczytująca klucz HMAC z pliku "private/auth.key" w folderze aplikacji. Jeśli plik ten nie istnieje, klucz hmac_key jest tworzony losowo. Jeśli wiele aplikacji współdzieli tą samą bazę danych uwierzytelniania, trzeba się upewnić, że używają ten sam klucz hmac_key. Nie jest to już konieczne w nowych aplikacjach, ponieważ hasła są solone indywidualnie losową solą.

Jeśli wiele aplikacji współdzieli tą samą bazę danych uwierzytelniania, można wyłączyć migracje:

 auth.define_tables(migrate=False)
 

Dla udostępnienia uwierzytelniania, w kontrolerze musi się również znajdować następująca funkcja (na przykład w "default.py"):

1
def user(): return dict(form=auth())

Obiekt auth i akcja user są już zdefiniowane w szkieletowej aplikacji.

Platforma web2py zawiera również przykładowy widok "welcome/views/default/user.html" do prawidłowego renderowania tej funkcji, który wygląda tak:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{{extend 'layout.html'}}
<h2>{{=T( request.args(0).replace('_',' ').capitalize() )}}</h2>
<div id="web2py_user_form">
  {{=form}}
  {{if request.args(0)=='login':}}
    {{if not 'register' in auth.settings.actions_disabled:}}
      <br/><a href="{{=URL(args='register')}}">register</a>
    {{pass}}
    {{if not 'request_reset_password' in auth.settings.actions_disabled:}}
      <br/>
      <a href="{{=URL(args='request_reset_password')}}">lost password</a>
    {{pass}}
  {{pass}}
</div>

Proszę zauważyć, że ta funkcja po prostu wyświetla form i dlatego może zostać dostosowana przy użyciu zwykłej sładni niestandardowych formularzy. Jedynym zastrzeżeniem jest to, że formularz wyświetlany przez form=auth() zależy od request.args(0). Dlatego jeśli zamieni się domyślny formularz logowania auth() na własny, być może zajdzie potrzeba użycia wyrażenia if, tak jak w tym widoku:

1
{{if request.args(0)=='login':}}...custom login form...{{pass}}

auth.impersonate
auth.is_impersonating

Powyższy kontroler udostępnia wiele akcji:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
http://.../[app]/default/user/register
http://.../[app]/default/user/login
http://.../[app]/default/user/logout
http://.../[app]/default/user/profile
http://.../[app]/default/user/change_password
http://.../[app]/default/user/verify_email
http://.../[app]/default/user/retrieve_username
http://.../[app]/default/user/request_reset_password
http://.../[app]/default/user/reset_password
http://.../[app]/default/user/impersonate
http://.../[app]/default/user/groups
http://.../[app]/default/user/not_authorized
  • register umożliwia rejestrację. Akcja ta jest zintegrowana z CAPTCHA, chociaż jest to domyślnie wyłączone. Jest też zintegrowana z kalkulatorem entropii działajacym po stronie klienta, zdefiniowanym w "web2py.js". Kalulator ten wskazuje siłę nowego hasła. Można też zastosować walidator IS_STRONG do zapobiegania akceptowaniu przez web2py słabych haseł.
  • login umożliwia zalogowanie się zarejestrowanym użytkownikom (jeśli rejestracja została już zweryfikowana lub jeśli weryfikacja nie jest wymagana, jeśli została już zatwierdzona lub nie wymaga zatwierdzenia i jeśli użytkownik nie jest zablokowany).
  • logout wykonuje to, czego można oczekiwać, ale również, jak inne metody, rejestruje zdarzenia i może zostać użyta do wywołania jakiegoś zdarzenia.
  • profile umożliwia użytkowników edytowanie swojego profilu, czyli zawartości tabeli auth_user. Tabela ta nie ma ustalonej struktury i może zostać dostosowana.
  • change_password umożliwia zmianę swojego hasła w bezawaryjny sposób.
  • verify_email. Jeśli włączona jest weryfikacja email, to po rejestracji do użytkownika zostanie wysłana wiadomość z odnośnikiem do weryfikacji podanych informacji. Odnośnik ten wskazuje właśnie na tą akcję.
  • retrieve_username. Klasa Auth używa do logowania domyślnie adresu email i hasła, ale można to zmienić, na nazwę użytkownika zamiast adresu email. W tym przypadku, jeśli użytkownik zapomni swojej nazwy, metoda retrieve_username umożliwia użytkowników wpisać swój adres email i odzyskać nazwę użytkownika poprzez wiadomość email.
  • request_reset_password umożliwia użytkownikom, którzy zapomnieli swoje hasło, uzyskanie nowego hasła. Otrzymają oni wiadomość email wskazującą stronę reset_password.
  • impersonate umożliwia użytkownikowi "podszycie" się pod innego użytkownika. Jest to ważne dla celów diagnostyki i obsługi. request.args[0] jest identyfikatorem użytkownika pod którego trzeba się podszyć. Jest to dopuszczalne tylko, jeśli zalogowany użytkownik ma odpowiednie uprawnienia: has_permission('impersonate', db.auth_user, user_id). Można użyć auth.is_impersonating() aby sprawdzić, czy bieżący użytkownik podszywa się pod kogoś.
  • groups wykazuje grupy, do których należy obecnie zalogowany użytkownik.
  • not_authorized wyświetla komunikat błędu, gdy użytkownik próbuje wykonać coś, do czego nie jest uprawniony.
  • navbar jest helperem generującym pasek z odnośnikami login/register/itd..

Dostęp do logout, profile, change_password, impersonate i groups wymaga zalogowania się.

Domyślnie wszystkie akcje są dostępne, ale istnieje możliwość ograniczenia dostępu tylko do niektórych z nich.

Wszystkie metody mogą zostać rozszerzone lub wymienione przez podklasy Auth.

Omawiane metody można używać w oddzielnych akcjach. Na przykład:

def mylogin(): return dict(form=auth.login())
def myregister(): return dict(form=auth.register())
def myprofile(): return dict(form=auth.profile())
...

Ograniczenie dostępu do jakiejś funkcji tylko do zalogowanych użytkowników osiąga się przez udekorowanie tej funkcji, tak jak w poniższym przykładzie:

1
2
3
@auth.requires_login()
def hello():
    return dict(message='hello %(first_name)s' % auth.user)

Udokorowana może być każda funkcja, nie tylko udostępniająca akcje. Oczywiście, powyższy przykład jest bardzo prostym przykładem kontroli dostępu. Bardziej złożone przykłady zostaną omówione dalej.

auth.user
auth.user_id
auth.user_groups.

Metoda auth.user zawiera kopię rekordów db.auth_user dla bieżąco zalogowanego użytkownika albo None. Jest tam również auth.user_id, który ma tą samą wartość co auth.user.id (czyli identyfikator obecnie zalogowanego użytkownika) lub None. Podobnie, auth.user_groups zawiera słownik, w którym każdy klucz jest identyfikatorem grupy, której członkiem jest obecnie zalogowany użytkownik, a wartość jest odpowiednią rolą grupy.

otherwise

Dekorator auth.requires_login(), taka jak inne dekoratory auth.requires_*, pobiera opcjonalny argument otherwise. Można go ustawić na łańcuch wskazujący miejsce przekierowania odwiedzającego po nieudanej rejestracji.

Ograniczenia dotyczące rejestracji

rejestracja użytkowników

Jeśli chce się zezwolić odwiedzającym rejestrowanie się ale bez możliwości zalogowania do czasu zatwierdzenia rejestracji przez administratora, trzeba zrobić tak:

1
auth.settings.registration_requires_approval = True

Rejestrację można zatwierdzić poprzez interfejs administracyjny. Proszę zajrzeć do tabeli auth_user. Oczekująca rejestracja ma pole registration_key ustawione na "pending". Rejestracja zostanie zatwierdzona, gdy to pole będzie puste.

Poprzez interfejs administracyjny aplikacji można również zablokować możliwość zalogowania się przez odwiedzającego. Znajdź użytkownika w tabeli auth_user i ustaw registration_key na "blocked". Użytkownicy z wartością "blocked" nie są dopuszczani do logowania. Proszę mieć na uwadze, że będzie to zapobiegać logowaniu się odwiedzających, ale nie wymusi to wylogowania już zalogowanego użytkownika. Zamiast słowa "blocked" może zostać użyte słowo "disabled", jeśli tak się chce, ale ma ono dokładnie takie samo działanie.

Można również całkowicie zablokować dostęp do strony "register" takim wyrażeniem:

1
auth.settings.actions_disabled.append('register')

Jeśli chce się dopuścić rejestrację i automatyczne zalogowanie użytkownika po rejestracji, ale też chce się wysłać wiadomość email w celu weryfikacji, tak że nie można się zalogować ponownie po wylogowaniu, dopóki użytkownik nie wykona w pełni instrukcji zawartej w wiadomości the email, można osiągnąć to w następujący sposób:

1
2
auth.settings.registration_requires_verification = True
auth.settings.login_after_registration = True

Inne metody Auth można ograniczyć w ten sam sposób.

Integracja z OpenID, Facebook itd.

Janrain
OpenID
Facebook
LinkedIn
Google
MySpace
Flickr

Można wykorzystać RBAC web2py i uwierzytelnianie z innych serwisów internetowych, takich jak OpenID, Facebook, LinkedIn, Google, Dropbox, MySpace, Flickr itd. Najprościej jest użyć Janrain Engage (dawniej RPX) (Janrain.com).

Dropbox jest omówiony jako szczególny przypadek w rozdziale 14, ponieważ nie tylko pozwala na zalogowanie się, ale również dostarcza usługi magazynowania danych dla zalogowanych użytkowników.

Janrain Engage jest serwisem dostarczającym uwierzytelnianie pośredniczące. Można zarejestrować się na Janrain.com, zarejestrować swoją domenę (nazwę aplikacji) i ustawić adresy URL, które będzie się używać i które zapewniać będą klucz API.

Edytujmy teraz model aplikacji web2py i umieśćmy tam następujące linie kodu, gdzieś po definicji obiektu auth:

1
2
3
4
5
6
from gluon.contrib.login_methods.rpx_account import RPXAccount
auth.settings.actions_disabled=['register','change_password','request_reset_password']
auth.settings.login_form = RPXAccount(request,
    api_key='...',
    domain='...',
    url = "http://your-external-address/%s/default/user/login" % request.application)

Pierwsza linia importuje nową metodę logowania, druga linia wyłącza lokalną rejestrację a trzecia linia odpytuje web2py o użycie metody logowania RPX. Tu trzeba wstawić swój api_key dostarczony przez Janrain.com, domenę podana podczas rejestracji i zewnetrzny url swojej strony logowania. W celu zalogowania się na janrain.com, trzeba następnie przejść do [Deployment][Application Settings]. Po prawej stronie znajduje się "Application Info", api_key ma nazwę "API Key (Secret)".

Domeną jest wartość pola "Application Domain" bez wiądącego "https://" i bez końcowego ".rpxnow.com/". Na przykład, jeśli masz zarekestrowana witryne jako "secure.mywebsite.org", Janrain włączy ją do Application Domain jako "https://secure-mywebsite.rpxnow.com".

image

Gdy nowy użytkownik loguje się po raz pierwszy, web2py tworzy nowy rekord db.auth_user powiązany z tym użytkownikiem. Wykorzystuje on pole registration_id do przechowywania unikalnego identyfikatora użytkownika. Wiekszość metod uwierzytelniania będzie też dostarczać nazwę użytkownika, adres email, imię i nazwisko, ale nie jest to pewne. dostępne pola zależą od metody logowania wybranej przez użytkownika. Jeśli ten sam użytkownik loguje się dwukrotnie używając roznych mechanizmów uwierzytelniania (na przykład raz na OpenID a drugi raz na Facebook), Janrain może tego nie rozpożnać i wydać inny registration_id.

Można dostosować mapowanie pomiędzy danymi dostarczonymi przez Janrain a danymi przechowywanymi w db.auth_user. Oto przykład dla Facebook:

1
2
3
4
5
auth.settings.login_form.mappings.Facebook = lambda profile:            dict(registration_id = profile["identifier"],
                 username = profile["preferredUsername"],
                 email = profile["email"],
                 first_name = profile["name"]["givenName"],
                 last_name = profile["name"]["familyName"])

Kluczami w słowniku są pola db.auth_user a wartościami są dane wprowadzine w obiekcie profilu dostarczonym przez Janrain. W celu zapożnania się ze szczegółami proszę zapożnać się z dokumentacją online Janrain.

Janrain dostarcza również statystyki dotyczące logowań swoich użytkowników.

Ten formularz logowania jest w pełni zintegrowany z RBAC web2py i można w dalszym ciagu tworzyć grupy, nadawać użytkownikom członkostwo w grupach, przypisywać uprawnienia, blokować użytkowników itd.

Bezpłatny serwis Janrain umożliwia obsługę do 2500 unikalnych logowań w roku. Przekrocznie tej ilości wymaga aktualizacji do którejś z płatnych opcji tego serwisu.

Jeśli wolisz nie używać Janrain i chcesz wykorzystywać inną metodę logowania (LDAP, PAM, Google, OpenID, OAuth/Facebook, LinkedIn itd) możesz to łatwo zrobić. API do realizacji tego jest opisane w dalszej części tego rozdziału.

CAPTCHA i reCAPTCHA

CAPTCHA
reCAPTCHA
PIL
W celu uniemożliwienia spamerom i botom rejestrowanie się na witrynie, można wymagać przy rejestracji kod CAPTCHA. W web2py umożliwia się obsługę reCAPTCHA[recaptcha] zaraz od uruchomienia. Powodem wyboru reCAPTCHA jest to, że jest to program bardzo dobrze zaprojektowany, darmowy, dostępny (może odczytywać słowo za odwiedzającego), łatwy w konfiguracji i nie wymagający instalacji oraz bibliotek zewnętrznych.

Oto co trzeba zrobić, aby zastosować reCAPTCHA:

  • Zarejestruj się na reCAPTCHA[recaptcha] i uzyskaj klucze (PUBLIC_KEY, PRIVATE_KEY) dla swojego konta. Są to tylko dwa łańcuchy.
  • Dołącz następujący kod do modelu, po definicji obiektu auth:
1
2
3
from gluon.tools import Recaptcha
auth.settings.captcha = Recaptcha(request,
    'PUBLIC_KEY', 'PRIVATE_KEY')

reCAPTCHA nie będzie działać, jeśli dostęp do witryny następuje z 'localhost' lub '127.0.0.1', ponieważ jest zarejestrowana do pracy tylko z witrynami publicznymi.

Konstruktor Recaptcha pobiera kilka opcjonalnych argumentów:

1
Recaptcha(..., use_ssl=False, error_message='invalid', label='Verify:', options='')

Argument ajax=True jest eksperymentalny i umożliwia wykorzystanie w recaptcha API Ajax. Może być używany w dowolnym recaptcha, ale został dodany specjalnie, aby umożliwić polom recpatcha działanie w formularzach LOAD (proszę zapożnać się z rozdziałem 12 w celu uzyskania informacji o LOAD, który pozwala użycie komponentów 'plugin' na stronach z Ajax ). Jest to eksperymentalne, ponieważ może zostać zamienione na automatyczne wykrywanie, gdy wymagany jest Ajax.

Prosze mieć na uwadze, że use_ssl=False jest domyślne.

Argument options jest łańcuchem konfiguracyjnym, np. options="theme:'white', lang:'fr'"

Więcej szczegółów: reCAPTCHA[recaptchagoogle] i dostosowywanie .

Jeśli nie chcesz używać reCAPTCHA, zajrzyj do definicji klasy Recaptcha w "gluon/tools.py", ponieważ z łatwością można ten kod wykorzystać w systemach CAPTCHA.

Klasa Recaptcha jest tylko helperem, który rozszerza DIV. Generuje ona sztuczne pole, które walidowane jest przy użyciu serwisu reCaptcha i dlatego może zostać użyte w dowolnym formularzu, w tym definiowanych za pomocą obiektów FORM:

1
form = FORM(INPUT(...),Recaptcha(...),INPUT(_type='submit'))

Można ją używać we wszystkich typach SQLFORM przez wstrzykiwanie:

1
2
form = SQLFORM(...) or SQLFORM.factory(...)
form.element('table').insert(-1,TR('',Recaptcha(...),''))

Dostosowywanie klasy Auth

klasa Auth

Wywołanie:

1
auth.define_tables()

definiuje wszystkie tabele Auth, które nie zostały jesze zdefiniowane. oznacza to, że jeśli chce się, to można tu zdefiniować własną tabelę auth_user.

Istnieje wiele sposobów na dostosowanie uwierzytelniania. Najprostszym z nich jest dodanie dodatkowych pól:

## after auth = Auth(db)
auth.settings.extra_fields['auth_user']= [
  Field('address'),
  Field('city'),
  Field('zip'),
  Field('phone')]
## before auth.define_tables(username=True)

Można zadeklarować pola nie tylko dla tabeli "auth_user" ale też dla innych tabel "auth_". Używanie extra_fields jest zalecanym sposobem, ponieważ nie załamie, żadnego wewnętrznego mechanizmu.

Inny sposób, ale nie zalecany, polega na samodzielnym zdefiniowaniu własnych tabel uwierzytelniania. Jeśli tabela zostanie zadeklarowana przed auth.define_tables(), to będzie uzywana zamiast tabeli domyślnej. Oto jak to zrobić:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
## after auth = Auth(db)
db.define_table(
    auth.settings.table_user_name,
    Field('first_name', length=128, default=''),
    Field('last_name', length=128, default=''),
    Field('email', length=128, default='', unique=True), # required
    Field('password', 'password', length=512,            # required
          readable=False, label='Password'),
    Field('address'),
    Field('city'),
    Field('zip'),
    Field('phone'),
    Field('registration_key', length=512,                # required
          writable=False, readable=False, default=''),
    Field('reset_password_key', length=512,              # required
          writable=False, readable=False, default=''),
    Field('registration_id', length=512,                 # required
          writable=False, readable=False, default=''))

## do not forget validators
custom_auth_table = db[auth.settings.table_user_name] # get the custom_auth_table
custom_auth_table.first_name.requires =   IS_NOT_EMPTY(error_message=auth.messages.is_empty)
custom_auth_table.last_name.requires =   IS_NOT_EMPTY(error_message=auth.messages.is_empty)
custom_auth_table.password.requires = [IS_STRONG(), CRYPT()]
custom_auth_table.email.requires = [
  IS_EMAIL(error_message=auth.messages.invalid_email),
  IS_NOT_IN_DB(db, custom_auth_table.email)]

auth.settings.table_user = custom_auth_table # tell auth to use custom_auth_table

## before auth.define_tables()

Można dodać dowolne pole i można zmienić walidatory, ale nie można usunąć, na przykład, pól oznaczonych jako "required".

Ważne jest, aby wykonać pola "password", "registration_key", "reset_password_key" i "registration_id" z opcjami readable=False i writable=False, ponieważ nie można dopuścić, aby odwiedzający mógł nimi manipulować.

Jeśli doda się pole o nazwie "username", to będzie ono używan do logowania zamiast pola "email". Gdy się to zrobi, trzeba będzie dodać również właściwe walidatory:

1
auth_table.username.requires = IS_NOT_IN_DB(db, auth_table.username)

Zmiana nazw tabel Auth

Rzeczywiste nazwy tabel Auth są przechowywane w:

1
2
3
4
5
auth.settings.table_user_name = 'auth_user'
auth.settings.table_group_name = 'auth_group'
auth.settings.table_membership_name = 'auth_membership'
auth.settings.table_permission_name = 'auth_permission'
auth.settings.table_event_name = 'auth_event'

Nazwy tabel można zmieniać, przypisując im ponownie powyższe zmienne po definicji obiektu auth i przed zdefiniowaniem tabel Auth. Na przykład:

1
2
3
4
auth = Auth(db)
auth.settings.table_user_name = 'person'
#...
auth.define_tables()

Rzeczywiste tabele mogą być również odnoszone, niezależnie od ich rzeczywistych nazw, przez:

1
2
3
4
5
auth.settings.table_user
auth.settings.table_group
----auth.settings.table_membership
auth.settings.table_permission
auth.settings.table_event

Uwaga: auth.signature zostaje zdefiniowane podczas tworzenia inicjacji obiektu Auth, co ma miejsce przed ustawieniem nazw indywidualnych tabel. Dla uniknięcia tego trzeba zrobić:

auth = Auth(db, signature=False)

W tym przypadku, auth.signature będzie zdefiniowane, gdy wywoła się auth.define_tables(), przez co wskazuje się nazwy własnych tabel, ktore są już ustawione.

Inne metody logowania i formularze logowania

LDAP
PAM

Klas Auth dostarcza wiele metod logowania oraz haki (ang. hooks) do tworzenia nowych metod logowania. Każda obsługiwana metoda logowania odpowiada plikowi w folderze:

1
gluon/contrib/login_methods/

Proszę się zapoznać z opisem dokumentacyjnym, znajdującym się w tych plikach. Oto kilka przykładów.

Przede wszystkim trzeba dokonać rozróżnienia pomiędzy dwoma rodzajami alternatywnych metod logowania:

  • metody logowania wykorzystujące formularz logowania web2py (chociaż weryfikacja poświadczeń może być wykonywana poza web2py). Przykładem jest LDAP.
  • metody logowania, które wymagają zewnętrznego formularza pojedynczego logowania (przykładem są Google i Facebook).

W tym drugim przypadku, web2py nigdy nie pobiera danych logowania, a tylko token logowania wydawany przez usługodawcę. Token ten przechowywany jest w db.auth_user.registration_id.

Rozpatrzmy kilka przykładów pierwszego rodzaju:

Dostęp podstawowy

Powiedzmy, że mamy serwis uwierzytelniania, na przykład po adresem URL:

1
https://basic.example.com

który akceptuje uwierzytelnianie dostępu podstawowego. Oznacza to, że serwer akceptuje żądania HTTP z nagłówkiem formularza:

1
2
3
GET /index.html HTTP/1.0
Host: basic.example.com
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==

W przypadku, gdy ostatni łańcuch jest zakodowaną w base64 parą username:password, usługa przesyła odpowiedź 200 OK, jeśli użytkownik jest uprawniony, a w przeciwnym razie 400, 401, 402, 403 lub 404.

Przyjmijmy, że chcemy wprowdzać nazwę użytkownika i hasło w standardowym formularzu logowania Auth i weryfikować poświadczenie tak jak w usłudze. Wszystko co trzeba zrobić, to dodać do aplikacji następujacy kod:

1
2
3
from gluon.contrib.login_methods.basic_auth import basic_auth
auth.settings.login_methods.append(
    basic_auth('https://basic.example.com'))

Proszę zwrócić uwagę, że auth.settings.login_methods jest listą metod uwierzytelniania, które są wykonywane kolejno. Domyślnie jest to ustawione na:

1
auth.settings.login_methods = [auth]

Gdy dodana zostanie alternatywana metoda, na przykład basic_auth, obiekt Auth najpierw spróbuje zalogować odwiedzającego w oparciu o zawartość auth_user i gdy się to nie uda, spróbuje następnej metody z listy. Jeśli metoda się powiedzie i jeśli auth.settings.login_methods[0]==auth, to obiekt Auth podejmie następujace działania:

  1. jeśli użytkownik nie istnieje w auth_user, tworzony jest nowy użytkownik i zapisywane są takie dane jak nazwa użytkownika albo adres email i hasło;
  2. jeśli użytkownik istnieje w auth_user, ale nowo zakceptowane hasło nie pasuje do przechowywanego starego hasła, stare hasło jest zamieniane na nowe (uwaga: hasła są przechowywane w postaci zaszyfrowanej, chyba że ustawiono to inaczej).

Jeśli nie chce się przechowywać nowego hasła w auth_user, wystarczy zmienić kolejność metod logowania lub usunąć auth z listy. Na przykład:

1
2
from gluon.contrib.login_methods.basic_auth import basic_auth
auth.settings.login_methods =     [basic_auth('https://basic.example.com')]

To samo dotyczy każdej innej metody logowania opisanej poniżej.

SMTP i Gmail

SMTP
Gmail

Można weryfikować poświadczenie logowania wykorzystując zdalny serwer SMTP, na przykład Gmail; czyli logować użytkownika, jeśli wprowadzony adres email i hasło są prawidłowym poświadczeniem dostępu do serwera SMTP Gmail (smtp.gmail.com:587). Wszystko co potrzeba, to następujący kod:

1
2
3
from gluon.contrib.login_methods.email_auth import email_auth
auth.settings.login_methods.append(
    email_auth("smtp.gmail.com:587", "@gmail.com"))

Pierwszy argument email_auth, to address:port serwera SMTP. Drugi argument, to domena email.

Działa to z każdym serwerem SMTP wymagającym uwierzytelniania TLS.

TLS

PAM
PAM

Uwierzytelnianie wykorzystujące Pluggable Authentication Modules (PAM) działa jak w poprzednich przypadkach. Pozwala, aby web2py uwierzytelniał użytkowników, wykorzystując konta systemu operacyjnego:

1
2
from gluon.contrib.login_methods.pam_auth import pam_auth
auth.settings.login_methods.append(pam_auth())
LDAP
LDAP

Uwierzytelnianie wykorzystujace LDAP działa bardzo podobnie jak w poprzednich przypadkach.

Użycie logowania LDAP w MS Active Directory:

Active Directory

1
2
3
4
from gluon.contrib.login_methods.ldap_auth import ldap_auth
auth.settings.login_methods.append(ldap_auth(mode='ad',
   server='my.domain.controller',
   base_dn='ou=Users,dc=domain,dc=com'))

Użycie logowania LDAP w Lotus Notes i Domino:

Lotus Notes
Domino

1
2
auth.settings.login_methods.append(ldap_auth(mode='domino',
   server='my.domino.server'))

Użycie logowania LDAP w OpenLDAP (z UID):

OpenLDAP

1
2
auth.settings.login_methods.append(ldap_auth(server='my.ldap.server',
   base_dn='ou=Users,dc=domain,dc=com'))

Użycie logowania LDAP w OpenLDAP (z CN):

1
2
auth.settings.login_methods.append(ldap_auth(mode='cn',
   server='my.ldap.server', base_dn='ou=Users,dc=domain,dc=com'))
Google App Engine
GAE login

Uwierzytelnianie wykorzystujace Google podczas uruchamiania na Google App Engine wyma pominięcia formularza logowania web2py i przekierowania na stronę logowania Google i powrót do aplikcacji po pomyślnym zalogowaniu. Ponieważ zachowanie to jest inne, niż w poprzednich przykładach, to API jest nieco inne.

1
2
from gluon.contrib.login_methods.gae_google_login import GaeGoogleAccount
auth.settings.login_form = GaeGoogleAccount()
OpenID
OpenID

Wcześniej omówiliśmy integrację z Janrain (który jest oparty na OpenID) i jest to najprostszy spsób użycie OpenID. Czasem jednak nie chce się korzystać z usług osób trzecich, woląc mieć dostęp do OpenID dostarczanego bezpośrednio przez konsumenta (swoją aplikację).

Oto przykład:

1
2
from gluon.contrib.login_methods.openid_auth import OpenIDAuth
auth.settings.login_form = OpenIDAuth(auth)

OpenIDAuth wymaga oddzielnego zainstalowania modułu python-openid. Ta metoda logowania definiuje "po maską" następującą tabelę:

1
2
3
4
db.define_table('alt_logins',
    Field('username', length=512, default=''),
    Field('type', length =128, default='openid', readable=False),
    Field('user', self.table_user, readable=False))

która przechowuje nazwy użytkowników openid dla każdego użytkownika. Jeśli chce się wyświetlić openids dla bieżąco zalogowanego użytkownika, trzeba posłużyć się takim wyrażeniem:

1
{{=auth.settings.login_form.list_user_openids()}}
OAuth2.0

OAuth
Facebook
Google
Twitter

Oprócz omówionej poprzednio metody opartej na OpenId można wykorzystać bezpośrednio uwierzytelnianie OAuth2.0. Na przykład, Facebook, Linkedin, Twitter, Google i inne serwisy społecznościowe dostarczają usługę uwierzytelniania OAuth2.0. Platforma web2py obsługuje przejrzyście przepływ OAuth2.0, tak że użytkownik może zostać zweryfikowany podczas logowania wobec każdego skonfigurowanego dostawcy OAuth2.0. Oprócz uwierzytelniania, dostawca OAuth2.0 może udzielać dostępu do zasobów użytkownika w dowolnej aplikacji web2py przy pomocy interfejsów własnościowych API dostawcy, czyli realizować funkcje autoryzacji. Google, Twitter, Facebook i inne serwisy mają interfejsy API, które mogą być łatwo dostępne przez aplikacji web2py.

Należy podkreślić, że OAuth2.0 ogranicza się tylko to uwierzytelniania i autoryzacji (na przykład CAS ma więcej możliwości), oznacza to, że każdy dostawca OAuth2.0 ma inny spsób na otrzymanie unikalnego identyfikatora z ich bazy danych użytkowników za pomocą ich własnych interfejsów API. Specyficzne metody są dobrze wyjaśnione z odpowiedniej dokumentacji dostawcy. Zazwyczaj polegają na bardzo prostym wywołaniu REST. Nie ma potrzeby przedstawiania tu kilku linijek kodu dla każdego dostawcy OAuth2.0.

Pierwszym krokiem przed napisaniem w modelu aplikacji jakiejkolwiek instrukcji jest potrzeba zarejestrowania nowej aplikacji na serwisie dostawcy OAuth2.0 - jest to wyjaśnione w dokumentacji dostawcy.

Jest kilka rzeczy, które trzeba znać, zanim przystąpi się do dodawania nowego dostawcy OAuth2.0 do swojej aplikacji:

  1. identyfikator URI uwierzytelniania;

  2. token żądania identyfikatora URI;

  3. token identyfikacyjny aplikacji i hasło, które otrzymuje się podczas rejestracji nowej aplikacji;

  4. uprawnienia. które dostawca musi przyznać aplikacji web2py, czyli "zakres" (zobacz w dokumentacji dostawcy);

  5. wywołanie API do otrzymywania identyfikatora UID uwierzytelnianego użytkownika, tak jak wyjaśniono to w dokumentacji dostawcy.

Rzeczy z punktów 1 do 4 są używane przy inicjowaniu zakończenia autoryzacji przez web2py do komunikowania się z dostawcą OAuth2.0. Unikalny identyfikator jest pobierany przez web2py w wywołaniu metody get_user(), gdy jest to potrzebne podczas procedury logowania, tam gdzie potrzebne jest wywołanie API podane w punkcie 5.

Oto istotne modyfikacje, które muszą być wykonane w modelu:

  • a. zaimportowanie klasy OAuthAccount;
  • b. zdefiniowanie pochodnej implementacji OAuthClass;
  • c. zastąpienie metody __init__() tej klasy;
  • d. zastąpienie metody get_user() tej klasy;
  • e. utworzenie instancji klasy z danymi podanymi w punktach 1-4 powyższej listy.

Po utworzeniu instancji tej klasy i po uwierzytelnieniu użytkownika, aplikacja web2py może uzyskać w każdej chwili dostęp do API dostawcy wykorzystując token dostępowy OAuth2.0 poprzez wywołanie metody accessToken() tej klasy.

Poniżej podany jest przykład, jak można to zrobić wykorzystując Facebook. Jest to podstawowy przykład użycia Facebook Graph API, przypominajacy, że pisząc właściwa metodę get_user() method, można zrobić wiele rzeczy. Przykład ten pokazuje, jak token dostępowy OAuth2.0 może być wykorzystany podczas wywołania zdalnego API dostawcy.

Po pierwsze, musi się zainstalować Facebook Python SDK.

Po drugie, trzeba umieścić w modelu następujący kod:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
## Define oauth application id and secret.
FB_CLIENT_ID='xxx'
FB_CLIENT_SECRET="yyyy"

## import required modules
try:
    import json
except ImportError:
    from gluon.contrib import simplejson as json
from facebook import GraphAPI, GraphAPIError
from gluon.contrib.login_methods.oauth20_account import OAuthAccount


## extend the OAUthAccount class
class FaceBookAccount(OAuthAccount):
    """OAuth impl for FaceBook"""
    AUTH_URL="https://graph.facebook.com/oauth/authorize"
    TOKEN_URL="https://graph.facebook.com/oauth/access_token"

    def __init__(self):
        OAuthAccount.__init__(self, None, FB_CLIENT_ID, FB_CLIENT_SECRET,
                              self.AUTH_URL, self.TOKEN_URL,
                              scope='email,user_about_me,user_activities, user_birthday, user_education_history, user_groups, user_hometown, user_interests, user_likes, user_location, user_relationships, user_relationship_details, user_religion_politics, user_subscriptions, user_work_history, user_photos, user_status, user_videos, publish_actions, friends_hometown, friends_location,friends_photos',
                              state="auth_provider=facebook",
                              display='popup')
        self.graph = None

    def get_user(self):
        '''Returns the user using the Graph API.
        '''
        if not self.accessToken():
            return None

        if not self.graph:
            self.graph = GraphAPI((self.accessToken()))

        user = None
        try:
            user = self.graph.get_object("me")
        except GraphAPIError, e:
            session.token = None
            self.graph = None

        if user:
            if not user.has_key('username'):
                username = user['id']
            else:
                username = user['username']
                
            if not user.has_key('email'):
                email = '%s.fakemail' %(user['id'])
            else:
                email = user['email']    

            return dict(first_name = user['first_name'],
                        last_name = user['last_name'],
                        username = username,
                        email = '%s' %(email) )

## use the above class to build a new login form
auth.settings.login_form=FaceBookAccount()
LinkedIn
LinkedIn

Poprzednio omówiliśmy integrację z serwisem Janrain (który obsługuje LinkedIn) podkreślając, że jest to najprostszy sposób użycia OAuth. Czasem jednak nie chce się korzystać z usługi osób trzecich lub chce się uzyskać bezposrednio dostęp do LinkedIn, aby uzyskać więcej możliwości, niż zapewnia to serwis Janrain.

Oto przykład:

1
2
from gluon.contrib.login_methods.linkedin_account import LinkedInAccount
auth.settings.login_form=LinkedInAccount(request,KEY,SECRET,RETURN_URL)

LinkedInAccount wymaga oddzielnego zainstalowania modułu "python-linkedin".

X509

Można też logować się poprzez przekazywanie do strony certyfikatu x509, co umożliwia wyodrębnienie poświadczenia z tego certyfikatu. Wymaga to zainstalowania biblioteki M2Crypto ze strony:

http://chandlerproject.org/bin/view/Projects/MeTooCrypto

Po zainstalowaniu M2Cryption można zrobić tak:

1
2
3
from gluon.contrib.login_methods.x509_auth import X509Account
auth.settings.actions_disabled=['register','change_password','request_reset_password']
auth.settings.login_form = X509Account()

Teraz już można się uwierzytelniać w web2py przekazując swój certyfikat x509. Jak to zrobić, zależy od przegladarki, ale najpewniej trzeba będzie używać certyfikatu wydanego dla usług internetowych. W takim przypadku można wykorzystać, na przykład, cURL do wypróbowania uwierzytelniania:

curl -d "firstName=John&lastName=Smith" -G -v --key private.key      --cert  server.crt https://example/app/default/user/profile

Działa to od razu z serwerem Rocket (serwerem wbudowanym w web2py), ale może trzeba będzie włożyć trochę pracy w konfigurację na innych serwerach obsługujacych witrynę. W szczególności należy poinformować serwer, gdzie umieszczone są certyfikaty na lokalnym hoście i to że musi się sprawdzić certyfikaty pochodzące od klientów. Sposób na zrobienie tego zależy od serwera internetowego i dlatego nie omawiamy tutaj tego.

Formularze wielorakiego logowania

Niektóre metody logowania zmieniają login_form, a niektóre nie. Gdy tak się stanie, mogą one nie współdzielić jednego formularza logowania. Jednak niektóre metody są do tego zdolne dostarczając formularze wielorakiego logowania na tej samej stronie. web2py dostarcza sposób na zrobienie tego. Oto przykład zmieszania zwykłego logowania (auth) z logowaniem RPX (janrain.com):

1
2
3
from gluon.contrib.login_methods.extended_login_form import ExtendedLoginForm
other_form = RPXAccount(request, api_key='...', domain='...', url='...')
auth.settings.login_form = ExtendedLoginForm(auth, other_form, signals=['token'])

Jeśli zostanie ustawiony signals i parametr w żądaniu jest zgodny z wartością signals, to zamiast tego zostanie zwrócone wywołanie other_form.login_form. other_form może obsługiwać pewne szczególne sytuacje, na przykład, wiele kroków logowania wewnątrz other_form.login_form.

W przeciwnym razie będzie renderowany zwykły formularz logowania razem z other_form.

Wersjonowanie rekordów

Można wykorzystać klasę Auth do pełnego wersjonowania rekordów:

1
2
3
4
db.enable_record_versioning(db,
    archive_db=None,
    archive_names='%(tablename)s_archive',
    current_record='current_record'):

Powiadamia to web2py, aby utworzył tabelę archiwalna dla każdej tabeli w db i przechował kopię każdego rekordu po zmodyfikowaniu. Stara kopia podlega zapisaniu, a nowa nie.

Ostatnie trzy parametry są opcjonalne:

  • archive_db pozwala określić inna bazę danych, gdzie będą przechowywane tabele archiwalne. Ustawienie na None jest tym samym, z ustawienie na db.
  • archive_names dostarcza wzorzec dla nazewnictwa każdej tabeli archiwalnej.
  • current_record określa nazwę odnoszonego pola do użycia w tabeli archiwalnej w celu odwołania się do oryginalnego, niezmodyfikowanego rekordu. Trzeba mieć na uwadze, że archive_db!=db to pole odniesienia, które jest po prostu polem liczbowym, ponieważ odniesienia krzyżowe w bazie danych są niemożliwe.

Archiwizowane będą tylko tabele z polami modified_by i modified_on (jako utworzone na przykład przez auth.signature).

Gdy użyje się enable_record_versioning, to jeśli rekordy mają poleis_active (również tworzone przez auth.signature), rekordy nie zostaną usunięte i zamiast tego ożnaczone jako is_active=False. W rzeczywistości , enable_record_versioning dodaje common_filter do każdej wersjonowanej tabeli, co filtruje rekordy z atrybutem is_active=False, więc w istocie stają się one niewidoczne.

Użycie enable_record_versioning powoduje, że nie powinno się stosować auth.archive lub crud.archive bo inaczej skończy się to zduplikowaniem rekordów. Funkcje te wykonują jawnie to, co wykonuje automatycznie enable_record_versioning i zostaną wkrótce zdeprecjonowane.

Interakcja klas Mail i Auth

klasa Mail
klasa Auth

Informacje o API web2py dla poczty elektronicznej i o jej konfigurowaniu można znaleźć w rozdziale 8. Tutaj ograniczamy się do omówienia interakcji pomiędzy klasa Mail i Auth.

Mailer można zdefiniować tak:

from gluon.tools import Mail
mail = Mail()
mail.settings.server = 'smtp.example.com:25'
mail.settings.sender = 'you@example.com'
mail.settings.login = 'username:password'

lub po prostu wykorzystując mailer dostarczany przez auth:

mail = auth.settings.mailer
mail.settings.server = 'smtp.example.com:25'
mail.settings.sender = 'you@example.com'
mail.settings.login = 'username:password'

Oczywiście, musi się zamienić parametry mail.settings na parametry właściwe dla konkretnego serwera SMTP. Jeśli serwer SMTP nie wymaga uwierzytelniania, trzeba ustawić mail.settings.login = None. Gdy nie chce się używać TLS, trzeba ustawić mail.settings.tls = False

W Auth, weryfikacja poczty elektronicznej jest domyślnie wyłączona. W celu włączenia poczty elektronicznej, trzeba dodać następujace linie do kodu modelu, w którym zdefiniowyny jest obiekt auth:

1
2
3
4
5
auth.settings.registration_requires_verification = True
auth.settings.registration_requires_approval = False
auth.settings.reset_password_requires_verification = True
auth.messages.verify_email = 'Click on the link  %(link)s to verify your email'
auth.messages.reset_password = 'Click on the link %(link)s to reset your password'

W dwóch powyższych liniach auth.messages, może zajść konieczność zamiany częściowego adresu URL na właściwy pełny adres URL akcji. Jest to niezbędne, ponieważ web2py może być instalowany za serwerem pośredniczącym, co powoduje, że nie będzie można ustalić jego publicznych adresów URL z absolutną pewnością. Powyższe przykłady (które mają domyślne wartości) powinny działać w większości przypadków.

Weryfikacja dwuetapowa

weryfikacja dwuetapowa
uwierzytelnianie dwuskładnikowe

Weryfikacja dwuetpowa (zwana też uwierzytelnianiem dwuskładnikowym) jest sposobem zwiększenia bezpieczeństwa uwierzytelniania i polega na dodaniu dodatkowego etapu w procesie logowania. W pierwszym etapie użytkownikowi jest wyświetlany standardowy formularz logowania z nazwą użytkownika i hasłem. Jeśli użytkownik pomyślnie przejdzie przez ten etap logowania, wprowadzając prawidłową nazwę użytkownika i hasło i włączone jest dwuskładnikowe uwierzytelnianie dla tego użykownika, serwer wyświetli drugi formularz, w którym użytkownik musi podać token jaki mu zostanie dostarczony po pierwszym etapie logowania albo jednorazowy token awaryjny, jaki otrzymał wcześniej. Uwierzytelnienia następuje wówczas po poprawnym wprowadzeniu takiego tokenu.

image

Włączanie weryfikacji dwuetapowej osobno dla każdego użytkownika

Ten przypadek ma zastosowanie dla aplikacji, w której poszczególni użytkownicy mogą dla siebie włączać lub wyłączać tą dwuetapową weryfikację.

W naszym przykładzie posłużymy się formularzem który wymagać będzie wprowadzenia 6-cyfrowego kodu, który zostanie przesłany na skrzynkę e-mail użytkownika po prawidłowym przejściu przez pierwszy etap uwierzytelniania.

Domyślnie użytkownik bedzie mógł wykonać 3 próby wprowadzenia kodu. Po trzech nieudanych próbach, użytkownik będzie musiał powtórzyć logowanie od pierwszego etapu.

  1. Utwórz grupę (rolę) dla weryfikacji dwuetapowej. W tym przykładzie nazwiemy ją auth2step i opiszemy jako Weryfikacja dwuetapowa.

  2. Nadaj prawa członkowskie użytkownikowi.

  3. Dodaj następujące ustawienie w modelu, w którym utworzony i skonfigurowany został obiekt auth (zwykle jest to model db.py):

auth.settings.two_factor_authentication_group = "auth2step"

Nie zapomnij skonfigurować w db.py serwera e-mail!

Włączanie weryfikacji dwuetapowej dla całej aplikacji

W tym przykładzie drugi etap weryfikacji działa tak samo jak poprzednio, ale dotyczy wszystkich użytkowników aplikacji (nie ma wybotu metody przez użytkownika).

W modelu, w którym utworzony i skonfigurowany jest obiekt auth (db.py) dodaj to ustawienie:

auth.settings.auth_two_factor_enabled = True

W takim przypadku można wyłączyć dwuetapową weryfikację dla określonego adresu IP. Na przykład, jeśli zewnętrzne IP sieci biura to 93.56.854.54 i nie potrzeba dla niej dwuetapowej weryfikacji, to w modelu trzeba wprowadzić nastęþujące ustawienie:

if request.env.remote_addr != '93.56.854.54':
    auth.settings.auth_two_factor_enabled = True
Przykłady innych rozwiązań
Przykład 1: Wysyłanie kodu przez SMS zamiast e-mail.

W modelu stwórz kod wedłu tego wzorca:

def _sendsms(user, auth_two_factor):
    #napisz tu kod procesu wysyłania kodu auth_two_factor przez SMS
    return  auth_two_factor

auth.settings.auth_two_factor_enabled = True
auth.messages.two_factor_comment = "Your code have been sent by SMS"
auth.settings.two_factor_methods = [lambda user, auth_two_factor: _sendsms(user, auth_two_factor)]

Metoda _sendsms(...) przyjmuje dwie wartości: user i auth_two_factor:

  • user: jest to wiersz ze wszystkimi parametrami. Można do nich uzyskać dostęp przez: user.email, user.first_name itd;
  • auth_two_factor: łańcuch zawierający kod uwierzytelniający(token).

W przypadku wysyłania tokenów poprzez SMS trzeba do tabelu auth_user dodać dodatkowe pole, np, phone. Wówczas dostęp do wartości tego pola uzyskuje się przez user.phone. Więcej informacji o wysyłaniu wiadomości SMS ppprzez web2py znajdziesz w rozdziale Poczta elektroniczna i SMS.

Przykład 2: Wysyłanie kodu przez SMS z wygenerowaniem własnego kodu
def _sendsms(user):
    auth_two_factor = #napisz własny algorytm generowania kodu.
    #napisz tu kod procesu wysyłania kodu auth_two_factor przez SMS
    return  auth_two_factor

auth.settings.two_factor_methods = [lambda user, auth_two_factor: _sendsms(user, auth_two_factor)]
Przykład 3: Kod jest generowany przez zewnętrznego klienta, na przykład Mobile OTP Client

MOTP (Mobile one time password) umożliwia logowanie hasłem jednorazowym (OTP) generowanym na kliencie MOTP. Klienci MOTP są praktycznie dostęþni na wszystkich platformach. Więcej o OTP można się dowiedzieć w artykule wiki One-time-password i odwiedzając stronę MOTP.

W następnym przykładzie użyjemy DroidOTP. Jest to darmowa aplikacja dostępna w Play Store for Android. Po zainstalowaniu:

  • Utwórz nowy profil, na przykład test;
  • Inicjuj tajny klucz wstrząśnięciem telefonu.

Skopiuj poniższy kod i wklej go do swojego modelu:

#Przed zdefiniowaniem tabel, dodajemy kilka dodatkowych pól do auth_user
auth.settings.extra_fields['auth_user'] = [
    Field('motp_secret', 'password', length=512, default='', label='MOTP Secret'),
    Field('motp_pin', 'string', length=128, default='', label='MOTP PIN')]

OFFSET = 60 #To ma być ta sama wartość co w kliencie OTP

#Ustawienie session.auth_two_factor na None, ponieważ kod jest generowany przez aplikację zewnętrzną. 
# Pozwala to uniknąć ustawienia domyśłnego i wysyłania kodu pocztą e-mail.
def _set_two_factor(user, auth_two_factor):
    return None

def verify_otp(user, otp):
  import time
  from hashlib import md5
  epoch_time = int(time.time())
  time_start = int(str(epoch_time - OFFSET)[:-1])
  time_end = int(str(epoch_time + OFFSET)[:-1])
  for t in range(time_start - 1, time_end + 1):
     to_hash = str(t) + user.motp_secret + user.motp_pin
     hash = md5(to_hash).hexdigest()[:6]
     if otp == hash:
       return hash

auth.settings.auth_two_factor_enabled = True
auth.messages.two_factor_comment = "Verify your OTP Client for the code."
auth.settings.two_factor_methods = [lambda user, auth_two_factor: _set_two_factor(user, auth_two_factor)]
auth.settings.two_factor_onvalidation = [lambda user, otp: verify_otp(user, otp)]

Tajny klucz wygenerowany wcześniej w telefonie musi być wprowadzony do pola motp_secret. Klucz ten nie powinien być uzywany ponownie ze wzgledów bezpieczeństwa. Wybierz jeden PIN. Mogą byc to same cyfry, litery lub ich mieszanka. Weź telefon, wybierz swój profil i wpisz PIN, który został wcześniej wprowadzony do formularza. Po małej chwili, dostaniesz SMS z kodem uwierzytelniającym dla swojej aplikacji.

image

Proszę mieź na uwadze, że dla tego sposobu dwuskładnikowego uwierzytelniania, telefon i serwer (gdzie aplikacja web2py jest hostem) muszą być czasowo zsynchronizowane, choć mogą się znajdować w różnych strefach czasowych. Jest tak dlatego, że OTP używa znacznika czasowego Unix. Śledzony jest czas od wygenerowania kodu w sekundach.

Kilka dodatkowych parametrów konfiguracyjnych

Ustawienie ilości prób logowania:

auth.setting.auth_two_factor_tries_left = 3

Komunikat zwracany w razie nieprawidłowego kodu:

auth.messages.invalid_two_factor_code = T('Incorrect code. {0} more attempt(s) remaining.')

Dostosowanie szablony wiadomości e-mail:

auth.messages.retrieve_two_factor_code=T('Your temporary login code is {0}')
auth.messages.retrieve_two_factor_code_subject=T('Your temporary login code is {0}')

Dostosowanie formularza dwuskładnikowego uwierzytelniania:

auth.messages.label_two_factor = T('Authentication code')
auth.messages.two_factor_comment = T('The code was emailed to you and is required for login.')

Autoryzacja

autoryzacja

Gdy rejestrowany jest nowy użytkownik, tworzona jest nowa grupa zawierająca tego użytkownika. Rola nowego użytkownika, to konwencjonalnie user_[id] gdzie [id] jest identyfikatorem nowo utworzonego użytkownika. Tworzenie grupy można wyłączyć przez:

1
auth.settings.create_user_groups = None

ale nie zalecamy tego. Proszę zwrócić uwagę, że create_user_groups nie jest wartością logiczną (choć może być False), lecz łańcuchem szablonowym:

1
auth.settings.create_user_groups="user_%(id)s"

Przechowuje to szablon nazwy grupy tworzonej dla id użytkownika.

Użytkownicy mają członkostwo w grupach. Każda grupa jest identyfikowana przez nazwę (rolę). Grupy mają uprawnienia. Użytkownicy uzyskują uprawnienia przez fakt przynależności do grup. Domyślnie każdy użytkownik jest członkiem własnej grupy.

Można również zrobić tak:

1
auth.settings.everybody_group_id = 5

aby uczynić automatycznie każdego użytkownika członkiem grupy o identyfikatorze 5. Tutaj 5 jest użyty jako przykład i zakładamy, że ta grupa została już utworzona.

Można tworzyć grupy, nadawać członkostwo i uprawnienia poprzez appadmin lub programowo, używając następujacych metod:

1
auth.add_group('role', 'description')

zwraca identyfikator nowo utworzonej grupy.

1
auth.del_group(group_id)

usuwa grupę identyfikowaną przez group_id.

1
auth.del_group(auth.id_group('user_7'))

usuwa grupę z rolą "user_7", czyli grupę jednoznacznie powiązaną z użytkownikiem numer 7.

1
auth.user_group(user_id)

zwraca identyfikator grupy jednoznacznie powiązanej z użytkownikiem o identyfikowanym przez user_id.

1
auth.add_membership(group_id, user_id)

nadaje użytkownikowi identyfikowanemu przez user_id członkostwo w grupiegroup_id. Jeśli nie określi się user_id, to web2py przyjmuje bieżąco zalogowanego użytkownika.

1
auth.del_membership(group_id, user_id)

odwołuje członkostwo user_id w grupie group_id. Jeśli nie określi się user_id, to web2py przyjmuje bieżąco zalogowanego użytkownika.

1
auth.has_membership(group_id, user_id, role)

sprawdza, czy user_id ma członkostwo w grupie group_id lub w grupie z określoną rolą. Powinno się przekazać do funkcji tylko group_id albo role, a nie obydwie wartości. Jeśli nie określi się user_id, to web2py przyjmuje bieżąco zalogowanego użytkownika.

1
auth.add_permission(group_id, 'name', 'object', record_id)

daje uprawnienia "name" (zdefiniowane przez użytkownika) obiektowi "object" (również zdefiniowanym przez użytkownika) do członkowstwa w grupie group_id. Jeśli parametr object jest nazwą tabeli, to uprawnienia mogą odnosić się do całej tabeli przez ustawienie record_id na wartość zero albo uprawnienia mogą się odnosić do określonego rekordu przez określenie wartości record_id innej niż zero. Podczas nadawania uprawnień dla tabel, powszechą praktyką jest używanie nazw uprawnień z zestawu ('create', 'read', 'update', 'delete', 'select'), ponieważ są one zrozumiałe i mogą być egzekwowane przez interfejsy API CRUD.

Jeśli group_id wynosi zero, web2py używa grupy jednoznacznie związanej z bieżąco zalogowanym użytkowników.

Można również zastosować auth.id_group(role="...") do pobrania identyfikatora grupy, biorąc pod uwagę jej nazwę.

id_group

1
auth.del_permission(group_id, 'name', 'object', record_id)

odwołuje uprawnienie.

1
auth.has_permission('name', 'object', record_id, user_id)

sprawdza, czy użytkownik identyfikowany przez user_id ma członkostwo w grupie z żądanymi uprawnieniami.

1
rows = db(auth.accessible_query('read', db.mytable, user_id))    .select(db.mytable.ALL)

zwraca wszystkie wiersze tabeli "mytable", dla których użytkownik user_id ma uprawnienia "read". Jeśli nie określi się user_id, to web2py przyjmuje bieżąco zalogowanego użytkownika. Zapytanie accessible_query(...) może być łączone z innymi zapytaniami w celu uzyskania bardziej złożonych zapytań. Zapytanie accessible_query(...) jest jedyną metodą Auth wymagającą złączenia JOIN, więc nie działa to na Google App Engine.

Przyjmijmy następujace definicje:

1
2
3
4
5
6
>>> from gluon.tools import Auth
>>> auth = Auth(db)
>>> auth.define_tables()
>>> secrets = db.define_table('document', Field('body'))
>>> james_bond = db.auth_user.insert(first_name='James',
                                     last_name='Bond')

a oto przykład:

1
2
3
4
5
6
7
8
>>> doc_id = db.document.insert(body = 'top secret')
>>> agents = auth.add_group(role = 'Secret Agent')
>>> auth.add_membership(agents, james_bond)
>>> auth.add_permission(agents, 'read', secrets)
>>> print auth.has_permission('read', secrets, doc_id, james_bond)
True
>>> print auth.has_permission('update', secrets, doc_id, james_bond)
False

Dekoratory

dekoratory

Najbardziej popularnym sposobem sprawdzania uprawnień nie jest jawne wywoływanie powyższych metod, ale dekorowanie funkcji, tak że uprawnienia są sprawdzane w zależności od zalogowanego użytkownika. Oto kilka przykładów:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def function_one():
    return 'this is a public function'

@auth.requires_login()
def function_two():
    return 'this requires login'

@auth.requires_membership('agents')
def function_three():
    return 'you are a secret agent'

@auth.requires_permission('read', secrets)
def function_four():
    return 'you can read secret documents'

@auth.requires_permission('delete', 'any file')
def function_five():
    import os
    for file in os.listdir('./'):
        os.unlink(file)
    return 'all files deleted'

@auth.requires(auth.user_id==1 or request.client=='127.0.0.1', requires_login=True)
def function_six():
    return 'you can read secret documents'

@auth.requires_permission('add', 'number')
def add(a, b):
    return a + b

def function_seven():
    return add(3, 4)

Warunkowy argument dekoratorów @auth.requires(condition) może być możliwy do wywołania i jeśli warunek jest prosty, to lepiej jest przekazać wywołanie zamiast warunku, ponieważ będzie to szybsze, jako że warunek będzie sprawdzany tylko w razie potrzeby. Na przykład:

1
2
3
@auth.requires(lambda: check_condition())
def action():
    ....

Dekoratory @auth.requires pobierają również opcjonalny argument requires_login, którego domyślna wartość to True. Jeśli ustawi się go na False, to logowanie nie będzie wymagane przed sprawdzeniem warunku jako prawda/fałsz. Warunek może być wartością logiczną lub funkcją ocenianą jako logiczna.

Należy pamiętać, że dostęp wszystkich funkcji, z wyjątkiem pierwszej, jest ograniczany na podstawie uprawnień, które odwiedzający może ale nie musi mieć.

Jeśli użytkownik nie jest zalogowany, to uprawnienia nie mogą zostać sprawdzone. Użytkownik zostanie przekierowany do strony logowania a następnie z powrotem do strony wymagającej uprawnień.

Łączenie wymagań

Czasem zachodzi potrzeba połączenia wymagań. Można to zrobić za pomocą ogólnego dekoratora requires, który pobiera pojedynczy argument, warunek prawdy lub fałszu. Na przykład, aby przyznać dostęp do grupy 'agents', ale tylko we wtorek:

1
2
3
4
@auth.requires(auth.has_membership(group_id='agents')
               and request.now.weekday()==1)
def function_seven():
    return 'Hello agent, it must be Tuesday!'

lub ewentualnie:

1
2
3
4
@auth.requires(auth.has_membership(role='Secret Agent')
               and request.now.weekday()==1)
def function_seven():
    return 'Hello agent, it must be Tuesday!'

Autoryzacja a CRUD

Zaimplementowanie kontroli dostępu można osiągnąć przez użycie dekoradorów albo jawne sprawdzanie uprawnień.

Innym sposobem na zaimplementowanie kontroli dostępu jest stałe używanie CRUD (a nie SQLFORM) dla ustalania dostępu do bazy danych i wypytywanie CRUD dla egzekwowania kontroli dostępu do tabel i rekordów bazy danych. Jest to realizowane przez połączenie klas Auth i CRUD w następujące wyrażenie:

1
crud.settings.auth = auth

Zapobiega to przed nieuprawnionym dostępem do każdej funkcji CRUD, chyba że odwiedzający jest zalogowany i ma jawnie przyznany dostęp. Na przykład, aby umożliwić odwiedzającemu komentowanie, ale tylko z możliwością poprawiania własnych komentarzy (zakładając, że crud, auth i db.comment są już zdefiniowane):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def give_create_permission(form):
    group_id = auth.id_group('user_%s' % auth.user.id)
    auth.add_permission(group_id, 'read', db.comment)
    auth.add_permission(group_id, 'create', db.comment)
    auth.add_permission(group_id, 'select', db.comment)

def give_update_permission(form):
    comment_id = form.vars.id
    group_id = auth.id_group('user_%s' % auth.user.id)
    auth.add_permission(group_id, 'update', db.comment, comment_id)
    auth.add_permission(group_id, 'delete', db.comment, comment_id)

auth.settings.register_onaccept = give_create_permission
crud.settings.auth = auth

def post_comment():
   form = crud.create(db.comment, onaccept=give_update_permission)
   comments = db(db.comment).select()
   return dict(form=form, comments=comments)

def update_comment():
   form = crud.update(db.comment, request.args(0))
   return dict(form=form)

Można również wybrać określone rekordy (trzeba mieć do nich dostęp 'read'):

1
2
3
4
5
def post_comment():
   form = crud.create(db.comment, onaccept=give_update_permission)
   query = auth.accessible_query('read', db.comment, auth.user.id)
   comments = db(query).select(db.comment.ALL)
   return dict(form=form, comments=comments)

Nazwy uprawnień egzekwowane przez:

1
crud.settings.auth = auth

to "read", "create", "update", "delete", "select", "impersonate".

Autoryzacja a pobieranie plików

Używanie dekoratorów i crud.settings.auth nie wymusza autoryzacji na polach pobierania przez zwykłą funkcję pobierania:

1
def download(): return response.download(request, db)

Jeśli chce się to zrobić, trzeba jawnie zadeklarować które pola "upload" zawierają pliki wymagające kontroli dostępu w momencie pobierania. Na przykład:

1
2
3
4
5
db.define_table('dog',
   Field('small_image', 'upload'),
   Field('large_image', 'upload'))

db.dog.large_image.authorize = lambda record:    auth.is_logged_in() and    auth.has_permission('read', db.dog, record.id, auth.user.id)

Atrybut authorize pola pobierającego może być wartość None (domyślnie) lub być funkcją, która decyduje czy użytkownik jest zalogowany i czy ma uprawnienia 'read' dla bieżącego rekordu. Na przykład, nie ma ograniczenia dla pobierania obrazów połączonych z polem "small_image", ale wymagana jest kontrola dostępu dla obrazów połączonych z polem "large_image".

Kontrola dostępu a podstawowe uwierzytelnianie

Czasem zachodzi konieczność udostępnienia akcji, która ma dekorator i wymaga kontroli dostępu, jako usługi, czyli możliwej do wywoływania z poziomu programu lub skryptu i nadal umożliwiającej wykorzystanie uwierzytelniania do sprawdzania autoryzacji.

Klasa Auth udostępnia logowanie poprzez uwierzytelnianie:

1
auth.settings.allow_basic_login = True

Z tym ustawieniem, akcja taka jak:

1
2
3
4
@auth.requires_login()
def give_me_time():
    import time
    return time.ctime()

będzie mogła być wywoływana, na przykład, z polecenia powłoki:

1
2
wget --user=[username] --password=[password]
    http://.../[app]/[controller]/give_me_time

Możliwe jest również zalogowanie się, poprzez wywołanie auth.basic() a nie przez użycie dekoratora @auth:

1
2
3
4
5
6
7
def give_me_time():
    import time
    auth.basic()
    if auth.user:
        return time.ctime()
    else:
        return 'Not authorized'

Podstawowe logowanie jest często jedynym rozwiązaniem dla usług (opisanych w następnym rozdziale), lecz jest to domyślnie wyłączone.

Zarządzanie aplikacją przez upoważnionych użytkowników (eksperymentalne)

Zwykle funkcje administracyjne, takie jak określanie użytkowników i grup są zarządzane przez administratora serwera. Jednak, może zajść potrzeba przydzielenia uprawnień administracyjnych w jakiejś aplikacji grupie uprawnionych użytkowników. Jest to możliwe w wersjach web2py począwszy od v2.5.1.

Rozbudowa istniejącej aplikacji będzie wymagać nowego kontrolera appadmin i nowego widoku appadmin.html, skopiowanych z aplikacji welcome. Ponadto, aplikacje utworzone w wersjach web2py wcześniejszych niż v2.6 bedą potrzebować nowego pliku JavaScript w welcome/static/js/web2py.js.

Koncepcja ta pozwala na różne ustawienia zarządzania, z których każda pozwala grupie użytkowników edytować określony zestaw tabel w aplikacji.

Przykład: Po pierwsze, utwórz grupę (zwaną również rolą) dla uprzywilejowanych użytkowników. W tym przykładzie, będziemy ją nazywać admin. Nadaj użytkowników członkostwo w tej grupie. Po drugie, pomyśl o nazwie opisującej to ustawienie zarządzania, takiej jak db_admin.

Dodaj następujace ustawienie w modelu, w którym jest utworzony i skonfigurowany obiekt auth (przypuszczalnie jest to model db):

1
2
3
auth.settings.manager_actions = dict(db_admin=dict(role='admin',
                                     heading='Manage Database',
                                     tables = db.tables))

Pozycja menu zawierać ma adres URL, jak niżej, przekazując nazwę ustawienia zarządzania jako argument:

1
URL('appadmin','manage',args=['db_admin'])

Ten adres URL będzie miał postać /appadmin/manage/auth.

Zaawansowane stosowanie

Powyżej omówiony mechanizm pozwala na wiele ustawień zarządzania. Każde dodatkowe ustawienie zarządzania jest po prostu innym kluczem zdefiniowanym w auth.settings.manager_actions.

Na przykład, można utworzyć grupę użytkowników (nazwijmy ją 'Super'), którzy mają dostęp do każdej tabeli w ustawieniu zarządzania o nazwie "db_admin" i inną grupę (nazwijmy ją 'Content Manager'), którzy mają dostęp administracyjny do tabel odnoszących się do zawartości w ustawieniu zarządzania o nazwie "content_admin".

Można to ustawić podobnie do tego:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
auth.settings.manager_actions = dict(
    db_admin=dict(role='Super', heading='Manage Database', tables=db.tables),
    content_admin=dict(role='Content Manager', tables=[content_db.articles,
                                                       content_db.recipes,
                                                       content_db.comments]),
    content_mgr_group_v2 = dict(role='Content Manager v2',
                                db=content_db,
                                tables=['articles','recipes','comments'],
                                smartgrid_args=dict(
                                        DEFAULT=dict(maxtextlength=50,
                                        paginate=30),
                                comments=dict(maxtextlength=100,editable=False)
                           )
     )

Klucz nagłówkowy jest opcjonalny. Jeśli go brakuje, zostanie inteligentnie użyta wartość domyślna.

Następnie można wykonać dwie nowe pozycje menu z tymi adresamu URL:

1
2
URL('appadmin','manage',args=['db_admin'])
URL('appadmin','manage',args=['content_admin'])

Ustawienie zarządzania o nazwie "content_mgr_group_v2" pokazuje trochę bardziej zaawansowane możliwości. Klucz smartgrid_args jest przekazywany do inteligentej siatki (smartgrid) wykorzystywanej do edytowania lub przeglądania tabel. Oprócz specjalnego klucza DEFAULT, nazwy tabel są przekazywane jako klucze (takich jak tabela o nazwie "comments"). W tym przykładzie składnia nazw tabel wykorzystuje do określenia bazy danych listę łańcuchów z kluczem db=content_db.

Ręczne uwierzytelnianie

Czasem trzeba zaimplementować swoją własną logikę i wykonywać "ręcznie" logowanie użytkownika. Można to zrealizować wywołując taką funkcję:

1
user = auth.login_bare(username,password)

Metoda login_bare zwraca obiekt użytkownka, jeśli użytkownik istnieje i hasło jest prawidłowe, w przeciwnym wypadku zwraca False. Parametr username jest adresem email, jeśli tabela "auth_user" nie ma pola "username".

Ustawienia Auth a komunikaty

Oto lista wszystkich parametrów, przy pomocy których można dostosować obiekt Auth.

Poniższe wyrażenie musi wskazywać obiekt gluon.tools.Mail aby umożliwić obiektowi auth wysyłanie waidomości email:

1
auth.settings.mailer = None

Więcej informacji o ustawieniach poczty elektronicznej znajdziesz w części [Mail a Auth #mail_and_auth]] tego podręcznika.

Poniższe wyrażenie musi być nazwą kontrolera w którym zdefiniowana jest akcja user:

1
auth.settings.controller = 'default'

Poniższe wyrażenie jest bardzo ważne w starszych wersjach web2py:

1
auth.settings.hmac_key = None

gdzie jest ustawione coś jak sha512:a-pass-phrase i przekazywany jest walidator CRYPT dla pola "password" tabeli auth_user, dostarczając algorytm i tajną frazę używaną do mieszania haseł. Jednak web2py, w nowszych wersjach, nie potrzebuje już dłużej tego ustawienia, ponieważ jest to obsługiwane automatycznie.

Domyślnie, obiekt Auth wymaga co najmniej 4-znakowej długości hasła. Można to zmienić:

1
auth.settings.password_min_length = 4

Dla wyłączenia jakiejś akcji, trzeba dołączyć jej nazwę do poniższej listy:

1
auth.settings.actions_disabled = []

Na przykład:

1
auth.settings.actions_disabled.append('register')

wyłączy rejestrację.

Jeśli chce się otrzymywać wiadomości email dla weryfikacji rejestracji, trzeba poniższe wyrażenie ustawić na True:

1
auth.settings.registration_requires_verification = False

W celu automatycznego logowania osób po ich rejestracji, nawet jeśli nie został zakończona procedura weryfikacji email, trzeba ustawić poniższe wyrażenie na True:

1
auth.settings.login_after_registration = False

Jeśli nowo rejestrująca się osoba musi oczekiwać na zatwierdzenie, zanim będzie mogła zalogować się, trzeba ustawić to wyrażenie na True:

1
auth.settings.registration_requires_approval = False

Zatwierdzenie polega na ustawieniu registration_key=='' poprzez appadmin lub programowo.

Jeśli nie chce się aby dla każdego nowego użytkownika była tworzona jego własna grupa, trzeba ustawić następujące wyrazenie na False:

1
auth.settings.create_user_groups = True

Następujące ustawienia określają alternatywane metody logowania i formularze logowania, tak jak to zostało poprzednio omówione:

1
2
auth.settings.login_methods = [auth]
auth.settings.login_form = auth

Czy chcesz ustawić podstawowe logowanie? Ustaw to na True:

1
auth.settings.allows_basic_login = False

Poniższe ustawienie jest adresem URL akcji login:

1
auth.settings.login_url = URL('user', args='login')

Jeśli użytkownik próbuje uzyskać dostęp do strony rejestracji, ale jest już zalogowany, zostanie przekierowany na ten adres URL:

1
auth.settings.logged_url = URL('user', args='profile')

W przypadku, gdy profil zawiera zdjęcie, to wyrażenie musi kierować na adres URL akcji pobierania:

1
auth.settings.download_url = URL('download')

Niżej pokazane ustawienie auth.settings muszą wskazywać adres URL na jaki chce sie przekierować użytkowników po różnych możliwych akcjach auth (w przypadku gdy akcja sama nie robi).

Uwaga: Jeśli aplikacja jest oparta na standardowej aplikacji szkieletowej Welcome, można użyć auth.navbar. Omawiane ustawienia przyniosą efekt, jeśli edytuje się layout.html i ustawi argument referrer_actions = None:

1
2
 
auth.navbar(mode='dropdown',referrer_actions=None)

Jest również możliwe otrzymanie referrer_actions dla niektórych zdarzeń auth. Na przykład:

1
auth.navbar(referrer_actions=['login', 'profile'])

Jeśli domyślne zachowanie pozostaje bez zmian, auth.navbar stosuje parametr URL _next i używa go do odesłania z powrotem użytkowników do stosownej strony. Jednakże, jeśli domyślne zachowanie automatycznego przekierowywania zostanie zmienione, zostaną wykorzystane poniższe ustawienia:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
auth.settings.login_next = URL('index')
auth.settings.logout_next = URL('index')
auth.settings.profile_next = URL('index')
auth.settings.register_next = URL('user', args='login')
auth.settings.retrieve_username_next = URL('index')
auth.settings.retrieve_password_next = URL('index')
auth.settings.change_password_next = URL('index')
auth.settings.request_reset_password_next = URL('user', args='login')
auth.settings.reset_password_next = URL('user', args='login')
auth.settings.verify_email_next = URL('user', args='login')

Jeśli użytkownik nie jest zalogowany i wywołuje funkcję wymagającą uwierzytelniania, zostaje on przekierowany do auth.settings.login_url - domyślnie URL('default','user/login'). Można to zmienić poprzez przedefiniowanie:

on_failed_authentication
1
auth.settings.on_failed_authentication = lambda url: redirect(url)

Jest to funkcja wywoływane w celu przekierowania. Przekazywany do tej funkcji argument url` jest adresem URL do strony logowania.

Jeśli użytkownik nie ma uprawnień dostępu do danej funkcji, jest on przekierowywany do adresu URL zdefiniowanego przez:

on_failed_authorization
1
auth.settings.on_failed_authorization =     URL('user',args='on_failed_authorization')

Można zmienić tą zmienną i przekierować użytkownika gdzie indziej.

Często on_failed_authorization jest adresem URL, ale może to być też funkcją zwracającą adres URL i wywoływana gdy autoryzacja się nie powiedzie.

Poniżej znajdują się listy wywołań zwrotnych, które powinny być wykonane po walidacji formularza dla każdej odpowiedniej akcji, ale przed interakcją z bazą danych:

1
2
3
4
5
auth.settings.login_onvalidation = []
auth.settings.register_onvalidation = []
auth.settings.profile_onvalidation = []
auth.settings.retrieve_password_onvalidation = []
auth.settings.reset_password_onvalidation = []

Każde wywołanie zwrotne musi być funkcją, która pobiera obiekt form i może modyfikować atrybuty obiektu form przed wykonaniem interakcji z bazą danych.

To jet wykaz wywołań zwrotnych, które powinny być wykonane po wykonaniu interakcji z bazą danych i przed przekierowaniem:

1
2
3
4
auth.settings.login_onaccept = []
auth.settings.register_onaccept = []
auth.settings.profile_onaccept = []
auth.settings.verify_email_onaccept = []

Oto przykład:

1
2
auth.settings.register_onaccept.append(lambda form:   mail.send(to='you@example.com',subject='new user',
             message='new user email is %s'%form.vars.email))

Można włączyć CAPTCHA dla każdej akcji auth:

1
2
3
4
5
auth.settings.captcha = None
auth.settings.login_captcha = None
auth.settings.register_captcha = None
auth.settings.retrieve_username_captcha = None
auth.settings.retrieve_password_captcha = None

Jeśli ustawienia .captcha wskazują na gluon.tools.Recaptcha, wszystkie formularze, dla których właściwa opcja (taka jak .login_captcha) jest ustawiona na None będą mieć CAPTCHA, podczas gdy te, dla których właściwa opcja jest ustawiona na False, nie będą. Jeśli zamiast tego .captcha jest ustawiona na None, to tylko te formularze, które mają właściwą opcję ustawioną na obiekt gluon.tools.Recaptcha, będą mieć CAPTCHA a inne nie.

To jest ustawienie czasu ważności sesji logowania:

1
auth.settings.expiration = 3600  # seconds

Można zmienić nazwę pola hasła (w Firebird na przykład "password" jest słowem kluczowym i nie może być uzyte jako nazwa pola):

1
auth.settings.password_field = 'password'

Zwykle formularz próbuje adres email. Można to wyłączyć zmieniając następujace ustawienie:

1
auth.settings.login_email_validate = True

Chcesz pokazać identyfikator rekordu na stronie edutowania profilu? Zrób tak:

1
auth.settings.showid = False

Dla własnych formularzy można automatycznie wyłączyć powiadomienie o błędzie w formularzu:

1
auth.settings.hideerror = False

Można również zmienić styl dla własnych formularzy:

1
auth.settings.formstyle = 'table3cols'

(może to być "table2cols", "divs" i "ul")

oraz można ustawić seperator dla formularzy generowanych przez auth:

1
auth.settings.label_separator =        ':'

Domyślnie formularz logowania udostęþnia opcję dla rozszerzania logowania o opcję "remember me". Czas ważności lub wyłaczenie opcji można osięgnąć poprzez takie ustawienia:

1
2
auth.settings.long_expiration = 3600*24*30 # one month
auth.settings.remember_me_form = True

Można również dostosować następujące komunikaty, których zastosowanie i kontekst powinny być dla czytelnika oczywiste:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
auth.messages.submit_button = 'Submit'
auth.messages.verify_password = 'Verify Password'
auth.messages.delete_label = 'Check to delete:'
auth.messages.function_disabled = 'Function disabled'
auth.messages.access_denied = 'Insufficient privileges'
auth.messages.registration_verifying = 'Registration needs verification'
auth.messages.registration_pending = 'Registration is pending approval'
auth.messages.login_disabled = 'Login disabled by administrator'
auth.messages.logged_in = 'Logged in'
auth.messages.email_sent = 'Email sent'
auth.messages.unable_to_send_email = 'Unable to send email'
auth.messages.email_verified = 'Email verified'
auth.messages.logged_out = 'Logged out'
auth.messages.registration_successful = 'Registration successful'
auth.messages.invalid_email = 'Invalid email'
auth.messages.unable_send_email = 'Unable to send email'
auth.messages.invalid_login = 'Invalid login'
auth.messages.invalid_user = 'Invalid user'
auth.messages.is_empty = "Cannot be empty"
auth.messages.mismatched_password = "Password fields don't match"
auth.messages.verify_email = ...
auth.messages.verify_email_subject = 'Password verify'
auth.messages.username_sent = 'Your username was emailed to you'
auth.messages.new_password_sent = 'A new password was emailed to you'
auth.messages.password_changed = 'Password changed'
auth.messages.retrieve_username = 'Your username is: %(username)s'
auth.messages.retrieve_username_subject = 'Username retrieve'
auth.messages.retrieve_password = 'Your password is: %(password)s'
auth.messages.retrieve_password_subject = 'Password retrieve'
auth.messages.reset_password = ...
auth.messages.reset_password_subject = 'Password reset'
auth.messages.invalid_reset_password = 'Invalid reset password'
auth.messages.profile_updated = 'Profile updated'
auth.messages.new_password = 'New password'
auth.messages.old_password = 'Old password'
auth.messages.group_description =     'Group uniquely assigned to user %(id)s'
auth.messages.register_log = 'User %(id)s Registered'
auth.messages.login_log = 'User %(id)s Logged-in'
auth.messages.logout_log = 'User %(id)s Logged-out'
auth.messages.profile_log = 'User %(id)s Profile updated'
auth.messages.verify_email_log = 'User %(id)s Verification email sent'
auth.messages.retrieve_username_log = 'User %(id)s Username retrieved'
auth.messages.retrieve_password_log = 'User %(id)s Password retrieved'
auth.messages.reset_password_log = 'User %(id)s Password reset'
auth.messages.change_password_log = 'User %(id)s Password changed'
auth.messages.add_group_log = 'Group %(group_id)s created'
auth.messages.del_group_log = 'Group %(group_id)s deleted'
auth.messages.add_membership_log = None
auth.messages.del_membership_log = None
auth.messages.has_membership_log = None
auth.messages.add_permission_log = None
auth.messages.del_permission_log = None
auth.messages.has_permission_log = None
auth.messages.label_first_name = 'First name'
auth.messages.label_last_name = 'Last name'
auth.messages.label_username = 'Username'
auth.messages.label_email = 'E-mail'
auth.messages.label_password = 'Password'
auth.messages.label_registration_key = 'Registration key'
auth.messages.label_reset_password_key = 'Reset Password key'
auth.messages.label_registration_id = 'Registration identifier'
auth.messages.label_role = 'Role'
auth.messages.label_description = 'Description'
auth.messages.label_user_id = 'User ID'
auth.messages.label_group_id = 'Group ID'
auth.messages.label_name = 'Name'
auth.messages.label_table_name = 'Table name'
auth.messages.label_record_id = 'Record ID'
auth.messages.label_time_stamp = 'Timestamp'
auth.messages.label_client_ip = 'Client IP'
auth.messages.label_origin = 'Origin'
auth.messages.label_remember_me = "Remember me (for 30 days)"
  • add|del|has: dzienniki członkostwa pozwlające użyć "%(user_id)s" i "%(group_id)s".
  • add|del|has: dzienniki uprawnień pozwalające uzyć "%(user_id)s", "%(name)s", "%(table_name)s" i "%(record_id)s".

Usługa centralnego uwierzytelniania

CAS
authentication

Platforma web2py umożliwia obsługę zewnętrznych mechanizmów uwierzytelniania i logowania się w wielu aplikacjach przy użyciu jednego loginu. Tutaj omawiamy usługę centralnego uwierzytelniania (ang. Central Authentication Service - CAS) która jest standardem branżowym. W web2py jest wbudowany zarówno klient jak i serwer CAS.

CAS jest otwartym protokołem rozproszonego uwierzytelniania i działa w następujący sposób. Gdy osoba odwiedzi naszą stronę, aplikacja sprawdza w sesji, czy ten użytkownik jest już uwierzytelniony (na przykład wykorzystując obiekt session.token). Gdy osoba ta nie jest uwierzytelniona, kontroler przekierowuje ją do mechanizmu CAS, gdzie może ona się zalogować, zarejestrować lub zarządzać swoim poświadczeniem (nazwą, adresem email i hasłem). Jeśli użytkownik zarejestruje się, otrzymuje wiadomość email z łączem kierującym go na strone logowania. Rejestracja nie zostanie zakończona, dopóki użytkownik nie uruchomi przesłanego łącza (w określonym czasie). Gdy użytkownik pomyślnie się zaloguje, mechanizm CAS przekierowuje go do naszej aplikacji wraz z odpowiednim kluczem. Nasza aplikacja wykorzystuje w ten klucz do uzyskania poświadczenia z serwera CAS poprzez żądanie HTTP.

Używając ten mechanizm, wiele aplikacji może wykorzystwywać jedno poświadczenie poprzez serwer CAS. Serwer dostarczający uwierzytelniania nazywany jest dostawcą usługi. Aplikacja dążąca do uwierzytelnienia odwiedzających nazywana jest konsumentem usługi.

CAS jest bardzo podobne do OpenID, z pewną istotną różnicą. W przypadku OpenID, odwiedzający sam wybiera dostawcę usługi. W przypadku CAS, wybór jest dokonany na poziomie aplikacji, czyniąc CAS bardziej bezpiecznym.

Uruchamianie dostawcy CAS web2py sprowadza się do skopiowania aplikacji szkieletowej. W rzeczywistości każda aplikacja web2py udostępnia akcję:

## in provider app
def user(): return dict(form=auth())

będącą dostawcą CAS 2.0 a jej usługa jest dostępna pod adresami URL:

1
2
3
http://.../provider/default/user/cas/login
http://.../provider/default/user/cas/validate
http://.../provider/default/user/cas/logout

(tu przyjęliśmy, że aplikacja nazywa się "provider"). Z poziomu każdej innej aplikacji internetowej (konsumenta) jest możliwy dostęp przez delegowanie uwierzytelniania do dostawcy:

1
2
## in consumer app
auth = Auth(db,cas_provider = 'http://127.0.0.1:8000/provider/default/user/cas')

Kiedy odwiedzi się adres URL logowania aplikacji konsumenta, zostanie się przekierowanym do aplikacji dostawcy, która przeprowadzi uwierzytelnianie i przekieruje z powrotem do konsumenta. Wszystkie procesy rejestracji, wylogowania, zmiany hasła, odzyskania hasła, powinny być zrealizowane przez aplikację dostawcy. Po stronie konsumenta zostanie utworzony wpis o zalogowaniu użytkownika, tak więc w aplikacji konsumenta można dodać specjalne pola i mieć w niej lokalny profil. Dzięki CAS 2.0 wszystkie pola, które są możliwe do odczytu w aplikacji dostawcy i mające soje odpowiedniki w tabeli auth_user aplikacji konsumenta będą automatycznie kopiowane.

Wyrażenie Auth(...,cas_provider='...') działa z zewnętrznymi dostawcami i obsługuje CAS 1.0 i 2.0. Wersja jest wykrywana automatycznie. Domyślnie buduje ono adresy URL dostawcy od podstaw (cas_provider w powyższym wyrażeniu) przez dodanie

1
2
3
/login
/validate
/logout

Może to być zmienione zarówno w aplikacji konsumenta jak i dostawcy:

## in consumer or provider app (must match)
auth.settings.cas_actions['login']='login'
auth.settings.cas_actions['validate']='validate'
auth.settings.cas_actions['logout']='logout'

Jeśli chce się połączyć z dostawcą CAS web2py z innej domeny, trzeba udostępnić ją przez dodanie jej do listy dozwolonych domen:

1
2
## in provider app
auth.settings.cas_domains.append('example.com')

Używanie web2py do autoryzacji aplikacji nie opartych na web2py

Jest to możliwe, ale zależy od serwera internetowego. Tutaj przyjmujemy, że mamy dwie aplikacje uruchomione na tym samym serwerze internetowym Apache z mod_wsgi. Pierwsza aplikacja, to web2py z aplikacją dostarczającą kontrolę dostępu poprzez klasę Auth. Druga może być skryptem CGI, programem PHP lub czymkolwiek innym. Chcemy poinstruować serwer internetowy, aby wypytał o uprawnienia tej drugiej aplikacji, gdy klient żąda dostępu.

Przede wszystkim musimy zmodyfikować aplikacje web2py i dodać następujący kontroler:

1
2
def check_access():
    return 'true' if auth.is_logged_in() else 'false'

co zwraca true gdy użytkownik jest zalogowany a w przeciwnym razie false. Teraz uruchamiamy w tle proces web2py:

nohup python web2py.py -a '' -p 8002

Port 8002 jest konieczny i nie ma potrzeby udostępniania zaplecza administracyjnego, więc nie ma też hasła administratora.

Następnie musimy edytować plik konfiguracyjny Apache (na przykład "/etc/apache2/sites-available/default") i poinstruować Apache, że gdy jest wywoływana aplikacja nie oparta na web2py, to powinien wywołać powyższą akcję check i tylko wtedy, gdy zwraca ona true i odpowiedzieć na żądanie, w przeciwnym razie należy odmówić dostępu.

Ponieważ web2py i aplikacja nie oparta na web2py są uruchamiane w tej samej domenie, to jeśli użytkownik jest zalogowany do aplikacji web2py, to cisateczko sesji web2py będzie przekazane do Apache, nawet gdy żądana jest inna aplikacja, co pozwoli na weryfikację poświadczenia.

W tym celu będziemy potrzebować skrypt, "web2py/scripts/access.wsgi", który może wykonać ten trik. Skrypt ten jest dostarczany wraz z web2py. Wszystko co musimy zrobić, to tylko powiadomić Apache, aby wywołał ten skrypt, dostarczyć adres URL aplikacji wymagającej kontroli dostępu i umieszczenie skrypty tam gdzie trzeba:

<VirtualHost *:80>
   WSGIDaemonProcess web2py user=www-data group=www-data
   WSGIProcessGroup web2py
   WSGIScriptAlias / /home/www-data/web2py/wsgihandler.py

   AliasMatch ^myapp/path/needing/authentication/myfile /path/to/myfile
   <Directory /path/to/>
     WSGIAccessScript /path/to/web2py/scripts/access.wsgi
   </Directory>
</VirtualHost>

Tutaj ^myapp/path/needing/authentication/myfile jest wyrażeniem regularnym, które powinno dopasowywać przychodzące żądanie a /path/to/ jest ścieżką bezwzględną lokalizacji folderu web2py.

Skrypt "access.wsgi" zawiera następująca linię:

URL_CHECK_ACCESS = 'http://127.0.0.1:8002/%(app)s/default/check_access'

co wskazuje na żądaną aplikację web2py, ale można to edytować, wskazując określoną aplikację, uruchamianą na porcie innym niż 8002.

można również pobrać akcję check_access() i uczynić jej logikę bardziej skomplikowaną. Akcja ta może pobierać adres URL, który był pierwotnie żądany, wykorzystując zmienną środowiskową:

request.env.request_uri

oraz można zaimplementować bardziej złożone zasady:

1
2
3
4
5
6
7
def check_access():
    if not auth.is_logged_in():
       return 'false'
    elif not user_has_access(request.env.request_uri):
       return 'false'
    else:
       return 'true'
 top