From 75293c9c70f3f04c86721039bedfd6e0bf0786a8 Mon Sep 17 00:00:00 2001 From: Konrad Lipinski Date: Fri, 14 Sep 2018 14:14:17 +0200 Subject: [PATCH] Replace smack rule storage with straight-from-db rule loader Details: * remove %{TZ_SYS_VAR}/security-manager/rules{,-merged} directories * add security-manager-rules-loader that ** performs database migration/recovery ** writes smack rules from a coherent database directly to load2 * add generate-rule-code generator that translates rule templates (*.smack files) into c++ code for use in the loader * remove security-manager-init-db binary and replace its invocation with sh$ security-manager-rules-loader no-load * replace dd invocation with security-manager-rules-loader in the rule loader service * add explicit dependency to ensure the loader runs before the manager * refactor manager code ** remove the majority of database migration/recovery code on grounds of loader having run beforehand ** replace defensive remnants of said code with an emergency invocation sh$ security-manager-rules-loader fallback-only to apply fallback on database schmea errors ** remove rule file maintenance (not needed anymore) TODO: * *.smack template files are still used by the manager at runtime, removing them is optional and would require a substantial refactor best placed in a separate commit Pros: * optimize flash usage (rule files were prone to quadratic explosion) * solve database-rulefiles coherence problem * make the rule loader performance more scalable and typically better * simplify and speed up the manager a bit by dropping rule file code Change-Id: I7d79d5ec7e66c9dfe6563dbb3f76bf6ab6669589 --- CMakeLists.txt | 3 +- packaging/security-manager.spec | 17 +- policy/CMakeLists.txt | 10 + policy/generate-rule-code | 152 + policy/updates/update-policy-to-v7.sh | 11 + src/client/client-security-manager.cpp | 10 +- src/common/CMakeLists.txt | 2 - src/common/config.cpp | 3 - src/common/filesystem.cpp | 5 - src/common/include/config.h | 17 +- src/common/include/filesystem.h | 1 - src/common/include/privilege_db.h | 19 +- src/common/include/service_impl.h | 3 +- src/common/include/smack-rules.h | 69 +- src/common/include/utils.h | 36 +- src/common/privilege_db.cpp | 249 +- src/common/service_impl.cpp | 26 +- src/common/smack-rules.cpp | 230 +- src/dpl/db/include/dpl/db/sql_connection.h | 10 +- src/server/CMakeLists.txt | 32 +- src/server/cleanup/security-manager-cleanup.cpp | 7 +- src/server/init-db/security-manager-init-db.cpp | 38 - src/server/main/server-main.cpp | 2 +- .../rules-loader/security-manager-rules-loader.cpp | 1077 + src/server/service/base-service.cpp | 3 +- src/server/service/include/base-service.h | 3 +- src/server/service/include/service.h | 3 +- src/server/service/service.cpp | 2 +- systemd/security-manager-rules-loader.service.in | 6 +- test/data/.security-manager-test-rules.db | Bin 0 -> 143360 bytes test/data/.security-manager-test-rules.txt | 71807 +++++++++++++++++++ test/privilege_db_fixture.cpp | 78 +- test/privilege_db_fixture.h | 15 +- test/test_privilege_db_migration.cpp | 190 +- test/test_privilege_db_privilege.cpp | 3 +- test/test_privilege_db_transactions.cpp | 21 +- test/test_smack-rules.cpp | 115 - 37 files changed, 73431 insertions(+), 844 deletions(-) create mode 100755 policy/generate-rule-code create mode 100755 policy/updates/update-policy-to-v7.sh delete mode 100644 src/server/init-db/security-manager-init-db.cpp create mode 100644 src/server/rules-loader/security-manager-rules-loader.cpp create mode 100644 test/data/.security-manager-test-rules.db create mode 100644 test/data/.security-manager-test-rules.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index 4eccedf..4e794c6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -103,7 +103,8 @@ SET(TARGET_COMMON "security-manager-commons") SET(TARGET_CMD "security-manager-cmd") SET(TARGET_CLEANUP "security-manager-cleanup") SET(TARGET_NSS "security-manager-nss") -SET(TARGET_INITDB "security-manager-init-db") +SET(TARGET_LOADER "security-manager-rules-loader") +SET(TARGET_TEST_LOADER "security-manager-test-rules-loader") ADD_SUBDIRECTORY(src) ADD_SUBDIRECTORY(pc) diff --git a/packaging/security-manager.spec b/packaging/security-manager.spec index 9ea1646..dea2b5f 100644 --- a/packaging/security-manager.spec +++ b/packaging/security-manager.spec @@ -137,8 +137,6 @@ touch %{buildroot}/%{TZ_SYS_DB}/.security-manager.db touch %{buildroot}/%{TZ_SYS_DB}/.security-manager.db-journal install -m 0755 -d %{buildroot}%{TZ_SYS_VAR}/security-manager -install -m 0700 -d %{buildroot}%{TZ_SYS_VAR}/security-manager/rules-merged/ -install -m 0600 /dev/null %{buildroot}%{TZ_SYS_VAR}/security-manager/rules-merged/rules.merged install -m 0444 /dev/null %{buildroot}%{TZ_SYS_VAR}/security-manager/apps-labels install -m 0444 /dev/null %{buildroot}%{TZ_SYS_VAR}/security-manager/policy-version @@ -151,6 +149,7 @@ echo -n > %{buildroot}/%{db_test_dir}/.security-manager-test-empty.db echo -n > %{buildroot}/%{db_test_dir}/.security-manager-test-empty.db-journal sqlite3 %{buildroot}/%{db_test_dir}/.security-manager-test-wrong-schema.db < db/db.sql sqlite3 %{buildroot}/%{db_test_dir}/.security-manager-test-wrong-schema.db "drop view client_license_view" +cp -a test/data/.security-manager-test-rules.{db,txt} %{buildroot}/%{db_test_dir} cp -a %{SOURCE1} %{SOURCE3} %{SOURCE4} %{SOURCE5} %{buildroot}%{_datadir}/ @@ -162,14 +161,14 @@ rm -rf %{buildroot} systemctl daemon-reload if [ $1 = 1 ]; then # installation - security-manager-init-db + security-manager-rules-loader no-load systemctl start security-manager.service security-manager.socket fi if [ $1 = 2 ]; then # update systemctl stop security-manager.service security-manager.socket - security-manager-init-db + security-manager-rules-loader no-load systemctl start security-manager.service security-manager.socket fi @@ -248,6 +247,8 @@ chsmack -a System %{db_test_dir}/.security-manager-test-empty.db chsmack -a System %{db_test_dir}/.security-manager-test-empty.db-journal chsmack -a System %{db_test_dir}/.security-manager-test-wrong-schema.db chsmack -a System %{db_test_dir}/.security-manager-test-wrong-schema.db-journal +chsmack -a System %{db_test_dir}/.security-manager-test-rules.db +chsmack -a System %{db_test_dir}/.security-manager-test-rules.txt %files -n security-manager %manifest %{_datadir}/security-manager.manifest @@ -256,14 +257,11 @@ chsmack -a System %{db_test_dir}/.security-manager-test-wrong-schema.db-journal %attr(755,root,root) %{_bindir}/security-manager %attr(755,root,root) %{_bindir}/security-manager-cmd %attr(755,root,root) %{_bindir}/security-manager-cleanup -%attr(755,root,root) %{_bindir}/security-manager-init-db +%attr(755,root,root) %{_bindir}/security-manager-rules-loader %attr(755,root,root) %{_sysconfdir}/gumd/useradd.d/50_security-manager-add.post %attr(755,root,root) %{_sysconfdir}/gumd/userdel.d/50_security-manager-remove.pre %config(noreplace) %attr(444,root,root) %{TZ_SYS_VAR}/security-manager/apps-labels %dir %attr(711,root,root) %{TZ_SYS_VAR}/security-manager/ -%dir %attr(700,root,root) %{TZ_SYS_VAR}/security-manager/rules -%dir %attr(700,root,root) %{TZ_SYS_VAR}/security-manager/rules-merged -%config(noreplace) %attr(600,root,root) %{TZ_SYS_VAR}/security-manager/rules-merged/rules.merged %{_libdir}/libsecurity-manager-commons.so.* %attr(-,root,root) %{_unitdir}/security-manager.* @@ -310,6 +308,7 @@ chsmack -a System %{db_test_dir}/.security-manager-test-wrong-schema.db-journal %manifest %{_datadir}/security-manager-tests.manifest %attr(755,root,root) %{_bindir}/security-manager-unit-tests %attr(755,root,root) %{_bindir}/security-manager-performance-tests +%attr(755,root,root) %{_bindir}/security-manager-test-rules-loader %attr(0600,root,root) %{db_test_dir}/.security-manager-test.db %attr(0600,root,root) %{db_test_dir}/.security-manager-test.db-journal %attr(0600,root,root) %{db_test_dir}/.security-manager-test-v0.db @@ -320,6 +319,8 @@ chsmack -a System %{db_test_dir}/.security-manager-test-wrong-schema.db-journal %attr(0600,root,root) %{db_test_dir}/.security-manager-test-empty.db-journal %attr(0600,root,root) %{db_test_dir}/.security-manager-test-wrong-schema.db %attr(0600,root,root) %{db_test_dir}/.security-manager-test-wrong-schema.db-journal +%attr(0600,root,root) %{db_test_dir}/.security-manager-test-rules.db +%attr(0600,root,root) %{db_test_dir}/.security-manager-test-rules.txt %files -n security-license-manager %{_libdir}/cynara/plugin/client/liblicense-manager-plugin-client.so diff --git a/policy/CMakeLists.txt b/policy/CMakeLists.txt index a4ed18d..381245d 100644 --- a/policy/CMakeLists.txt +++ b/policy/CMakeLists.txt @@ -17,3 +17,13 @@ INSTALL(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/security-manager-policy-reload DEST # FOTA updater INSTALL(FILES 241.security-manager.policy-update.sh DESTINATION ${FOTA_DIR}) + +SET(GEN_FILE ${GEN_PATH}/generated-rule-templates.h) +SET(GENERATOR "./generate-rule-code") + +ADD_CUSTOM_COMMAND(OUTPUT ${GEN_FILE} + COMMAND mkdir -p "${GEN_PATH}" + COMMAND ${GENERATOR} *.smack > ${GEN_FILE} + DEPENDS ${GENERATOR} "*.smack" +) +ADD_CUSTOM_TARGET(generate_rule_template_code DEPENDS ${GEN_FILE}) diff --git a/policy/generate-rule-code b/policy/generate-rule-code new file mode 100755 index 0000000..50c270e --- /dev/null +++ b/policy/generate-rule-code @@ -0,0 +1,152 @@ +#!/usr/bin/env perl +# Copyright (c) 2018 Samsung Electronics Co., Ltd All Rights Reserved +# +# Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License + +# the generated file is included verbatim into security-manager-rules-writer.cpp +# therein lie more comments + +use strict; use warnings; use 5.10.1; +sub member { my $m = shift; grep { $_ eq $m } @_ } + +# rule groups (roughly correspond to variables used and functions generated) +my @rulesAuthor; +my @rulesPkgLabelAuthor; +my @rulesPkgLabel; +my @rulesPathRW; +my @rulesPath; +my @rulesSharedRO; + +# one rule per line, either on stdin or in file arguments +my @lines; +while (<>) { chomp; push @lines, $_; } + +# process lines in sorted order so that similar rules are clustered together +for (sort @lines) { + # make sure the rule is canonical - it's important for the writer (which makes assumptions about rule width and such) + die "heading whitespace ($_)" if /^\s/; + die "trailing whitespace ($_)" if /\s$/; + die "non-space whitespace ($_)" if grep {/[^ ]/} /(\s)/g; + die "two or more consecutive spaces ($_)" if / /; + + # the rule is split into an odd number of segments (3 or 5): verbatim variable verbatim (variable verbatim)? + # ex. "System ~PROCESS~ rwxat" -> ("System ", "PROCESS", " rwxat") + # even elements are variables, odd ones verbatim strings + my @segments = split /~/; + + # segment-based validation + die "rule without variables ($_)" if @segments < 2; + die "too many variables ($_)" if @segments > 5; + die "even number of segments ($_)" if !(@segments % 2); + die "ending segment empty ($_)" if !length $segments[-1]; + my %varCount; + for (@segments[grep {$_%2} 0..$#segments]) { + die "unknown var ($_)" if !member $_, qw(PATH_TRUSTED PROCESS PATH_RO PATH_RW PATH_SHARED_RO); + ++$varCount{$_}; + } + die "first segment ending with non-space character ($_)" if length $segments[0] && $segments[0] !~ / $/; + die "last segment starting with non-space character ($_)" if length $segments[-1] && $segments[-1] !~ /^ /; + die "variables not surrounded with whitespace ($_)" if grep {/^$/ || /^[^ ]/ || /[^ ]$/} @segments[grep {!($_%2)} 2..$#segments-2]; + + # make sure the permission string is a valid constant + my ($prePerm, $permChars) = $segments[-1] =~ /(.* )([^ ]*)$/; + my %perm; + for (split //, $permChars) { + die "unknown permission char ($_) in ($segments[-1])" if !member $_, split //, "rwxatlb"; + die "duplicate permission char ($_) in ($segments[-1])" if exists $perm{$_}; + $perm{$_} = 1; + } + + # sort permission string characters to improve constant sharing in the writer + $segments[-1] = $prePerm . join '', grep {exists $perm{$_}} split //, "rwxatlb"; + + # partition rules into rough groups + if (1 == keys %varCount) { + # single variable rules + if (exists $varCount{PATH_TRUSTED}) { + push @rulesAuthor, [@segments]; + } elsif (exists $varCount{PATH_SHARED_RO}) { + push @rulesSharedRO, [@segments]; + } elsif (exists $varCount{PATH_RO}) { + push @rulesPath, [@segments]; + } elsif (exists $varCount{PATH_RW}) { + push @rulesPathRW, [@segments]; + } else { # PROCESS + push @rulesPkgLabel, [@segments]; + } + } else { + # multi variable rules + die "multi-variable rule ($_) does not contain ~PROCESS~" if !exists $varCount{PROCESS}; + if (exists $varCount{PATH_TRUSTED}) { + push @rulesPkgLabelAuthor, [@segments]; + } elsif (exists $varCount{PATH_RO} || exists $varCount{PATH_RW}) { + push @rulesPkgLabel, [@segments]; + } else { + die "unsupported multi-variable rule ($_)"; + } + } +} + +# for non-hybrid packages, ~PATH_RW~ == ~PROCESS~ +# this may lead to rule duplication between @rulesPathRW and @rulesPkgLabel +# +# in order to avoid this, @rulesPathRW is split into two groups: +# rules having an isomorphic ~PROCESS~ rule end up in @rulesPathRWHybridOnly (not to be applied to non-hybrid packages) +# other rules end up in @rulesPath (applied to all packages) +my @pureProcessRulesAsPathRWRule = map {3 != @$_ ? () : ($_->[0].'~PATH_RW~'.$_->[2])} @rulesPkgLabel; +my @rulesPathRWHybridOnly; +push @rulesPath, grep { + my $asRule = $_->[0].'~PATH_RW~'.$_->[2]; + my $hasRedudnantProcessRule = member $asRule, @pureProcessRulesAsPathRWRule; + push @rulesPathRWHybridOnly, $_ if $hasRedudnantProcessRule; + !$hasRedudnantProcessRule; +} @rulesPathRW; + +# generate a function that writes a sequence of rules +sub rules { + my $functionName = shift; + my $mustBeNonempty = shift; + die "($functionName()) has no rules - remove the call and propagate to make sure the loader stays fast" if $mustBeNonempty && !@_; + say "inl void $functionName() {"; + for (@_) { + my @segments = @$_; + print " rule("; + for (0..$#segments/2-1) { + my $verbatim = $segments[2*$_]; + my $wildcard = $segments[2*$_ + 1]; + my $var = 'pkgL'; + if ($wildcard eq 'PROCESS') { + $segments[2*$_+2] =~ s/^ //; + $var = 'pl'; + } elsif ($wildcard eq 'PATH_TRUSTED') { + $verbatim .= 'User::Author::'; + $var = 'pathTrusted'; + } elsif ($wildcard eq 'PATH_RO') { + $segments[2*$_+2] = '::RO'.$segments[2*$_+2]; + } elsif ($wildcard eq 'PATH_SHARED_RO') { + $segments[2*$_+2] = '::SharedRO'.$segments[2*$_+2]; + } + print "\"$verbatim\", " if length $verbatim; + print "$var, "; + } + say "\"$segments[-1]\");"; + } + say "}"; +} + +rules 'rulesAuthor', 0, @rulesAuthor; +rules 'rulesPkgLabelAuthor', 1, @rulesPkgLabelAuthor; +rules 'rulesPkgLabel', 0, @rulesPkgLabel; +rules 'rulesPathRWHybridOnly', 0, @rulesPathRWHybridOnly; +rules 'rulesPath', 0, @rulesPath; +rules 'rulesSharedRO', 0, @rulesSharedRO; diff --git a/policy/updates/update-policy-to-v7.sh b/policy/updates/update-policy-to-v7.sh new file mode 100755 index 0000000..4f0b450 --- /dev/null +++ b/policy/updates/update-policy-to-v7.sh @@ -0,0 +1,11 @@ +#!/bin/sh -e + +export PATH=/sbin:/usr/sbin:/bin:/usr/bin + +. /etc/tizen-platform.conf + +systemctl stop security-manager.service security-manager.socket + +rm -rf "$TZ_SYS_VAR"/security-manager/rules{,-merged} + +systemctl start security-manager.service security-manager.socket diff --git a/src/client/client-security-manager.cpp b/src/client/client-security-manager.cpp index 49a742f..876ea84 100644 --- a/src/client/client-security-manager.cpp +++ b/src/client/client-security-manager.cpp @@ -298,7 +298,7 @@ int security_manager_app_install(const app_inst_req *p_req) if (offlineMode.isOffline()) { Credentials creds = offlineMode.getCredentials(); app_inst_req req(*p_req); - return SecurityManager::ServiceImpl().appInstall(creds, req); + return ServiceImpl(ServiceImpl::Offline::yes).appInstall(creds, req); } else { return ClientRequest(SecurityModuleCall::APP_INSTALL).send(p_req).getStatus(); } @@ -324,7 +324,7 @@ int security_manager_app_update(const app_inst_req *p_req) if (offlineMode.isOffline()) { Credentials creds = offlineMode.getCredentials(); app_inst_req req(*p_req); - return SecurityManager::ServiceImpl().appUpdate(creds, req); + return ServiceImpl(ServiceImpl::Offline::yes).appUpdate(creds, req); } else { return ClientRequest(SecurityModuleCall::APP_UPDATE).send(p_req).getStatus(); } @@ -348,7 +348,7 @@ int security_manager_app_uninstall(const app_inst_req *p_req) if (offlineMode.isOffline()) { Credentials creds = offlineMode.getCredentials(); app_inst_req req(*p_req); - return SecurityManager::ServiceImpl().appUninstall(creds, req); + return ServiceImpl(ServiceImpl::Offline::yes).appUninstall(creds, req); } else { return ClientRequest(SecurityModuleCall::APP_UNINSTALL).send(p_req).getStatus(); } @@ -991,7 +991,7 @@ int security_manager_user_add(const user_req *p_req) ClientOffline offlineMode; if (offlineMode.isOffline()) { Credentials creds = offlineMode.getCredentials(); - retval = SecurityManager::ServiceImpl().userAdd(creds, p_req->uid, p_req->utype); + retval = ServiceImpl(ServiceImpl::Offline::yes).userAdd(creds, p_req->uid, p_req->utype); } else { //server is working retval = ClientRequest(SecurityModuleCall::USER_ADD).send( @@ -1700,7 +1700,7 @@ int security_manager_paths_register(const path_req *p_req) ClientOffline offlineMode; if (offlineMode.isOffline()) { Credentials creds = offlineMode.getCredentials(); - retval = SecurityManager::ServiceImpl().pathsRegister(creds, *p_req); + retval = ServiceImpl(ServiceImpl::Offline::yes).pathsRegister(creds, *p_req); } else { return ClientRequest(SecurityModuleCall::PATHS_REGISTER).send( p_req->pkgName, diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index e8b72cd..cfd9047 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -110,6 +110,4 @@ TARGET_LINK_LIBRARIES(${TARGET_COMMON} ) INSTALL(TARGETS ${TARGET_COMMON} DESTINATION ${LIB_INSTALL_DIR}) -INSTALL(DIRECTORY DESTINATION ${LOCAL_STATE_DIR}/security-manager/rules) -INSTALL(DIRECTORY DESTINATION ${LOCAL_STATE_DIR}/security-manager/rules-merged) INSTALL(DIRECTORY DESTINATION ${DATA_ROOT_DIR}/${PROJECT_NAME}/dummy) diff --git a/src/common/config.cpp b/src/common/config.cpp index 9c036f5..2189f56 100644 --- a/src/common/config.cpp +++ b/src/common/config.cpp @@ -63,9 +63,6 @@ std::string getPrivilegeDbFallbackPath() { ".security-manager.db"); } -std::string dbRecoveryFlagFileName(const std::string &dbPath) { - return dbPath + "-recovered"; -} }; } /* namespace SecurityManager */ diff --git a/src/common/filesystem.cpp b/src/common/filesystem.cpp index 33be2e0..a196400 100644 --- a/src/common/filesystem.cpp +++ b/src/common/filesystem.cpp @@ -62,11 +62,6 @@ FileNameVector getSubDirectoriesFromDirectory(const std::string &path, bool noDo return getDirectoryContents(path, S_IFDIR, noDot); } -FileNameVector getFilesFromDirectory(const std::string &path) -{ - return getDirectoryContents(path, S_IFREG); -} - FileNameVector getLinksFromDirectory(const std::string &path) { return getDirectoryContents(path, S_IFLNK); diff --git a/src/common/include/config.h b/src/common/include/config.h index d7a9afe..ec59824 100644 --- a/src/common/include/config.h +++ b/src/common/include/config.h @@ -63,9 +63,20 @@ extern const bool IS_ASKUSER_ENABLED; std::string getPrivilegeDbPath(); std::string getPrivilegeDbFallbackPath(); -/* a file flag for other system components that there was - a Db failure and DB was recovered to initial (preloaded-apps only) state */ -std::string dbRecoveryFlagFileName(const std::string &dbPath); }; } /* namespace SecurityManager */ + +// If database initialization fails, restoration to a fallback snapshot is +// attempted. If the restoration succeeds, a file flag is created to notify +// other system components. +// For database placed in "$f" the filename is ("$f" DB_RECOVERED_SUFFIX). +#define DB_RECOVERED_SUFFIX "-recovered" +#define DB_JOURNAL_SUFFIX "-journal" + +#define DB_OK_MARKER "/tmp/.security-manager.db.ok" +#define RULES_LOADER_CMD "/usr/bin/security-manager-rules-loader" +#define TEST_DB_OK_MARKER "/tmp/.security-manager-test.db.ok" +#define TEST_RULES_LOADER_CMD "/usr/bin/security-manager-test-rules-loader" +#define TEST_DB_PATH "/tmp/.security-manager-test.db" +#define TEST_PRIVILEGE_FALLBACK_DB_PATH "/tmp/.security-manager-test.fallback.db" diff --git a/src/common/include/filesystem.h b/src/common/include/filesystem.h index d1ba711..049ed90 100644 --- a/src/common/include/filesystem.h +++ b/src/common/include/filesystem.h @@ -35,7 +35,6 @@ typedef std::vector FileNameVector; std::string getTextFileContents(const std::string &path); FileNameVector getDirectoryContents(const std::string &path, const mode_t &mode, bool noDot = false); -FileNameVector getFilesFromDirectory(const std::string &path); FileNameVector getLinksFromDirectory(const std::string &path); FileNameVector getSubDirectoriesFromDirectory(const std::string &path, bool noDot = false); std::string getDirectoryName(const std::string &path); diff --git a/src/common/include/privilege_db.h b/src/common/include/privilege_db.h index 1872e40..17cb804 100644 --- a/src/common/include/privilege_db.h +++ b/src/common/include/privilege_db.h @@ -41,16 +41,13 @@ #include #include +#include #include "security-manager-types.h" #include "pkg-info.h" namespace SecurityManager { -std::string genJournalPath(const std::string &dbPath); -enum class InitDbDidFallback : uint8_t { no, yes }; -InitDbDidFallback initDb(); - enum class StmtType : uint8_t { EAddApplication, ERemoveApplication, @@ -92,7 +89,7 @@ enum class StmtType : uint8_t { EGetLicenseForClientPrivilegeAndPkg, EIsUserPkgInstalled, }; -enum : uint8_t { StmtTypeCount = static_cast(StmtType::EIsUserPkgInstalled) + 1 }; +enum : uint8_t { StmtTypeCount = underlying(StmtType::EIsUserPkgInstalled) + 1 }; // privilege, app_defined_privilege_type, license typedef std::tuple AppDefinedPrivilege; @@ -153,6 +150,8 @@ public: DECLARE_EXCEPTION_TYPE(Base, ConstraintError) }; + enum class Offline : bool { no, yes }; + enum class Db : bool { standard, test }; /** * Constructor * @exception PrivilegeDb::Exception::IOError on problems with database access @@ -160,15 +159,7 @@ public: * configuration * */ - PrivilegeDb(void); - /** - * Constructor - * @exception PrivilegeDb::Exception::IOError on problems with database access - * - */ - explicit PrivilegeDb(const std::string &path, const std::string &roFallbackPath); - - static PrivilegeDb &getInstance(); + explicit PrivilegeDb(Offline offline, Db db = Db::standard); /** * Begin transaction diff --git a/src/common/include/service_impl.h b/src/common/include/service_impl.h index a0a031e..cabeef0 100644 --- a/src/common/include/service_impl.h +++ b/src/common/include/service_impl.h @@ -65,7 +65,8 @@ struct UninstallHelper { class ServiceImpl { public: - ServiceImpl(); + using Offline = PrivilegeDb::Offline; + explicit ServiceImpl(Offline offline); virtual ~ServiceImpl(); /** diff --git a/src/common/include/smack-rules.h b/src/common/include/smack-rules.h index 736df18..46c3a3f 100644 --- a/src/common/include/smack-rules.h +++ b/src/common/include/smack-rules.h @@ -50,7 +50,6 @@ public: const std::string &permissions); void addModify(const std::string &subject, const std::string &object, const std::string &allowPermissions, const std::string &denyPermissions); - void loadFromFile(const std::string &path); void addFromTemplate( const RuleVector &templateRules, @@ -66,7 +65,6 @@ public: void apply() const; void clear() const; - void saveToFile(const std::string &path) const; /** * Create cross dependencies for all applications in a package @@ -84,6 +82,9 @@ public: * * @param[in] pkgsLabels vector of process labels per each existing package * @param[in] allPkgs vector of PkgInfo objects of all existing packages + * + * TODO rule files are no longer a thing, this function is overkill + * should be replaced with an update (akin to a reverse revokeSharedRORules) */ static void generateSharedRORules(PkgsLabels &pkgsLabels, std::vector &allPkgs); @@ -101,15 +102,14 @@ public: * Install package-specific smack rules plus add rules for specified external apps. * * Function creates smack rules using predefined template. Rules are applied - * to the kernel and saved on persistent storage so they are loaded on system boot. + * to the kernel. * - * @param[in] appName - application identifier + * @param[in] appProcessLabel - process label of the application * @param[in] pkgName - package identifier * @param[in] authorId - author id of application * @param[in] pkgLabels - a list of process labels of all applications inside this package */ static void installApplicationRules( - const std::string &appName, const std::string &appProcessLabel, const std::string &pkgName, const int authorId, @@ -118,23 +118,23 @@ public: /** * Uninstall package-specific smack rules. * - * Function loads package-specific smack rules, revokes them from the kernel - * and removes them from the persistent storage. + * Function loads package-specific smack rules, revokes them from the kernel. * * @param[in] pkgName - package identifier + * @param[in] pkgLabels - a list of process labels of all applications inside this package */ - static void uninstallPackageRules(const std::string &pkgName); + static void uninstallPackageRules(const std::string &pkgName, const Labels &pkgLabels); /** * Uninstall application-specific smack rules. * - * Function removes application specific rules from the kernel, and - * removes them for persistent storage. + * Function removes application specific rules from the kernel. * - * @param[in] appName - application identifier - * @param[in] appLabel - application process label + * @param[in] appProcessLabel - application process label + * @param[in] pkgName - package identifier that the application is in + * @param[in] authorId - identification (datbase key) of the author */ - static void uninstallApplicationRules(const std::string &appName, const std::string &appLabel); + static void uninstallApplicationRules(const std::string &appProcessLabel, const std::string &pkgName, const int authorId); /** * Update package specific rules @@ -203,53 +203,14 @@ public: bool isPathSharedNoMore, bool isTargetSharingNoMore); - /** - * This function will read all rules created by security-manager and - * save them in one file. This file will be used during next system - * boot. - */ - static void mergeRules(); - private: static void useTemplate( const std::string &templatePath, - const std::string &outputPath, const std::string &appProcessLabel, const std::string &pkgName, const int authorId = -1); /** - * Create a path for package rules - * - */ - static std::string getPackageRulesFilePath(const std::string &pkgName); - - /** - * Create a path for application rules - */ - static std::string getApplicationRulesFilePath(const std::string &appName); - - /** - * Create a path for application rules - */ - static std::string getPkgRulesFilePath(const std::string &pkgName); - - /** - * Create a path for author rules - */ - static std::string getAuthorRulesFilePath(int authorId); - - /** - * Uninstall rules inside a specified file path - * - * This is a utility function that will clear all - * rules in the file specified by path - * - * @param[in] path - path to the file that contains the rules - */ - static void uninstallRules(const std::string &path); - - /** * Helper method: replace all occurrences of \ref needle in \ref haystack * with \ref replace. * @@ -265,9 +226,9 @@ private: /** * Revoke rules for which label of given \ref appName is a subject. * - * @param[in] appLabel = application process label + * @param[in] appProcessLabel = application process label */ - static void revokeAppSubject(const std::string &appLabel); + static void revokeAppSubject(const std::string &appProcessLabel); }; } // namespace SecurityManager diff --git a/src/common/include/utils.h b/src/common/include/utils.h index ff59af4..b2d3a80 100644 --- a/src/common/include/utils.h +++ b/src/common/include/utils.h @@ -24,10 +24,13 @@ #pragma once +#include #include #include +#include +#include +#include #include -#include namespace SecurityManager { @@ -61,3 +64,34 @@ static void vectorRemoveDuplicates(std::vector &vec) } } /* namespace SecurityManager */ + +template +bool forkExecWaitpid(const char *prog, const T *...args) { + const auto pid = fork(); + if (pid < 0) + return false; + if (pid) { + int r; + return waitpid(pid, &r, 0) >= 0 && WIFEXITED(r) && !WEXITSTATUS(r); + } + execl(prog, prog, args..., nullptr); + exit(EXIT_FAILURE); +} + +template +constexpr bool allTrue(T (&array)[S]) { + for (auto &a : array) + if (!a) + return false; + return true; +} + +template +constexpr auto max(const T &a, const U &b) { + return a < b ? b : a; +} + +template +constexpr auto underlying(const E &e) { + return typename std::underlying_type::type(e); +} diff --git a/src/common/privilege_db.cpp b/src/common/privilege_db.cpp index 7c4eb15..62b09e2 100644 --- a/src/common/privilege_db.cpp +++ b/src/common/privilege_db.cpp @@ -45,74 +45,52 @@ namespace SecurityManager { namespace { -template -constexpr size_t arraySize(T (&)[S]) -{ - return S; -} - constexpr const char *g_queries[StmtTypeCount] = { - [int(StmtType::EAddApplication)] = "INSERT INTO user_app_pkg_view (app_name, pkg_name, uid, version, author_name, is_hybrid)" - " VALUES (?, ?, ?, ?, ?, ?)", - [int(StmtType::ERemoveApplication)] = "DELETE FROM user_app_pkg_view WHERE app_name=? AND uid=?", - [int(StmtType::EPkgNameExists)] = "SELECT count(*) FROM pkg WHERE name=?", - [int(StmtType::EAppNameExists)] = "SELECT count(*) FROM app WHERE name=?", - [int(StmtType::EGetAppPkgName)] = "SELECT pkg_name FROM user_app_pkg_view WHERE app_name = ?", - [int(StmtType::EGetAppVersion)] = "SELECT version FROM app WHERE name = ?", - [int(StmtType::EGetPathSharedCount)] = "SELECT COUNT(*) FROM app_private_sharing_view WHERE path = ?", - [int(StmtType::EGetTargetPathSharedCount)] = "SELECT COUNT(*) FROM app_private_sharing_view WHERE target_app_name = ? AND path = ?", - [int(StmtType::EGetOwnerTargetSharedCount)] = "SELECT COUNT(*) FROM app_private_sharing_view WHERE owner_app_name = ? AND target_app_name = ?", - [int(StmtType::EAddPrivatePathSharing)] = "INSERT INTO app_private_sharing_view(owner_app_name, target_app_name, path, path_label) VALUES(?, ?, ?, ?)", - [int(StmtType::ERemovePrivatePathSharing)] = "DELETE FROM app_private_sharing_view WHERE owner_app_name = ? AND target_app_name = ? AND path = ?", - [int(StmtType::EGetAllSharedPaths)] = "SELECT DISTINCT owner_app_name, path FROM app_private_sharing_view ORDER BY owner_app_name", - [int(StmtType::EGetSharingForOwner)] = "SELECT target_app_name, path FROM app_private_sharing_view WHERE owner_app_name = ?", - [int(StmtType::EGetSharingForTarget)] = "SELECT owner_app_name, path FROM app_private_sharing_view WHERE target_app_name = ?", - [int(StmtType::ESquashSharing)] = "UPDATE app_private_sharing_view SET counter = 1 WHERE target_app_name = ? AND path = ?", - [int(StmtType::EClearSharing)] = "DELETE FROM app_private_sharing;", - [int(StmtType::EClearPrivatePaths)] = "DELETE FROM shared_path;", - [int(StmtType::EGetUserApps)] = "SELECT app_name FROM user_app_pkg_view WHERE uid=?", - [int(StmtType::EGetUserAppsFromPkg)] = "SELECT app_name FROM user_app_pkg_view WHERE uid = ? AND pkg_name = ?", - [int(StmtType::EGetUserPkgs)] = "SELECT DISTINCT pkg_name FROM user_app_pkg_view WHERE uid=?", - [int(StmtType::EGetAllPackages)] = "SELECT DISTINCT pkg_name FROM user_app_pkg_view", - [int(StmtType::EGetAppsInPkg)] = " SELECT app_name FROM user_app_pkg_view WHERE pkg_name = ?", - [int(StmtType::EGetGroupsRelatedPrivileges)] = "SELECT DISTINCT group_name, privilege_name FROM privilege_group", - [int(StmtType::EGetPkgAuthorId)] = "SELECT author_id FROM pkg WHERE name = ? AND author_id IS NOT NULL", - [int(StmtType::EAuthorIdExists)] = "SELECT count(*) FROM author where author_id=?", - [int(StmtType::EGetAuthorIdByName)] = "SELECT author_id FROM author WHERE name=?", - [int(StmtType::ESetPackageSharedRO)] = "UPDATE pkg SET shared_ro=1 WHERE name=?", - [int(StmtType::EIsPackageSharedRO)] = "SELECT shared_ro FROM pkg WHERE name=?", - [int(StmtType::EIsPackageHybrid)] = "SELECT is_hybrid FROM pkg WHERE name=?", - [int(StmtType::EGetPackagesInfo)] = "SELECT name, shared_ro, is_hybrid FROM pkg", - [int(StmtType::EAddAppDefinedPrivilege)] = "INSERT INTO app_defined_privilege_view (app_name, uid, privilege, type, license) VALUES (?, ?, ?, ?, ?)", - [int(StmtType::EAddClientPrivilege)] = "INSERT INTO client_license_view (app_name, uid, privilege, license) VALUES (?, ?, ?, ?)", - [int(StmtType::ERemoveAppDefinedPrivileges)] = "DELETE FROM app_defined_privilege_view WHERE app_name = ? AND uid = ?", - [int(StmtType::ERemoveClientPrivileges)] = "DELETE FROM client_license_view WHERE app_name = ? AND uid = ?", - [int(StmtType::EGetAppDefinedPrivileges)] = "SELECT privilege, type, license FROM app_defined_privilege_view WHERE app_name = ? AND uid = ?", - [int(StmtType::EGetAppPkgLicenseForAppDefinedPrivilege)] = "SELECT app_name, pkg_name, license FROM app_defined_privilege_view WHERE uid = ? AND privilege = ?", - [int(StmtType::EGetLicenseForClientPrivilegeAndApp)] = "SELECT license FROM client_license_view WHERE app_name = ? AND uid = ? AND privilege = ? ", - [int(StmtType::EGetLicenseForClientPrivilegeAndPkg)] = "SELECT license FROM client_license_view WHERE pkg_name = ? AND uid = ? AND privilege = ? ", - [int(StmtType::EIsUserPkgInstalled)] = "SELECT count(*) FROM user_app_pkg_view WHERE pkg_name = ? AND uid = ?", + [underlying(StmtType::EAddApplication)] = "INSERT INTO user_app_pkg_view (app_name, pkg_name, uid, version, author_name, is_hybrid)" + " VALUES (?, ?, ?, ?, ?, ?)", + [underlying(StmtType::ERemoveApplication)] = "DELETE FROM user_app_pkg_view WHERE app_name=? AND uid=?", + [underlying(StmtType::EPkgNameExists)] = "SELECT count(*) FROM pkg WHERE name=?", + [underlying(StmtType::EAppNameExists)] = "SELECT count(*) FROM app WHERE name=?", + [underlying(StmtType::EGetAppPkgName)] = "SELECT pkg_name FROM user_app_pkg_view WHERE app_name = ?", + [underlying(StmtType::EGetAppVersion)] = "SELECT version FROM app WHERE name = ?", + [underlying(StmtType::EGetPathSharedCount)] = "SELECT COUNT(*) FROM app_private_sharing_view WHERE path = ?", + [underlying(StmtType::EGetTargetPathSharedCount)] = "SELECT COUNT(*) FROM app_private_sharing_view WHERE target_app_name = ? AND path = ?", + [underlying(StmtType::EGetOwnerTargetSharedCount)] = "SELECT COUNT(*) FROM app_private_sharing_view WHERE owner_app_name = ? AND target_app_name = ?", + [underlying(StmtType::EAddPrivatePathSharing)] = "INSERT INTO app_private_sharing_view(owner_app_name, target_app_name, path, path_label) VALUES(?, ?, ?, ?)", + [underlying(StmtType::ERemovePrivatePathSharing)] = "DELETE FROM app_private_sharing_view WHERE owner_app_name = ? AND target_app_name = ? AND path = ?", + [underlying(StmtType::EGetAllSharedPaths)] = "SELECT DISTINCT owner_app_name, path FROM app_private_sharing_view ORDER BY owner_app_name", + [underlying(StmtType::EGetSharingForOwner)] = "SELECT target_app_name, path FROM app_private_sharing_view WHERE owner_app_name = ?", + [underlying(StmtType::EGetSharingForTarget)] = "SELECT owner_app_name, path FROM app_private_sharing_view WHERE target_app_name = ?", + [underlying(StmtType::ESquashSharing)] = "UPDATE app_private_sharing_view SET counter = 1 WHERE target_app_name = ? AND path = ?", + [underlying(StmtType::EClearSharing)] = "DELETE FROM app_private_sharing;", + [underlying(StmtType::EClearPrivatePaths)] = "DELETE FROM shared_path;", + [underlying(StmtType::EGetUserApps)] = "SELECT app_name FROM user_app_pkg_view WHERE uid=?", + [underlying(StmtType::EGetUserAppsFromPkg)] = "SELECT app_name FROM user_app_pkg_view WHERE uid = ? AND pkg_name = ?", + [underlying(StmtType::EGetUserPkgs)] = "SELECT DISTINCT pkg_name FROM user_app_pkg_view WHERE uid=?", + [underlying(StmtType::EGetAllPackages)] = "SELECT DISTINCT pkg_name FROM user_app_pkg_view", + [underlying(StmtType::EGetAppsInPkg)] = " SELECT app_name FROM user_app_pkg_view WHERE pkg_name = ?", + [underlying(StmtType::EGetGroupsRelatedPrivileges)] = "SELECT DISTINCT group_name, privilege_name FROM privilege_group", + [underlying(StmtType::EGetPkgAuthorId)] = "SELECT author_id FROM pkg WHERE name = ? AND author_id IS NOT NULL", + [underlying(StmtType::EAuthorIdExists)] = "SELECT count(*) FROM author where author_id=?", + [underlying(StmtType::EGetAuthorIdByName)] = "SELECT author_id FROM author WHERE name=?", + [underlying(StmtType::ESetPackageSharedRO)] = "UPDATE pkg SET shared_ro=1 WHERE name=?", + [underlying(StmtType::EIsPackageSharedRO)] = "SELECT shared_ro FROM pkg WHERE name=?", + [underlying(StmtType::EIsPackageHybrid)] = "SELECT is_hybrid FROM pkg WHERE name=?", + [underlying(StmtType::EGetPackagesInfo)] = "SELECT name, shared_ro, is_hybrid FROM pkg", + [underlying(StmtType::EAddAppDefinedPrivilege)] = "INSERT INTO app_defined_privilege_view (app_name, uid, privilege, type, license) VALUES (?, ?, ?, ?, ?)", + [underlying(StmtType::EAddClientPrivilege)] = "INSERT INTO client_license_view (app_name, uid, privilege, license) VALUES (?, ?, ?, ?)", + [underlying(StmtType::ERemoveAppDefinedPrivileges)] = "DELETE FROM app_defined_privilege_view WHERE app_name = ? AND uid = ?", + [underlying(StmtType::ERemoveClientPrivileges)] = "DELETE FROM client_license_view WHERE app_name = ? AND uid = ?", + [underlying(StmtType::EGetAppDefinedPrivileges)] = "SELECT privilege, type, license FROM app_defined_privilege_view WHERE app_name = ? AND uid = ?", + [underlying(StmtType::EGetAppPkgLicenseForAppDefinedPrivilege)] = "SELECT app_name, pkg_name, license FROM app_defined_privilege_view WHERE uid = ? AND privilege = ?", + [underlying(StmtType::EGetLicenseForClientPrivilegeAndApp)] = "SELECT license FROM client_license_view WHERE app_name = ? AND uid = ? AND privilege = ? ", + [underlying(StmtType::EGetLicenseForClientPrivilegeAndPkg)] = "SELECT license FROM client_license_view WHERE pkg_name = ? AND uid = ? AND privilege = ? ", + [underlying(StmtType::EIsUserPkgInstalled)] = "SELECT count(*) FROM user_app_pkg_view WHERE pkg_name = ? AND uid = ?", }; - -template -constexpr bool allTrue(T (&array)[S]) { - for (auto &a : array) { - if (!a) - return false; - } - return true; -} - static_assert(allTrue(g_queries)); template -constexpr const char *query = g_queries[size_t(i)]; - -static_assert(dbSchema); -// this ensures that parsing the sql files was done correctly and we have proper -// number of update scripts -static_assert(dbVersion == arraySize(dbUpdateScript)); -static_assert(allTrue(dbUpdateScript)); +constexpr const char *query = g_queries[underlying(i)]; auto prepare(DB::SqlConnection &db, const char *fmt) { Assert(fmt); @@ -121,46 +99,6 @@ auto prepare(DB::SqlConnection &db, const char *fmt) { return cmd; } -bool dbEmpty(const std::string &dbPath) { - auto s = FS::fileSize(dbPath); - if (s < 0) - ThrowMsg(DB::SqlConnection::Exception::Base, "Failed to stat db file"); - return !s; -} - -void connectMigrateVerify(DB::SqlConnection &db, const std::string &path) { - db.Connect(path, DB::SqlConnection::Flag::None, DB::SqlConnection::Flag::RW); - using Ex = DB::SqlConnection::Exception::Base; - int32_t version; - { - auto cmd = prepare(db, "PRAGMA user_version"); - if (!cmd->Step()) - ThrowMsg(Ex, "Error getting database version"); - version = cmd->GetColumnInt32(0); - } - if (version != dbVersion) { - if (version < 0) - ThrowMsg(Ex, "Corrupted database version: " << version); - if (version > dbVersion) - ThrowMsg(Ex, "Database downgrade not supported; db version: " << version << ", manager binary version: " << dbVersion); - if (version || !dbEmpty(path)) - do db.ExecCommand(dbUpdateScript[version]); while (++version < dbVersion); - db.ExecCommand(dbSchema); - } - - { - auto cmd = prepare(db, "PRAGMA integrity_check"); - if (!cmd->Step()) - ThrowMsg(Ex, "Integrity check returned no result"); - if (cmd->GetColumnString(0) != "ok") - ThrowMsg(Ex, "Integrity check failed"); - } - - auto cmd = prepare(db, "PRAGMA foreign_key_check"); - if (cmd->Step()) - ThrowMsg(Ex, "Foreign key check failed"); -} - /* Common code for handling SqlConnection exceptions */ template T try_catch(const std::function &func) @@ -188,16 +126,6 @@ void throwDbInitEx(const std::string &errDesc) { ThrowMsg(PrivilegeDb::Exception::IOError, s); } -void createRecoveryFlagFile(const std::string &dbPath) { - if (SECURITY_MANAGER_SUCCESS != FS::createFile(Config::dbRecoveryFlagFileName(dbPath))) - throwDbInitEx("Error creating db recovery flag file"); -} - -void removeRecoveryFlagFile(const std::string &dbPath) { - if (SECURITY_MANAGER_SUCCESS == FS::removeFile(Config::dbRecoveryFlagFileName(dbPath))) - LogWarning("Recovery DB flag file removed - booting 1st time after DB recovery"); -} - template void tryCatchDbInit(F &&f) { try { @@ -206,65 +134,44 @@ void tryCatchDbInit(F &&f) { throwDbInitEx(e.DumpToString()); } } - -void applyFallbackDb(DB::SqlConnection &conn, const std::string &dbPath, const std::string &roFallbackPath) { - if (SECURITY_MANAGER_SUCCESS != FS::overwriteFile(roFallbackPath, dbPath)) - throwDbInitEx("Error overwriting database " + dbPath + " with fallback: " + roFallbackPath); - if (SECURITY_MANAGER_SUCCESS != FS::truncateFile(genJournalPath(dbPath))) - throwDbInitEx("Error truncating journal"); - tryCatchDbInit([&]{ connectMigrateVerify(conn, dbPath); }); -} - -InitDbDidFallback initDb(DB::SqlConnection &conn, const std::string &path, const std::string &roFallbackPath) { - removeRecoveryFlagFile(path); - if (!FS::fileStatus(path)) { - createRecoveryFlagFile(path); - LogError("Database file " + path + " missing, attempting fallback"); - applyFallbackDb(conn, path, roFallbackPath); - } else try { - connectMigrateVerify(conn, path); - return InitDbDidFallback::no; - } catch (DB::SqlConnection::Exception::Base &e) { - createRecoveryFlagFile(path); - LogError("Database initialization error (" << e.DumpToString() << "), attempting fallback"); - tryCatchDbInit([&]{ conn.Disconnect(); }); - applyFallbackDb(conn, path, roFallbackPath); - } - return InitDbDidFallback::yes; -} } //namespace -std::string genJournalPath(const std::string &dbPath) { - return dbPath + "-journal"; -} - -InitDbDidFallback initDb() { - DB::SqlConnection conn; - return initDb(conn, Config::getPrivilegeDbPath(), Config::getPrivilegeDbFallbackPath()); -} +PrivilegeDb::PrivilegeDb(Offline offline, Db db) { + std::string path; + const char *okMarkerPath, *loaderCmd; + switch (db) { + case Db::standard: + path = Config::getPrivilegeDbPath(); + okMarkerPath = DB_OK_MARKER; + loaderCmd = RULES_LOADER_CMD; + break; + case Db::test: + path = TEST_DB_PATH; + okMarkerPath = TEST_DB_OK_MARKER; + loaderCmd = TEST_RULES_LOADER_CMD; + break; + } -PrivilegeDb::PrivilegeDb() - : PrivilegeDb(Config::getPrivilegeDbPath(), Config::getPrivilegeDbFallbackPath()) -{ -} + bool didFallback = false; + if (!underlying(offline) && !FS::fileExists(okMarkerPath) && !(didFallback = FS::fileExists(path + DB_RECOVERED_SUFFIX))) + throwDbInitEx("loader failed to initialize db - giving up"); -PrivilegeDb::PrivilegeDb(const std::string &path, const std::string &roFallbackPath) -{ - const auto didFallback = initDb(mSqlConnection, path, roFallbackPath); + tryCatchDbInit([&]{ mSqlConnection.Connect(path); }); try { - tryCatchDbInit([&]{ initDataCommands(); }); - } catch (...) { - createRecoveryFlagFile(path); - switch (didFallback) { - case InitDbDidFallback::no: - LogError("Database initialization error during query preparation - attempting fallback"); - tryCatchDbInit([&]{ mSqlConnection.Disconnect(); }); - applyFallbackDb(mSqlConnection, path, roFallbackPath); - tryCatchDbInit([&]{ initDataCommands(); }); - break; - case InitDbDidFallback::yes: - throwDbInitEx("Database initialization error during query preparation on fallback db - giving up"); - } + initDataCommands(); + } catch (DB::SqlConnection::Exception::Base &e) { + if (underlying(offline)) + throwDbInitEx("failed to initialize db in offline mode - giving up"); + if (didFallback) + throwDbInitEx("Database initialization error during query preparation on fallback db - giving up"); + LogError("Database initialization error during query preparation - attempting fallback"); + tryCatchDbInit([&]{ + mSqlConnection.Disconnect(); + if (!forkExecWaitpid(loaderCmd, "fallback-only")) + throwDbInitEx("Failure rerunning the loader to apply fallback - giving up"); + mSqlConnection.Connect(path); + initDataCommands(); + }); } } @@ -296,13 +203,7 @@ DB::SqlConnection::DataCommand* PrivilegeDb::StatementWrapper::operator->() PrivilegeDb::StatementWrapper PrivilegeDb::getStatement(StmtType queryType) { - return StatementWrapper(m_commands[int(queryType)]); -} - -PrivilegeDb &PrivilegeDb::getInstance() -{ - static PrivilegeDb privilegeDb; - return privilegeDb; + return StatementWrapper(m_commands[underlying(queryType)]); } void PrivilegeDb::BeginTransaction(void) diff --git a/src/common/service_impl.cpp b/src/common/service_impl.cpp index e8cff86..943cca4 100644 --- a/src/common/service_impl.cpp +++ b/src/common/service_impl.cpp @@ -160,7 +160,8 @@ bool verifyAppDefinedPrivileges(app_inst_req &req) } // end of anonymous namespace -ServiceImpl::ServiceImpl() : +ServiceImpl::ServiceImpl(Offline offline) : + m_privilegeDb(offline), m_NSMountLogic(m_cynara) { PrivilegeGids::GroupPrivileges group_privileges; @@ -668,18 +669,19 @@ int ServiceImpl::appInstallSmackRules(app_inst_req &req, InstallHelper &ih) SmackRules::Labels pkgLabels; try { + m_privilegeDb.GetPkgAuthorId(req.pkgName, authorId); + // Check if hybridity is changed if the package is installed if (ih.isUserPkgInstalled && ih.isOldPkgHybrid != req.isHybrid) { for (auto &app : req.apps) { std::string oldAppLabel = SmackLabels::generateProcessLabel( app.appName, req.pkgName, ih.isOldPkgHybrid); - SmackRules::uninstallApplicationRules(app.appName, oldAppLabel); + SmackRules::uninstallApplicationRules(oldAppLabel, req.pkgName, authorId); if (req.isHybrid) // was not hybrid - all labels were the same break; } } - m_privilegeDb.GetPkgAuthorId(req.pkgName, authorId); m_privilegeDb.GetPackagesInfo(pkgsInfo); getPkgsProcessLabels(pkgsInfo, pkgsProcessLabels); getPkgLabels(req.pkgName, pkgLabels); @@ -690,15 +692,12 @@ int ServiceImpl::appInstallSmackRules(app_inst_req &req, InstallHelper &ih) std::string appLabel = SmackLabels::generateProcessLabel( app.appName, req.pkgName, req.isHybrid); - SmackRules::installApplicationRules(app.appName, appLabel, req.pkgName, - authorId, pkgLabels); + SmackRules::installApplicationRules(appLabel, req.pkgName, authorId, pkgLabels); if (!req.isHybrid) // is not hybrid - all labels are the same break; } SmackRules::generateSharedRORules(pkgsProcessLabels, pkgsInfo); - - SmackRules::mergeRules(); } catch (const SmackException::InvalidLabel &e) { LogError("Error while generating Smack labels: " << e.DumpToString()); return SECURITY_MANAGER_ERROR_SERVER_ERROR; @@ -1021,6 +1020,9 @@ void ServiceImpl::appUninstallCynaraPolicies(const std::string &processLabel, ap int ServiceImpl::appUninstallSmackRules(app_inst_req &req, UninstallHelper &uh) { try { + LogDebug("Removing Smack rules for pkgName " << req.pkgName); + SmackRules::uninstallPackageRules(req.pkgName, uh.pkgLabels); + for (unsigned appIdx = 0; appIdx < req.apps.size(); ++appIdx) { if (uh.removeApps[appIdx]) { const std::string &appName = req.apps[appIdx].appName; @@ -1031,7 +1033,7 @@ int ServiceImpl::appUninstallSmackRules(app_inst_req &req, UninstallHelper &uh) * Nonhybrid apps have the same label, so revoking it is unnecessary * unless whole package is being removed. */ - SmackRules::uninstallApplicationRules(appName, processLabel); + SmackRules::uninstallApplicationRules(processLabel, req.pkgName, uh.authorId); } if (!uh.removePkg) { uh.pkgLabels.erase(std::remove(uh.pkgLabels.begin(), uh.pkgLabels.end(), processLabel), @@ -1040,8 +1042,6 @@ int ServiceImpl::appUninstallSmackRules(app_inst_req &req, UninstallHelper &uh) } } - LogDebug("Removing Smack rules for pkgName " << req.pkgName); - SmackRules::uninstallPackageRules(req.pkgName); if (!uh.removePkg) { LogDebug("Recreating Smack rules for pkgName " << req.pkgName); SmackRules::updatePackageRules(req.pkgName, uh.pkgLabels); @@ -1054,15 +1054,12 @@ int ServiceImpl::appUninstallSmackRules(app_inst_req &req, UninstallHelper &uh) }), uh.pkgsProcessLabels.end()); - SmackRules::generateSharedRORules(uh.pkgsProcessLabels, uh.pkgsInfo); SmackRules::revokeSharedRORules(uh.pkgsProcessLabels, req.pkgName); } if (uh.authorId != -1 && uh.removeAuthor) { LogDebug("Removing Smack rules for authorId " << uh.authorId); SmackRules::uninstallAuthorRules(uh.authorId); } - - SmackRules::mergeRules(); } catch (const SmackException::Base &e) { LogError("Error while removing Smack rules for application: " << e.DumpToString()); return SECURITY_MANAGER_ERROR_SETTING_FILE_LABEL_FAILED; @@ -1995,7 +1992,6 @@ int ServiceImpl::pathsRegister(const Credentials &creds, path_req req) getPkgsProcessLabels(pkgsInfo, pkgsLabels); SmackRules::generateSharedRORules(pkgsLabels, pkgsInfo); - SmackRules::mergeRules(); } trans.commit(); } @@ -2027,7 +2023,7 @@ int ServiceImpl::shmAppName(const Credentials &creds, const std::string &shmName if (shmName.empty() || appName.empty()) return SECURITY_MANAGER_ERROR_INPUT_PARAM; - if (!PrivilegeDb::getInstance().AppNameExists(appName)) { + if (!m_privilegeDb.AppNameExists(appName)) { LogError("Unknown application id: " << appName); return SECURITY_MANAGER_ERROR_NO_SUCH_OBJECT; } diff --git a/src/common/smack-rules.cpp b/src/common/smack-rules.cpp index c7684ca..83ab413 100644 --- a/src/common/smack-rules.cpp +++ b/src/common/smack-rules.cpp @@ -23,14 +23,10 @@ * */ -#include #include #include #include -#include -#include #include -#include #include #include #include @@ -38,7 +34,6 @@ #include "config-file.h" #include "dpl/log/log.h" #include "dpl/errno_string.h" -#include "dpl/fstream_accessors.h" #include "filesystem.h" #include "smack-labels.h" #include "tzplatform-config.h" @@ -94,10 +89,6 @@ std::string getSharedRORulesTemplateFilePath() return path; } -const std::string SMACK_RULES_PATH_MERGED = LOCAL_STATE_DIR "/security-manager/rules-merged/rules.merged"; -const std::string SMACK_RULES_PATH_MERGED_T = LOCAL_STATE_DIR "/security-manager/rules-merged/rules.merged.temp"; -const std::string SMACK_RULES_PATH = LOCAL_STATE_DIR "/security-manager/rules"; -const std::string SMACK_RULES_SHARED_RO_PATH = LOCAL_STATE_DIR "/security-manager/rules/shared_ro"; const std::string SMACK_APP_IN_PACKAGE_PERMS = "rwxat"; const std::string SMACK_APP_CROSS_PKG_PERMS = "rxl"; const std::string SMACK_APP_PATH_OWNER_PERMS = "rwxat"; @@ -108,7 +99,6 @@ const std::string SMACK_SYSTEM = "System"; const std::string SMACK_SYSTEM_PRIVILEGED = "System::Privileged"; const std::string SMACK_APP_PATH_SYSTEM_PERMS = "rwxat"; const std::string SMACK_APP_PATH_USER_PERMS = "rwxat"; -const std::string TEMPORARY_FILE_SUFFIX = ".temp"; SmackRules::SmackRules() { @@ -149,64 +139,6 @@ void SmackRules::apply() const } -void SmackRules::loadFromFile(const std::string &path) -{ - int fd; - - fd = TEMP_FAILURE_RETRY(open(path.c_str(), O_RDONLY)); - if (fd == -1) { - LogError("Failed to open file: " << path); - ThrowMsg(SmackException::FileError, "Failed to open file: " << path); - } - - if (smack_accesses_add_from_file(m_handle, fd)) { - LogError("Failed to load smack rules from file: " << path); - ThrowMsg(SmackException::LibsmackError, "Failed to load smack rules from file: " << path); - } - - if (close(fd) == -1) { - // don't change the return code, the descriptor should be closed despite the error. - LogWarning("Error while closing the file: " << path << ", error: " << GetErrnoString(errno)); - } -} - -void SmackRules::saveToFile(const std::string &destPath) const -{ - int fd; - int flags = O_CREAT | O_WRONLY | O_TRUNC; - std::string path = destPath + TEMPORARY_FILE_SUFFIX; - - fd = TEMP_FAILURE_RETRY(open(path.c_str(), flags, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)); - if (fd == -1) { - LogError("Failed to create file: " << path); - ThrowMsg(SmackException::FileError, "Failed to create file: " << path); - } - - if (smack_accesses_save(m_handle, fd)) { - LogError("Failed to save rules to file: " << path); - unlink(path.c_str()); - ThrowMsg(SmackException::LibsmackError, "Failed to save rules to file: " << path); - } - - if (close(fd) == -1) { - if (errno == EIO) { - LogError("I/O Error occured while closing the file: " << path << ", error: " << GetErrnoString(errno)); - unlink(path.c_str()); - ThrowMsg(SmackException::FileError, "I/O Error occured while closing the file: " << path << ", error: " << GetErrnoString(errno)); - } else { - // non critical error - // don't change the return code, the descriptor should be closed despite the error. - LogWarning("Error while closing the file: " << path << ", error: " << GetErrnoString(errno)); - } - } - - if (0 > rename(path.c_str(), destPath.c_str())) { - LogError("Error moving file " << path << " to " << destPath << ". Errno: " << GetErrnoString(errno)); - unlink(path.c_str()); - ThrowMsg(SmackException::FileError, "Error moving file " << path << " to " << destPath << ". Errno: " << GetErrnoString(errno)); - } -} - void SmackRules::addFromTemplateFile( const std::string &templatePath, const std::string &appProcessLabel, @@ -264,7 +196,7 @@ void SmackRules::addFromTemplate( void SmackRules::generatePackageCrossDeps(const Labels &pkgLabels) { - LogDebug ("Generating cross-package rules"); + LogDebug("Generating cross-package rules"); std::string appsInPackagePerms = SMACK_APP_IN_PACKAGE_PERMS; @@ -273,7 +205,7 @@ void SmackRules::generatePackageCrossDeps(const Labels &pkgLabels) if (object == subject) continue; - LogDebug ("Trying to add rule subject: " << subject + LogDebug("Trying to add rule subject: " << subject << " object: " << object << " perms: " << appsInPackagePerms); add(subject, object, appsInPackagePerms); } @@ -312,8 +244,6 @@ void SmackRules::generateSharedRORules(PkgsLabels &pkgsLabels, std::vector bool { - if (path.size() < TEMPORARY_FILE_SUFFIX.size()) - return false; - return std::equal( - TEMPORARY_FILE_SUFFIX.rbegin(), - TEMPORARY_FILE_SUFFIX.rend(), - path.rbegin()); - }), - files.end()); - - std::ofstream dst(SMACK_RULES_PATH_MERGED_T, std::ios::binary); - - if (dst.fail()) { - LogError("Error creating file: " << SMACK_RULES_PATH_MERGED_T); - ThrowMsg(SmackException::FileError, "Error creating file: " << SMACK_RULES_PATH_MERGED_T); - } - - for(auto const &e : files) { - std::ifstream src(std::string(SMACK_RULES_PATH) + "/" + e, std::ios::binary); - src.seekg(0, std::ios::end); - size_t size = src.tellg(); - - std::vector buffer(size); - src.seekg(0); - src.read(buffer.data(), size); - dst.write(buffer.data(), size); - - if (!buffer.empty() && buffer[size-1] != '\n') - dst << '\n'; - - if (dst.bad()) { - LogError("I/O Error. File " << SMACK_RULES_PATH_MERGED << " will not be updated!"); - unlink(SMACK_RULES_PATH_MERGED_T.c_str()); - ThrowMsg(SmackException::FileError, - "I/O Error. File " << SMACK_RULES_PATH_MERGED << " will not be updated!"); - } - - if (dst.fail()) { - // src.rdbuf() was empty - dst.clear(); - } - } - - if (dst.flush().fail()) { - LogError("Error flushing file: " << SMACK_RULES_PATH_MERGED_T); - unlink(SMACK_RULES_PATH_MERGED_T.c_str()); - ThrowMsg(SmackException::FileError, "Error flushing file: " << SMACK_RULES_PATH_MERGED_T); - } - - if (0 > fsync(DPL::FstreamAccessors::GetFd(dst))) { - LogError("Error fsync on file: " << SMACK_RULES_PATH_MERGED_T); - unlink(SMACK_RULES_PATH_MERGED_T.c_str()); - ThrowMsg(SmackException::FileError, "Error fsync on file: " << SMACK_RULES_PATH_MERGED_T); - } - - dst.close(); - if (dst.fail()) { - LogError("Error closing file: " << SMACK_RULES_PATH_MERGED_T); - unlink(SMACK_RULES_PATH_MERGED_T.c_str()); - ThrowMsg(SmackException::FileError, "Error closing file: " << SMACK_RULES_PATH_MERGED_T); - } - - if ((tmp = rename(SMACK_RULES_PATH_MERGED_T.c_str(), SMACK_RULES_PATH_MERGED.c_str())) == 0) - return; - - int err = errno; - - LogError("Error during file rename: " - << SMACK_RULES_PATH_MERGED_T << " to " - << SMACK_RULES_PATH_MERGED << " Errno: " << GetErrnoString(err)); - unlink(SMACK_RULES_PATH_MERGED_T.c_str()); - ThrowMsg(SmackException::FileError, "Error during file rename: " - << SMACK_RULES_PATH_MERGED_T << " to " - << SMACK_RULES_PATH_MERGED << " Errno: " << GetErrnoString(err)); -} - void SmackRules::useTemplate( const std::string &templatePath, - const std::string &outputPath, const std::string &appProcessLabel, const std::string &pkgName, const int authorId) @@ -448,23 +278,18 @@ void SmackRules::useTemplate( if (smack_check()) smackRules.apply(); - - smackRules.saveToFile(outputPath); } void SmackRules::installApplicationRules( - const std::string &appName, const std::string &appProcessLabel, const std::string &pkgName, const int authorId, const Labels &pkgLabels) { - useTemplate(getAppRulesTemplateFilePath(), getApplicationRulesFilePath(appName), - appProcessLabel, pkgName, authorId); + useTemplate(getAppRulesTemplateFilePath(), appProcessLabel, pkgName, authorId); if (authorId >= 0) - useTemplate(getAuthorRulesTemplateFilePath(), getAuthorRulesFilePath(authorId), - appProcessLabel, pkgName, authorId); + useTemplate(getAuthorRulesTemplateFilePath(), appProcessLabel, pkgName, authorId); updatePackageRules(pkgName, pkgLabels); } @@ -484,8 +309,6 @@ void SmackRules::updatePackageRules( if (smack_check()) smackRules.apply(); - - smackRules.saveToFile(getPackageRulesFilePath(pkgName)); } @@ -495,45 +318,22 @@ void SmackRules::revokeAppSubject(const std::string &appLabel) ThrowMsg(SmackException::LibsmackError, "smack_revoke_subject"); } -void SmackRules::uninstallPackageRules(const std::string &pkgName) +void SmackRules::uninstallPackageRules(const std::string &pkgName, const Labels &pkgLabels) { - uninstallRules(getPackageRulesFilePath(pkgName)); + SmackRules smackRules; + smackRules.addFromTemplateFile(getPkgRulesTemplateFilePath(), {}, pkgName, -1); + smackRules.generatePackageCrossDeps(pkgLabels); + smackRules.clear(); } -void SmackRules::uninstallApplicationRules(const std::string &appName, const std::string &appLabel) +void SmackRules::uninstallApplicationRules(const std::string &appLabel, const std::string &pkgName, const int authorId) { - uninstallRules(getApplicationRulesFilePath(appName)); + SmackRules smackRules; + smackRules.addFromTemplateFile(getAppRulesTemplateFilePath(), appLabel, pkgName, authorId); + smackRules.clear(); revokeAppSubject(appLabel); } -void SmackRules::uninstallRules(const std::string &path) -{ - if (access(path.c_str(), F_OK) == -1) { - if (errno == ENOENT) { - LogWarning("Smack rules not found in file: " << path); - return; - } - - LogWarning("Cannot access smack rules path: " << path); - ThrowMsg(SmackException::FileError, "Cannot access smack rules path: " << path); - } - - try { - SmackRules rules; - rules.loadFromFile(path); - if (smack_check()) - rules.clear(); - } catch (const SmackException::Base &e) { - LogWarning("Failed to clear smack kernel rules from file: " << path); - // don't stop uninstallation - } - - if (unlink(path.c_str()) == -1) { - LogWarning("Failed to remove smack rules file: " << path); - ThrowMsg(SmackException::FileError, "Failed to remove smack rules file: " << path); - } -} - void SmackRules::strReplace(std::string &haystack, const std::string &needle, const std::string &replace) { @@ -544,7 +344,9 @@ void SmackRules::strReplace(std::string &haystack, const std::string &needle, void SmackRules::uninstallAuthorRules(const int authorId) { - uninstallRules(getAuthorRulesFilePath(authorId)); + SmackRules smackRules; + smackRules.addFromTemplateFile(getAuthorRulesTemplateFilePath(), {}, {}, authorId); + smackRules.clear(); } void SmackRules::applyPrivateSharingRules( diff --git a/src/dpl/db/include/dpl/db/sql_connection.h b/src/dpl/db/include/dpl/db/sql_connection.h index 7bb5c3c..4dad249 100644 --- a/src/dpl/db/include/dpl/db/sql_connection.h +++ b/src/dpl/db/include/dpl/db/sql_connection.h @@ -365,13 +365,7 @@ class SqlConnection final enum Option { RO = SQLITE_OPEN_NOMUTEX | SQLITE_OPEN_READONLY, - /** - * *TODO: please remove CREATE option from RW flag when all places - * that need that switched do CRW - */ - RW = SQLITE_OPEN_NOMUTEX | SQLITE_OPEN_READWRITE | - SQLITE_OPEN_CREATE, - CRW = RW | SQLITE_OPEN_CREATE + RW = SQLITE_OPEN_NOMUTEX | SQLITE_OPEN_READWRITE, }; }; @@ -439,7 +433,7 @@ class SqlConnection final */ void Connect(const std::string &address, Flag::Type type = Flag::None, - Flag::Option flag = Flag::RO); + Flag::Option flag = Flag::RW); /** * Disconnect SQL connection diff --git a/src/server/CMakeLists.txt b/src/server/CMakeLists.txt index 373ab02..9db7821 100644 --- a/src/server/CMakeLists.txt +++ b/src/server/CMakeLists.txt @@ -58,19 +58,31 @@ TARGET_LINK_LIBRARIES(${TARGET_CLEANUP} ${TARGET_COMMON} ) -########## INIT-DB SERVICE ###################################### -SET(INITDB_SOURCES - ${SERVER_PATH}/init-db/security-manager-init-db.cpp +########## RULES LOADER ################################# +PKG_CHECK_MODULES(TARGET_LOADER_DEP REQUIRED sqlite3 libtzplatform-config) +SET(RULES_LOADER_SOURCES + ${SERVER_PATH}/rules-loader/security-manager-rules-loader.cpp + ${GEN_PATH}/generated-rule-templates.h + ${GEN_PATH}/db.h ) -ADD_EXECUTABLE(${TARGET_INITDB} ${INITDB_SOURCES}) -SET_TARGET_PROPERTIES(${TARGET_INITDB} +SET_SOURCE_FILES_PROPERTIES(${GEN_PATH}/generated-rule-templates.h PROPERTIES GENERATED 1) +SET_SOURCE_FILES_PROPERTIES(${GEN_PATH}/db.h PROPERTIES GENERATED 1) + +ADD_EXECUTABLE(${TARGET_LOADER} ${RULES_LOADER_SOURCES}) +ADD_DEPENDENCIES(${TARGET_LOADER} generate_rule_template_code generate) +TARGET_LINK_LIBRARIES(${TARGET_LOADER} ${TARGET_LOADER_DEP_LIBRARIES}) +SET_TARGET_PROPERTIES(${TARGET_LOADER} PROPERTIES - COMPILE_FLAGS "-D_GNU_SOURCE -fvisibility=hidden") + COMPILE_FLAGS "-Wno-maybe-uninitialized -fvisibility=hidden -fno-rtti -fno-exceptions -O3") -TARGET_LINK_LIBRARIES(${TARGET_INITDB} - ${TARGET_COMMON} - ) +ADD_EXECUTABLE(${TARGET_TEST_LOADER} ${RULES_LOADER_SOURCES}) +ADD_DEPENDENCIES(${TARGET_TEST_LOADER} generate_rule_template_code generate) +TARGET_LINK_LIBRARIES(${TARGET_TEST_LOADER} ${TARGET_LOADER_DEP_LIBRARIES}) +SET_TARGET_PROPERTIES(${TARGET_TEST_LOADER} + PROPERTIES + COMPILE_FLAGS "-DTEST_LOADER -Wno-maybe-uninitialized -fvisibility=hidden -fno-rtti -fno-exceptions -O3") INSTALL(TARGETS ${TARGET_CLEANUP} DESTINATION ${BIN_INSTALL_DIR}) -INSTALL(TARGETS ${TARGET_INITDB} DESTINATION ${BIN_INSTALL_DIR}) INSTALL(TARGETS ${TARGET_SERVER} DESTINATION ${BIN_INSTALL_DIR}) +INSTALL(TARGETS ${TARGET_LOADER} DESTINATION ${BIN_INSTALL_DIR}) +INSTALL(TARGETS ${TARGET_TEST_LOADER} DESTINATION ${BIN_INSTALL_DIR}) diff --git a/src/server/cleanup/security-manager-cleanup.cpp b/src/server/cleanup/security-manager-cleanup.cpp index 20986bd..b115f66 100644 --- a/src/server/cleanup/security-manager-cleanup.cpp +++ b/src/server/cleanup/security-manager-cleanup.cpp @@ -61,11 +61,12 @@ int main(void) try { std::map> appPathMap; - PrivilegeDb::getInstance().GetAllPrivateSharing(appPathMap); + PrivilegeDb db(PrivilegeDb::Offline::no); + db.GetAllPrivateSharing(appPathMap); for (auto &appPaths : appPathMap) { try { std::string pkgName; - PrivilegeDb::getInstance().GetAppPkgName(appPaths.first, pkgName); + db.GetAppPkgName(appPaths.first, pkgName); for (const auto &path : appPaths.second) { //FIXME Make this service run as slave and master SmackLabels::setupPath(pkgName, path, SECURITY_MANAGER_PATH_RW); @@ -78,7 +79,7 @@ int main(void) LogError("Got unknown exception, ignoring"); } } - PrivilegeDb::getInstance().ClearPrivateSharing(); + db.ClearPrivateSharing(); } catch (const SecurityManager::Exception &e) { std::cerr << "Exception throw, msg: " << e.GetMessage() << std::endl; } catch (...) { diff --git a/src/server/init-db/security-manager-init-db.cpp b/src/server/init-db/security-manager-init-db.cpp deleted file mode 100644 index 6be07c5..0000000 --- a/src/server/init-db/security-manager-init-db.cpp +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2018 Samsung Electronics Co., Ltd All Rights Reserved - * - * Contact: Rafal Krypa - * - * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ -/* - * @file security-manager-init-db.cpp - * @author Dariusz Michaluk (d.michaluk@samsung.com) - * @version 1.0 - * @brief Implementation of security-manager-init-db service - */ - -#include -#include - -int main(void) -{ - try { - SecurityManager::initDb(); - return EXIT_SUCCESS; - } catch (...) { - std::cerr << "Unknown exception thrown" << std::endl; - } - - return EXIT_FAILURE; -} diff --git a/src/server/main/server-main.cpp b/src/server/main/server-main.cpp index c00bc54..ccd3fa7 100644 --- a/src/server/main/server-main.cpp +++ b/src/server/main/server-main.cpp @@ -47,7 +47,7 @@ T* registerSocketService(SecurityManager::SocketManager &manager, { T *service = NULL; try { - service = new T(); + service = new T(T::Offline::no); service->RegisterChannel(std::move(channel)); service->Start(); manager.RegisterSocketService(service); diff --git a/src/server/rules-loader/security-manager-rules-loader.cpp b/src/server/rules-loader/security-manager-rules-loader.cpp new file mode 100644 index 0000000..8b2d120 --- /dev/null +++ b/src/server/rules-loader/security-manager-rules-loader.cpp @@ -0,0 +1,1077 @@ +/* + * Copyright (c) 2018 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +#include +#include +#include // dprintf +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include // SMACK_LABEL_LEN +#include + +#include +#include + +namespace { + +// header generated by db/generate.sh +// contains: +// +// // current database version +// constexpr int32_t dbVersion; +// +// // stringification of db/db.sql (database schema) +// constexpr char dbSchema[]; +// +// // stringifications of db/updates/update-db-to-v{1..dbVersion} (database update sql scripts) +// constexpr const char *dbUpdateScript[dbVersion]; +#include "../gen/db.h" + +template +constexpr size_t arraySize(T (&)[S]) { + return S; +} + +static_assert(dbSchema); +// this ensures that parsing the sql files was done correctly and we have proper +// number of update scripts +static_assert(dbVersion == arraySize(dbUpdateScript)); +static_assert(allTrue(dbUpdateScript)); + +// the loader is compiled in two variants +// * the default (TEST_LOADER not defined) +// * for testing (TEST_LOADER defined) +enum : bool { testLoader = +#ifdef TEST_LOADER + true +#else + false +#endif +}; + +enum { + //TODO boost test doesn't seem to reliably handle fork/exec/waitpid for binaries + // returning anything other than EXIT_SUCCESS + // at the time of this writing, the reason/fix are unknown, thus the workaround + // that forces the test loader to always succeed + exitFailure = testLoader ? EXIT_SUCCESS : EXIT_FAILURE, + + // stock loader writes rules directly to load2, test loader to stdout + load2fd = testLoader ? 1 : 0 +}; + +enum : size_t { + // page size hardcoded to allow use with compile-time constructs + pageSize = 4096 +}; + +// created if the db (not fallback) is deemed ok, removed when forcing fallback +constexpr const char *dbOkMarker = testLoader ? TEST_DB_OK_MARKER : DB_OK_MARKER; + +// most functions are forcedly inlined to prevent the dumb gcc from harming performance +#define inl inline __attribute__((always_inline)) + +#define likely(x) __builtin_expect(bool(x), true) +#define unlikely(x) __builtin_expect(bool(x), false) + +static_assert(SMACK_LABEL_LEN <= 255U, + "code written with one byte label length in mind - not the case anymore, need change"); + + +// string view (O(1) memory pointer to a string with no trailing \0) +// +// Views are used to avoid data copying and foster memory locality. +// The lack of trailing \0 allows use of overlapping substrings, allowing even better locality. +struct StrView { + // pointer to the string's first character + const char *str; + // the string's size (0 denotes an empty string) + size_t size; + + // uninitialized by default + inl StrView() = default; + + // initialize from a string with a given size + inl StrView(const char *string, size_t stringSize) + : str(string), size(stringSize) {} + + // initialize from a string literal of size N (known at compile time) + // the last byte is assumed to be the trailing \0 + template + inl StrView(const char (&stringLiteral)[N]) + : str(stringLiteral), size(N - 1 /* drop the trailing 0 */) {} +}; + +// write to stderr +#define toStderr(FMT, ...) do {\ + dprintf(2, FMT "\n", ##__VA_ARGS__);\ +} while (0) + +#define fail(FMT, ...) do {\ + toStderr(FMT, ##__VA_ARGS__);\ + exit(exitFailure);\ +} while (0) + +// copy the memory pointed to be a string view, return its size +inl size_t cpy(void *buf, const StrView &s) { + auto sz = s.size; + memcpy(buf, s.str, sz); + return sz; +} + +// copy multiple string views in sequence, return their total size +template +inl size_t cpy(void *buf, const StrView &s0, const StrView &s1, const T&...args) { + auto sz = cpy(buf, s0); + return sz + cpy(reinterpret_cast(uintptr_t(buf) + sz), s1, args...); +} + +// memcpy a compile-time-sized array +template +inl void acpy(void *buf, const T (&a)[S]) { + memcpy(buf, a, sizeof(T) * S); +} + + +// load2 accepts at most pageSize bytes fed into a single write, provided the last byte is not '\n' +// in order for writing to load2 to be quick, a page-aligned global buffer is used +// +// rules are written to the buffer one by one, the buffer being flushed on overflow +// after the page-sized buffer comes a rule-wide piece of scratch space +// every rule is completely written to the buffer first, overflow is checked afterwards +// the method outperforms the check-size-before-write in practice (tighter code, less register pressure) +// +// all subsequent global variables come after the buffer to possibly land on the same page as the scratch +// most variables are global to decrease register pressure and prevent gcc from dropping perf + +// page-aligned buffer with some trailing surplus +alignas(pageSize) char writeBuf[pageSize + 2 * (SMACK_LABEL_LEN + 1) + 10]; +// number of characters in writeBuf +size_t writeLen; + +// write the first len bytes from writeBuf to load2fd +inl void doWrite(size_t len) { + // every rule is '\n'-terminated + // drop the trailing '\n' from the buffer when actually writing to load2 + // why? load2 doesn't accept page-sized writes with a trailing '\n' + if (!testLoader) + len--; + + // retry write in the case of EINTR + ssize_t r; + while ((r = write(load2fd, writeBuf, len)) < 0 && EINTR == errno); + + // it's a sub-pageSize write to load2, guaranteed to fully succeed if ok at all + if (unlikely(len != size_t(r))) + fail("write to load2 failed"); +} + +// a non-'\0'-terminated string of size at most N, usable as a string view +template +struct SmallString { + // string size + size_t size; + // the actual string characters + char str[N]; + + // construct from a sequence of string views + template + inl SmallString(const T &...args) { + size = ::cpy(str, args...); + } + + // append a sequence of string views + template + inl void cpy(const T &...args) { + size += ::cpy(str + size, args...); + } + + // implicitly convert to a string view + inl operator StrView() const { + return {str, size}; + } +}; + +// string memory (not \0-terminated) used to contain labels for rules +// it's always prefixed with "User::Pkg::" (all labels start with the prefix) +// at its widest, for a hybrid package $pkg application $pp, the string may be +// "User::Pkg::$pkg::App::$app User::Pkg::" +// +// the trailing " User::Pkg::" is an optimization to increase memory locality +// and minimize the number of distinct memcpy invocations +// +// the label (even the full "User::Pkg::$pkg::App::$app") takes up at most SMACK_LABEL_LEN bytes +// the trailing suffix accounts for subsequent bytes +SmallString pkgL("User::Pkg::"); + + +// write a rule (given as a sequence of string views) to writeBuf +template +inl void rule(const T &...args) { + auto oldLen = writeLen; + // copy to the buffer + writeLen += cpy(writeBuf + writeLen, args...); + // terminate with a newline + writeBuf[writeLen++] = '\n'; + + // on overflow + if (unlikely(writeLen > pageSize)) { + doWrite(oldLen); + auto newLen = writeLen - oldLen; + // move the rule to writeBuf's beginning + writeLen = cpy(writeBuf, {writeBuf + oldLen, newLen}); + } +} + +// align a value up to be a multitude of a given alignment +template +constexpr T align(T value, T alignment) { + return (value + alignment - T(1)) & ~(alignment - T(1)); +} + +// page-grained growable tapes +// +// processing done by the loader amounts to a set of linear passes through a constant number of variable-width vectors +// the number of passes is linear, yielding quadratic output +// +// in order to make processing faster and improve memory locality, the PageTape is used in place of a vector +// all data is stored/retrieved sequentially left-to-right with no pointer jumps +// buffers are allocated/reallocated in page-sized granularity, playing well with the page cache +// +// thanks to the way paged virtual memory works, multiple PageTape instances can be initialized with a single mmap +// growing a PageTape involves calling mremap (reserved size is doubled every time) +struct PageTape { + // current size (0 for an empty tape) and reserved size (a multitude of pageSize) + size_t size, reserved; + // pointer to the first page + char *t; + + // initialize size to 0 and reserved size to n pages, DO NOT mmap (will be done later via init(PageTape...&)) + explicit inl PageTape(size_t nPages) + : size(0), reserved(nPages * pageSize) {} + + // reserve an additional number of bytes (more < pageSize) + inl void reserve(size_t more) { + if (unlikely(size + more > reserved)) + expand(); + } + + // empty the tape + inl void clear() { + size = 0; + } + + // align size up to alignment <= pageSize + inl void align(size_t alignment) { + size = ::align(size, alignment); + } + + // append string view contents (NOT length) + inl PageTape &operator+=(const StrView &s) { + size += cpy(t+size, s); + return *this; + } + + // append an integral value (size must be properly aligned beforehand, possibly via align(alignof(T))) + template + inl PageTape &operator+=(const T &v) { + static_assert(std::is_integral::value, "only integral types supported"); + *reinterpret_cast(t + size) = v; + size += sizeof(T); + return *this; + } + + // a handle for sequential tape traversal (the tape is assumed read only throughout the handle's lifetime) + struct Iterator { + // current pointer (aka cursor) and the end (stl-like convention) + uintptr_t ptr, end; + + // whether the iterator is not yet at an end + inl explicit operator bool() const { + return ptr < end; + } + + // align cursor up to alignment + inl void align(size_t alignment) { + ptr = ::align(ptr, alignment); + } + + // retrieve an integral value the cursor points to, adjust the cursor + // proper alignment must be ensured beforehand, possibly via align(alignof(T)) + template + inl T get() { + size_t oldPtr = ptr; + ptr += sizeof(T); + return *reinterpret_cast(oldPtr); + } + + // retrieve a single byte (convenience shorthand), adjust cursor + inl uint8_t operator()() { + return get(); + } + + // retrieve a string view of a given size, adjust cursor + // since all string views we're dealing with here have one byte length (SMACK_LABEL_LEN <= 255U), + // one can usually store a string view length first, then retrieve it via iterator(iterator()) + // a commonly used idiom + inl StrView operator()(size_t s) { + auto oldPtr = ptr; + ptr += s; + return {reinterpret_cast(oldPtr), s}; + } + + // advance cursor by n bytes + inl void operator+=(size_t n) { + ptr += n; + } + }; + + // create an iterator to the tape's contents + inl Iterator start() const { + return {uintptr_t(t), uintptr_t(t) + size}; + } + + // initialize multiple tapes with a single mmap based on reserved sizes they were constructed with + template + static inl void init(T &...args) { + size_t size = totalReserved(args...); + auto p = mmap(nullptr, size, PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_PRIVATE|MAP_POPULATE, -1, 0); + if (unlikely(MAP_FAILED == p)) + fail("mmap failed"); + storePtrs(p, args...); + } + +private: + // expand the tape by doubling its reserved space + void expand() { + const auto oldReserved = reserved; + reserved *= 2U; + t = static_cast(mremap(t, oldReserved, reserved, MREMAP_MAYMOVE)); + if (unlikely(MAP_FAILED == t)) + fail("mremap failed"); + } + + static inl size_t totalReserved() { + return 0; + } + + // total reserved size of a sequence of PageTape references + template + static inl size_t totalReserved(const PageTape &t, const T &...args) { + return t.reserved + totalReserved(args...); + } + + static inl void storePtrs(void *) { + } + + // distribute a contiguous block of memory pages among a sequence of PageTape references + template + static inl void storePtrs(void *p, PageTape &t, T &...args) { + t.t = static_cast(p); + storePtrs(reinterpret_cast(uintptr_t(p) + t.reserved), args...); + } +}; + +// when initializing the database, the memory is used as a single filename +// when writing rules: metadata of all packages (see 'PkgFlag definition below for the format) +PageTape pkgsInfo{3}; + +// package information is retrieved via "SELECT name, shared_ro, is_hybrid, author_id, pkg_id FROM pkg ORDER BY pkg_id" +// the result is scanned row by row and serialize into the pkgsInfo tape as follows: +// uint8_t flags = PkgFlag::sharedRO * bool(shared_ro) + PkgFlag::hybrid * bool(is_hybrid) + PkgFlag::authorLen * strlen(to_string(author_id)) +// if author_id not null +// char[strlen(to_string(author_id))] to_string(author_id) // no length (stored already in flags) nor trailing \0 +// uint8_t strlen(name) +// char[strlen(name)] name // no trailing \0 +// if is_hybrid +// align up to alignof(sqlite3_int64) +// sqlite3_int64 pkg_id +// pkg_id is only needed for hybrid packages in order to do a linear join with appq to retrieve applications for a given package +enum PkgFlag : uint8_t { + sharedRO = 1, + hybrid = 2, + authorLen = 4, +}; + + +// names of sharedRO packages, each stored as: +// uint8_t nameLen +// char[nameLen] name +PageTape sharedROPkgLabels{2}; + +// when gathering package information: author identifier dictionary; each entry stored as: +// sqlite3_int64 author_id +// uint8_t strlen(to_string(author_id)) +// char[strlen(to_string(author_id))] to_string(author_id) // no trailing \0 +// // align up to alignof(sqlite3_int64) for the next entry +// +// when writing package rules: process label suffixes for the current package +// non-hybrid: +// uint8_t 0 // there's only one process label +// hybrid: +// for each app_name +// uint8_t strlen(app_name) +// char[strlen(app_name)] app_name +PageTape pkgLabels{1}; + + +// string view denoting a pkgL prefix (at least "User::Pkg::$pkgName") +// length changes depending on the context +StrView pl{pkgL.str, 0}; + +// stringification of author_id +// ("User::Author::" + pathTrusted) corresponds to the ~PATH_TRUSTED~ rule template variable +StrView pathTrusted; + +// "User::Pkg::$pkgName::App::" (a string view into pkgL) +// (pkgLApp + $appName) corresponds to the ~PROCESS~ rule template variable for hybrid packages +// used when writing app-to-app rules within the same package +StrView pkgLApp{pkgL.str, 0}; + +// valid only for sharedRO packages +// a contiguous 0.. index of said package as sorted by pkg_id +size_t sharedROPkgIndex = ~size_t(0); + +// temporary buffer for decimal author_id stringification (not \0-terminated) +char authorBuf[std::numeric_limits::digits10 + 1]; + + +// rule template code generated by sh$ policy/generate-rule-code policy/*.smack +// included in anonymous namespace at this particular point to: +// * detect unused functions +// * let the functions access global string views, namely pkgL, pathTrusted and pl +// +// contains the following functions (described at call sites): +// void rulesAuthor(); +// void rulesPkgLabelAuthor(); +// void rulesPkgLabel(); +// void rulesPathRWHybridOnly(); +// void rulesPath(); +// void rulesSharedRO(); +#include "../gen/generated-rule-templates.h" + + +// database file path +const char *dbPath; + +// database handle +sqlite3 *db; + +// sqlite statements +sqlite3_stmt *verq; // user version query +sqlite3_stmt *intq; // integrity check +sqlite3_stmt *keyq; // foreign key check +sqlite3_stmt *pkgq; // package information +sqlite3_stmt *appq; // application information + +enum class FailOnErr : bool { no, yes }; + +// retrieve a row from an sqlite statement +// FailOnErr::no is used during database bringup, yes is used during rule writing +inl bool row(sqlite3_stmt *stmt, FailOnErr failOnErr = FailOnErr::yes) { + int ret; + while ((ret = sqlite3_step(stmt)) == SQLITE_BUSY); + if (SQLITE_ROW == ret) + return true; + if (underlying(failOnErr) && unlikely(SQLITE_DONE != ret)) + fail("sqlite3_step failed"); + return false; +} + +// convenience wrapper around sqlite3_column_text (which returns const unsigned char *) +inl auto colStr(sqlite3_stmt *stmt, int i) { + return reinterpret_cast(sqlite3_column_text(stmt, i)); +} + +// prepare an sqlite statement +inl sqlite3_stmt *prep(const StrView &query) { + sqlite3_stmt *s; + if (unlikely(SQLITE_OK != sqlite3_prepare_v2(db, query.str, query.size, &s, nullptr))) + return nullptr; + return s; +} + +// execute an sqlite statement with no results expected +inl bool dbExec(const char *query) { + int r; + while (SQLITE_BUSY == (r = sqlite3_exec(db, query, nullptr, nullptr, nullptr))); + return SQLITE_OK == r; +} + +inl bool fileEmpty(const char *filePath) { + struct stat st; + return !lstat(filePath, &st) && !st.st_size; +} + +// open dbPath, potentially migrating to newer version and applying schema, then checking integrity +// return true iff successful +bool dbUp() { + if (unlikely(SQLITE_OK != sqlite3_open_v2(dbPath, &db, SQLITE_OPEN_READWRITE|SQLITE_OPEN_NOMUTEX|SQLITE_OPEN_PRIVATECACHE, nullptr))) { + toStderr("db open (%s) failed", dbPath); + return false; + } + + // good for robustness in the face of unlikely poweroffs, must be turned on per connection + if (unlikely(!dbExec("PRAGMA synchronous=EXTRA"))) { + toStderr("pragma synchronous failed"); + return false; + } + + // prevent thrashing the database file just in case + if (unlikely(!dbExec("PRAGMA temp_store=MEMORY"))) { + toStderr("pragma temp_store failed"); + return false; + } + + // retrieve user_version + verq = prep("PRAGMA user_version"); + if (unlikely(!verq) || unlikely(!row(verq, FailOnErr::no))) { + toStderr("pragma user_version failed"); + return false; + } + auto version = sqlite3_column_int(verq, 0); + + // version outdated, exec update scripts and reapply schema + if (unlikely(dbVersion != version)) { + // version is an int32_t but it must be in the 0..dbVersion range - can check both bounds by casting to register-wise unsigned + if (unlikely(size_t(version) > size_t(dbVersion))) { + toStderr("db version in the future - downgrade not supported"); + return false; + } + + // finalize all statements, otherwise modifying the db will be blocked + sqlite3_finalize(verq); + verq = nullptr; + + // an empty database file is recognized by sqlite as having user_version 0 but needs no update + // merely applying the schema is enough in that case + if (version || !fileEmpty(dbPath)) { + // update-db-to-v$i.sql is stored as dbUpdateScript[$i - 1] + do { + if (unlikely(!dbExec(dbUpdateScript[version]))) { + toStderr("db update script from version (%u) failed", unsigned(version)); + return false; + } + } while (++version < dbVersion); + } + + // apply schema + if (!dbExec(dbSchema)) { + toStderr("db schema failed"); + return false; + } + } + + // successful integrity check returns a row containing the string "ok" + intq = prep("PRAGMA integrity_check"); + if (unlikely(!intq) || unlikely(!row(intq, FailOnErr::no))) { + toStderr("pragma integrity_check failed"); + return false; + } + const auto intr = colStr(intq, 0); + if (unlikely(!intr) || unlikely(strcmp(intr, "ok"))) { + toStderr("pragma integrity_check failed"); + return false; + } + + // successful foreign key check returns no rows + keyq = prep("PRAGMA foreign_key_check"); + if (unlikely(!keyq) || unlikely(row(keyq, FailOnErr::no))) { + toStderr("pragma foreign_key_check failed"); + return false; + } + + // start a transaction to encompass subsequent SELECTs + if (unlikely(!dbExec("BEGIN"))) { + toStderr("begin transaction failed"); + return false; + } + + // the fastest discovered method for obtaining all package and associated application info + // order both packages and application by pkg_id by performing exactly two SELECT queries + // results of both queries can then be processed in sorted order to achieve a linear complexity join + + // package metadata query sorted by pkg_id + if (unlikely(!(pkgq = prep("SELECT name, shared_ro, is_hybrid, author_id, pkg_id FROM pkg ORDER BY pkg_id")))) { + toStderr("package metadata query preparation failed"); + return false; + } + + // application names query sorted by pkg_id + // application names are only relevant for hybrid packages (each name spawn a different process label then) + if (unlikely(!(appq = prep("SELECT app.pkg_id, app.name FROM app, pkg WHERE app.pkg_id = pkg.pkg_id AND pkg.is_hybrid != 0 ORDER BY app.pkg_id")))) { + toStderr("application metadata query preparation failed"); + return false; + } + + return true; +} + + +// prepare pkgsInfo and sharedROPkgLabels tapes, emit author-only rules +inl void readPkgsInfoAndEmitAuthors() { + while (row(pkgq)) { + // retrieve package name (the only text column, valid until row(pkgq) is executed) + const auto name = colStr(pkgq, 0); + const auto nameLen = strlen(name); + // make sure the label ("User::Pkg::" + name) is not longer than SMACK_LABEL_LEN + if (unlikely(nameLen > SMACK_LABEL_LEN - (sizeof "User::Pkg::" - 1))) + fail("package name too long to accommodate smack label"); + // string view into the name to prevent length recalculation + const StrView nv{name, nameLen}; + + // if it's a sharedRO package, add its name to the sharedROPkgLabels tape + const bool sharedRO = sqlite3_column_int(pkgq, 1); + if (sharedRO) { + sharedROPkgLabels.reserve(1 + nameLen); + sharedROPkgLabels += uint8_t(nameLen); + sharedROPkgLabels += nv; + } + + // start constructing pkgFlags + const bool hybrid = sqlite3_column_int(pkgq, 2); + const bool haveAuthor = SQLITE_INTEGER == sqlite3_column_type(pkgq, 3); + size_t pkgFlags = sharedRO * PkgFlag::sharedRO + hybrid * PkgFlag::hybrid; + + // the package has an author - retrieve its stringification and emit author-only rules + // + // author_id stringification is expensive, particularly because author_id is an sqlite_int64 even on 32-bit platforms + // author-only rules should also be emitted just once for a particular author_id to avoid duplication and its runtime impact + // + // since the expected number of authors is small (say < 200 or so) and typically extremely small (< 50), + // pkgLabels is used as a linear dictionary holding (authorId, to_string(authorId)) tuples + // every distinct authorId is stringified exactly once + if (haveAuthor) { + const auto authorId = sqlite3_column_int64(pkgq, 3); + + // search for authorId in the linear dictionary + bool found = false; + auto auit = pkgLabels.start(); + while (auit) { + const auto id = auit.get(); + // string view into a stringification already in the tape + pathTrusted = auit(auit()); + if (authorId == id) { + found = true; + break; + } + auit.align(alignof(decltype(authorId))); + } + + // not found - add to dictionary + if (unlikely(!found)) { + // stringify into authorBuf right-to-left + size_t len = 0; + typename std::make_unsigned::type>::type n = authorId; + do { + authorBuf[sizeof authorBuf - 1 - len++] = '0' + n % 10U; + } while (n /= 10U); + + // make pathTrusted point to the stringification + pathTrusted.str = authorBuf + (sizeof authorBuf - len); + pathTrusted.size = len; + + // emit author rules now that pathTrusted denotes a newly discovered author + // rules contain author as the sole variable, ex. "User ~PATH_TRUSTED~ rwxat" + // + // ~PATH_TRUSTED~ == "User::Author::" + pathTrusted + rulesAuthor(); + + // append the (authorId,pathTrusted) tuple to the linear dictionary + pkgLabels.reserve(2 * sizeof authorId + sizeof authorBuf); + pkgLabels += authorId; + pkgLabels += uint8_t(len); + pkgLabels += pathTrusted; + pkgLabels.align(alignof(decltype(authorId))); + } + + // add authorId stringification length to pkgFlags + pkgFlags += pathTrusted.size * PkgFlag::authorLen; + } + + // more precise calculations would be wasteful because padding and author len + pkgsInfo.reserve(1 + sizeof authorBuf + 1 + SMACK_LABEL_LEN + alignof(sqlite3_int64) - 1 + sizeof(sqlite3_int64)); + + // add package info to the pkgsInfo tape + pkgsInfo += uint8_t(pkgFlags); + if (haveAuthor) + pkgsInfo += pathTrusted; + pkgsInfo += uint8_t(nameLen); + pkgsInfo += nv; + if (hybrid) { + pkgsInfo.align(alignof(decltype(sqlite3_column_int64(pkgq, 4)))); + pkgsInfo += sqlite3_column_int64(pkgq, 4); + } + } +} + + +// join packages and associated applications, writing rules on the fly +inl void emitPkgLabelRules() { + auto pkit = pkgsInfo.start(); + bool haveRow = row(appq); + do { + // pkgLabels will now store process label suffixes for the package at pkit + pkgLabels.clear(); + + // deserialize package metadata entry from pkgsInfo + const size_t pkgFlags = pkit(); + const bool sharedRO = pkgFlags & PkgFlag::sharedRO; + const auto authorLen = pkgFlags / PkgFlag::authorLen; + if (authorLen) { + pathTrusted.str = reinterpret_cast(pkit.ptr); + pathTrusted.size = authorLen; + pkit += authorLen; + } + + // put pkg.name right after "User::Pkg::" in pkgL so that pkgL = "User::Pkg::" + pkg.name + // this corresponds to ~PATH_RW~ (or ~PROCESS~ for non-hybrid packages)) + pkgL.cpy(pkit(pkit())); + // pl now stands for ~PROCESS~ aka "User::Pkg::" + pkg.name (for non-hybrid packages) + pl.size = pkgL.size; + + if (pkgFlags & PkgFlag::hybrid) { + // a hybrid package - each application spawns a ~PROCESS~ label + // add "::App::" suffix to the end of pl (pl = "User::Pkg::" + pkg.name + "::App::") + pl.size += cpy(pkgL.str + pkgL.size, "::App::"); + + // retrieve pkg_id in order to get application names for this package + // (packages and applications are ordered by pkg_id ascending) + pkit.align(alignof(sqlite3_int64)); + const auto pkgId = pkit.get(); + + // while there are more applications and the first one belongs to this package + while (likely(haveRow) && likely(pkgId == sqlite3_column_int64(appq, 0))) { + // get the application's name + const auto str = colStr(appq, 1); + const auto len = strlen(str); + + // ("User::Pkg::" + pkg.name + "::App::" + app.name) must not be longer than SMACK_LABEL_LEN + if (unlikely(len > SMACK_LABEL_LEN - pl.size)) + fail("app name too long to accommodate smack label"); + + // add app.name to pkgLabels + pkgLabels.reserve(1 + len); + pkgLabels += uint8_t(len); + pkgLabels += StrView{str, len}; + + // retrieve next row to move to the next application + haveRow = row(appq); + } + + // rules containing PATH_RW as the sole variable, ex. "System ~PATH_RW~ rwxat" + // that are not emitted for non-hybrid packages to avoid duplication wrt rulesPkgLabel() + // due to already being covered by PROCESS-based rules + // ("System ~PROCESS~ rwxat" already emits the same rule) + // + // ~PATH_RW~ == pkgL + rulesPathRWHybridOnly(); + } else { + // a non-hybrid package - there's just one empty ~PROCESS~ suffix + pkgLabels += uint8_t(0); + } + + // package labels now are: map (pl+) pkgLabels + + // a sharedRO package + if (sharedRO) { + // increase index of this shared ro package wrt sharedROPkgLabels tape + sharedROPkgIndex++; + + // rules containing PATH_SHARED_RO as the sole variable, ex. "User ~PATH_SHARED_RO~ rwxat" + // + // ~PATH_SHARED_RO~ = pkgL + "::SharedRO" + rulesSharedRO(); + } + + // rules containing PATH_RO or PATH_RW as the sole variable, ex. "System ~PATH_RO~ rwxat" + // + // ~PATH_RW~ == pkgL + // ~PATH_RO~ == pkgL + "::RO" + rulesPath(); + + // 0.. contiguous index of label suffix wrt pkgLabels + size_t sub = 0; + + // string view initialized once, only used/valid for hybrid packages + // pkgLApp = "User::Pkg::" + pkg.name + "::App::" + pkgLApp.size = pkgL.size + sizeof("::App::") - 1; + + // iterate over ~PROCESS~ labels for the current package + auto plit = pkgLabels.start(); + while (likely(plit)) { + // retrieve ~PROCESS~ label suffix ("" for non-hybrid, app.name for hybrid) + const auto plTail = plit(plit()); + + // pl = ~PROCESS~ + // "User::Pkg::" + pkg.name for non-hybrid + // "User::Pkg::" + pkg.name + "::App::" + app.name for hybrid + pl.size += cpy(pkgL.str + pl.size, plTail); + + // place " User::Pkg::" at end of pkgL memory + // raw pkgL memory now starts with "~PROCESS~ User::Pkg::" + cpy(pkgL.str + pl.size, " User::Pkg::"); + + // pl = "~PROCESS~ " (by expanding by one byte to include the space appended just above) + pl.size++; + + // rules containing 2 variables: PATH_TRUSTED and PROCESS, ex. ~PROCESS~ ~PATH_TRUSTED~ rwxat + // + // "~PROCESS~ " == pl + // ~PATH_TRUSTED~ == "User::Author::" + pathTrusted + if (authorLen) + rulesPkgLabelAuthor(); + + // rules containing PROCESS as one of its variables, ex. "System ~PROCESS~ rwxat" or "~PROCESS~ ~PATH_RW~ rwxat" + // + // "~PROCESS~ " == pl + // ~PATH_RW~ == pkgL + // ~PATH_RO~ == pkgL + "::RO" + rulesPkgLabel(); + + // application-to-different-application rules within the package + size_t obj = 0; + auto obit = pkgLabels.start(); + do { + // l = app.name + const auto l = obit(obit()); + // "~PROCESS~ User::Pkg::" + pkg.name + "::App::" + app.name + " rwxat" + if (sub != obj) + rule(pl, pkgLApp, l, " rwxat"); + obj++; + } while (obit); + + // pl = "~PROCESS~ User::Pkg::" + // + // pl used to be == "~PROCESS~ " (with a trailing space), a string view pointing into a pkgL prefix + // pkgL already had "User::Pkg::" appended after the prefix + // all one needs to do is bump up pl.size to include the extra characters + // + // the optimization allows dropping one memcpy from the sharedRO rule loop, + // making the dominating (quadratically exploding) loop in the whole loader slightly tighter + pl.size += sizeof("User::Pkg::")-1; + + // shared ro rules + auto roit = sharedROPkgLabels.start(); + // index of shared ro package wrt sharedROPkgLabels + size_t pkgk = 0; + while (likely(roit)) { + rule(pl, roit(roit()), + // self-rules get rwxat, others rxl + // + // pkgk and sharedROPkgIndex are both 0.. contiguous indices into sharedROPkgLabels + // sharedROPkgIndex is only valid for sharedRO packages + // thus, it's a self-rules iff sharedRO && sharedROPkgIndex == pkgk + // sharedROPkgIndex == pkgk is less likely to occur, thus put first together with pkgk incrementation + unlikely(sharedROPkgIndex == pkgk++) && sharedRO + ? StrView{"::SharedRO rwxat"} + : StrView{"::SharedRO rxl"}); + } + + // strip ~PROCESS~ label suffix and " User::Pkg::" from pl before the next loop step + pl.size -= plTail.size + sizeof("User::Pkg::"); + sub++; + } + pkgL.size = sizeof("User::Pkg::")-1; // "User::Pkg::" + } while (pkit); +} + + +inl void overwriteDbFileWithFallback(size_t dbPathLen) { + // finalize all prior statements before closing database connection (nullptr is harmless) + for (const auto q : {verq, intq, keyq, pkgq, appq}) + sqlite3_finalize(q); + + // close the database connection before overwriting the database file + if (unlikely(SQLITE_OK != sqlite3_close(db))) + fail("closing db failed"); + + // retrieve fallback database file path + // WARNING: dbPath memory has been made invalid (will restore later) due to tzplatform_mkpath* using a shared scratch buffer + char const *fallbackPath = testLoader + ? TEST_PRIVILEGE_FALLBACK_DB_PATH + : tzplatform_mkpath3(TZ_SYS_RO_SHARE, "security-manager", ".security-manager.db"); + if (unlikely(!fallbackPath)) + fail("tzplatform_mkpath(fallback) failed"); + + // open the fallback database regular file and get its size + const int fallbackfd = open(fallbackPath, O_RDONLY); + if (unlikely(fallbackfd < 0)) + fail("open fallback (%s) failed", fallbackPath); + struct stat fallbackSt; + if (unlikely(fstat(fallbackfd, &fallbackSt)) || unlikely(!S_ISREG(fallbackSt.st_mode))) + fail("fallback not a regular file"); + + // truncate main database journal to avoid stale data + acpy(pkgsInfo.t + dbPathLen, DB_JOURNAL_SUFFIX); + if (unlikely(creat(pkgsInfo.t, 0644) < 0)) + fail("journal truncation failed"); + + // restore dbPath (now points to the copy stored in pkgsInfo) + pkgsInfo.t[dbPathLen] = '\0'; + dbPath = pkgsInfo.t; + + // truncate the main database file + const int dbfd = creat(dbPath, 0644); + if (unlikely(dbfd < 0)) + fail("db truncation failed"); + + // overwrite with the fallback file + while (fallbackSt.st_size) { + auto r = sendfile(dbfd, fallbackfd, nullptr, fallbackSt.st_size); + if (unlikely(r <= 0)) + fail("sendfile failed"); + fallbackSt.st_size -= r; + } + + // close the database descriptor before opening the database connection + if (unlikely(close(dbfd))) + fail("closing dbfd failed"); +} + +// command line options +bool fallbackOnly; +bool noLoad2; + +#define HELP "usage:\n"\ +" security-manager-rules-loader : initialize database (potentially with fallback), write rules to load2\n"\ +" security-manager-rules-loader f|fallback-only : initialize database with fallback, write rules to load2\n"\ +" security-manager-rules-loader n|no-load : initialize database (potentially with fallback)\n" + +inl void parseOptions(int argc, char *argv[]) { + // parse command line arguments (as outlined by the help string literal) + if (unlikely(argc > 1)) + // just check the first letter, the rest is window dressing + switch (argv[1][0]) { + // skip main database bringup and start applying fallback right away + // + // the option is used by the manager if a fallback has not been applied yet + // ("database successfully recovered" marker file does not exist) + // and a schema error has been detected + case 'f': + fallbackOnly = true; + break; + // just perform the database bringup, do not write rules + // used as a replacement for the security-manager-init-db binary of old; potentially invoked during image creation + case 'n': + noLoad2 = true; + break; + // argument's first letter unknown - print help + default: + fail(HELP); + } +} + +inl bool unlinkIfExists(const char *path) { + return likely(!unlink(path)) || ENOENT == errno; +} + +} // namespace + +int main(int argc, char *argv[]) { + // test loader uses hardcoded database paths but otherwise strives to stay as faithful to the stock loader as feasible + dbPath = testLoader + ? TEST_DB_PATH + : tzplatform_mkpath(TZ_SYS_DB, ".security-manager.db"); + if (unlikely(!dbPath)) + fail("tzplatform_mkpath(.security-manager.db) failed"); + + const auto dbPathLen = strlen(dbPath); + + // need the following filenames: + // dbPath : to open the database + // dbPath + DB_RECOVERED_SUFFIX : to remove/create the "database successfully recovered" marker file + // dbPath + DB_JOURNAL_SUFFIX : to potentially truncate the journal file when overwriting database with fallback + // + // the pkgsInfo tape memory is used to store these names (the tape is otherwise unused during database bringup) + const auto maxFilenameSize = dbPathLen + max(sizeof DB_RECOVERED_SUFFIX, sizeof DB_JOURNAL_SUFFIX); + // adjust the pkgsInfo tape size if needed to accommodate filename size + if (unlikely(maxFilenameSize > pkgsInfo.reserved)) + pkgsInfo.reserved = align(maxFilenameSize, underlying(pageSize)); + + // initialize all tapes in one fell swoop + PageTape::init(pkgLabels, sharedROPkgLabels, pkgsInfo); + + // pkgsInfo.t = dbPath (\0-unterminated) + memcpy(pkgsInfo.t, dbPath, dbPathLen); + // pkgsInfo.t = dbPath + DB_RECOVERED_SUFFIX (\0-terminated) + acpy(pkgsInfo.t + dbPathLen, DB_RECOVERED_SUFFIX); + // remove the "database successfully recovered" marker file (it's not supposed to survive reboot but is stored on permanent flash) + if (unlikely(!unlinkIfExists(pkgsInfo.t))) + fail("unlink(db" DB_RECOVERED_SUFFIX ") failed"); + + parseOptions(argc, argv); + + // try to bring up the main database if fallback-only was not requested, otherwise remove tmpfs marker to avoid ambiguity + bool mainDbUp = false; + if (likely(!fallbackOnly)) + mainDbUp = dbUp(); + else if (unlikely(!unlinkIfExists(dbOkMarker))) + fail("unlink(dbOkMarker) failed"); + + if (likely(mainDbUp)) { + // if the main database is up, create the "main database ok" tmpfs marker + if (unlikely(0 > creat(dbOkMarker, 0644))) + fail("creat(dbOkMarker) failed"); + } else { + // main db initialization failed or not attempted, apply fallback + toStderr("overwriting db with fallback"); + overwriteDbFileWithFallback(dbPathLen); + + // try to bring up the restored database + if (unlikely(!dbUp())) + fail("fallback db bringup failed"); + + // create the "database successfully recovered" marker file + acpy(pkgsInfo.t + dbPathLen, DB_RECOVERED_SUFFIX); + if (unlikely(creat(pkgsInfo.t, 0644) < 0)) + fail("creat(.security-manager.db" DB_RECOVERED_SUFFIX ") failed"); + } + + // database bringup successful, stop now if not going to write rules + if (unlikely(noLoad2) || (testLoader && unlikely(fallbackOnly))) + return EXIT_SUCCESS; + + // open load2 for writing + if (!testLoader) { + close(load2fd); + if (load2fd != open("/sys/fs/smackfs/load2", O_WRONLY)) + fail("open(load2) failed"); + } + + // prepare pkgsInfo and sharedROPkgLabels tapes, emit author-only rules + readPkgsInfoAndEmitAuthors(); + + // nothing to do if the database contains no packages + if (unlikely(!pkgsInfo.size)) + return EXIT_SUCCESS; + + // join packages and associated applications, writing rules on the fly + emitPkgLabelRules(); + + if (likely(writeLen)) + doWrite(writeLen); + return EXIT_SUCCESS; +} diff --git a/src/server/service/base-service.cpp b/src/server/service/base-service.cpp index 131aa77..a650d63 100644 --- a/src/server/service/base-service.cpp +++ b/src/server/service/base-service.cpp @@ -35,7 +35,8 @@ namespace SecurityManager { -BaseService::BaseService() +BaseService::BaseService(Offline offline) + : serviceImpl(offline) { } diff --git a/src/server/service/include/base-service.h b/src/server/service/include/base-service.h index e2205a9..b8e042b 100644 --- a/src/server/service/include/base-service.h +++ b/src/server/service/include/base-service.h @@ -44,7 +44,8 @@ class BaseService : public SecurityManager::ServiceThread { public: - BaseService(); + using Offline = ServiceImpl::Offline; + explicit BaseService(Offline offline); virtual ServiceDescriptionVector GetServiceDescription() = 0; DECLARE_THREAD_EVENT(AcceptEvent, accept) diff --git a/src/server/service/include/service.h b/src/server/service/include/service.h index ebfd1d3..b4db154 100644 --- a/src/server/service/include/service.h +++ b/src/server/service/include/service.h @@ -42,7 +42,8 @@ class Service : public SecurityManager::BaseService { public: - Service(); + using Offline = BaseService::Offline; + explicit Service(Offline offline); ServiceDescriptionVector GetServiceDescription(); void RegisterChannel(Channel channel) { serviceImpl.RegisterChannel(std::move(channel)); diff --git a/src/server/service/service.cpp b/src/server/service/service.cpp index fe3335c..9a169b5 100644 --- a/src/server/service/service.cpp +++ b/src/server/service/service.cpp @@ -38,7 +38,7 @@ namespace SecurityManager { const InterfaceID IFACE = 1; -Service::Service(){} +Service::Service(Offline offline) : BaseService(offline) {} GenericSocketService::ServiceDescriptionVector Service::GetServiceDescription() { diff --git a/systemd/security-manager-rules-loader.service.in b/systemd/security-manager-rules-loader.service.in index 9f597c4..ab33648 100644 --- a/systemd/security-manager-rules-loader.service.in +++ b/systemd/security-manager-rules-loader.service.in @@ -1,14 +1,14 @@ [Unit] Description=SMACK merged rules loading After=local-fs.target -Before=basic.target +Before=basic.target security-manager.socket security-manager-cleanup.service DefaultDependencies=no [Service] Type=oneshot -ExecStart=/bin/dd if=@LOCAL_STATE_DIR@/security-manager/rules-merged/rules.merged of=/sys/fs/smackfs/load2 bs=20M +ExecStart=/usr/bin/security-manager-rules-loader SmackProcessLabel=System::Privileged [Install] -WantedBy=basic.target +WantedBy=basic.target security-manager.socket security-manager-cleanup.service diff --git a/test/data/.security-manager-test-rules.db b/test/data/.security-manager-test-rules.db new file mode 100644 index 0000000000000000000000000000000000000000..db1fd8b885392d078e158af558525855d052ae23 GIT binary patch literal 143360 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 -#include -#include #include #include +#include #include #include @@ -31,6 +30,7 @@ #include #include +#include #include "privilege_db.h" #include "privilege_db_fixture.h" @@ -49,41 +49,65 @@ std::string genName(const std::string &prefix, int i) std::string caseName(boost::unit_test::framework::current_test_case().p_name); return prefix + std::to_string(i) + "_" + caseName; } +void testMarkerFile(const char *f, bool present) { + struct stat st; + if (lstat(f, &st)) { + if (ENOENT != errno) BOOST_FAIL("marker file (" << f << ") lstat failed"); + if (present) BOOST_FAIL("marker file (" << f << ") does not exist"); + } else { + if (!present) BOOST_FAIL("marker file (" << f << ") exists"); + if (!S_ISREG(st.st_mode)) BOOST_FAIL("marker file (" << f << ") not a regular file"); + if (st.st_size) BOOST_FAIL("marker file (" << f << ") not empty"); + } +} +void checkMarker(PrivilegeDBFixture::Marker marker) { + testMarkerFile(TEST_DB_OK_MARKER, underlying(marker) & underlying(PrivilegeDBFixture::Marker::standard)); + testMarkerFile(TEST_DB_PATH DB_RECOVERED_SUFFIX, underlying(marker) & underlying(PrivilegeDBFixture::Marker::fallback)); + if (underlying(marker)) { + struct stat st; + BOOST_REQUIRE(!lstat(TEST_DB_PATH, &st)); + BOOST_REQUIRE(S_ISREG(st.st_mode)); + BOOST_REQUIRE(st.st_size); + BOOST_REQUIRE(!lstat(TEST_DB_PATH DB_JOURNAL_SUFFIX, &st)); + BOOST_REQUIRE(S_ISREG(st.st_mode)); + } +} +constexpr const char *dbFiles[] = { + TEST_DB_PATH, + TEST_DB_PATH DB_JOURNAL_SUFFIX, + TEST_DB_PATH DB_RECOVERED_SUFFIX, + TEST_PRIVILEGE_FALLBACK_DB_PATH, + TEST_DB_OK_MARKER, +}; } //namespace -void requireNoDb(const std::string &dbPath) { - BOOST_REQUIRE(!FS::fileStatus(dbPath)); - BOOST_REQUIRE(!FS::fileStatus(genJournalPath(dbPath))); - BOOST_REQUIRE(!FS::fileStatus(Config::dbRecoveryFlagFileName(dbPath))); +void requireNoDb() { + for (const auto f : dbFiles) + BOOST_REQUIRE_MESSAGE(!FS::fileStatus(f), "file (" << f << ") wrongfully exists"); +} +void purgeDb() { + for (const auto f : dbFiles) + remove(f); + requireNoDb(); } -PrivilegeDBFixture::PrivilegeDBFixture(const std::string &src, const std::string &fallback, - HaveBrokenFlagFile haveBrokenFlagFile, const std::string &dst) - : dbPath(dst) -{ - requireNoDb(dst); - putFile(src, dst); - putFile(genJournalPath(src), genJournalPath(dst)); - - testPrivDb = new PrivilegeDb(dst, fallback); - const auto brokenFlagFileName = Config::dbRecoveryFlagFileName(dst); - const auto flagFileStatus = FS::fileStatus(brokenFlagFileName); - if (haveBrokenFlagFile == HaveBrokenFlagFile::no) { - BOOST_REQUIRE(!flagFileStatus); - } else { - BOOST_REQUIRE(flagFileStatus > 0); - BOOST_REQUIRE(!FS::fileSize(brokenFlagFileName)); - BOOST_REQUIRE(!remove(brokenFlagFileName.c_str())); +PrivilegeDBFixture::PrivilegeDBFixture(const std::string &src, const std::string &fallback, Marker preMgr, PostMgrMarker postMgr) { + purgeDb(); + if (!src.empty()) { + putFile(src, TEST_DB_PATH); + putFile(src + DB_JOURNAL_SUFFIX, TEST_DB_PATH DB_JOURNAL_SUFFIX); } + if (!fallback.empty()) + putFile(fallback, TEST_PRIVILEGE_FALLBACK_DB_PATH); + forkExecWaitpid(TEST_RULES_LOADER_CMD, "no-load"); + checkMarker(preMgr); + testPrivDb = new PrivilegeDb(PrivilegeDb::Offline::no, PrivilegeDb::Db::test); + checkMarker(PostMgrMarker::unchanged == postMgr ? preMgr : Marker::fallback); } PrivilegeDBFixture::~PrivilegeDBFixture() { delete testPrivDb; - BOOST_REQUIRE_MESSAGE(remove(dbPath.c_str()) == 0, "Could not delete test database file: " << dbPath); - auto journalPath = genJournalPath(dbPath); - BOOST_REQUIRE_MESSAGE(remove(journalPath.c_str()) == 0, "Could not delete test database journal file: " << journalPath); - BOOST_REQUIRE(!FS::fileStatus(Config::dbRecoveryFlagFileName(dbPath))); } PrivilegeDb* PrivilegeDBFixture::getPrivDb() { diff --git a/test/privilege_db_fixture.h b/test/privilege_db_fixture.h index f26b2dd..c13d13c 100644 --- a/test/privilege_db_fixture.h +++ b/test/privilege_db_fixture.h @@ -29,19 +29,21 @@ #define PRIVILEGE_DB_EMPTY DB_TEST_DIR"/.security-manager-test-empty.db" #define PRIVILEGE_DB_WRONG_SCHEMA DB_TEST_DIR"/.security-manager-test-wrong-schema.db" -#define TEST_PRIVILEGE_DB_PATH "/tmp/.security-manager-test.db" -#define TEST_PRIVILEGE_DB_PATH_2 "/tmp/.security-manager-test-2.db" +#define PRIVILEGE_DB_EXAMPLE_RULES DB_TEST_DIR"/.security-manager-test-rules.db" +#define PRIVILEGE_DB_EXAMPLE_RULES_OUTPUT DB_TEST_DIR"/.security-manager-test-rules.txt" using namespace SecurityManager; -void requireNoDb(const std::string &dbPath); +void purgeDb(); +void requireNoDb(); struct PrivilegeDBFixture { public: - enum class HaveBrokenFlagFile : bool { no, yes }; + enum class Marker : uint8_t { none=0, standard=1, fallback=2 }; + enum class PostMgrMarker : uint8_t { unchanged, fallback }; explicit PrivilegeDBFixture(const std::string &src = PRIVILEGE_DB_TEMPLATE, const std::string &fallback = PRIVILEGE_DB_TEMPLATE, - HaveBrokenFlagFile = HaveBrokenFlagFile::no, const std::string &dst = TEST_PRIVILEGE_DB_PATH); + Marker preMgr = Marker::standard, PostMgrMarker postMgr = PostMgrMarker::unchanged); ~PrivilegeDBFixture(); PrivilegeDb* getPrivDb(); @@ -74,7 +76,4 @@ public: protected: PrivilegeDb *testPrivDb; - -private: - std::string dbPath; }; diff --git a/test/test_privilege_db_migration.cpp b/test/test_privilege_db_migration.cpp index fda3a32..2663047 100644 --- a/test/test_privilege_db_migration.cpp +++ b/test/test_privilege_db_migration.cpp @@ -27,19 +27,30 @@ #include "privilege_db_fixture.h" namespace { -bool fileContentsSame(const std::string &aPath, const std::string &bPath) { +bool fileContentsSame(const std::string &aPath, const std::string &bPath, std::initializer_list onOffOffsets = {}) { boost::iostreams::mapped_file_source a(aPath), b(bPath); auto s = a.size(); - return s == b.size() && !memcmp(a.data(), b.data(), s); + if (s != b.size()) + return false; + size_t off = 0; + bool check = 1; + for (const auto o : onOffOffsets) { + if (o > s || o <= off) + return false; + if (check ^= 1) { + off = o; + } else { + if (memcmp(a.data() + off, b.data() + off, o - off)) + return false; + } + } + return !check || !memcmp(a.data() + off, b.data() + off, s - off); } void requireTestDbContents(const std::string &db) { - BOOST_REQUIRE(fileContentsSame(TEST_PRIVILEGE_DB_PATH, db)); -} - -void requireTestDbAndJournalContents(const std::string &db) { - requireTestDbContents(db); - BOOST_REQUIRE(fileContentsSame(genJournalPath(TEST_PRIVILEGE_DB_PATH), genJournalPath(db))); + // omit SQLITE_VERSION_NUMBER to save ourselves the hassle by permitting discrepancies + // between version used to build the rpm and platform runtime version + BOOST_REQUIRE(fileContentsSame(TEST_DB_PATH, db, {96, 100})); } void translateIOError(PrivilegeDb::Exception::IOError e) { @@ -61,123 +72,86 @@ struct PrivilegeV0DBFixture : PrivilegeDBFixture { struct PrivilegeEmptyDBFixture : PrivilegeDBFixture { PrivilegeEmptyDBFixture() : PrivilegeDBFixture(PRIVILEGE_DB_EMPTY) {} }; -struct PrivilegeFallbackDBFixture : PrivilegeDBFixture { - PrivilegeFallbackDBFixture() : PrivilegeDBFixture(PRIVILEGE_DB_CORRUPTED, PRIVILEGE_DB_TEMPLATE, HaveBrokenFlagFile::yes) {} -}; -struct PrivilegeFallbackV0DBFixture : PrivilegeDBFixture { - PrivilegeFallbackV0DBFixture() : PrivilegeDBFixture(PRIVILEGE_DB_CORRUPTED, PRIVILEGE_DB_EXAMPLE_V0, HaveBrokenFlagFile::yes) {} -}; -struct PrivilegeFallbackEmptyDBFixture : PrivilegeDBFixture { - PrivilegeFallbackEmptyDBFixture() : PrivilegeDBFixture(PRIVILEGE_DB_CORRUPTED, PRIVILEGE_DB_EMPTY, HaveBrokenFlagFile::yes) {} -}; -struct PrivilegeFallbackWrongDBFixture : PrivilegeDBFixture { - PrivilegeFallbackWrongDBFixture() : PrivilegeDBFixture(PRIVILEGE_DB_WRONG_SCHEMA, PRIVILEGE_DB_TEMPLATE, HaveBrokenFlagFile::yes) {} -}; -struct PrivilegeFallbackWrongV0DBFixture : PrivilegeDBFixture { - PrivilegeFallbackWrongV0DBFixture() : PrivilegeDBFixture(PRIVILEGE_DB_WRONG_SCHEMA, PRIVILEGE_DB_EXAMPLE_V0, HaveBrokenFlagFile::yes) {} -}; -struct PrivilegeFallbackWrongEmptyDBFixture : PrivilegeDBFixture { - PrivilegeFallbackWrongEmptyDBFixture() : PrivilegeDBFixture(PRIVILEGE_DB_WRONG_SCHEMA, PRIVILEGE_DB_EMPTY, HaveBrokenFlagFile::yes) {} -}; - -void testFallback() { - requireTestDbContents(PRIVILEGE_DB_TEMPLATE); - BOOST_REQUIRE(!FS::fileSize(genJournalPath(TEST_PRIVILEGE_DB_PATH))); -} -void testFallbackSchemaApplication() { - requireTestDbAndJournalContents(PRIVILEGE_DB_TEMPLATE); -} -void testFallbackMigration() { - PrivilegeDBFixture v0(PRIVILEGE_DB_EXAMPLE_V0, PRIVILEGE_DB_TEMPLATE, - PrivilegeDBFixture::HaveBrokenFlagFile::no, TEST_PRIVILEGE_DB_PATH_2); - requireTestDbContents(TEST_PRIVILEGE_DB_PATH_2); -} -void testFallbackMissingMigration() { - const std::string missingDbPath("/tmp/thisNotExists.db"); - requireNoDb(missingDbPath); - const auto missingDbPathJournal = genJournalPath(missingDbPath); - const auto missingDbPathFlag = Config::dbRecoveryFlagFileName(missingDbPath); - BOOST_REQUIRE_NO_THROW(PrivilegeDb(missingDbPath, PRIVILEGE_DB_EXAMPLE_V0)); - requireTestDbContents(missingDbPath); - BOOST_REQUIRE(!remove(missingDbPath.c_str())); - BOOST_REQUIRE(!remove(missingDbPathJournal.c_str())); - BOOST_REQUIRE(!FS::fileSize(missingDbPathFlag)); - BOOST_REQUIRE(!remove(missingDbPathFlag.c_str())); -} } //namespace BOOST_GLOBAL_FIXTURE(TestConfig); BOOST_FIXTURE_TEST_SUITE(PRIVILEGE_DB_TEST_EMPTY, PrivilegeEmptyDBFixture) BOOST_AUTO_TEST_CASE(T1500_schema_application) { - requireTestDbAndJournalContents(PRIVILEGE_DB_TEMPLATE); -} -BOOST_AUTO_TEST_SUITE_END() - -BOOST_FIXTURE_TEST_SUITE(PRIVILEGE_DB_TEST_FALLBACK, PrivilegeFallbackDBFixture) -BOOST_AUTO_TEST_CASE(T1510_fallback) { - testFallback(); -} -BOOST_AUTO_TEST_SUITE_END() - -BOOST_FIXTURE_TEST_SUITE(PRIVILEGE_DB_TEST_FALLBACK_EMPTY, PrivilegeFallbackEmptyDBFixture) -BOOST_AUTO_TEST_CASE(T1520_fallback_schema_application) { - testFallbackSchemaApplication(); -} -BOOST_AUTO_TEST_SUITE_END() - -BOOST_FIXTURE_TEST_SUITE(PRIVILEGE_DB_TEST_FALLBACK_V0, PrivilegeFallbackV0DBFixture) - -BOOST_AUTO_TEST_CASE(T1530_fallback_migration) { - testFallbackMigration(); -} - -BOOST_AUTO_TEST_CASE(T1540_db_missing_fallback_migration) { - testFallbackMissingMigration(); -} - -BOOST_AUTO_TEST_SUITE_END() - -BOOST_FIXTURE_TEST_SUITE(PRIVILEGE_DB_TEST_FALLBACK_WRONG, PrivilegeFallbackWrongDBFixture) -BOOST_AUTO_TEST_CASE(T1550_fallback_wrong) { - testFallback(); -} -BOOST_AUTO_TEST_SUITE_END() - -BOOST_FIXTURE_TEST_SUITE(PRIVILEGE_DB_TEST_FALLBACK_WRONG_EMPTY, PrivilegeFallbackWrongEmptyDBFixture) -BOOST_AUTO_TEST_CASE(T1560_fallback_wrong_schema_application) { - testFallbackSchemaApplication(); + requireTestDbContents(PRIVILEGE_DB_TEMPLATE); + BOOST_REQUIRE(fileContentsSame(TEST_DB_PATH DB_JOURNAL_SUFFIX, PRIVILEGE_DB_TEMPLATE DB_JOURNAL_SUFFIX)); } BOOST_AUTO_TEST_SUITE_END() -BOOST_FIXTURE_TEST_SUITE(PRIVILEGE_DB_TEST_FALLBACK_WRONG_V0, PrivilegeFallbackWrongV0DBFixture) +BOOST_FIXTURE_TEST_SUITE(PRIVILEGE_DB_TEST_FALLBACK_MIGRATION_FAILURE, Empty) -BOOST_AUTO_TEST_CASE(T1570_fallback_wrong_migration) { - testFallbackMigration(); +BOOST_AUTO_TEST_CASE(T1510_loader_output) { + BOOST_REQUIRE(SECURITY_MANAGER_SUCCESS == FS::overwriteFile(PRIVILEGE_DB_EXAMPLE_RULES, TEST_DB_PATH)); + BOOST_REQUIRE(!system(TEST_RULES_LOADER_CMD " | LC_ALL=C sort > /tmp/out")); + BOOST_REQUIRE(fileContentsSame("/tmp/out", PRIVILEGE_DB_EXAMPLE_RULES_OUTPUT)); + remove("/tmp/out"); } -BOOST_AUTO_TEST_CASE(T1580_db_missing_fallback_wrong_migration) { - testFallbackMissingMigration(); +BOOST_AUTO_TEST_CASE(T1570_fallback_canonicity) { + using Marker = PrivilegeDBFixture::Marker; + const auto go = [&](std::string db, Marker preMgr, bool empty) { + std::unique_ptr fix; + BOOST_REQUIRE_NO_THROW(fix.reset(new PrivilegeDBFixture(db, empty ? PRIVILEGE_DB_EMPTY : PRIVILEGE_DB_TEMPLATE, preMgr, PrivilegeDBFixture::PostMgrMarker::fallback))); + fix.reset(); + requireTestDbContents(PRIVILEGE_DB_TEMPLATE); + if (empty) + BOOST_REQUIRE(fileContentsSame(TEST_DB_PATH DB_JOURNAL_SUFFIX, PRIVILEGE_DB_TEMPLATE DB_JOURNAL_SUFFIX)); + else + BOOST_REQUIRE(!FS::fileSize(TEST_DB_PATH DB_JOURNAL_SUFFIX)); + }; + go({}, Marker::fallback, false); + go({}, Marker::fallback, true); + go(PRIVILEGE_DB_CORRUPTED, Marker::fallback, false); + go(PRIVILEGE_DB_CORRUPTED, Marker::fallback, true); + go(PRIVILEGE_DB_WRONG_SCHEMA, Marker::standard, false); + go(PRIVILEGE_DB_WRONG_SCHEMA, Marker::standard, true); +} + +BOOST_AUTO_TEST_CASE(T1580_fallback_migration_canonicity) { + const std::string v0migratedDb = TEST_DB_PATH ".migrated"; + { + PrivilegeV0DBFixture f; + BOOST_REQUIRE(SECURITY_MANAGER_SUCCESS == FS::overwriteFile(TEST_DB_PATH, v0migratedDb)); + } + using Marker = PrivilegeDBFixture::Marker; + using PostMgrMarker = PrivilegeDBFixture::PostMgrMarker; + const auto go = [&](std::string db, Marker preMgr, PostMgrMarker postMgr = PostMgrMarker::unchanged) { + std::unique_ptr fix; + BOOST_REQUIRE_NO_THROW(fix.reset(new PrivilegeDBFixture(db, PRIVILEGE_DB_EXAMPLE_V0, preMgr, postMgr))); + fix.reset(); + requireTestDbContents(v0migratedDb); + }; + go({}, Marker::fallback); + go(PRIVILEGE_DB_CORRUPTED, Marker::fallback); + go(PRIVILEGE_DB_WRONG_SCHEMA, Marker::standard, PostMgrMarker::fallback); } -BOOST_AUTO_TEST_SUITE_END() - -BOOST_FIXTURE_TEST_SUITE(PRIVILEGE_DB_TEST_FALLBACK_MIGRATION_FAILURE, Empty) BOOST_AUTO_TEST_CASE(T1590_fallback_migration_failure) { - const auto go = [](const char *db, const char *fallback){ + using Marker = PrivilegeDBFixture::Marker; + const auto go = [](std::string db, std::string fallback, Marker preMgr) {; std::unique_ptr fix; - BOOST_REQUIRE_THROW(fix.reset(new PrivilegeDBFixture(db, fallback, PrivilegeDBFixture::HaveBrokenFlagFile::yes)), PrivilegeDb::Exception::IOError); - fix.reset(); - BOOST_REQUIRE(!remove(TEST_PRIVILEGE_DB_PATH)); - auto journal = genJournalPath(TEST_PRIVILEGE_DB_PATH); - BOOST_REQUIRE(!remove(journal.c_str())); - auto recovered = Config::dbRecoveryFlagFileName(TEST_PRIVILEGE_DB_PATH); - BOOST_REQUIRE(!remove(recovered.c_str())); + BOOST_REQUIRE_THROW(fix.reset(new PrivilegeDBFixture(db, fallback, preMgr)), PrivilegeDb::Exception::IOError); }; - go(PRIVILEGE_DB_CORRUPTED, PRIVILEGE_DB_CORRUPTED); - go(PRIVILEGE_DB_CORRUPTED, PRIVILEGE_DB_WRONG_SCHEMA); - go(PRIVILEGE_DB_WRONG_SCHEMA, PRIVILEGE_DB_CORRUPTED); - go(PRIVILEGE_DB_WRONG_SCHEMA, PRIVILEGE_DB_WRONG_SCHEMA); + go({}, {}, Marker::none); + go({}, PRIVILEGE_DB_CORRUPTED, Marker::none); + go({}, PRIVILEGE_DB_WRONG_SCHEMA, Marker::fallback); + go(PRIVILEGE_DB_CORRUPTED, {}, Marker::none); + go(PRIVILEGE_DB_CORRUPTED, PRIVILEGE_DB_CORRUPTED, Marker::none); + go(PRIVILEGE_DB_CORRUPTED, PRIVILEGE_DB_WRONG_SCHEMA, Marker::fallback); + go(PRIVILEGE_DB_WRONG_SCHEMA, {}, Marker::standard); + go(PRIVILEGE_DB_WRONG_SCHEMA, PRIVILEGE_DB_CORRUPTED, Marker::standard); + go(PRIVILEGE_DB_WRONG_SCHEMA, PRIVILEGE_DB_WRONG_SCHEMA, Marker::standard); + //TODO even though the test loader (unlike the production loader) + // always returns EXIT_SUCCESS, the exception is thrown during initDataCommands + // so this just happens to work.. + // + // maybe one day sb will find out why boost test misbehaves when loader fails + // turning this test into a proper form will be possible then } BOOST_AUTO_TEST_SUITE_END() diff --git a/test/test_privilege_db_privilege.cpp b/test/test_privilege_db_privilege.cpp index 53bac0a..acff28c 100644 --- a/test/test_privilege_db_privilege.cpp +++ b/test/test_privilege_db_privilege.cpp @@ -28,6 +28,7 @@ #include #include +#include // TEST_DB_PATH #include "privilege_db.h" #include "privilege_db_fixture.h" @@ -45,7 +46,7 @@ BOOST_AUTO_TEST_CASE(T820_get_groups_related_privileges_from_empty_db) BOOST_AUTO_TEST_CASE(T830_get_groups_related_privileges) { - int ret = system("sqlite3 " TEST_PRIVILEGE_DB_PATH " " + int ret = system("sqlite3 " TEST_DB_PATH " " "\"BEGIN; " "INSERT INTO privilege_group (privilege_name, group_name) VALUES ('privilege30', 'group3'); " "INSERT INTO privilege_group (privilege_name, group_name) VALUES ('privilege10', 'group1'); " diff --git a/test/test_privilege_db_transactions.cpp b/test/test_privilege_db_transactions.cpp index 839e4a9..cbfa273 100644 --- a/test/test_privilege_db_transactions.cpp +++ b/test/test_privilege_db_transactions.cpp @@ -44,24 +44,11 @@ BOOST_FIXTURE_TEST_CASE(T100_privilegedb_constructor, Empty) { std::unique_ptr testPrivDb; - BOOST_REQUIRE_NO_THROW(testPrivDb.reset(new PrivilegeDb())); - - std::string nExist("/tmp/thisNotExists"), nExist2("/tmp/neitherDoesThis"); - requireNoDb(nExist); - requireNoDb(nExist2); - BOOST_REQUIRE_THROW(testPrivDb.reset(new PrivilegeDb(nExist, nExist2)), - PrivilegeDb::Exception::IOError); - const auto flagFile = Config::dbRecoveryFlagFileName(nExist); - BOOST_REQUIRE(!FS::fileSize(flagFile)); - BOOST_REQUIRE(!remove(flagFile.c_str())); - requireNoDb(nExist); - - // fallback existent but db can't be created w/out mkdir -p - std::string nExistDeep("/this/not/exists"); - requireNoDb(nExistDeep); - BOOST_REQUIRE_THROW(testPrivDb.reset(new PrivilegeDb(nExistDeep, PRIVILEGE_DB_TEMPLATE)), + purgeDb(); + // db init must fail w/ no loader having run beforehand + BOOST_REQUIRE_THROW(testPrivDb.reset(new PrivilegeDb(PrivilegeDb::Offline::no, PrivilegeDb::Db::test)), PrivilegeDb::Exception::IOError); - requireNoDb(nExistDeep); + requireNoDb(); } // Transactions diff --git a/test/test_smack-rules.cpp b/test/test_smack-rules.cpp index 4503acf..00de30e 100644 --- a/test/test_smack-rules.cpp +++ b/test/test_smack-rules.cpp @@ -22,29 +22,17 @@ #include #include -#include -#include -#include #include #include using namespace SecurityManager; using namespace SecurityManager::SmackLabels; -typedef std::tuple Rule; -typedef std::vector Rules; - struct RulesFixture { RulesFixture() { - if (std::ifstream(smackRulesFilePath)) - BOOST_REQUIRE_MESSAGE(unlink(smackRulesFilePath) == 0, - "Error while unlink the file: " << smackRulesFilePath); - if (std::ifstream(smackRulesBackupFilePath)) - BOOST_REQUIRE_MESSAGE(unlink(smackRulesBackupFilePath) == 0, - "Error while unlink the file: " << smackRulesBackupFilePath); if (std::ifstream(templateRulesFilePath)) BOOST_REQUIRE_MESSAGE(unlink(templateRulesFilePath) == 0, "Error while unlink the file: " << templateRulesFilePath); @@ -52,91 +40,18 @@ struct RulesFixture ~RulesFixture() { - if (std::ifstream(smackRulesFilePath)) - BOOST_WARN_MESSAGE(unlink(smackRulesFilePath) == 0, - "Error while unlink the file: " << smackRulesFilePath); - if (std::ifstream(smackRulesBackupFilePath)) - BOOST_WARN_MESSAGE(unlink(smackRulesBackupFilePath) == 0, - "Error while unlink the file: " << smackRulesBackupFilePath); if (std::ifstream(templateRulesFilePath)) BOOST_WARN_MESSAGE(unlink(templateRulesFilePath) == 0, "Error while unlink the file: " << templateRulesFilePath); } - const static char* smackRulesFilePath; - const static char* smackRulesBackupFilePath; const static char* templateRulesFilePath; - const static Rules rules; }; -const char* RulesFixture::smackRulesFilePath = "/tmp/SecurityManagerUTSmackRules.rules"; -const char* RulesFixture::smackRulesBackupFilePath = "/tmp/SecurityManagerUTSmackRulesBackup.rules"; const char* RulesFixture::templateRulesFilePath = "/tmp/SecurityManagerUTTemplateRules.rules"; -const Rules RulesFixture::rules = { Rule("music-player", "audio", "rwxa--"), - Rule("email", "gallery", "rwxat-"), - Rule("maps", "gps", "r-x--l"), - Rule("browser", "camera", "-wx---"), - Rule("message", "nfc", "-----l") }; - BOOST_AUTO_TEST_SUITE(SMACK_RULES_TEST) -BOOST_FIXTURE_TEST_CASE(T1100_add_save_load_smack_rules, RulesFixture) -{ - SmackRules smackRules, smackRulesBackup; - std::ifstream smackRulesFile, smackRulesBackupFile; - std::string smackRuleFromFile, smackRuleFromBackupFile, smackRuleOriginal; - - for (auto r : rules) - BOOST_REQUIRE_NO_THROW(smackRules.add(std::get<0>(r), std::get<1>(r), std::get<2>(r))); - - BOOST_REQUIRE_NO_THROW(smackRules.saveToFile(smackRulesFilePath)); - BOOST_REQUIRE_NO_THROW(smackRulesBackup.loadFromFile(smackRulesFilePath)); - BOOST_REQUIRE_NO_THROW(smackRulesBackup.saveToFile(smackRulesBackupFilePath)); - - smackRulesFile.open(smackRulesFilePath); - smackRulesBackupFile.open(smackRulesBackupFilePath); - - for (auto r : rules) { - std::getline(smackRulesFile, smackRuleFromFile); - std::getline(smackRulesBackupFile, smackRuleFromBackupFile); - - smackRuleOriginal = std::get<0>(r) + " " + std::get<1>(r) + " " + std::get<2>(r); - - BOOST_REQUIRE(smackRuleFromFile == smackRuleOriginal); - BOOST_REQUIRE(smackRuleFromFile == smackRuleFromBackupFile); - } - - smackRulesFile.close(); - smackRulesBackupFile.close(); -} - -BOOST_FIXTURE_TEST_CASE(T1110_add_modify_save_smack_rules, RulesFixture) -{ - SmackRules smackRules; - std::ifstream smackRulesFile; - std::string smackRuleFromFile, smackRuleModify; - - for (auto r : rules) { - BOOST_REQUIRE_NO_THROW(smackRules.add(std::get<0>(r), std::get<1>(r), std::get<2>(r))); - BOOST_REQUIRE_NO_THROW(smackRules.addModify(std::get<0>(r), std::get<1>(r), "xatl", "")); - BOOST_REQUIRE_NO_THROW(smackRules.addModify(std::get<0>(r), std::get<1>(r), "", "tl")); - } - - BOOST_REQUIRE_NO_THROW(smackRules.saveToFile(smackRulesFilePath)); - smackRulesFile.open(smackRulesFilePath); - - for (auto r : rules) { - std::getline(smackRulesFile, smackRuleFromFile); - - smackRuleModify = std::get<0>(r) + " " + std::get<1>(r) + " " + std::get<2>(r).substr(0, 2) + "xa--"; - - BOOST_REQUIRE(smackRuleFromFile == smackRuleModify); - } - - smackRulesFile.close(); -} - BOOST_AUTO_TEST_CASE(T1120_smack_rules_exception) { SmackRules smackRules; @@ -156,9 +71,6 @@ BOOST_AUTO_TEST_CASE(T1120_smack_rules_exception) SmackException::LibsmackError); BOOST_REQUIRE_THROW(smackRules.addModify("", "object", "rw", "xt"), SmackException::LibsmackError); - - const std::string noExistingFilePath = "/tmp/SecurityManagerUTNoExistingFile"; - BOOST_REQUIRE_THROW(smackRules.loadFromFile(noExistingFilePath), SmackException::FileError); } BOOST_FIXTURE_TEST_CASE(T1130_smack_rules_templates, RulesFixture) @@ -179,13 +91,6 @@ BOOST_FIXTURE_TEST_CASE(T1130_smack_rules_templates, RulesFixture) } templateRulesFile.close(); - std::vector expectedRules = { "System User::Pkg::pkgNameT1130 rwxat-", - "User::Pkg::pkgNameT1130 System -wx---", - "User::Pkg::pkgNameT1130 User::Pkg::pkgNameT1130 rwxat-", - "User::Pkg::pkgNameT1130 User::Pkg::pkgNameT1130::RO r-x--l", - "User::Pkg::pkgNameT1130 User::Pkg::pkgNameT1130::SharedRO rwxat-", - "User::Pkg::pkgNameT1130 User::Author::5000 rwxat-" }; - const std::string appName = "appNameT1130"; const std::string pkgName = "pkgNameT1130"; const std::string appProcessLabel = generateProcessLabel(appName, pkgName, false); @@ -197,8 +102,6 @@ BOOST_FIXTURE_TEST_CASE(T1130_smack_rules_templates, RulesFixture) pkgName, authorId)); - BOOST_REQUIRE_NO_THROW(smackRulesFromTemplate.saveToFile(smackRulesFilePath)); - const std::string noExistingFilePath = "/tmp/SecurityManagerUTNoExistingFile"; BOOST_REQUIRE_THROW(smackRulesFromFileTemplate.addFromTemplateFile(noExistingFilePath, appProcessLabel, @@ -210,24 +113,6 @@ BOOST_FIXTURE_TEST_CASE(T1130_smack_rules_templates, RulesFixture) appProcessLabel, pkgName, authorId)); - - BOOST_REQUIRE_NO_THROW(smackRulesFromFileTemplate.saveToFile(smackRulesBackupFilePath)); - - std::ifstream smackRulesFile, smackRulesBackupFile; - std::string smackRuleFromFile, smackRuleFromBackupFile; - smackRulesFile.open(smackRulesFilePath); - smackRulesBackupFile.open(smackRulesBackupFilePath); - - for (auto expectedRule : expectedRules) { - std::getline(smackRulesFile, smackRuleFromFile); - std::getline(smackRulesBackupFile, smackRuleFromBackupFile); - - BOOST_REQUIRE(smackRuleFromFile == expectedRule); - BOOST_REQUIRE(smackRuleFromBackupFile == expectedRule); - } - - smackRulesFile.close(); - smackRulesBackupFile.close(); } BOOST_AUTO_TEST_SUITE_END() -- 2.7.4