===== Oracle Apex 5 Securtiy und Microsoft Active Directory Integration ===== **Erstellt 05/2016** Um Zugriffe auf eine Apex Applikation zu steuern, kann auch das Active Directory eingebunden werden. Zwei Aufgaben sind dabei zu lösen, den User zu authentisieren und den Zugriff auf die Pages der Applikation zu autorisieren. Im folgenden Beispiel erfolgt die Authentifizierung über den Apache Webserver mit NTLM und für die Autorisierung von Teilen der Applikation wird auf die Hinterlegten Gruppen im AD zurückgegriffen. Die AD Gruppen werden dynamisch in den Session Kontext des aktuellen Users übernommen und werden dann in der Apex Applikation über einen "Authorisation Scheme" referenziert. Übersicht: {{ :prog:apex:active_directory_integration_apex_v5.png?direct | Apex und Microsoft AD Integration}} ---- ==== Authentisieren über den Webserver ==== Ob ein User sich in der Domaine angemeldet hat muss der eingesetzte Webserver prüfen. ===Single Sign On unter Windows 2012 mit Apache HTTP und dem NTLM Module === Unter Windows kann am einfachsten mit dem Apache HTTP Server NTLM mit **Mod Auth NTLM ** eingesetzt werden. Siehe dazu => [[prog:oracle_rest_data_service_tomcat#single_sign_on_mit_dem_apache_einrichten|Oracle ORDS 3.0 (Oracle REST Data Services) mit dem Apache Application Server Tomcat für Single Sign On unter Windows 2012 betreiben]] Über das Modul wird geprüft, ob der User sich gültig in der Windows Domain angemeldet hat, ist das True wird auf die Apex Application weitergleitet. Die weitere Prüfung, ob der User fachlich die Anwendung bzw. einzelnen Seiten oder Menüpunkte sehen darf, muss in der Apex Applikation getestet werden. Der Domain Name des User wird in den HTTP Header geschrieben (siehe die NTLM Konfiguration des obigen Beispiels) und in eine von uns definiert Variable abgelegt, im diesem Beispiel in "WIN_USER". ---- ==== Apex - Authentication Scheme einrichten - Wie meldet man sich in Apex an?==== Die Windows Anmeldung bzw. in Prinzip nur der Windows User Name wird an die Single Sign On Konfiguration von Apex für die jeweilige Applikation weitergereicht. Damit sich nun der User nicht nicht nochmal anmelden muss, muss in APEX das "Authentification Scheme" auf Applikationsebene gesetzt werden. * In der Applikation auf "Shared Components" klicken, "Authentication Schemes" auswählen {{ :prog:apex:apex_single_sign_on_01.png?direct&300 | Authentication Schemes anlegen }} * Create Button rechte wählen * Vorlage auswählen : Based on a pre-configured scheme from the gallery => Next{{ :prog:apex:apex_single_sign_on_02.png?direct&300 | Authentication Schemes anlegen}} * Nun einen Namen vergeben und die Methode **HTTP HEADER Variable** auswählen {{ :prog:apex:apex_single_sign_on_03.png?direct&300 |Authentication Schemes anlegen}} * Hinterlegen in welchen Header Feld der Username steht {{ :prog:apex:apex_single_sign_on_04.png?direct&300 | Authentication Scheme Header Variable hinterlegen}} * Speichern In den Application Settings prüfen, ob auch das richtige Schema gewählt wurde: {{ :prog:apex:apex_single_sign_on_05.png | Authentication Schemes überprüfen }} Nun kann der Test beginnen ob das auch geklappt hat. Außerhalb der Domain sollte jetzt eine Benutzerabfrage erfolgen, angemeldet an der Domain sollte sich die Applikation ohne Login starten lassen. ==Debuggen== Klappt es nicht, die Header Variablen auf einer Seite mit Hilfe von "owa_util.print_cgi_env;" anzeigen lassen (Page mit einer Region auf PL/SQL Basis): begin htp.p(owa_util.get_cgi_env('WIN_USER')); htp.p('

'); owa_util.print_cgi_env end; Den Variablen Namen haben wir zuvor in der Apache Konfiguration auf NTLM Ebene festgelegt! ---- ---- ==== Autorisieren - In Apex prüfen ob der jeweilige User die Seite auch wirklich sehen darf ==== In der Windows Welt ist das Gruppen Konzept selbstverständlich um Rechte auf Objekte im Betriebssystem zu definieren. Das gleiche kann auch in Apex erfolgen, über Gruppen kann der Zugriff auf Seiten, einzelne Elemente der Seite und Menüeinträge gesteuert werden. In Apex 5 kann die Zugehörigkeit zu einer Gruppe für den aktuelle angemeldeten Anwender beim Login gesetzt werden. Um das Verhalten einfach zu testen ein statisches Beispiel als Procedure zwei Gruppen zum Testen (angelegt im Parsing Schema der Apex Application) erstellen: create procedure setRole is begin apex_authorization.enable_dynamic_groups ( p_group_names => apex_t_varchar2('KOSTENSTELLE', 'EINKAUF') ); end; / => https://docs.oracle.com/cd/E59726_01/doc.50/e39149/apex_authorization.htm#AEAPI29592 Nun wird diese Procedure im **Authentication Scheme** als die "Post-Authentication Procedure" hinterlegt: {{ :prog:apex:apex_authentication_scheme_set_post_procedure_name_v01.png | 'Post-Authentication Procedure Name' hinterlegen}} Der Trick dahinter ist es nun über das Active Directory die Gruppen des Windows Users auszulesen und diese Windows Gruppen dynamisch in den Session Kontext von Apex zu schrieben. === Apex - Authorization Schemes verwenden === In Apex wird ein "Authorization Scheme" anlegt, mit dem geprüft wird ob der Apex User die notwendige Gruppe für das Apex Element besitzt. In der Apex Application wird dann auf Seiten oder Menü Ebene dieses "Authorization Scheme" referenziert, um zu prüfen ob die Seite/der Menüpunkt angezeigt werden kann. * In der Applikation auf „Shared Components“ klicken, „Authorization Schemes“ auswählen * Mit den "Create Button" den Wizard für ein neues Scheme starten * "From Scratch" auswählen * Name vergeben * Scheme Type "Is in Group" wählen * Eine Gruppe einfach eintragen (nicht über die Dialog suchen,werden ja dann später erst dynamisch hinterlegt!) * {{ :prog:apex:apex_create_authorization_scheme_v01.png | ein Apex Authorization Schemes anlegen}} * Mit "Create Authorization Scheme" anlegen In der Applikation nun auf der jeweiligen Seite das Authorization Scheme in den Security Settings hinterlegen: * {{ :prog:apex:apex_use_authorization_scheme_v01.png |Authorization Scheme in den Apex Seite hinterlegen}} ---- ==== Mit DBMS_LDAP das AD abfragen ==== Nach dem nun in den obigen Schritte mit der statischen Methode alles geklappt hat, gilt es nun das ganze dynamisch auch aus dem AD zu füllen. Im ersten Schritt muss geprüft werden ob aus der Datenbank überhaupt auf das AD zugegriffen werden kann. Ein sehr gutes Beispiel um das zu testen findet sich hier => https://oracle-base.com/articles/9i/ldap-from-plsql-9i . Den Beispiel Code aus diesem guten Beispiel in eine Datei kopieren und UNTER dem Parsing Schema der Apex Applikation den Zugriff auf das AD prüfen. Meist muss unter 11g und 12c eine Netzwerk ACL hinterlegt werden, um das überhaupt verwenden zu können, siehe dazu mehr am Ende dieser Seite. Mit diesen Schritt werden dann die notwendigen Zugangsdaten (AD IP Adresse und Port) + Username und Passwort ermittelt und die richtige LDAP Abfrage Struktur wird geklärt. Mit diese Daten kann dann mein Code Beispiel ldap_ad_util ergänzt werden. === LDAP_AD_UTIL === Mit Hilfe des Windows User Name wird das AD abgefragt, die gefunden Gruppe werden in den Session Kontext von Apex geschrieben. Das Schreiben in den Session Kontext von APEX erfolgt durch das Hinterlegen einer Procedure in der current "Authentifcation Scheme" der Application im Bereich "post_authentication" mit dem 'Post-Authentication Procedure name' Attribut. == Der Code == Für die dynamische Übernahme der Gruppen aus dem AD nach Apex wird der folgende PL/SQL verwendet (muss im Pasing Schema liegen oder von dort mit entsprechenden Rechten lesbar sein). Spezifikation: create or replace package ldap_ad_util is -- +============================================================================ -- NAME: ldap_ad_util -- PURPOSE: Read User information from the active directory -- -- +============================================================================ -- exception handling g_pck constant varchar2 (30) := 'ldap_util'; ex_gperrors exception; pragma exception_init (ex_gperrors, -20100); g_emerrors varchar2 (100) := 'An error occured. Please view the ERRORS-table for more information.'; -- global variables -- You have to edit carefully this section to set all the values of your enviroment! -- IP or name of the AD and the port g_ldap_host varchar2(256) := '10.10.10.180'; g_ldap_port varchar2(256) := '389'; -- user to read from the AD g_ldap_user varchar2(256) := 'ORASYSTEM'; g_ldap_passwd varchar2(256) := 'secret_password'; -- The entry to the AD tree -- check the cn ! and adjust to your needs! g_ldap_base varchar2(256) := 'cn=Users,dc=pipperr,dc=local'; -- how to seach the user in the ad -- check how the loginname in your domain is defined! g_ad_user_type varchar2(255) := 'cn='; -- +===========================================================+ -- function : connectAD -- +===========================================================+ function connectad return dbms_ldap.session; -- +===========================================================+ -- procedure : disconnectAD -- +===========================================================+ procedure disconnectad( p_session in out dbms_ldap.session); -- +===========================================================+ -- procedure : disconnectAD -- +===========================================================+ function readgroups( p_session dbms_ldap.session , p_username varchar2) return apex_t_varchar2; -- +===========================================================+ -- procedure : setApexGroups -- set in the Apex Session dynamic groups -- +===========================================================+ procedure setapexgroups( p_username varchar2 default sys_context( 'APEX$SESSION', 'APP_USER')); end ldap_ad_util; / Body: create or replace package body ldap_ad_util is -- +============================================================================ -- NAME: ldap_ad_util -- PURPOSE: Read User information from the active directory -- GPI 2016 -- +============================================================================ -- +===========================================================+ -- function : getADPWD -- get the Password for the AD User -- later we will save the pwd encrypted in the database -- +===========================================================+ function getadpwd return varchar2 is begin return g_ldap_passwd; end; -- +===========================================================+ -- function : connectAD -- connect to the AD -- +===========================================================+ function connectad return dbms_ldap.session is v_session dbms_ldap.session; v_retval pls_integer; begin -- choose to raise exceptions. dbms_ldap.use_exception := true; dbms_ldap.utf8_conversion:=false; -- connect to the ldap server. v_session := dbms_ldap.init(hostname => g_ldap_host, portnum => g_ldap_port); -- connect with this user to the AD v_retval := dbms_ldap.simple_bind_s(ld => v_session , dn => g_ldap_user , passwd => getadpwd ); if v_retval != DBMS_LDAP.SUCCESS then dbms_output.put_line('-- Error at::'||$$plsql_unit||' :: dbms_ldap.simple_bind_s for User ::'|| g_ldap_user ); raise_application_error( -21001 , '-- Error at::'||$$plsql_unit||' :: dbms_ldap.simple_bind_s for User ::'|| g_ldap_user ); end if; return v_session; exception when others then dbms_output.put_line('-- Error at::'||$$plsql_unit||' :: '||sqlerrm); raise_application_error( sqlcode , '-- Error at::'||$$plsql_unit||' :: '||sqlerrm); end connectad; -- +===========================================================+ -- procedure : disconnectAD -- +===========================================================+ procedure disconnectad( p_session in out dbms_ldap.session) is v_retval pls_integer; begin v_retval := dbms_ldap.unbind_s(ld => p_session); if v_retval != DBMS_LDAP.SUCCESS then dbms_output.put_line('-- Error at::'||$$plsql_unit||' :: Can not close connection to LDAP'); raise_application_error( -21009 , '-- Error at::'||$$plsql_unit||' :: Can not close connection to LDAP'); end if; exception when others then dbms_output.put_line('-- Error at::'||$$plsql_unit||' :: '||sqlerrm); end disconnectad; -- +===========================================================+ -- procedure : disconnectAD -- Code Logic copied from https://oracle-base.com/articles/9i/ldap-from-plsql-9i . -- Thanks to Tim Hall -- +===========================================================+ function readgroups( p_session dbms_ldap.session , p_username varchar2) return apex_t_varchar2 is v_retval pls_integer; v_attrs dbms_ldap.string_collection; v_message dbms_ldap.message; v_entry dbms_ldap.message; v_attr_name varchar2(256); v_ber_element dbms_ldap.ber_element; v_vals dbms_ldap.string_collection; -- empty collection v_group_tab apex_t_varchar2:=apex_t_varchar2(); v_apex_ary apex_application_global.vc_arr2; begin v_attrs(1) := 'memberOf'; -- retrieve all attributes v_retval := dbms_ldap.search_s(ld => p_session , base => g_ldap_base , scope => dbms_ldap.scope_subtree , filter => g_ad_user_type||p_username , attrs => v_attrs , attronly => 0 , res => v_message ); if v_retval != DBMS_LDAP.SUCCESS then dbms_output.put_line('-- Error at::'||$$plsql_unit||' :: dbms_ldap.search_s for filter ::'|| g_ad_user_type||p_username ); raise_application_error( -21002 , '-- Error at::'||$$plsql_unit||' :: dbms_ldap.search_s for filter ::'|| g_ad_user_type||p_username); end if; if dbms_ldap.count_entries(ld => p_session , msg => v_message) > 0 then -- Get all the entries returned by our search. v_entry := dbms_ldap.first_entry( ld => p_session ,msg => v_message); << entry_loop >> while v_entry is not null loop -- Get all the attributes for this entry. dbms_output.put_line('------------------------------------'); v_attr_name := dbms_ldap.first_attribute(ld => p_session, ldapentry => v_entry, ber_elem => v_ber_element); << attributes_loop >> while v_attr_name is not null loop -- Get all the values for this attribute. v_vals := dbms_ldap.get_values (ld => p_session , ldapentry => v_entry , attr => v_attr_name); begin << values_loop >> for i in v_vals.first .. v_vals.last loop dbms_output.put_line('-- Info: Found: ' || v_attr_name || ' = ' || substr(v_vals(i),1,500)); -- decode memberOf = CN=ORA_ASMDBA,CN=Users,DC=pipperr,DC=local -- to the the group name v_apex_ary:=apex_util.string_to_table(p_string=> v_vals(i),p_separator => ',' ); for y in v_apex_ary.first .. v_apex_ary.last loop if v_apex_ary.exists(y) then dbms_output.put_line('-- Info: Catch Group Details ' || v_apex_ary(y)); if y=1 then v_group_tab.extend; v_group_tab( v_group_tab.last ) := (replace(v_apex_ary(y),'CN=','')); dbms_output.put_line('-- Info: Found Group ' || replace(v_apex_ary(y),'CN=','')); end if; end if; end loop; end loop values_loop; exception when others then dbms_output.put_line('-- Error read Attribute: ' || v_attr_name || ' :: Errror '||sqlerrm); end; v_attr_name := dbms_ldap.next_attribute(ld => p_session , ldapentry => v_entry , ber_elem => v_ber_element); end loop attibutes_loop; v_entry := dbms_ldap.next_entry(ld => p_session , msg => v_entry); end loop entry_loop; end if; return v_group_tab; end readgroups; -- +===========================================================+ -- procedure : setApexGroups -- set in the Apex Session dynamic groups -- +===========================================================+ procedure setapexgroups( p_username varchar2 default sys_context( 'APEX$SESSION', 'APP_USER')) is v_session dbms_ldap.session; v_group_tab apex_t_varchar2; begin -- connect to LDAP v_session:=connectad; -- read the groups into dbms_output.put_line('-- Info: Get AD Groups for ' || p_username); v_group_tab:=readgroups(p_session => v_session, p_username => p_username); -- disconnect the LDAP Session disconnectad(p_session => v_session); -- add this groups to the Apex Session if v_group_tab.count > 0 then for i in v_group_tab.first .. v_group_tab.last loop if v_group_tab.exists(i) then dbms_output.put_line('-- Info: set Group in Apex Session ' || v_group_tab(i)); end if; end loop; else dbms_output.put_line('-- Info: No Groups for this user found' ||p_username ); end if; -- set the groups with the group collection -- apex_t_varchar2('KOSTENSTELLE','EINKAUF') apex_authorization.enable_dynamic_groups ( p_group_names => v_group_tab); -- exception when others then dbms_output.put_line('-- Error at::'||$$plsql_unit||' :: '||sqlerrm); -- check that the connection to the ldap is closed! -- Check if connection ist still open is in the function! disconnectad(p_session => v_session); end setapexgroups; begin -- Initialization null; end ldap_ad_util ; / Ein noch zu lösendes Problem ist das Passwort im Package, in einer produktiven Umgebung sollte das entweder verschlüsselt hinterlegt werden oder anderweitig geschützt gespeichert sein! Um das ganze zu testen, das Package mit den richtigen Globalen Einstellungen einspielen und in SQL*Plus im APEX Parsing Schema mit einen gekannten AD User aufrufen: set serveroutput on exec setApexGroups('ORACLE_ADMIN'); Da der Default die Apex Session Info ist, braucht später keine User übergeben zu werden! === PWD des Domain Users verstecken === Wollte jetzt keine eigenen Tabelle für das eine Password anlegen, Idee: * Key setzt sich aus der DB Laufzeit Umgebung zusammen * Key wird ergänzt um einen privaten Key bei Aufruf der Entschlüsselung. * Funktion oder Object wird in der DB für das Speichern des verschlüsselten Wertes verwendet Lösung: siehe => [[dba:passwort_in_psql_schuetzen|Passwörter und ähnliche Schlüssel in PL/SQL Packages schützen]] ==== Fehler Suche - ORA-24247: network access denied by access control list (ACL) ==== **Oracle 12c** Netzwerk ACL setzen! Fehler: ERROR at line 1: ORA-24247: network access denied by access control list (ACL) ORA-06512: at "SYS.DBMS_LDAP_API_FFI", line 25 ORA-06512: at "SYS.DBMS_LDAP", line 48 ORA-06512: at "GPI.LDAP_UTIL", line 23 ORA-06512: at "GPI.LDAP_UTIL", line 148 ORA-06512: at line 1 BEGIN DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE ( host => '10.10.10.180' , lower_port => 389 , upper_port => 389 , ace => xs$ace_type( privilege_list => xs$name_list('connect') , principal_name => 'GPI' , principal_type => xs_acl.ptype_db) ); END; / Siehe dazu => http://oracle.informatik.haw-hamburg.de/network.121/e17607/fine_grained_access.htm#DBSEG114 ACL in 11g hinterlegen: * https://matthiashoys.wordpress.com/2012/04/24/ora-24247-during-ldap-authentication-from-apex-4-1-1-on-oracle-11gr2/ ==== Quellen ==== LDAP * http://www.idevelopment.info/data/Oracle/DBA_tips/LDAP/LDAP_21.shtml * http://sql-plsql-de.blogspot.de/2007/09/ldap-server-abfragen-mit-sql.html * https://docs.oracle.com/cd/B10501_01/network.920/a96577/concepts.htm#725943 Verschlüsseln: * http://www.oracleflash.com/41/Encrypt-or-Decrypt-sensitive-data-using-PLSQL---DBMS_CRYPTO.html