16 septembrie 2013

Expirăciuni Oracle

Acum două săptămâni am fost "on-call", adică a trebuit să stau "pe țeavă" și să intervin în cazul în care apare ceva critic. Sâmbătă n-au fost probleme. A doua zi, duminică, mă duc cu nevasta și juniorul pe la Palas. Toate bune și frumoase, doar că pe la vreo șapte seara mă sună de la "Service Desk":

- Alo, Alexandru?
- Da!
- Avem un P1. S-ar parea ca nu mai merge nimic la baza de date.
- Ups, zic. Hai că vin pe la firmă să văd care-i baiul.

Zis și făcut! Mă sui în mașină, bag "pedală", ajung de urgență la serviciu. Care-o fi cauza? WTF? Ce se întâmplă? Când colo, să vezi drăcie! Iaca ditamai mesajul de eroare in log:
ORA-28001: the password has expired
Super, zic! Hai să vedem cum rezolvăm problema! Și repede, dacă se poate, că doar e un P1 (adică, pentru cei mai puțin obișnuiți cu jargonul "de specialitate", prioritate 1).

Bun, putem debloca un cont expirat? Ceva de genul, "ALTER USER ACCOUNT UNLOCK"? Of course, not! Nu funcționează pentru conturi expirate. Singura soluție e să resetăm omului parola! Bun, bun, da' care-i parola omului? O știm? Nuoooooo! Ok, nu-i bai! Putem folosi parola hash-uită pe care o extragem frumușel din tabela SYS.USER$. Sau, putem fi șmecheri, și sa obținem hash-ul cu pricina folosind DBMS_METADATA.GET_DDL. Să vedem care-i metoda cea mai bună.
SQL> select password from sys.user$ where name='MUMU';

PASSWORD        
----------------
2DB90C8E4C2DCA28
Avem hash-ul omului. De-acu, ar trebui să fie simplu: "ALTER USER MUMU IDENTIFIED BY VALUES ...". Haideți să vedem și cu varianta DBMS_METADATA.GET_DDL.
SQL> set longc 300
SQL> set long 300
SQL> select dbms_metadata.get_ddl('USER', 'MUMU') def from dual;

DEF
------------------------------------------------------------------------------------------
CREATE USER "MUMU" IDENTIFIED BY 
  VALUES 'S:5905A16074D32338FC7148AA8012E5D11DB70A060CCC125C8BF839B4CB87;2DB90C8E4C2DCA28'
      DEFAULT TABLESPACE "USERS"
      TEMPORARY TABLESPACE "TEMP"
      PROFILE "TEST"
      PASSWORD EXPIRE
WTF? Hash-urile sunt diferite! Interesant e că sufixul hash-ului din DBMS_METADATA este fix hash-ul din tabela USER$. Intersant... N-avem timp de pierdut cu investigațiile (e un P1, deaaa?), dar vom cerceta dup-aia. Hash-ul din DBMS_METADATA e mai credibil, pentru că e obținut printr-un API public, documentat, spre deosebire de tabelele interne de gen SYS.USER$. Prin urmare, mergem pe varianta hash-ului "grăsunel" și resetăm parola folosind acest hash.
ALTER USER "MUMU" IDENTIFIED BY
  VALUES 'S:5905A16074D32338FC7148AA8012E5D11DB70A060CCC125C8BF839B4CB87;2DB90C8E4C2DCA28';
Bun, ia sa vedem cum arata contul acum:
SQL> select username, account_status
  2    from dba_users
  3   where username='MUMU';

USERNAME ACCOUNT_STATUS
-------- --------------
MUMU     OPEN          
Foarte frumos, arată bine! Primim confirmarea de la client că treaba-i bună și putem răsufla ușurați. Din păcate, clientul e deja supărat și ne-a solicitat să investigăm de ce s-a ajuns aici, adică de ce au expirat conturile cu pricina. Ne prindem repede ce s-a întamplat: s-ar parea că era vorba de niște baze de date de 11g care fuseseră instalate cu "Next, Next, Next" și s-a omis faptul că în 11g profilul DEFAULT are o politică de expirare a parolei de 180 de zile. Dat fiind că respectivele conturi aveau asociate profilul DEFAULT s-a ajuns, bine-înțeles, aici. Clientul s-a declarat mulțumit, mai ales că bazele de date cu pricina n-au fost instalate de noi, prin urmare nu prea mai avea ce să mai spună.

Care-i faza cu HASH-ul?

A rămas întrebarea cu hash-urile: de ce DBMS_METADATA folosește un alt hash pentru parolă, diferit de cel din tabela SYS.USER? Să testăm! Creăm un user, cobai:
SQL> grant create session to cobai identified by "XXX";

Grant succeeded.
Să vedem hash-urile:
SQL> select password from sys.user$ where name='COBAI';

PASSWORD        
----------------
BA26C99FA9DD27DD

