/* * PROGRAM: JRD Access Method * MODULE: scl.epp * DESCRIPTION: Security class handler * * The contents of this file are subject to the Interbase Public * License Version 1.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy * of the License at http://www.Inprise.com/IPL.html * * Software distributed under the License is distributed on an * "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express * or implied. See the License for the specific language governing * rights and limitations under the License. * * The Original Code was created by Inprise Corporation * and its predecessors. Portions created by Inprise Corporation are * Copyright (C) Inprise Corporation. * * All Rights Reserved. * Contributor(s): ______________________________________. * 2001.6.12 Claudio Valderrama: the role should be wiped out if invalid. * 2001.8.12 Claudio Valderrama: Squash security bug when processing * identifiers with embedded blanks: check_procedure, check_relation * and check_string, the latter being called from many places. * */ #include "firebird.h" #include #include #include #include "../jrd/jrd.h" #include "../jrd/ods.h" #include "../jrd/scl.h" #include "../jrd/acl.h" #include "../jrd/blb.h" #include "../jrd/irq.h" #include "../jrd/obj.h" #include "../jrd/req.h" #include "../jrd/tra.h" #include "../common/gdsassert.h" #include "../jrd/blb_proto.h" #include "../jrd/cmp_proto.h" #include "../jrd/err_proto.h" #include "../jrd/exe_proto.h" #include "../jrd/ini_proto.h" #include "../yvalve/gds_proto.h" #include "../common/isc_proto.h" #include "../jrd/met_proto.h" #include "../jrd/grant_proto.h" #include "../jrd/scl_proto.h" #include "../jrd/constants.h" #include "fb_exception.h" #include "../common/utils_proto.h" #include "../common/classes/array.h" #include "../common/config/config.h" #include "../common/os/os_utils.h" #include "../common/classes/ClumpletWriter.h" #include "../jrd/PreparedStatement.h" #include "../jrd/ResultSet.h" const int UIC_BASE = 10; using namespace Jrd; using namespace Firebird; DATABASE DB = FILENAME "ODS.RDB"; static bool check_number(const UCHAR*, USHORT); static bool check_user_group(thread_db* tdbb, const UCHAR*, USHORT); static bool check_string(const UCHAR*, const Firebird::MetaName&); static SecurityClass::flags_t compute_access(thread_db* tdbb, const SecurityClass*, const jrd_rel*, SLONG, const Firebird::MetaName&); static SecurityClass::flags_t walk_acl(thread_db* tdbb, const Acl&, const jrd_rel*, SLONG, const Firebird::MetaName&); static void raiseError(SecurityClass::flags_t mask, SLONG type, const Firebird::MetaName& name, const Firebird::MetaName& r_name); namespace { struct P_NAMES { SecurityClass::flags_t p_names_priv; USHORT p_names_acl; const TEXT* p_names_string; }; const P_NAMES p_names[] = { { SCL_alter, priv_alter, "ALTER" }, { SCL_control, priv_control, "CONTROL" }, { SCL_drop, priv_drop, "DROP" }, { SCL_insert, priv_insert, "INSERT" }, { SCL_update, priv_update, "UPDATE" }, { SCL_delete, priv_delete, "DELETE" }, { SCL_select, priv_select, "SELECT" }, { SCL_references, priv_references, "REFERENCES" }, { SCL_execute, priv_execute, "EXECUTE" }, { SCL_usage, priv_usage, "USAGE" }, { SCL_create, priv_create, "CREATE" }, { 0, 0, "" } }; // Database is never requested through CMP_post_access. const char* const object_str_database = "DATABASE"; //const char* const object_str_schema = "SCHEMA"; const char* const object_str_table = "TABLE"; const char* const object_str_package = "PACKAGE"; const char* const object_str_procedure = "PROCEDURE"; const char* const object_str_function = "FUNCTION"; const char* const object_str_column = "COLUMN"; const char* const object_str_charset = "CHARACTER SET"; const char* const object_str_collation = "COLLATION"; const char* const object_str_domain = "DOMAIN"; const char* const object_str_exception = "EXCEPTION"; const char* const object_str_generator = "GENERATOR"; const char* const object_str_view = "VIEW"; const char* const object_str_role = "ROLE"; const char* const object_str_filter = "FILTER"; struct SecObjectNamePriority { const char* name; SLONG num; }; const SecObjectNamePriority priorities[] = { {object_str_database, SCL_object_database}, //{object_str_schema, SCL_object_schema}, {object_str_table, SCL_object_table}, {object_str_package, SCL_object_package}, {object_str_procedure, SCL_object_procedure}, {object_str_function, SCL_object_function}, {object_str_column, SCL_object_column}, {object_str_charset, SCL_object_charset}, {object_str_collation, SCL_object_collation}, {object_str_domain, SCL_object_domain}, {object_str_exception, SCL_object_exception}, {object_str_generator, SCL_object_generator}, {object_str_view, SCL_object_view}, {object_str_role, SCL_object_role}, {object_str_filter, SCL_object_filter}, {"", 0} }; /* Unused, may be needed SLONG accTypeStrToNum(const char* name) { for (const SecObjectNamePriority* p = priorities; p->num != 0; ++p) { if (strcmp(p->name, name) == 0) return p->num; } fb_assert(false); return 0; } */ const char* accTypeNumToStr(const SLONG num) { for (const SecObjectNamePriority* p = priorities; p->num != 0; ++p) { if (p->num == num) return p->name; } fb_assert(false); return ""; } } // anonymous namespace static void raiseError(SecurityClass::flags_t mask, SLONG type, const Firebird::MetaName& name, const Firebird::MetaName& r_name) { const P_NAMES* names; for (names = p_names; names->p_names_priv; names++) { if (names->p_names_priv & mask) break; } const char* const typeAsStr = accTypeNumToStr(type); const Firebird::string fullName = r_name.hasData() ? r_name.c_str() + Firebird::string(".") + name.c_str() : name.c_str(); ERR_post(Arg::Gds(isc_no_priv) << Arg::Str(names->p_names_string) << Arg::Str(typeAsStr) << Arg::Str(fullName)); } void SCL_check_access(thread_db* tdbb, const SecurityClass* s_class, SLONG view_id, SLONG obj_type, const Firebird::MetaName& obj_name, SecurityClass::flags_t mask, SLONG type, bool recursive, const Firebird::MetaName& name, const Firebird::MetaName& r_name) { /************************************** * * S C L _ c h e c k _ a c c e s s * ************************************** * * Functional description * Check security class for desired permission. * **************************************/ SET_TDBB(tdbb); if (tdbb->tdbb_flags & TDBB_trusted_ddl) return; if (s_class && (s_class->scl_flags & SCL_corrupt)) { ERR_post(Arg::Gds(isc_no_priv) << Arg::Str("(ACL unrecognized)") << Arg::Str("security_class") << Arg::Str(s_class->scl_name)); } // Check global DDL permissions with ANY option which allow user to make changes non owned objects const SecurityClass::flags_t obj_mask = SCL_get_object_mask(type); if (mask & obj_mask) return; if (!s_class || (mask & s_class->scl_flags)) return; const jrd_rel* view = NULL; if (view_id) view = MET_lookup_relation_id(tdbb, view_id, false); if ((view || obj_name.hasData()) && (compute_access(tdbb, s_class, view, obj_type, obj_name) & mask)) { return; } // Allow recursive procedure/function call if (recursive && ((type == SCL_object_procedure && obj_type == id_procedure) || (type == SCL_object_function && obj_type == id_function)) && obj_name == name) { return; } raiseError(mask, type, name, r_name); } void SCL_check_create_access(thread_db* tdbb, int type) { /************************************** * * S C L _ c h e c k _ c r e a t e _ a c c e s s * ************************************** * * Functional description * Check create access on a database object (DDL access) * **************************************/ SET_TDBB(tdbb); const SecurityClass::flags_t obj_mask = SCL_get_object_mask(type); if (!(obj_mask & SCL_create)) ERR_post(Arg::Gds(isc_dyn_no_priv)); } void SCL_check_charset(thread_db* tdbb, const MetaName& name, SecurityClass::flags_t mask) { /************************************** * * S C L _ c h e c k _ c h a r s e t * ************************************** * * Functional description * Given a character set name, check for a set of privileges. * **************************************/ SET_TDBB(tdbb); Jrd::Attachment* const attachment = tdbb->getAttachment(); const SecurityClass* s_class = NULL; AutoCacheRequest request(tdbb, irq_cs_security, IRQ_REQUESTS); FOR (REQUEST_HANDLE request) CS IN RDB$CHARACTER_SETS WITH CS.RDB$CHARACTER_SET_NAME EQ name.c_str() { if (!CS.RDB$SECURITY_CLASS.NULL) s_class = SCL_get_class(tdbb, CS.RDB$SECURITY_CLASS); } END_FOR SCL_check_access(tdbb, s_class, 0, 0, name, mask, SCL_object_charset, false, name); } void SCL_check_collation(thread_db* tdbb, const MetaName& name, SecurityClass::flags_t mask) { /************************************** * * S C L _ c h e c k _ c o l l a t i o n * ************************************** * * Functional description * Given a collation name, check for a set of privileges. * **************************************/ SET_TDBB(tdbb); Jrd::Attachment* const attachment = tdbb->getAttachment(); const SecurityClass* s_class = NULL; AutoCacheRequest request(tdbb, irq_coll_security, IRQ_REQUESTS); FOR (REQUEST_HANDLE request) COLL IN RDB$COLLATIONS WITH COLL.RDB$COLLATION_NAME EQ name.c_str() { if (!COLL.RDB$SECURITY_CLASS.NULL) s_class = SCL_get_class(tdbb, COLL.RDB$SECURITY_CLASS); } END_FOR SCL_check_access(tdbb, s_class, 0, 0, name, mask, SCL_object_collation, false, name); } void SCL_check_database(thread_db* tdbb, SecurityClass::flags_t mask) { /************************************** * * S C L _ c h e c k _ d a t a b a s e * ************************************** * * Functional description * Check for a set of privileges of current database. * **************************************/ SET_TDBB(tdbb); Jrd::Attachment* const attachment = tdbb->getAttachment(); const SecurityClass* const att_class = attachment->att_security_class; if (att_class && (att_class->scl_flags & mask)) return; if (mask == SCL_alter && attachment->locksmith(tdbb, USE_NBACKUP_UTILITY)) return; const P_NAMES* names; for (names = p_names; names->p_names_priv; names++) { if (names->p_names_priv & mask) break; } ERR_post(Arg::Gds(isc_no_priv) << Arg::Str(names->p_names_string) << Arg::Str(object_str_database) << Arg::Str("")); } void SCL_check_domain(thread_db* tdbb, const MetaName& name, SecurityClass::flags_t mask) { /************************************** * * S C L _ c h e c k _ d o m a i n * ************************************** * * Functional description * Given a domain name, check for a set of privileges. * **************************************/ SET_TDBB(tdbb); Jrd::Attachment* const attachment = tdbb->getAttachment(); const SecurityClass* s_class = NULL; AutoCacheRequest request(tdbb, irq_gfld_security, IRQ_REQUESTS); FOR (REQUEST_HANDLE request) FLD IN RDB$FIELDS WITH FLD.RDB$FIELD_NAME EQ name.c_str() { if (!FLD.RDB$SECURITY_CLASS.NULL) s_class = SCL_get_class(tdbb, FLD.RDB$SECURITY_CLASS); } END_FOR SCL_check_access(tdbb, s_class, 0, 0, name, mask, SCL_object_domain, false, name); } void SCL_check_exception(thread_db* tdbb, const MetaName& name, SecurityClass::flags_t mask) { /************************************** * * S C L _ c h e c k _ e x c e p t i o n * ************************************** * * Functional description * Given an exception name, check for a set of privileges. * **************************************/ SET_TDBB(tdbb); Jrd::Attachment* const attachment = tdbb->getAttachment(); const SecurityClass* s_class = NULL; AutoCacheRequest request(tdbb, irq_exc_security, IRQ_REQUESTS); FOR (REQUEST_HANDLE request) XCP IN RDB$EXCEPTIONS WITH XCP.RDB$EXCEPTION_NAME EQ name.c_str() { if (!XCP.RDB$SECURITY_CLASS.NULL) s_class = SCL_get_class(tdbb, XCP.RDB$SECURITY_CLASS); } END_FOR SCL_check_access(tdbb, s_class, 0, 0, name, mask, SCL_object_exception, false, name); } void SCL_check_generator(thread_db* tdbb, const MetaName& name, SecurityClass::flags_t mask) { /************************************** * * S C L _ c h e c k _ g e n e r a t o r * ************************************** * * Functional description * Given a generator name, check for a set of privileges. * **************************************/ SET_TDBB(tdbb); Jrd::Attachment* const attachment = tdbb->getAttachment(); const SecurityClass* s_class = NULL; AutoCacheRequest request(tdbb, irq_gen_security, IRQ_REQUESTS); FOR (REQUEST_HANDLE request) GEN IN RDB$GENERATORS WITH GEN.RDB$GENERATOR_NAME EQ name.c_str() { if (!GEN.RDB$SECURITY_CLASS.NULL) s_class = SCL_get_class(tdbb, GEN.RDB$SECURITY_CLASS); } END_FOR SCL_check_access(tdbb, s_class, 0, 0, name, mask, SCL_object_generator, false, name); } void SCL_check_index(thread_db* tdbb, const Firebird::MetaName& index_name, UCHAR index_id, SecurityClass::flags_t mask) { /****************************************************** * * S C L _ c h e c k _ i n d e x * ****************************************************** * * Functional description * Given a index name (as a TEXT), check for a * set of privileges on the table that the index is on and * on the fields involved in that index. * CVC: Allow the same function to use the zero-based index id, too. * The idx.idx_id value is zero based but system tables use * index id's being one based, hence adjust the incoming value * before calling this function. If you use index_id, index_name * becomes relation_name since index ids are relative to tables. * *******************************************************/ SET_TDBB(tdbb); Jrd::Attachment* const attachment = tdbb->getAttachment(); const SecurityClass* s_class = NULL; const SecurityClass* default_s_class = NULL; // No security to check for if the index is not yet created if ((index_name.length() == 0) && (index_id < 1)) { return; } Firebird::MetaName reln_name, aux_idx_name; const Firebird::MetaName* idx_name_ptr = &index_name; const Firebird::MetaName* relation_name_ptr = &index_name; AutoRequest request; int systemFlag = 0; // No need to cache this request handle, it's only used when // new constraints are created if (index_id < 1) { FOR(REQUEST_HANDLE request) IND IN RDB$INDICES CROSS REL IN RDB$RELATIONS OVER RDB$RELATION_NAME WITH IND.RDB$INDEX_NAME EQ index_name.c_str() { reln_name = REL.RDB$RELATION_NAME; if (!REL.RDB$SECURITY_CLASS.NULL) s_class = SCL_get_class(tdbb, REL.RDB$SECURITY_CLASS); if (!REL.RDB$DEFAULT_CLASS.NULL) default_s_class = SCL_get_class(tdbb, REL.RDB$DEFAULT_CLASS); systemFlag = REL.RDB$SYSTEM_FLAG; } END_FOR } else { idx_name_ptr = &aux_idx_name; FOR (REQUEST_HANDLE request) IND IN RDB$INDICES CROSS REL IN RDB$RELATIONS OVER RDB$RELATION_NAME WITH IND.RDB$RELATION_NAME EQ relation_name_ptr->c_str() AND IND.RDB$INDEX_ID EQ index_id { reln_name = REL.RDB$RELATION_NAME; aux_idx_name = IND.RDB$INDEX_NAME; if (!REL.RDB$SECURITY_CLASS.NULL) s_class = SCL_get_class(tdbb, REL.RDB$SECURITY_CLASS); if (!REL.RDB$DEFAULT_CLASS.NULL) default_s_class = SCL_get_class(tdbb, REL.RDB$DEFAULT_CLASS); systemFlag = REL.RDB$SYSTEM_FLAG; } END_FOR } if (systemFlag == 1 && !attachment->isRWGbak()) { // Someone is going to reference system table in FK // Usually it's not good idea raiseError(mask, SCL_object_table, reln_name, ""); } // Check if the relation exists. It may not have been created yet. // Just return in that case. if (reln_name.isEmpty()) return; SCL_check_access(tdbb, s_class, 0, 0, NULL, mask, SCL_object_table, false, reln_name); request.reset(); // Check if the field used in the index has the appropriate // permission. If the field in question does not have a security class // defined, then the default security class for the table applies for that // field. // No need to cache this request handle, it's only used when // new constraints are created FOR(REQUEST_HANDLE request) ISEG IN RDB$INDEX_SEGMENTS CROSS RF IN RDB$RELATION_FIELDS OVER RDB$FIELD_NAME WITH RF.RDB$RELATION_NAME EQ reln_name.c_str() AND ISEG.RDB$INDEX_NAME EQ idx_name_ptr->c_str() { s_class = (!RF.RDB$SECURITY_CLASS.NULL) ? SCL_get_class(tdbb, RF.RDB$SECURITY_CLASS) : default_s_class; SCL_check_access(tdbb, s_class, 0, 0, NULL, mask, SCL_object_column, false, RF.RDB$FIELD_NAME, reln_name); } END_FOR } void SCL_check_package(thread_db* tdbb, const dsc* dsc_name, SecurityClass::flags_t mask) { /************************************** * * S C L _ c h e c k _ p a c k a g e * ************************************** * * Functional description * Given a package name, check for a set of privileges. The * package in question may or may not have been created, let alone * scanned. This is used exclusively for meta-data operations. * **************************************/ SET_TDBB(tdbb); // Get the name in CSTRING format, ending on NULL or SPACE fb_assert(dsc_name->dsc_dtype == dtype_text); const Firebird::MetaName name(reinterpret_cast(dsc_name->dsc_address), dsc_name->dsc_length); Jrd::Attachment* const attachment = tdbb->getAttachment(); const SecurityClass* s_class = NULL; AutoCacheRequest request(tdbb, irq_pkg_security, IRQ_REQUESTS); FOR (REQUEST_HANDLE request) PKG IN RDB$PACKAGES WITH PKG.RDB$PACKAGE_NAME EQ name.c_str() { if (!PKG.RDB$SECURITY_CLASS.NULL) s_class = SCL_get_class(tdbb, PKG.RDB$SECURITY_CLASS); } END_FOR SCL_check_access(tdbb, s_class, 0, id_package, name, mask, SCL_object_package, false, name); } void SCL_check_procedure(thread_db* tdbb, const dsc* dsc_name, SecurityClass::flags_t mask) { /************************************** * * S C L _ c h e c k _ p r o c e d u r e * ************************************** * * Functional description * Given a procedure name, check for a set of privileges. The * procedure in question may or may not have been created, let alone * scanned. This is used exclusively for meta-data operations. * **************************************/ SET_TDBB(tdbb); // Get the name in CSTRING format, ending on NULL or SPACE fb_assert(dsc_name->dsc_dtype == dtype_text); const Firebird::MetaName name(reinterpret_cast(dsc_name->dsc_address), dsc_name->dsc_length); Jrd::Attachment* const attachment = tdbb->getAttachment(); const SecurityClass* s_class = NULL; AutoCacheRequest request(tdbb, irq_p_security, IRQ_REQUESTS); FOR (REQUEST_HANDLE request) SPROC IN RDB$PROCEDURES WITH SPROC.RDB$PROCEDURE_NAME EQ name.c_str() AND SPROC.RDB$PACKAGE_NAME MISSING { if (!SPROC.RDB$SECURITY_CLASS.NULL) s_class = SCL_get_class(tdbb, SPROC.RDB$SECURITY_CLASS); } END_FOR SCL_check_access(tdbb, s_class, 0, id_procedure, name, mask, SCL_object_procedure, false, name); } void SCL_check_function(thread_db* tdbb, const dsc* dsc_name, SecurityClass::flags_t mask) { /************************************** * * S C L _ c h e c k _ f u n c t i o n * ************************************** * * Functional description * Given a function name, check for a set of privileges. The * function in question may or may not have been created, let alone * scanned. This is used exclusively for meta-data operations. * **************************************/ SET_TDBB(tdbb); // Get the name in CSTRING format, ending on NULL or SPACE fb_assert(dsc_name->dsc_dtype == dtype_text); const Firebird::MetaName name(reinterpret_cast(dsc_name->dsc_address), dsc_name->dsc_length); Jrd::Attachment* const attachment = tdbb->getAttachment(); const SecurityClass* s_class = NULL; AutoCacheRequest request(tdbb, irq_f_security, IRQ_REQUESTS); FOR (REQUEST_HANDLE request) SFUN IN RDB$FUNCTIONS WITH SFUN.RDB$FUNCTION_NAME EQ name.c_str() AND SFUN.RDB$PACKAGE_NAME MISSING { if (!SFUN.RDB$SECURITY_CLASS.NULL) s_class = SCL_get_class(tdbb, SFUN.RDB$SECURITY_CLASS); } END_FOR SCL_check_access(tdbb, s_class, 0, id_function, name, mask, SCL_object_function, false, name); } void SCL_check_filter(thread_db* tdbb, const MetaName &name, SecurityClass::flags_t mask) { /************************************** * * S C L _ c h e c k _ f i l t e r * ************************************** * * Functional description * Given a filter name, check for a set of privileges. The * filter in question may or may not have been created, let alone * scanned. This is used exclusively for meta-data operations. * **************************************/ SET_TDBB(tdbb); Jrd::Attachment* const attachment = tdbb->getAttachment(); const SecurityClass* s_class = NULL; AutoCacheRequest request(tdbb, irq_f_security, IRQ_REQUESTS); FOR (REQUEST_HANDLE request) F IN RDB$FILTERS WITH F.RDB$FUNCTION_NAME EQ name.c_str() { if (!F.RDB$SECURITY_CLASS.NULL) s_class = SCL_get_class(tdbb, F.RDB$SECURITY_CLASS); } END_FOR SCL_check_access(tdbb, s_class, 0, id_filter, name, mask, SCL_object_filter, false, name); } void SCL_check_relation(thread_db* tdbb, const dsc* dsc_name, SecurityClass::flags_t mask, bool protectSys) { /************************************** * * S C L _ c h e c k _ r e l a t i o n * ************************************** * * Functional description * Given a relation name, check for a set of privileges. The * relation in question may or may not have been created, let alone * scanned. This is used exclusively for meta-data operations. * **************************************/ SET_TDBB(tdbb); // Get the name in CSTRING format, ending on NULL or SPACE fb_assert(dsc_name->dsc_dtype == dtype_text); const Firebird::MetaName name(reinterpret_cast(dsc_name->dsc_address), dsc_name->dsc_length); Jrd::Attachment* const attachment = tdbb->getAttachment(); const SecurityClass* s_class = NULL; AutoCacheRequest request(tdbb, irq_v_security_r, IRQ_REQUESTS); FOR(REQUEST_HANDLE request) REL IN RDB$RELATIONS WITH REL.RDB$RELATION_NAME EQ name.c_str() { if (protectSys && REL.RDB$SYSTEM_FLAG == 1 && !attachment->isRWGbak()) { // Someone is going to modify system table layout // Usually it's not good idea raiseError(mask, SCL_object_table, name, ""); } if (!REL.RDB$SECURITY_CLASS.NULL) s_class = SCL_get_class(tdbb, REL.RDB$SECURITY_CLASS); } END_FOR SCL_check_access(tdbb, s_class, 0, 0, NULL, mask, SCL_object_table, false, name); } void SCL_check_view(thread_db* tdbb, const dsc* dsc_name, SecurityClass::flags_t mask) { /************************************** * * S C L _ c h e c k _ v i e w * ************************************** * * Functional description * Given a view name, check for a set of privileges. The * relation in question may or may not have been created, let alone * scanned. This is used exclusively for meta-data operations. * **************************************/ SET_TDBB(tdbb); // Get the name in CSTRING format, ending on NULL or SPACE fb_assert(dsc_name->dsc_dtype == dtype_text); const Firebird::MetaName name(reinterpret_cast(dsc_name->dsc_address), dsc_name->dsc_length); Jrd::Attachment* const attachment = tdbb->getAttachment(); const SecurityClass* s_class = NULL; AutoCacheRequest request(tdbb, irq_v_security_v, IRQ_REQUESTS); FOR(REQUEST_HANDLE request) REL IN RDB$RELATIONS WITH REL.RDB$RELATION_NAME EQ name.c_str() { if (!REL.RDB$SECURITY_CLASS.NULL) { s_class = SCL_get_class(tdbb, REL.RDB$SECURITY_CLASS); } } END_FOR SCL_check_access(tdbb, s_class, 0, 0, NULL, mask, SCL_object_view, false, name); } void SCL_check_role(thread_db* tdbb, const Firebird::MetaName& name, SecurityClass::flags_t mask) { /************************************** * * S C L _ c h e c k _ r o l e * ************************************** * * Functional description * Given a role name, check for a set of privileges. * **************************************/ SET_TDBB(tdbb); Jrd::Attachment* const attachment = tdbb->getAttachment(); const SecurityClass* s_class = NULL; AutoCacheRequest request(tdbb, irq_v_security_o, IRQ_REQUESTS); FOR(REQUEST_HANDLE request) R IN RDB$ROLES WITH R.RDB$ROLE_NAME EQ name.c_str() { if (!R.RDB$SECURITY_CLASS.NULL) { s_class = SCL_get_class(tdbb, R.RDB$SECURITY_CLASS); } } END_FOR SCL_check_access(tdbb, s_class, 0, 0, NULL, mask, SCL_object_role, false, name); } SecurityClass* SCL_get_class(thread_db* tdbb, const TEXT* par_string) { /************************************** * * S C L _ g e t _ c l a s s * ************************************** * * Functional description * Look up security class first in memory, then in database. If * we don't find it, just return NULL. If we do, return a security * class block. * **************************************/ SET_TDBB(tdbb); // Name may be absent or terminated with NULL or blank. Clean up name. if (!par_string) return NULL; Firebird::MetaName string(par_string); if (string.isEmpty()) return NULL; Jrd::Attachment* const attachment = tdbb->getAttachment(); // Look for the class already known SecurityClassList* list = attachment->att_security_classes; if (list && list->locate(string)) return list->current(); // Class isn't known. So make up a new security class block. MemoryPool& pool = *attachment->att_pool; SecurityClass* const s_class = FB_NEW_POOL(pool) SecurityClass(pool, string); s_class->scl_flags = compute_access(tdbb, s_class, NULL, 0, NULL); if (s_class->scl_flags & SCL_exists) { if (!list) { attachment->att_security_classes = list = FB_NEW_POOL(pool) SecurityClassList(pool); } list->add(s_class); return s_class; } delete s_class; return NULL; } SecurityClass::flags_t SCL_get_mask(thread_db* tdbb, const TEXT* relation_name, const TEXT* field_name) { /************************************** * * S C L _ g e t _ m a s k * ************************************** * * Functional description * Get a protection mask for a named object. If field and * relation names are present, get access to field. If just * relation name, get access to relation. * **************************************/ SET_TDBB(tdbb); Jrd::Attachment* const attachment = tdbb->getAttachment(); SecurityClass::flags_t access = ~0; // If there's a relation, track it down jrd_rel* relation; if (relation_name && (relation = MET_lookup_relation(tdbb, relation_name))) { MET_scan_relation(tdbb, relation); const SecurityClass* s_class; if ( (s_class = SCL_get_class(tdbb, relation->rel_security_name.c_str())) ) { access &= s_class->scl_flags; } const jrd_fld* field; SSHORT id; if (field_name && (id = MET_lookup_field(tdbb, relation, field_name)) >= 0 && (field = MET_get_field(relation, id)) && (s_class = SCL_get_class(tdbb, field->fld_security_name.c_str()))) { access &= s_class->scl_flags; } } return access & (SCL_select | SCL_drop | SCL_control | SCL_insert | SCL_update | SCL_delete | SCL_alter | SCL_references | SCL_execute | SCL_usage); } bool SCL_role_granted(thread_db* tdbb, const UserId& usr, const TEXT* sql_role) { /************************************** * * S C L _ r o l e _ g r a n t e d * ************************************** * * Functional description * Check is sql_role granted to the user. * **************************************/ SET_TDBB(tdbb); Jrd::Attachment* const attachment = tdbb->getAttachment(); if (!strcmp(sql_role, NULL_ROLE)) { return true; } bool found = false; AutoCacheRequest request(tdbb, irq_verify_role_name, IRQ_REQUESTS); // CVC: The caller has hopefully uppercased the role or stripped quotes. Of course, // uppercase should only happen if the role wasn't enclosed in quotes. // Shortsighted developers named the field rdb$relation_name instead of rdb$object_name. // This request is not exactly the same than irq_get_role_mem, sorry, I can't reuse that. // If you think that an unknown role cannot be granted, think again: someone made sure // in DYN that SYSDBA can do almost anything, including invalid grants. FOR (REQUEST_HANDLE request) FIRST 1 RR IN RDB$ROLES CROSS UU IN RDB$USER_PRIVILEGES WITH RR.RDB$ROLE_NAME EQ sql_role AND RR.RDB$ROLE_NAME EQ UU.RDB$RELATION_NAME AND UU.RDB$OBJECT_TYPE EQ obj_sql_role AND (UU.RDB$USER EQ usr.getUserName().c_str() OR UU.RDB$USER EQ "PUBLIC") AND UU.RDB$USER_TYPE EQ obj_user AND UU.RDB$PRIVILEGE EQ "M" { if (!UU.RDB$USER.NULL) found = true; } END_FOR return found; } void UserId::findGrantedRoles(thread_db* tdbb) const { SET_TDBB(tdbb); Jrd::Attachment* const attachment = tdbb->getAttachment(); PreparedStatement::Builder sql; MetaName usr_get_role; string usr_get_priv; sql << "with recursive role_tree as ( " << " select rdb$relation_name as nm, 0 as ur from rdb$user_privileges " << " where rdb$privilege = 'M' and rdb$field_name = 'D' and rdb$user = " << usr_user_name << " and rdb$user_type = 8 " << " union all " << " select rdb$role_name as nm, 1 as ur from rdb$roles " << " where rdb$role_name = " << usr_sql_role_name << " union all " << " select p.rdb$relation_name as nm, t.ur from rdb$user_privileges p " << " join role_tree t on t.nm = p.rdb$user " << " where p.rdb$privilege = 'M' and (p.rdb$field_name = 'D' or t.ur = 1)) " << "select " << sql("r.rdb$role_name, ", usr_get_role) << sql("r.rdb$system_privileges ", usr_get_priv) << " from role_tree t join rdb$roles r on t.nm = r.rdb$role_name "; AutoPreparedStatement stmt(attachment->prepareStatement(tdbb, attachment->getSysTransaction(), sql)); AutoResultSet rs(stmt->executeQuery(tdbb, attachment->getSysTransaction())); usr_granted_roles.clear(); usr_privileges.clearAll(); while (rs->fetch(tdbb)) { if (!usr_granted_roles.exist(usr_get_role)) // SQL request can return duplicates { usr_granted_roles.add(usr_get_role); Privileges p; p.load(usr_get_priv.c_str()); usr_privileges |= p; } } usr_flags &= ~USR_newrole; } void UserId::setRoleTrusted() { if (!usr_trusted_role.hasData()) Arg::Gds(isc_miss_trusted_role).raise(); setSqlRole(usr_trusted_role); } void UserId::sclInit(thread_db* tdbb, bool create, const UserId& tempId) { /************************************** * * S C L _ i n i t * ************************************** * * Functional description * Check database access control list. * * Finally fills UserId information * (role, flags, etc.). * **************************************/ SET_TDBB(tdbb); Database* const dbb = tdbb->getDatabase(); Jrd::Attachment* const attachment = tdbb->getAttachment(); const TEXT* sql_role = tempId.getSqlRole().nullStr(); // CVC: We'll verify the role and wipe it out when it doesn't exist if (tempId.getUserName().hasData() && !create) { const TEXT* login_name = tempId.getUserName().c_str(); AutoCacheRequest request(tdbb, irq_get_role_name, IRQ_REQUESTS); FOR(REQUEST_HANDLE request) X IN RDB$ROLES WITH X.RDB$ROLE_NAME EQ login_name { ERR_post(Arg::Gds(isc_login_same_as_role_name) << Arg::Str(login_name)); } END_FOR } // CVC: If we aren't creating a db and sql_role was specified, // then verify it against rdb$roles and rdb$user_privileges if (!create && sql_role && *sql_role) { if (!SCL_role_granted(tdbb, tempId, sql_role)) sql_role = NULL; } if (!sql_role) sql_role = tempId.getTrustedRole().nullStr(); MetaName role_name(sql_role ? sql_role : NULL_ROLE); MemoryPool& pool = *attachment->att_pool; UserId* const user = FB_NEW_POOL(pool) UserId(pool, tempId); user->setSqlRole(role_name); attachment->att_user = user; if (!create) { AutoCacheRequest request(tdbb, irq_get_att_class, IRQ_REQUESTS); FOR(REQUEST_HANDLE request) X IN RDB$DATABASE { if (!X.RDB$SECURITY_CLASS.NULL) attachment->att_security_class = SCL_get_class(tdbb, X.RDB$SECURITY_CLASS); } END_FOR if (dbb->dbb_owner.isEmpty()) { AutoRequest request2; FOR(REQUEST_HANDLE request2) FIRST 1 REL IN RDB$RELATIONS WITH REL.RDB$RELATION_NAME EQ "RDB$DATABASE" { if (!REL.RDB$OWNER_NAME.NULL) dbb->dbb_owner = REL.RDB$OWNER_NAME; } END_FOR } } else { dbb->dbb_owner = user->getUserName(); user->usr_privileges.load(INI_owner_privileges().c_str()); user->usr_granted_roles.clear(); user->usr_granted_roles.add("RDB$ADMIN"); user->usr_flags &= ~USR_newrole; } } bool SCL_move_priv(SecurityClass::flags_t mask, Acl& acl) { /************************************** * * S C L _ m o v e _ p r i v * ************************************** * * Functional description * Given a mask of privileges, move privileges types to acl. * **************************************/ // Terminate identification criteria, and move privileges acl.push(ACL_end); acl.push(ACL_priv_list); bool rc = false; for (const P_NAMES* priv = p_names; priv->p_names_priv; priv++) { if (mask & priv->p_names_priv) { fb_assert(priv->p_names_acl <= MAX_UCHAR); acl.push(priv->p_names_acl); rc = true; } } acl.push(0); return rc; } SecurityClass* SCL_recompute_class(thread_db* tdbb, const TEXT* string) { /************************************** * * S C L _ r e c o m p u t e _ c l a s s * ************************************** * * Functional description * Something changed with a security class, recompute it. If we * can't find it, return NULL. * **************************************/ SET_TDBB(tdbb); SecurityClass* s_class = SCL_get_class(tdbb, string); if (!s_class) return NULL; s_class->scl_flags = compute_access(tdbb, s_class, NULL, 0, NULL); if (s_class->scl_flags & SCL_exists) return s_class; // Class no long exists - get rid of it! const Firebird::MetaName m_string(string); SecurityClassList* list = tdbb->getAttachment()->att_security_classes; if (list && list->locate(m_string)) { list->fastRemove(); delete s_class; } return NULL; } void SCL_release_all(SecurityClassList*& list) { /************************************** * * S C L _ r e l e a s e _ a l l * ************************************** * * Functional description * Release all security classes. * **************************************/ if (!list) return; if (list->getFirst()) { do { delete list->current(); } while (list->getNext()); } delete list; list = NULL; } SecurityClass::flags_t SCL_get_object_mask(const int object_type) { /************************************** * * S C L _ g e t _ o b j e c t _ m a s k * ************************************** * * Functional description * Get a protection mask for database object. * **************************************/ thread_db* tdbb = JRD_get_thread_data(); Database* dbb = tdbb->getDatabase(); const TEXT* object_name = get_object_name(object_type); if (!*object_name) return 0; const Jrd::SecurityClass* s_class = SCL_recompute_class(tdbb, object_name); if (s_class) return s_class->scl_flags; return -1 & ~SCL_corrupt; } ULONG SCL_get_number(const UCHAR* acl) { /************************************** * * g e t _ n u m b e r * ************************************** * * Functional description * Get value of acl numeric string. * **************************************/ ULONG n = 0; USHORT l = *acl++; if (l) { do { n = n * UIC_BASE + *acl++ - '0'; } while (--l); } return n; } static bool check_number(const UCHAR* acl, USHORT number) { /************************************** * * c h e c k _ n u m b e r * ************************************** * * Functional description * Check a string against and acl numeric string. If they don't match, * return true. * **************************************/ return (SCL_get_number(acl) != number); } static bool check_user_group(thread_db* tdbb, const UCHAR* acl, USHORT number) { /************************************** * * c h e c k _ u s e r _ g r o u p * ************************************** * * Functional description * * Check a string against an acl numeric string. * * logic: * * If the string contains user group name, * then * converts user group name to numeric user group id. * else * converts character user group id to numeric user group id. * * Check numeric user group id against an acl numeric string. * If they don't match, return true. * **************************************/ SET_TDBB(tdbb); ULONG n = 0; USHORT l = *acl++; if (l) { if (isdigit(*acl)) // this is a group id { do { n = n * UIC_BASE + *acl++ - '0'; } while (--l); } else // processing group name { Firebird::string user_group_name; do { const TEXT one_char = *acl++; user_group_name += LOWWER(one_char); } while (--l); // convert unix group name to unix group id n = os_utils::get_user_group_id(user_group_name.c_str()); } } return (n != number); } static bool check_string(const UCHAR* acl, const Firebird::MetaName& string) { /************************************** * * c h e c k _ s t r i n g * ************************************** * * Functional description * Check a string against and acl string. If they don't match, * return true. * **************************************/ fb_assert(acl); const FB_SIZE_T length = *acl++; const TEXT* const ptr = (TEXT*) acl; return (string.compare(ptr, length) != 0); } static void get_string(const UCHAR* acl, Firebird::MetaName& string) { /************************************** * * g e t _ s t r i n g * ************************************** * * Functional description * Get a string from acl string. * **************************************/ fb_assert(acl); const size_t length = *acl++; const TEXT* const ptr = (TEXT*) acl; string.assign(ptr, length); } static SecurityClass::flags_t compute_access(thread_db* tdbb, const SecurityClass* s_class, const jrd_rel* view, SLONG obj_type, const Firebird::MetaName& obj_name) { /************************************** * * c o m p u t e _ a c c e s s * ************************************** * * Functional description * Compute access for security class. If a relation block is * present, it is a view, and we should check for enhanced view * access permissions. Return a flag word of recognized privileges. * **************************************/ Acl acl; SET_TDBB(tdbb); Jrd::Attachment* const attachment = tdbb->getAttachment(); jrd_tra* sysTransaction = attachment->getSysTransaction(); SecurityClass::flags_t privileges = 0; SecurityClass::flags_t sysPriv = SCL_exists; const SecurityClass::flags_t selectAnyObject = SCL_select | SCL_references; const SecurityClass::flags_t accessAnyObject = SCL_insert | SCL_update | SCL_delete | SCL_execute | SCL_usage | selectAnyObject; const SecurityClass::flags_t anyDdl = SCL_create | SCL_alter | SCL_control | SCL_drop; if (attachment->locksmith(tdbb, ACCESS_ANY_OBJECT_IN_DATABASE)) sysPriv |= accessAnyObject; else if (attachment->locksmith(tdbb, SELECT_ANY_OBJECT_IN_DATABASE)) sysPriv |= selectAnyObject; if (attachment->locksmith(tdbb, MODIFY_ANY_OBJECT_IN_DATABASE)) sysPriv |= anyDdl; AutoCacheRequest request(tdbb, irq_l_security, IRQ_REQUESTS); FOR(REQUEST_HANDLE request) X IN RDB$SECURITY_CLASSES WITH X.RDB$SECURITY_CLASS EQ s_class->scl_name.c_str() { privileges |= sysPriv; blb* blob = blb::open(tdbb, sysTransaction, &X.RDB$ACL); UCHAR* buffer = acl.getBuffer(ACL_BLOB_BUFFER_SIZE); UCHAR* end = buffer; while (true) { end += blob->BLB_get_segment(tdbb, end, (USHORT) (acl.getCount() - (end - buffer)) ); if (blob->blb_flags & BLB_eof) break; // There was not enough space, realloc point acl to the correct location if (blob->getFragmentSize()) { const ptrdiff_t old_offset = end - buffer; buffer = acl.getBuffer(acl.getCount() + ACL_BLOB_BUFFER_SIZE); end = buffer + old_offset; } } blob->BLB_close(tdbb); blob = NULL; acl.shrink(end - buffer); if (acl.getCount() > 0) privileges |= walk_acl(tdbb, acl, view, obj_type, obj_name); } END_FOR return privileges; } static SecurityClass::flags_t walk_acl(thread_db* tdbb, const Acl& acl, const jrd_rel* view, SLONG obj_type, const Firebird::MetaName& obj_name) { /************************************** * * w a l k _ a c l * ************************************** * * Functional description * Walk an access control list looking for a hit. If a hit * is found, return privileges. * **************************************/ SET_TDBB(tdbb); Jrd::Attachment* const attachment = tdbb->getAttachment(); // Munch ACL. If we find a hit, eat up privileges. UserId user = *attachment->att_user; if (view && (view->rel_flags & REL_sql_relation)) { // Use the owner of the view to perform the sql security // checks with: (1) The view user must have sufficient privileges // to the view, and (2a) the view owner must have sufficient // privileges to the base table or (2b) the view must have // sufficient privileges on the base table. user.setUserName(view->rel_owner_name.c_str()); } SecurityClass::flags_t privilege = 0; const UCHAR* a = acl.begin(); if (*a++ != ACL_version) { BUGCHECK(160); // msg 160 wrong ACL version } const TEXT* p; bool hit = false; UCHAR c; while ( (c = *a++) ) { switch (c) { case ACL_id_list: hit = true; while ( (c = *a++) ) { switch (c) { case id_person: if (!(p = user.getUserName().nullStr()) || check_string(a, p)) hit = false; break; case id_project: if (!(p = user.usr_project_name.nullStr()) || check_string(a, p)) hit = false; break; case id_organization: if (!(p = user.usr_org_name.nullStr()) || check_string(a, p)) hit = false; break; case id_group: if (check_user_group(tdbb, a, user.usr_group_id)) hit = false; break; case id_sql_role: { Firebird::MetaName role_name; get_string(a, role_name); if (!user.roleInUse(tdbb, role_name)) hit = false; break; } case id_view: if (!view || check_string(a, view->rel_name)) hit = false; break; case id_package: case id_procedure: case id_trigger: case id_function: if (c != obj_type || check_string(a, obj_name)) hit = false; break; case id_views: // Disable this catch-all that messes up the view security. // Note that this id_views is not generated anymore, this code // is only here for compatibility. id_views was only // generated for SQL. hit = false; if (!view) hit = false; break; case id_user: if (check_number(a, user.usr_user_id)) hit = false; break; case id_privilege: if (!user.locksmith(tdbb, SCL_get_number(a))) hit = false; break; case id_node: break; default: return SCL_corrupt; } a += *a + 1; } break; case ACL_priv_list: if (hit) { while ( (c = *a++) ) { switch (c) { case priv_control: privilege |= SCL_control; break; case priv_select: // Note that SELECT access must imply REFERENCES // access for upward compatibility of existing // security classes privilege |= SCL_select | SCL_references; break; case priv_insert: privilege |= SCL_insert; break; case priv_delete: privilege |= SCL_delete; break; case priv_references: privilege |= SCL_references; break; case priv_update: privilege |= SCL_update; break; case priv_drop: privilege |= SCL_drop; break; case priv_alter: privilege |= SCL_alter; break; case priv_execute: privilege |= SCL_execute; break; case priv_usage: privilege |= SCL_usage; break; case priv_write: // unused, but supported for backward compatibility privilege |= SCL_insert | SCL_update | SCL_delete; break; case priv_grant: // unused break; case priv_create: privilege |= SCL_create; break; default: return SCL_corrupt; } } // For a relation the first hit does not give the privilege. // Because, there could be some permissions for the table // (for user1) and some permissions for a column on that // table for public/user2, causing two hits. // Hence, we do not return at this point. // -- Madhukar Thakur (May 1, 1995) } else while (*a++); break; default: return SCL_corrupt; } } fb_assert(a == acl.end()); return privilege; } void UserId::populateDpb(Firebird::ClumpletWriter& dpb) { if (usr_auth_block.hasData()) dpb.insertBytes(isc_dpb_auth_block, usr_auth_block.begin(), usr_auth_block.getCount()); else dpb.insertString(isc_dpb_user_name, usr_user_name); } void UserId::makeRoleName(Firebird::MetaName& role, const int dialect) { if (role.isEmpty()) return; switch (dialect) { case SQL_DIALECT_V5: // Invoke utility twice: first to strip quotes, next to uppercase if needed // For unquoted string nothing bad happens fb_utils::dpbItemUpper(role); fb_utils::dpbItemUpper(role); break; case SQL_DIALECT_V6_TRANSITION: case SQL_DIALECT_V6: fb_utils::dpbItemUpper(role); break; default: break; } } // get privilege bit by name USHORT SCL_convert_privilege(thread_db* tdbb, jrd_tra* transaction, const Firebird::string& priv) { static GlobalPtr privCacheMutex; static bool cacheFlag = false; typedef NonPooled CachedPriv; static GlobalPtr > privCache; if (!cacheFlag) { MutexLockGuard g(privCacheMutex, FB_FUNCTION); if (!cacheFlag) { privCache->clear(); AutoCacheRequest request(tdbb, irq_get_priv_bit, IRQ_REQUESTS); FOR(REQUEST_HANDLE request TRANSACTION_HANDLE transaction) T IN RDB$TYPES WITH T.RDB$FIELD_NAME EQ 'RDB$SYSTEM_PRIVILEGES' { privCache->put(T.RDB$TYPE_NAME, T.RDB$TYPE); } END_FOR cacheFlag = true; } } USHORT rc; if (!privCache->get(priv, rc)) (Arg::Gds(isc_wrong_prvlg) << priv).raise(); return rc; }