SQL> select dbms_metadata.get_ddl('USER', 'COBAI') def from dual;

DEF
------------------------------------------------------------------------------------------
CREATE USER "COBAI" IDENTIFIED BY 
  VALUES 'S:2DAEEF52366652141EA7D8BA93008B6509EC5C7B7B3CFAEDD4EDD73342E2;BA26C99FA9DD27DD'
      DEFAULT TABLESPACE "USERS"
      TEMPORARY TABLESPACE "TEMP"
Ce se întâmplă dacă folsim doar hash-ul din tabela USER$? Să vedem:
SQL> alter user cobai identified by values 'BA26C99FA9DD27DD';

User altered.

SQL> connect cobai/xxx
Connected.
A mers! Oare? A se observa că parola lui COBAI e trei de X, majuscule. Deci, in 11g, cu setările de parolă cu "case-sensitivity", comanda de conectare de mai sus ar fi trebuit să "crape". Ei bine, avem deci un răspuns la întrebarea noastră. Dacă folosim doar hash-ul din SYS.USER$, parola respectivului utilizator se va comporta ca o parolă de 10g, adică "case-insensitive". Verificăm...
SQL> alter user cobai identified by
  2  values 'S:2DAEEF52366652141EA7D8BA93008B6509EC5C7B7B3CFAEDD4EDD73342E2;BA26C99FA9DD27DD'
/

User altered.

SQL> connect cobai/xxx
ERROR:
ORA-01017: invalid username/password; logon denied


Warning: You are no longer connected to ORACLE.
SQL> connect cobai/"XXX"
Connected.
Am mai învățat ceva nou!

Cum scăpăm de expirare?

La vreo două zile după P1-ul buclucaș, primim la departamentul DBA ceva de genul:

"Vă rugăm să investigați dacă mai sunt si alte baze de date cu aceeași problemă și, dacă da, scoateți politica de expirare. Semnat, CLIENTUL!"

Bun, determinăm noi că mai sunt și alte baze de date care ar putea avea aceeași problemă, după care propunem o soluție banală:
ALTER PROFILE DEFAULT LIMIT PASSWORD_LIFE_TIME UNLIMITED;
Nimic deosebit! Doar că, nea Clientu' ne-a întrebat:

"Dacă facem modificarea în profil, să înțeleg că toate conturile asociate nu vor mai expira? Sau, dat fiind că aceste conturi au deja un EXPIRY_DATE, va trebui să le resetăm la mână?"

Hmmm... asta mă enervează la ORACLE. Un milion de spețe la care nici nu ți-a trecut prin cap să te gândești! Bunul simț ar trebui să indice că o modificare în profil ar trebui să se reflecte asupra tuturor conturilor asociate. Și aparent așa și este. Dar, doar dacă respectivele conturi sunt "OPEN". Dacă conturile sunt în perioada de grație sau au expirat deja, atunci modificarea din profil nu va avea nici un impact asupra acelor conturi. Dovada:
SQL> select * from dba_profiles
2   where profile='DEFAULT' and resource_name='PASSWORD_LIFE_TIME';

PROFILE RESOURCE_NAME      RESOURCE LIMIT
------- ------------------ -------- -----
DEFAULT PASSWORD_LIFE_TIME PASSWORD 180  

SQL> select username, account_status, expiry_date
2    from dba_users
3   where username in ('COBAI', 'DBSNMP', 'BUBU');

USERNAME ACCOUNT_STATUS EXPIRY_DA
-------- -------------- ---------
DBSNMP   EXPIRED        14-SEP-13
BUBU     EXPIRED(GRACE) 15-SEP-13
COBAI    OPEN           15-MAR-14

SQL> alter profile default limit password_life_time unlimited;

Profile altered.

SQL> select username, account_status, expiry_date
2    from dba_users
3   where username in ('COBAI', 'DBSNMP', 'BUBU');

USERNAME ACCOUNT_STATUS EXPIRY_DA
-------- -------------- ---------
DBSNMP   EXPIRED        14-SEP-13
BUBU     EXPIRED(GRACE) 15-SEP-13
COBAI    OPEN                    
Deci, raspunsul corect e: "deeeepindeeee!"

Nota: Am testat scenariile de mai sus pe o masina virtuală de VirtualBox. Bine-înțeles că a trebuit să ma joc cu ceasul sistemului astfel încât să pot păcăli serverul Oracle să creadă că expirarea contului trebuie să se producă sau pentru a activa perioada de grație. Problema e că mașina virtuală își sincronizează automat ceasul cu mașina gazdă, prin urmare o operație banală de dare a ceasului înainte s-ar putea dovedi o adevărată provocare și, prin extensie, o sursă de înjurături de care, personal, nu sunt mândru deloc. Soluția a fost să opresc serviciul de "VirtualBox Additions".

0 commentarii: