diff --git a/cf-agent/acl_posix.c b/cf-agent/acl_posix.c index e01aff160f..b3d5d88133 100644 --- a/cf-agent/acl_posix.c +++ b/cf-agent/acl_posix.c @@ -33,6 +33,8 @@ #include #include #include /* GetGroupID(), GetUserID() */ +#include +#include "fsattrs.h" #ifdef HAVE_ACL_H # include @@ -389,6 +391,11 @@ static bool CheckPosixLinuxACEs(EvalContext *ctx, Rlist *aces, AclMethod method, acl_free(acl_text_str); return false; } + + bool was_immutable = false; + const bool override_immutable = EvalContextOverrideImmutableGet(ctx); + FSAttrsResult res = TemporarilyClearImmutableBit(changes_path, override_immutable, &was_immutable); + if ((retv = acl_set_file(changes_path, acl_type, acl_new)) != 0) { RecordFailure(ctx, pp, a, @@ -406,6 +413,8 @@ static bool CheckPosixLinuxACEs(EvalContext *ctx, Rlist *aces, AclMethod method, } acl_free(acl_text_str); + ResetTemporarilyClearedImmutableBit(changes_path, override_immutable, res, was_immutable); + RecordChange(ctx, pp, a, "%s ACL on '%s' successfully changed", acl_type_str, file_path); *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE); } diff --git a/cf-agent/files_edit.c b/cf-agent/files_edit.c index 6833efc2d3..a3e27e4eff 100644 --- a/cf-agent/files_edit.c +++ b/cf-agent/files_edit.c @@ -128,7 +128,7 @@ void FinishEditContext(EvalContext *ctx, EditContext *ec, const Attributes *a, c RecordNoChange(ctx, pp, a, "No edit changes to file '%s' need saving", ec->filename); } - else if (SaveItemListAsFile(ec->file_start, ec->changes_filename, a, ec->new_line_mode)) + else if (SaveItemListAsFile(ctx, ec->file_start, ec->changes_filename, a, ec->new_line_mode)) { RecordChange(ctx, pp, a, "Edited file '%s'", ec->filename); *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE); @@ -151,7 +151,7 @@ void FinishEditContext(EvalContext *ctx, EditContext *ec, const Attributes *a, c ec->filename); } } - else if (SaveXmlDocAsFile(ec->xmldoc, ec->changes_filename, a, ec->new_line_mode)) + else if (SaveXmlDocAsFile(ctx, ec->xmldoc, ec->changes_filename, a, ec->new_line_mode)) { RecordChange(ctx, pp, a, "Edited xml file '%s'", ec->filename); *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE); @@ -254,8 +254,8 @@ static bool SaveXmlCallback(const char *dest_filename, void *param, /*********************************************************************/ -bool SaveXmlDocAsFile(xmlDocPtr doc, const char *file, const Attributes *a, NewLineMode new_line_mode) +bool SaveXmlDocAsFile(EvalContext *ctx, xmlDocPtr doc, const char *file, const Attributes *a, NewLineMode new_line_mode) { - return SaveAsFile(&SaveXmlCallback, doc, file, a, new_line_mode); + return SaveAsFile(ctx, &SaveXmlCallback, doc, file, a, new_line_mode); } #endif /* HAVE_LIBXML2 */ diff --git a/cf-agent/files_edit.h b/cf-agent/files_edit.h index 6bb8551b3e..9c19c39786 100644 --- a/cf-agent/files_edit.h +++ b/cf-agent/files_edit.h @@ -61,7 +61,7 @@ void FinishEditContext(EvalContext *ctx, EditContext *ec, #ifdef HAVE_LIBXML2 bool LoadFileAsXmlDoc(xmlDocPtr *doc, const char *file, EditDefaults ed, bool only_checks); -bool SaveXmlDocAsFile(xmlDocPtr doc, const char *file, +bool SaveXmlDocAsFile(EvalContext *ctx, xmlDocPtr doc, const char *file, const Attributes *a, NewLineMode new_line_mode); #endif diff --git a/cf-agent/verify_files.c b/cf-agent/verify_files.c index 83b9db8c73..165dccbca2 100644 --- a/cf-agent/verify_files.c +++ b/cf-agent/verify_files.c @@ -60,6 +60,8 @@ #include #include /* PrepareChangesChroot(), RecordFileChangedInChroot() */ #include +#include +#include static PromiseResult FindFilePromiserObjects(EvalContext *ctx, const Promise *pp); static PromiseResult VerifyFilePromise(EvalContext *ctx, char *path, const Promise *pp); @@ -345,6 +347,67 @@ static PromiseResult VerifyFilePromise(EvalContext *ctx, char *path, const Promi changes_path = chrooted_path; } + bool is_immutable = false; /* We assume not in case of failure */ + FSAttrsResult res = FSAttrsGetImmutableFlag(changes_path, &is_immutable); + if (res != FS_ATTRS_SUCCESS) + { + Log((res == FS_ATTRS_FAILURE) ? LOG_LEVEL_ERR : LOG_LEVEL_VERBOSE, + "Failed to get the state of the immutable bit from file '%s': %s", + changes_path, FSAttrsErrorCodeToString(res)); + } + + if (a.havefsattrs && a.fsattrs.haveimmutable && !a.fsattrs.immutable) + { + /* Here we only handle the clearing of the immutable the immutable + * bit. Later we'll handle the setting of the immutable bit. */ + if (is_immutable) + { + res = FSAttrsUpdateImmutableFlag(changes_path, false); + switch (res) + { + case FS_ATTRS_SUCCESS: + RecordChange(ctx, pp, &a, + "Cleared the immutable bit on file '%s'", + changes_path); + result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE); + break; + case FS_ATTRS_FAILURE: + RecordFailure(ctx, pp, &a, + "Failed to clear the immutable bit on file '%s'", + changes_path); + result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL); + break; + case FS_ATTRS_NOT_SUPPORTED: + /* We will not treat this as a promise failure because this + * will happen on many platforms and filesystems. Instead we + * will log a verbose message to make it apparent for the + * users. */ + Log(LOG_LEVEL_VERBOSE, + "Failed to clear the immutable bit on file '%s': %s", + changes_path, FSAttrsErrorCodeToString(res)); + break; + case FS_ATTRS_DOES_NOT_EXIST: + /* File does not exist. Nothing to do really, but let's log a + * debug message for good measures */ + Log(LOG_LEVEL_DEBUG, + "Failed to clear the immutable bit on file '%s': %s", + changes_path, FSAttrsErrorCodeToString(res)); + break; + } + } + else + { + RecordNoChange(ctx, pp, &a, + "The immutable bit is not set on file '%s' as promised", + changes_path); + } + } + + /* If we encounter any promises to mutate the file and the immutable + * attribute in body fsattrs is "true", we will override the immutable bit + * by temporarily clearing it when ever needed. */ + EvalContextOverrideImmutableSet(ctx, a.havefsattrs && a.fsattrs.haveimmutable && a.fsattrs.immutable && is_immutable); + if (lstat(changes_path, &oslb) == -1) /* Careful if the object is a link */ { if ((a.create) || (a.touch)) @@ -586,6 +649,51 @@ static PromiseResult VerifyFilePromise(EvalContext *ctx, char *path, const Promi } } + if (a.havefsattrs && a.fsattrs.haveimmutable && a.fsattrs.immutable) + { + /* Here we only handle the setting of the immutable bit. Previously we + * handled the clearing of the immutable bit. */ + if (is_immutable) + { + RecordNoChange(ctx, pp, &a, + "The immutable bit is already set on file '%s' as promised", + changes_path); + } + else + { + res = FSAttrsUpdateImmutableFlag(changes_path, true); + switch (res) + { + case FS_ATTRS_SUCCESS: + Log(LOG_LEVEL_VERBOSE, "Set the immutable bit on file '%s'", + changes_path); + break; + case FS_ATTRS_FAILURE: + /* Things still may be fine as long as the agent does not try to mutate the file */ + Log(LOG_LEVEL_VERBOSE, + "Failed to set the immutable bit on file '%s': %s", + changes_path, FSAttrsErrorCodeToString(res)); + break; + case FS_ATTRS_NOT_SUPPORTED: + /* We will not treat this as a promise failure because this + * will happen on many platforms and filesystems. Instead we + * will log a verbose message to make it apparent for the + * users. */ + Log(LOG_LEVEL_VERBOSE, + "Failed to set the immutable bit on file '%s': %s", + changes_path, FSAttrsErrorCodeToString(res)); + break; + case FS_ATTRS_DOES_NOT_EXIST: + /* File does not exist. Nothing to do really, but let's log a + * debug message for good measures */ + Log(LOG_LEVEL_DEBUG, + "Failed to set the immutable bit on file '%s': %s", + changes_path, FSAttrsErrorCodeToString(res)); + break; + } + } + } + // Once more in case a file has been created as a result of editing or copying exists = (lstat(changes_path, &osb) != -1); @@ -603,6 +711,9 @@ static PromiseResult VerifyFilePromise(EvalContext *ctx, char *path, const Promi } exit: + /* Reset this to false before next file promise */ + EvalContextOverrideImmutableSet(ctx, false); + free(chrooted_path); if (AttrHasNoAction(&a)) { @@ -686,6 +797,7 @@ static PromiseResult WriteContentFromString(EvalContext *ctx, const char *path, if (!HashesMatch(existing_content_digest, promised_content_digest, CF_DEFAULT_DIGEST)) { + bool override_immutable = EvalContextOverrideImmutableGet(ctx); if (!MakingChanges(ctx, pp, attr, &result, "update file '%s' with content '%s'", path, attr->content)) @@ -693,20 +805,28 @@ static PromiseResult WriteContentFromString(EvalContext *ctx, const char *path, return result; } - FILE *f = safe_fopen(changes_path, "w"); + char override_path[PATH_MAX]; + if (!OverrideImmutableBegin(changes_path, override_path, sizeof(override_path), override_immutable)) + { + RecordFailure(ctx, pp, attr, "Failed to override immutable bit on file '%s'", changes_path); + return PromiseResultUpdate(result, PROMISE_RESULT_FAIL); + } + + FILE *f = safe_fopen(override_path, "w"); if (f == NULL) { RecordFailure(ctx, pp, attr, "Cannot open file '%s' for writing", path); + OverrideImmutableCommit(changes_path, override_path, override_immutable, true); return PromiseResultUpdate(result, PROMISE_RESULT_FAIL); } + bool override_abort = false; Writer *w = FileWriter(f); if (WriterWriteLen(w, attr->content, bytes_to_write) == bytes_to_write ) { RecordChange(ctx, pp, attr, "Updated file '%s' with content '%s'", path, attr->content); - result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE); } else @@ -714,9 +834,16 @@ static PromiseResult WriteContentFromString(EvalContext *ctx, const char *path, RecordFailure(ctx, pp, attr, "Failed to update file '%s' with content '%s'", path, attr->content); + override_abort = true; result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL); } WriterClose(w); + + if (!OverrideImmutableCommit(changes_path, override_path, override_immutable, override_abort)) + { + RecordFailure(ctx, pp, attr, "Failed to override immutable bit on file '%s'", changes_path); + result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL); + } } return result; @@ -861,7 +988,7 @@ static PromiseResult RenderTemplateMustache(EvalContext *ctx, edcontext->filename, message); result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL); } - else if (SaveAsFile(SaveBufferCallback, output_buffer, + else if (SaveAsFile(ctx, SaveBufferCallback, output_buffer, edcontext->changes_filename, attr, edcontext->new_line_mode)) { diff --git a/cf-agent/verify_files_utils.c b/cf-agent/verify_files_utils.c index 541c68bd89..ac30564485 100644 --- a/cf-agent/verify_files_utils.c +++ b/cf-agent/verify_files_utils.c @@ -67,6 +67,7 @@ #include #include /* PrepareChangesChroot(), RecordFileChangedInChroot() */ #include /* GetGroupName(), GetUserName() */ +#include #include @@ -1925,7 +1926,8 @@ bool CopyRegularFile(EvalContext *ctx, const char *source, const char *dest, con else #endif { - if (rename(changes_new, changes_dest) == 0) + bool override_immutable = EvalContextOverrideImmutableGet(ctx); + if (OverrideImmutableRename(changes_new, changes_dest, override_immutable)) { RecordChange(ctx, pp, attr, "Moved '%s' to '%s'", new, dest); *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE); @@ -1937,7 +1939,7 @@ bool CopyRegularFile(EvalContext *ctx, const char *source, const char *dest, con dest, GetErrorStr()); *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL); - if (backupok && (rename(changes_backup, changes_dest) == 0)) + if (backupok && OverrideImmutableRename(changes_backup, changes_dest, override_immutable)) { RecordChange(ctx, pp, attr, "Restored '%s' from '%s'", dest, backup); *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE); @@ -1998,7 +2000,7 @@ static bool TransformFile(EvalContext *ctx, char *file, const Attributes *attr, if (!IsExecutable(CommandArg0(BufferData(command)))) { - RecordFailure(ctx, pp, attr, "Transformer '%s' for file '%s' failed", attr->transformer, file); + RecordFailure(ctx, pp, attr, " '%s' for file '%s' failed", attr->transformer, file); *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL); BufferDestroy(command); return false; @@ -2041,6 +2043,10 @@ static bool TransformFile(EvalContext *ctx, char *file, const Attributes *attr, } } + const bool override_immutable = EvalContextOverrideImmutableGet(ctx); + bool was_immutable = false; + FSAttrsResult res = TemporarilyClearImmutableBit(file, override_immutable, &was_immutable); + Log(LOG_LEVEL_INFO, "Transforming '%s' with '%s'", file, command_str); if ((pop = cf_popen(changes_command, "r", true)) == NULL) { @@ -2084,6 +2090,8 @@ static bool TransformFile(EvalContext *ctx, char *file, const Attributes *attr, transRetcode = cf_pclose(pop); + ResetTemporarilyClearedImmutableBit(file, override_immutable, res, was_immutable); + if (VerifyCommandRetcode(ctx, transRetcode, attr, pp, result)) { Log(LOG_LEVEL_INFO, "Transformer '%s' => '%s' seemed to work ok", file, command_str); @@ -2174,10 +2182,10 @@ static PromiseResult VerifyName(EvalContext *ctx, char *path, const struct stat { if (!FileInRepository(newname)) { - if (rename(changes_path, changes_newname) == -1) + const bool override_immutable = EvalContextOverrideImmutableGet(ctx); + if (!OverrideImmutableRename(changes_path, changes_newname, override_immutable)) { - RecordFailure(ctx, pp, attr, "Error occurred while renaming '%s'. (rename: %s)", - path, GetErrorStr()); + RecordFailure(ctx, pp, attr, "Error occurred while renaming '%s'", path); result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL); } else @@ -2312,7 +2320,8 @@ static PromiseResult VerifyName(EvalContext *ctx, char *path, const struct stat if (MakingChanges(ctx, pp, attr, &result, "rename file '%s' to '%s'", path, newname)) { - if (safe_chmod(changes_path, newperm) == 0) + const bool override_immutable = EvalContextOverrideImmutableGet(ctx); + if (OverrideImmutableChmod(changes_path, newperm, override_immutable)) { RecordChange(ctx, pp, attr, "Changed permissions of '%s' to 'mode %04jo'", path, (uintmax_t)newperm); @@ -2327,10 +2336,9 @@ static PromiseResult VerifyName(EvalContext *ctx, char *path, const struct stat if (!FileInRepository(newname)) { - if (rename(changes_path, changes_newname) == -1) + if (!OverrideImmutableRename(changes_path, changes_newname, override_immutable)) { - RecordFailure(ctx, pp, attr, "Error occurred while renaming '%s'. (rename: %s)", - path, GetErrorStr()); + RecordFailure(ctx, pp, attr, "Error occurred while renaming '%s'", path); result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL); free(chrooted_path); return result; @@ -2417,8 +2425,8 @@ static PromiseResult VerifyDelete(EvalContext *ctx, { if (!S_ISDIR(sb->st_mode)) /* file,symlink */ { - int ret = unlink(lastnode); - if (ret == -1) + bool override_immutable = EvalContextOverrideImmutableGet(ctx); + if (!OverrideImmutableDelete(lastnode, override_immutable)) { RecordFailure(ctx, pp, attr, "Couldn't unlink '%s' tidying. (unlink: %s)", path, GetErrorStr()); @@ -2482,15 +2490,16 @@ static PromiseResult TouchFile(EvalContext *ctx, char *path, const Attributes *a PromiseResult result = PROMISE_RESULT_NOOP; if (MakingChanges(ctx, pp, attr, &result, "update time stamps for '%s'", path)) { - if (utime(ToChangesPath(path), NULL) != -1) + bool override_immutable = EvalContextOverrideImmutableGet(ctx); + if (OverrideImmutableUtime(ToChangesPath(path), override_immutable, NULL)) { RecordChange(ctx, pp, attr, "Touched (updated time stamps) for path '%s'", path); result = PromiseResultUpdate(result, PROMISE_RESULT_CHANGE); } else { - RecordFailure(ctx, pp, attr, "Touch '%s' failed to update timestamps. (utime: %s)", - path, GetErrorStr()); + RecordFailure(ctx, pp, attr, "Touch '%s' failed to update timestamps", + path); result = PromiseResultUpdate(result, PROMISE_RESULT_FAIL); } } @@ -2644,7 +2653,8 @@ static PromiseResult VerifyFileAttributes(EvalContext *ctx, const char *file, co if (MakingChanges(ctx, pp, attr, &result, "change permissions of '%s' from %04jo to %04jo", file, (uintmax_t)dstat->st_mode & 07777, (uintmax_t)newperm & 07777)) { - if (safe_chmod(changes_file, newperm & 07777) == -1) + const bool override_immutable = EvalContextOverrideImmutableGet(ctx); + if (!OverrideImmutableChmod(changes_file, newperm & 07777, override_immutable)) { RecordFailure(ctx, pp, attr, "Failed to change permissions of '%s'. (chmod: %s)", file, GetErrorStr()); diff --git a/libpromises/Makefile.am b/libpromises/Makefile.am index b2123440de..88387e12bb 100644 --- a/libpromises/Makefile.am +++ b/libpromises/Makefile.am @@ -137,6 +137,7 @@ libpromises_la_SOURCES = \ modes.c \ monitoring_read.c monitoring_read.h \ ornaments.c ornaments.h \ + override_fsattrs.c override_fsattrs.h \ policy.c policy.h \ parser.c parser.h \ parser_helpers.h \ diff --git a/libpromises/acl_tools.h b/libpromises/acl_tools.h index 80d1176db4..cd9ec4a073 100644 --- a/libpromises/acl_tools.h +++ b/libpromises/acl_tools.h @@ -25,6 +25,19 @@ #ifndef CFENGINE_ACL_TOOLS_H #define CFENGINE_ACL_TOOLS_H +#include +#include + + +/** + * @brief Get ACLs from a file or directory + * @param Path to file or directory + * @param access Get access ACLs if true, otherwise default ACLs + * @return List of ACLs. On error, NULL is returned and errno is set to + * indicate the error. + */ +Rlist *GetACLs(const char *path, bool access); + bool CopyACLs(const char *src, const char *dst, bool *change); /** diff --git a/libpromises/acl_tools_posix.c b/libpromises/acl_tools_posix.c index 5fc2304763..8b21c70e24 100644 --- a/libpromises/acl_tools_posix.c +++ b/libpromises/acl_tools_posix.c @@ -259,8 +259,39 @@ bool AllowAccessForUsers(const char *path, StringSet *users, bool allow_writes, return true; } +Rlist *GetACLs(const char *path, bool access) +{ + assert(path != NULL); + + acl_t acl = acl_get_file(path, access ? ACL_TYPE_ACCESS : ACL_TYPE_DEFAULT); + if (acl == NULL) + { + return NULL; + } + + char *text = acl_to_any_text(acl, NULL, ',', 0); + if (text == NULL) + { + acl_free(acl); + return NULL; + } + + Rlist *lst = RlistFromSplitString(text, ','); + + acl_free(text); + acl_free(acl); + return lst; +} + #elif !defined(__MINGW32__) /* !HAVE_LIBACL */ +Rlist *GetACLs(ARG_UNUSED const char *path, ARG_UNUSED bool access) +{ + /* TODO: Handle Windows ACLs (see ENT-13019) */ + errno = ENOTSUP; + return NULL; +} + bool CopyACLs(ARG_UNUSED const char *src, ARG_UNUSED const char *dst, bool *change) { if (change != NULL) diff --git a/libpromises/attributes.c b/libpromises/attributes.c index 3f5f1380a2..b9fe8674ba 100644 --- a/libpromises/attributes.c +++ b/libpromises/attributes.c @@ -55,6 +55,7 @@ Attributes GetFilesAttributes(const EvalContext *ctx, const Promise *pp) attr.haveselect = PromiseGetConstraintAsBoolean(ctx, "file_select", pp); attr.haverename = PromiseGetConstraintAsBoolean(ctx, "rename", pp); attr.havedelete = PromiseGetConstraintAsBoolean(ctx, "delete", pp); + attr.havefsattrs = PromiseBundleOrBodyConstraintExists(ctx, "fsattrs", pp); attr.content = PromiseGetConstraintAsRval(pp, "content", RVAL_TYPE_SCALAR); attr.haveperms = PromiseGetConstraintAsBoolean(ctx, "perms", pp); attr.havechange = PromiseGetConstraintAsBoolean(ctx, "changes", pp); @@ -89,6 +90,7 @@ Attributes GetFilesAttributes(const EvalContext *ctx, const Promise *pp) attr.perms = GetPermissionConstraints(ctx, pp); attr.select = GetSelectConstraints(ctx, pp); attr.delete = GetDeleteConstraints(ctx, pp); + attr.fsattrs = GetFSAttrsConstraints(ctx, pp); attr.rename = GetRenameConstraints(ctx, pp); attr.change = GetChangeMgtConstraints(ctx, pp); attr.copy = GetCopyConstraints(ctx, pp); @@ -830,6 +832,23 @@ FileDelete GetDeleteConstraints(const EvalContext *ctx, const Promise *pp) return f; } +/*******************************************************************/ + +FileFSAttrs GetFSAttrsConstraints(const EvalContext *ctx, const Promise *pp) +{ + assert(ctx != NULL); + assert(pp != NULL); + + FileFSAttrs f = + { + .immutable = PromiseGetConstraintAsBoolean(ctx, "immutable", pp), + .haveimmutable = PromiseBundleOrBodyConstraintExists(ctx, "immutable", pp), + }; + + return f; +} + + /*******************************************************************/ FileRename GetRenameConstraints(const EvalContext *ctx, const Promise *pp) diff --git a/libpromises/attributes.h b/libpromises/attributes.h index f7089c1688..02237f08b1 100644 --- a/libpromises/attributes.h +++ b/libpromises/attributes.h @@ -68,6 +68,7 @@ ENTERPRISE_FUNC_0ARG_DECLARE(HashMethod, GetBestFileChangeHashMethod); FileChange GetChangeMgtConstraints(const EvalContext *ctx, const Promise *pp); FileCopy GetCopyConstraints(const EvalContext *ctx, const Promise *pp); FileDelete GetDeleteConstraints(const EvalContext *ctx, const Promise *pp); +FileFSAttrs GetFSAttrsConstraints(const EvalContext *ctx, const Promise *pp); FileLink GetLinkConstraints(const EvalContext *ctx, const Promise *pp); FileRename GetRenameConstraints(const EvalContext *ctx, const Promise *pp); FileSelect GetSelectConstraints(const EvalContext *ctx, const Promise *pp); diff --git a/libpromises/cf3.defs.h b/libpromises/cf3.defs.h index 039afee5c8..05d311308b 100644 --- a/libpromises/cf3.defs.h +++ b/libpromises/cf3.defs.h @@ -1061,6 +1061,14 @@ typedef struct /*************************************************************************/ +typedef struct +{ + int immutable; + int haveimmutable; +} FileFSAttrs; + +/*************************************************************************/ + typedef struct { char *newname; @@ -1530,6 +1538,7 @@ typedef struct FilePerms perms; FileCopy copy; FileDelete delete; + FileFSAttrs fsattrs; char *content; FileRename rename; FileChange change; @@ -1581,6 +1590,7 @@ typedef struct int haveselect; int haverename; int havedelete; + int havefsattrs; int haveperms; int havechange; int havecopy; diff --git a/libpromises/eval_context.c b/libpromises/eval_context.c index 50a433dc11..13b9685723 100644 --- a/libpromises/eval_context.c +++ b/libpromises/eval_context.c @@ -192,8 +192,21 @@ struct EvalContext_ RemoteVarPromisesMap *remote_var_promises; bool dump_reports; + bool override_immutable; }; +void EvalContextOverrideImmutableSet(EvalContext *ctx, bool should_override) +{ + assert(ctx != NULL); + ctx->override_immutable = should_override; +} + +bool EvalContextOverrideImmutableGet(EvalContext *ctx) +{ + assert(ctx != NULL); + return ctx->override_immutable; +} + void EvalContextSetConfig(EvalContext *ctx, const GenericAgentConfig *config) { assert(ctx != NULL); diff --git a/libpromises/eval_context.h b/libpromises/eval_context.h index 6ad09c08b3..48fefd76c2 100644 --- a/libpromises/eval_context.h +++ b/libpromises/eval_context.h @@ -128,6 +128,9 @@ void EvalContextHeapPersistentSave(EvalContext *ctx, const char *name, unsigned void EvalContextHeapPersistentRemove(const char *context); void EvalContextHeapPersistentLoadAll(EvalContext *ctx); +void EvalContextOverrideImmutableSet(EvalContext *ctx, bool should_override); +bool EvalContextOverrideImmutableGet(EvalContext *ctx); + /** * Sets negated classes (persistent classes that should not be defined). * diff --git a/libpromises/evalfunction.c b/libpromises/evalfunction.c index 498476d5af..b5cc972221 100644 --- a/libpromises/evalfunction.c +++ b/libpromises/evalfunction.c @@ -82,6 +82,10 @@ #include #include +#include +#include +#include +#include #ifdef HAVE_LIBCURL #include @@ -654,6 +658,43 @@ static Rlist *GetHostsFromLastseenDB(Seq *host_data, time_t horizon, HostsSeenFi /*********************************************************************/ +static FnCallResult FnCallGetACLs(ARG_UNUSED EvalContext *ctx, + ARG_UNUSED const Policy *policy, + const FnCall *fp, + const Rlist *final_args) +{ + assert(fp != NULL); + assert(final_args != NULL); + assert(final_args->next != NULL); + + const char *path = RlistScalarValue(final_args); + const char *type = RlistScalarValue(final_args->next); + assert(StringEqual(type, "default") || StringEqual(type, "access")); + +#ifdef _WIN32 + /* TODO: Policy function to read Windows ACLs (ENT-13019) */ + Rlist *acls = NULL; + errno = ENOTSUP; +#else + Rlist *acls = GetACLs(path, StringEqual(type, "access")); +#endif /* _WIN32 */ + if (acls == NULL) + { + Log((errno != ENOTSUP) ? LOG_LEVEL_ERR : LOG_LEVEL_VERBOSE, + "Function %s failed to get ACLs for '%s': %s", + fp->name, path, GetErrorStr()); + + if (errno != ENOTSUP) + { + return FnFailure(); + } /* else we'll just return an empty list instead */ + } + + return (FnCallResult) { FNCALL_SUCCESS, { acls, RVAL_TYPE_LIST } }; +} + +/*********************************************************************/ + static FnCallResult FnCallAnd(EvalContext *ctx, ARG_UNUSED const Policy *policy, ARG_UNUSED const FnCall *fp, @@ -946,7 +987,7 @@ static FnCallResult FnCallFindLocalUsers(EvalContext *ctx, ARG_UNUSED const Poli assert(fp != NULL); bool allocated = false; JsonElement *json = VarNameOrInlineToJson(ctx, fp, finalargs, false, &allocated); - + // we failed to produce a valid JsonElement, so give up if (json == NULL) { @@ -961,7 +1002,7 @@ static FnCallResult FnCallFindLocalUsers(EvalContext *ctx, ARG_UNUSED const Poli JsonDestroyMaybe(json, allocated); return FnFailure(); } - + JsonElement *parent = JsonObjectCreate(10); setpwent(); struct passwd *pw; @@ -1006,7 +1047,7 @@ static FnCallResult FnCallFindLocalUsers(EvalContext *ctx, ARG_UNUSED const Poli { char uid_string[PRINTSIZE(pw->pw_uid)]; int ret = snprintf(uid_string, sizeof(uid_string), "%u", pw->pw_uid); - + if (ret < 0) { Log(LOG_LEVEL_ERR, "Couldn't convert the uid of '%s' to string in function '%s'", @@ -1038,7 +1079,7 @@ static FnCallResult FnCallFindLocalUsers(EvalContext *ctx, ARG_UNUSED const Poli assert((size_t) ret < sizeof(gid_string)); if (!StringMatchFull(value, gid_string)) - { + { can_add_to_json = false; } } @@ -1063,13 +1104,13 @@ static FnCallResult FnCallFindLocalUsers(EvalContext *ctx, ARG_UNUSED const Poli can_add_to_json = false; } } - else + else { Log(LOG_LEVEL_ERR, "Invalid attribute '%s' in function '%s': not supported", attribute, fp->name); JsonDestroyMaybe(json, allocated); JsonDestroy(parent); - return FnFailure(); + return FnFailure(); } element = JsonIteratorNextValue(&iter); } @@ -1367,7 +1408,7 @@ static FnCallResult FnCallGetGid(ARG_UNUSED EvalContext *ctx, ARG_UNUSED const P #endif } -/*********************************************************************/ +/*********************************************************************/ static FnCallResult no_entry(int ret, const FnCall *fp, const char *group_name, bool is_user_db) { @@ -1391,7 +1432,7 @@ static FnCallResult FnCallUserInGroup(ARG_UNUSED EvalContext *ctx, ARG_UNUSED co assert(finalargs != NULL); #ifdef _WIN32 Log(LOG_LEVEL_ERR, "Function '%s' is POSIX specific", fp->name); - return FnFailure(); + return FnFailure(); #else const char *user_name = RlistScalarValue(finalargs); @@ -1405,7 +1446,7 @@ static FnCallResult FnCallUserInGroup(ARG_UNUSED EvalContext *ctx, ARG_UNUSED co struct group *grent; char gr_buf[GETGR_R_SIZE_MAX] = {0}; ret = getgrnam_r(group_name, &grp, gr_buf, GETGR_R_SIZE_MAX, &grent); - + if (grent == NULL) { // Group does not exist at all, so cannot be @@ -1432,7 +1473,7 @@ static FnCallResult FnCallUserInGroup(ARG_UNUSED EvalContext *ctx, ARG_UNUSED co return no_entry(ret, fp, user_name, true); } return FnReturnContext(grent->gr_gid == pwent->pw_gid); - + #endif } @@ -9776,6 +9817,13 @@ static const FnCallArg AND_ARGS[] = {NULL, CF_DATA_TYPE_NONE, NULL} }; +static const FnCallArg GET_ACLS_ARGS[] = +{ + {CF_ABSPATHRANGE, CF_DATA_TYPE_STRING, "Path to file or directory"}, + {"default,access", CF_DATA_TYPE_OPTION, "Whether to get default or access ACL"}, + {NULL, CF_DATA_TYPE_NONE, NULL}, +}; + static const FnCallArg AGO_ARGS[] = { {"0,1000", CF_DATA_TYPE_INT, "Years"}, @@ -10800,7 +10848,7 @@ static const FnCallArg IS_DATATYPE_ARGS[] = {NULL, CF_DATA_TYPE_NONE, NULL} }; static const FnCallArg FIND_LOCAL_USERS_ARGS[] = -{ +{ {CF_ANYSTRING, CF_DATA_TYPE_STRING, "Filter list"}, {NULL, CF_DATA_TYPE_NONE, NULL} }; @@ -10820,6 +10868,8 @@ const FnCallType CF_FNCALL_TYPES[] = FNCALL_OPTION_NONE, FNCALL_CATEGORY_FILES, SYNTAX_STATUS_NORMAL), FnCallTypeNew("accumulated", CF_DATA_TYPE_INT, ACCUM_ARGS, &FnCallAccumulatedDate, "Convert an accumulated amount of time into a system representation", FNCALL_OPTION_NONE, FNCALL_CATEGORY_DATA, SYNTAX_STATUS_NORMAL), + FnCallTypeNew("getacls", CF_DATA_TYPE_STRING_LIST, GET_ACLS_ARGS, &FnCallGetACLs, "Get ACLs of a given file", + FNCALL_OPTION_NONE, FNCALL_CATEGORY_FILES, SYNTAX_STATUS_NORMAL), FnCallTypeNew("ago", CF_DATA_TYPE_INT, AGO_ARGS, &FnCallAgoDate, "Convert a time relative to now to an integer system representation", FNCALL_OPTION_NONE, FNCALL_CATEGORY_DATA, SYNTAX_STATUS_NORMAL), FnCallTypeNew("and", CF_DATA_TYPE_CONTEXT, AND_ARGS, &FnCallAnd, "Calculate whether all arguments evaluate to true", diff --git a/libpromises/files_operators.c b/libpromises/files_operators.c index f8d212edb3..d450b8a7e5 100644 --- a/libpromises/files_operators.c +++ b/libpromises/files_operators.c @@ -47,6 +47,7 @@ #include #include #include +#include bool MoveObstruction(EvalContext *ctx, char *from, const Attributes *attr, const Promise *pp, PromiseResult *result) @@ -149,7 +150,7 @@ bool MoveObstruction(EvalContext *ctx, char *from, const Attributes *attr, const /*********************************************************************/ -bool SaveAsFile(SaveCallbackFn callback, void *param, const char *file, const Attributes *a, NewLineMode new_line_mode) +bool SaveAsFile(EvalContext *ctx, SaveCallbackFn callback, void *param, const char *file, const Attributes *a, NewLineMode new_line_mode) { assert(a != NULL); struct stat statbuf; @@ -277,7 +278,8 @@ bool SaveAsFile(SaveCallbackFn callback, void *param, const char *file, const At unlink(backup); } - if (rename(new, BufferData(deref_file)) == -1) + const bool override_immutable = EvalContextOverrideImmutableGet(ctx); + if (!OverrideImmutableRename(new, BufferData(deref_file), override_immutable)) { Log(LOG_LEVEL_ERR, "Can't rename '%s' to %s - so promised edits could not be moved into place. (rename: %s)", new, BufferData(pretty_file), GetErrorStr()); @@ -331,10 +333,10 @@ static bool SaveItemListCallback(const char *dest_filename, void *param, NewLine /*********************************************************************/ -bool SaveItemListAsFile(Item *liststart, const char *file, const Attributes *a, NewLineMode new_line_mode) +bool SaveItemListAsFile(EvalContext *ctx, Item *liststart, const char *file, const Attributes *a, NewLineMode new_line_mode) { assert(a != NULL); - return SaveAsFile(&SaveItemListCallback, liststart, file, a, new_line_mode); + return SaveAsFile(ctx, &SaveItemListCallback, liststart, file, a, new_line_mode); } // Some complex logic here to enable warnings of diffs to be given diff --git a/libpromises/files_operators.h b/libpromises/files_operators.h index 9ef4816a68..6d531c4d3a 100644 --- a/libpromises/files_operators.h +++ b/libpromises/files_operators.h @@ -31,8 +31,8 @@ bool MoveObstruction(EvalContext *ctx, char *from, const Attributes *attr, const Promise *pp, PromiseResult *result); typedef bool (*SaveCallbackFn)(const char *dest_filename, void *param, NewLineMode new_line_mode); -bool SaveAsFile(SaveCallbackFn callback, void *param, const char *file, const Attributes *a, NewLineMode new_line_mode); -bool SaveItemListAsFile(Item *liststart, const char *file, const Attributes *a, NewLineMode new_line_mode); +bool SaveAsFile(EvalContext *ctx, SaveCallbackFn callback, void *param, const char *file, const Attributes *a, NewLineMode new_line_mode); +bool SaveItemListAsFile(EvalContext *ctx, Item *liststart, const char *file, const Attributes *a, NewLineMode new_line_mode); bool CompareToFile(EvalContext *ctx, const Item *liststart, const char *file, const Attributes *a, const Promise *pp, PromiseResult *result); diff --git a/libpromises/mod_files.c b/libpromises/mod_files.c index 7f2abf904a..5ee17053ee 100644 --- a/libpromises/mod_files.c +++ b/libpromises/mod_files.c @@ -227,6 +227,15 @@ static const ConstraintSyntax delete_constraints[] = static const BodySyntax delete_body = BodySyntaxNew("delete", delete_constraints, NULL, SYNTAX_STATUS_NORMAL); +static const ConstraintSyntax fsattrs_constraints[] = +{ + CONSTRAINT_SYNTAX_GLOBAL, + ConstraintSyntaxNewBool("immutable", "true to set / false to clear the immutable flag", SYNTAX_STATUS_NORMAL), + ConstraintSyntaxNewNull() +}; + +static const BodySyntax fsattrs_body = BodySyntaxNew("fsattrs", fsattrs_constraints, NULL, SYNTAX_STATUS_NORMAL); + static const ConstraintSyntax rename_constraints[] = { CONSTRAINT_SYNTAX_GLOBAL, @@ -339,6 +348,7 @@ static const ConstraintSyntax CF_FILES_BODIES[] = ConstraintSyntaxNewBody("copy_from", ©_from_body, "Criteria for copying file from a source", SYNTAX_STATUS_NORMAL), ConstraintSyntaxNewBool("create", "true/false whether to create non-existing file. Default value: false", SYNTAX_STATUS_NORMAL), ConstraintSyntaxNewBody("delete", &delete_body, "Criteria for deleting files", SYNTAX_STATUS_NORMAL), + ConstraintSyntaxNewBody("fsattrs", &fsattrs_body, "Control file system attributes", SYNTAX_STATUS_NORMAL), ConstraintSyntaxNewString("content", CF_ANYSTRING, "Complete content the promised file should contain", SYNTAX_STATUS_NORMAL), ConstraintSyntaxNewBody("depth_search", &depth_search_body, "Criteria for file depth searches", SYNTAX_STATUS_NORMAL), ConstraintSyntaxNewBody("edit_defaults", &edit_defaults_body, "Default promise details for file edits", SYNTAX_STATUS_NORMAL), diff --git a/libpromises/override_fsattrs.c b/libpromises/override_fsattrs.c new file mode 100644 index 0000000000..b4cae2f6d4 --- /dev/null +++ b/libpromises/override_fsattrs.c @@ -0,0 +1,260 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +bool OverrideImmutableBegin( + const char *orig, char *copy, size_t copy_len, bool override) +{ + if (!override) + { + size_t ret = strlcpy(copy, orig, copy_len); + if (ret >= copy_len) + { + Log(LOG_LEVEL_ERR, + "Failed to copy filename '%s': Filename too long (%zu >= %zu)", + orig, + ret, + copy_len); + return false; + } + return true; + } + + srand(time(NULL)); /* Seed random number generator */ + int rand_number = rand() % 999999; + assert(rand_number >= 0); + + /* Inspired by mkstemp(3) */ + int ret = snprintf(copy, copy_len, "%s.%06d" CF_NEW, orig, rand_number); + if (ret < 0 || (size_t) ret >= copy_len) + { + Log(LOG_LEVEL_ERR, + "Failed to generate name for temporary copy of '%s': Filename is too long (%d >= %zu)", + orig, + ret, + copy_len); + return false; + } + + /* We'll match the original file permissions on commit */ + if (!CopyRegularFileDiskPerms(orig, copy, 0600)) + { + Log(LOG_LEVEL_ERR, + "Failed to copy file '%s' to temporary file '%s'", + orig, + copy); + return false; + } + + return true; +} + +bool OverrideImmutableCommit( + const char *orig, const char *copy, bool override, bool abort) +{ + if (!override) + { + return true; + } + + if (abort) + { + return unlink(copy) == 0; + } + + struct stat sb; + if (lstat(orig, &sb) == -1) + { + Log(LOG_LEVEL_ERR, + "Failed to stat file '%s' during immutable operations", + orig); + unlink(copy); + return false; + } + + if (chmod(copy, sb.st_mode) == -1) + { + Log(LOG_LEVEL_ERR, + "Failed to change mode bits on file '%s' to %04jo during immutable operations: %s", + orig, + (uintmax_t) sb.st_mode, + GetErrorStr()); + unlink(copy); + return false; + } + + return OverrideImmutableRename(copy, orig, override); +} + +FSAttrsResult TemporarilyClearImmutableBit( + const char *filename, bool override, bool *was_immutable) +{ + if (!override) + { + return FS_ATTRS_FAILURE; + } + + FSAttrsResult res = FSAttrsGetImmutableFlag(filename, was_immutable); + if (res == FS_ATTRS_SUCCESS) + { + if (*was_immutable) + { + res = FSAttrsUpdateImmutableFlag(filename, false); + if (res == FS_ATTRS_SUCCESS) + { + Log(LOG_LEVEL_VERBOSE, + "Temporarily cleared immutable bit for file '%s'", + filename); + } + else + { + if (res == FS_ATTRS_FAILURE) + { + Log(LOG_LEVEL_ERR, + "Failed to temporarily clear immutable bit for file '%s': %s", + filename, + FSAttrsErrorCodeToString(res)); + } + else + { + Log(LOG_LEVEL_VERBOSE, + "Could not temporarily clear immutable bit for file '%s': %s", + filename, + FSAttrsErrorCodeToString(res)); + } + } + } + else + { + Log(LOG_LEVEL_DEBUG, + "The immutable bit is not set on file '%s'", + filename); + } + } + else + { + Log((res == FS_ATTRS_FAILURE) ? LOG_LEVEL_ERR : LOG_LEVEL_VERBOSE, + "Failed to get immutable bit from file '%s': %s", + filename, + FSAttrsErrorCodeToString(res)); + } + + return res; +} + +void ResetTemporarilyClearedImmutableBit( + const char *filename, bool override, FSAttrsResult res, bool was_immutable) +{ + if (!override) + { + return; + } + + if ((res == FS_ATTRS_SUCCESS) && was_immutable) + { + res = FSAttrsUpdateImmutableFlag(filename, true); + if (res == FS_ATTRS_SUCCESS) + { + Log(LOG_LEVEL_VERBOSE, + "Reset immutable bit after temporarily clearing it from file '%s'", + filename); + } + else + { + Log((res == FS_ATTRS_FAILURE) ? LOG_LEVEL_ERR : LOG_LEVEL_VERBOSE, + "Failed to reset immutable bit after temporarily clearing it from file '%s': %s", + filename, + FSAttrsErrorCodeToString(res)); + } + } +} + +bool OverrideImmutableChmod(const char *filename, mode_t mode, bool override) +{ + assert(filename != NULL); + + bool is_immutable; + FSAttrsResult res = + TemporarilyClearImmutableBit(filename, override, &is_immutable); + + int ret = safe_chmod(filename, mode); + if (ret == -1) + { + Log(LOG_LEVEL_ERR, + "Failed to change mode on file '%s': %s", + filename, + GetErrorStr()); + } + + ResetTemporarilyClearedImmutableBit(filename, override, res, is_immutable); + + return ret == 0; +} + +bool OverrideImmutableRename( + const char *old_filename, const char *new_filename, bool override) +{ + assert(old_filename != NULL); + assert(new_filename != NULL); + + bool new_is_immutable; + TemporarilyClearImmutableBit(new_filename, override, &new_is_immutable); + + bool old_is_immutable; + FSAttrsResult res_old = TemporarilyClearImmutableBit( + old_filename, override, &old_is_immutable); + + if (rename(old_filename, new_filename) == -1) + { + Log(LOG_LEVEL_ERR, + "Failed to replace original file '%s' with copy '%s': %s", + new_filename, + old_filename, + GetErrorStr()); + unlink(old_filename); + return false; + } + + ResetTemporarilyClearedImmutableBit( + new_filename, override, res_old, old_is_immutable); + + return true; +} + +bool OverrideImmutableDelete(const char *filename, bool override) +{ + assert(filename != NULL); + + bool is_immutable = false; + TemporarilyClearImmutableBit(filename, override, &is_immutable); + + return unlink(filename) == 0; +} + +bool OverrideImmutableUtime( + const char *filename, bool override, const struct utimbuf *times) +{ + assert(filename != NULL); + + bool is_immutable; + FSAttrsResult res = + TemporarilyClearImmutableBit(filename, override, &is_immutable); + + int ret = utime(filename, times); + if (ret == -1) + { + Log(LOG_LEVEL_ERR, + "Failed to update access and modification times of file '%s'", + filename); + } + + ResetTemporarilyClearedImmutableBit(filename, override, res, is_immutable); + + return ret == 0; +} diff --git a/libpromises/override_fsattrs.h b/libpromises/override_fsattrs.h new file mode 100644 index 0000000000..e6502736a9 --- /dev/null +++ b/libpromises/override_fsattrs.h @@ -0,0 +1,123 @@ +/* + Copyright 2025 Northern.tech AS + + This file is part of CFEngine 3 - written and maintained by Northern.tech AS. + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the + Free Software Foundation; version 3. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + + To the extent this program is licensed as part of the Enterprise + versions of CFEngine, the applicable Commercial Open Source License + (COSL) may apply to this file if you as a licensee so wish it. See + included file COSL.txt. +*/ + +#ifndef CFENGINE_OVERRIDE_FSATTRS_H +#define CFENGINE_OVERRIDE_FSATTRS_H + +#include +#include +#include +#include +#include + +/** + * @brief Creates a mutable copy of the original file + * @param orig The original file (may be immutable) + * @param copy Updated to contain the filename of the mutable copy + * @param copy_len The size of the buffer to store the filename of the copy + * @param override Whether to actually do override (i.e. temporarily clear it). + * The original filename is copied to copy-buffer if false + * @return false in case of failure + */ +bool OverrideImmutableBegin( + const char *orig, char *copy, size_t copy_len, bool override); + +/** + * @brief Temporarily clears the immutable bit of the original file and + * replaces it with the mutated copy + * @param orig The original file (may be immutable) + * @param copy The mutated copy to replace the original + * @param override Whether to actually do override (i.e. temporarily clear it) + * @param abort Whether to abort the override + * @return false in case of failure + * @note The immutable bit is reset to it's original state + */ +bool OverrideImmutableCommit( + const char *orig, const char *copy, bool override, bool abort); + +/** + * @brief Change mode on an immutable file + * @param filename Name of the file + * @param mode The file mode + * @param override Whether to actually do override (i.e. temporarily clear it) + * @return false in case of failure + * @note It uses safe_chmod() under the hood + */ +bool OverrideImmutableChmod(const char *filename, mode_t mode, bool override); + +/** + * @brief Temporarily clears the immutable bit of the old file and renames the + * new to the old + * @param old_filename Filename of the old file + * @param new_filename Filename of the new file + * @param override Whether to actually do override (i.e. temporarily clear it) + * @return false in case of failure + */ +bool OverrideImmutableRename( + const char *old_filename, const char *new_filename, bool override); + +/** + * @brief Delete immutable file + * @param filename Name of the file to delete + * @param override Whether to actually do override (i.e. temporarily clear it) + * @return false in case of failure + */ +bool OverrideImmutableDelete(const char *filename, bool override); + +/** + * @brief Temporarily clears the immutable bit and changes access and + * modification times of the inode + * @param filename Name of the file to touch + * @param override Whether to actually do override (i.e. temporarily clear it) + * @param times Modification times (can be NULL) + * @return false in case of failure + */ +bool OverrideImmutableUtime( + const char *filename, bool override, const struct utimbuf *times); + +/** + * @brief Temporarily clears the immutable bit (best effort / no guarantees) + * @param filename Name of the file to clear the immutable bit on + * @param override Whether to actually do override (i.e. temporarily clear it) + * @param is_immutable Whether or not the file actually was immutable + * @return Result of clearing the immutable bit (no to be interpreted by the + * caller) + */ +FSAttrsResult TemporarilyClearImmutableBit( + const char *filename, bool override, bool *was_immutable); + +/** + * @brief Reset temporarily cleared immutable bit + * @param filename Name of the file to clear the immutable bit on + * @param override Whether to actually do override (i.e. temporarily clear it) + * @param res The result from previously clearing it + * @param is_immutable Whether or not the file actually was immutable + */ +void ResetTemporarilyClearedImmutableBit( + const char *filename, + bool override, + FSAttrsResult res, + bool was_immutable); + +#endif /* CFENGINE_OVERRIDE_FSATTRS_H */ diff --git a/tests/acceptance/10_files/12_acl/getacls.cf b/tests/acceptance/10_files/12_acl/getacls.cf new file mode 100644 index 0000000000..619792c8f8 --- /dev/null +++ b/tests/acceptance/10_files/12_acl/getacls.cf @@ -0,0 +1,90 @@ +############################################################################## +# +# Test policy function getacls() +# +############################################################################## + +body common control +{ + inputs => { "../../default.cf.sub" }; + bundlesequence => { default("$(this.promise_filename)") }; + version => "1.0"; +} + +############################################################################## + +body acl user_root_rwx_acl +{ + acl_method => "append"; + acl_default => "access"; + aces => { "user:root:rwx" }; +} + +############################################################################## + +bundle agent init +{ + files: + "$(G.testdir)/." + create => "true", + acl => user_root_rwx_acl, + handle => "Default ACLs is set"; + "$(G.testdir)/foo" + create => "true", + depends_on => { "Default ACLs is set" }, + comment => "Inherits ACLs from parent directory"; +} + +############################################################################## + +bundle agent test +{ + meta: + "description" -> { "CFE-4529" } + string => "Test policy function getacls()"; + + "test_soft_fail" + string => "windows", + meta => { "ENT-13019" }; + + vars: + "default_acls" + slist => getacls("$(G.testdir)", "default"), + if => fileexists("$(G.testdir)"); + "access_acls" + slist => getacls("$(G.testdir)/foo", "access"), + if => fileexists("$(G.testdir)/foo"); +} + +############################################################################## + +bundle agent check +{ + classes: + "acls_not_supported" + expression => eval("$(with) == 0", "class", "infix"), + with => length("test.default_acls"), + comment => "getacls() returns empty list if unsupported"; + "default_ok" + expression => some("$(expected)", "test.default_acls"); + "access_ok" + expression => some("$(expected)", "test.access_acls"); + + vars: + "expected" + string => ".*user:root:rwx.*"; + + reports: + acls_not_supported:: + "$(this.promise_filename) Skip/unsupported"; + default_ok&access_ok:: + "$(this.promise_filename) Pass"; + !(default_ok&access_ok):: + "$(this.promise_filename) FAIL"; + "Expecting one match of '$(expected)' in default ACLs [$(with)]" + with => join(", ", "test.default_acls"); + "Expecting one match of '$(expected)' in access ACLs [$(with)]" + with => join(", ", "test.access_acls"); +} + +############################################################################## diff --git a/tests/acceptance/10_files/unsafe/00_immutable.cf b/tests/acceptance/10_files/unsafe/00_immutable.cf new file mode 100644 index 0000000000..54d27cadf4 --- /dev/null +++ b/tests/acceptance/10_files/unsafe/00_immutable.cf @@ -0,0 +1,69 @@ +############################################################################## +# +# Test that agent can set the immutable bit on a file +# +############################################################################## + +body common control +{ + inputs => { "../../default.cf.sub" }; + bundlesequence => { default("$(this.promise_filename)") }; + version => "1.0"; +} + +bundle agent global +{ + vars: + "testfile" + string => "/tmp/00_immutable.txt"; +} + +body fsattrs set_immutable +{ + immutable => "true"; +} + +bundle agent init +{ + files: + "$(global.testfile)" + create => "true"; + + commands: + "chattr -i $(global.testfile)" + contain => in_shell; +} + +bundle agent test +{ + meta: + "description" -> { "CFE-1840", "ENT-10961" } + string => "Test that agent can set the immutable bit on a file"; + + "test_skip_unsupported" + string => "hpux|aix|solaris|windows"; + + files: + "$(global.testfile)" + fsattrs => set_immutable; +} + +bundle agent check +{ + methods: + "check" + usebundle => dcs_passif_output(".*Immutable.*", "", "lsattr -l $(global.testfile)", "$(this.promise_filename)"); +} + +bundle agent destroy +{ + commands: + "chattr -i $(global.testfile)" + contain => in_shell, + handle => "is mutable"; + + files: + "$(global.testfile)" + delete => tidy, + depends_on => { "is mutable" }; +} diff --git a/tests/acceptance/10_files/unsafe/01_immutable.cf b/tests/acceptance/10_files/unsafe/01_immutable.cf new file mode 100644 index 0000000000..95e21234fa --- /dev/null +++ b/tests/acceptance/10_files/unsafe/01_immutable.cf @@ -0,0 +1,69 @@ +############################################################################## +# +# Test that agent can clear the immutable bit on a file +# +############################################################################## + +body common control +{ + inputs => { "../../default.cf.sub" }; + bundlesequence => { default("$(this.promise_filename)") }; + version => "1.0"; +} + +bundle agent global +{ + vars: + "testfile" + string => "/tmp/01_immutable.txt"; +} + +body fsattrs clear_immutable +{ + immutable => "false"; +} + +bundle agent init +{ + files: + "$(global.testfile)" + create => "true"; + + commands: + "chattr +i $(global.testfile)" + contain => in_shell; +} + +bundle agent test +{ + meta: + "description" -> { "CFE-1840", "ENT-10961" } + string => "Test that agent can clear the immutable bit on a file"; + + "test_skip_unsupported" + string => "hpux|aix|solaris|windows"; + + files: + "$(global.testfile)" + fsattrs => clear_immutable; +} + +bundle agent check +{ + methods: + "check" + usebundle => dcs_passif_output(".*", ".*Immutable.*", "lsattr -l $(global.testfile)", "$(this.promise_filename)"); +} + +bundle agent destroy +{ + commands: + "chattr -i $(global.testfile)" + contain => in_shell, + handle => "is mutable"; + + files: + "$(global.testfile)" + delete => tidy, + depends_on => { "is mutable" }; +} diff --git a/tests/acceptance/10_files/unsafe/02_immutable.cf b/tests/acceptance/10_files/unsafe/02_immutable.cf new file mode 100644 index 0000000000..3e201d7856 --- /dev/null +++ b/tests/acceptance/10_files/unsafe/02_immutable.cf @@ -0,0 +1,85 @@ +############################################################################## +# +# Test that agent can override the immutable bit on a file while using the +# content attribute +# +############################################################################## + +body common control +{ + inputs => { "../../default.cf.sub" }; + bundlesequence => { default("$(this.promise_filename)") }; + version => "1.0"; +} + +bundle agent global +{ + vars: + "testfile" + string => "/tmp/02_immutable.txt"; +} + +body fsattrs set_immutable +{ + immutable => "true"; +} + +bundle agent init +{ + files: + "$(global.testfile)" + content => "I'm immutable"; + + commands: + "chattr +i $(global.testfile)" + contain => in_shell; +} + +bundle agent test +{ + meta: + "description" -> { "CFE-1840", "ENT-10961" } + string => "Test that agent can override the immutable bit on a file while using the content attribute"; + + "test_skip_unsupported" + string => "hpux|aix|solaris|windows"; + + files: + "$(global.testfile)" + fsattrs => set_immutable, + content => "But agent can override"; +} + +bundle agent check +{ + vars: + "expected" + string => "But agent can override"; + "actual" + string => readfile("$(global.testfile)"); + + classes: + "ok" + expression => strcmp("$(actual)", "$(expected)"); + + reports: + ok:: + "$(this.promise_filename) Pass"; + !ok:: + "$(this.promise_filename) FAIL"; + any:: + "Expected: '$(expected)', actual: '$(actual)'"; +} + +bundle agent destroy +{ + commands: + "chattr -i $(global.testfile)" + contain => in_shell, + handle => "is mutable"; + + files: + "$(global.testfile)" + delete => tidy, + depends_on => { "is mutable" }; +} diff --git a/tests/acceptance/10_files/unsafe/03_immutable.cf b/tests/acceptance/10_files/unsafe/03_immutable.cf new file mode 100644 index 0000000000..e18cdc3191 --- /dev/null +++ b/tests/acceptance/10_files/unsafe/03_immutable.cf @@ -0,0 +1,91 @@ +############################################################################## +# +# Test that agent can override the immutable bit when using copy_from +# attribute +# +############################################################################## + +body common control +{ + inputs => { "../../default.cf.sub" }; + bundlesequence => { default("$(this.promise_filename)") }; + version => "1.0"; +} + +bundle agent global +{ + vars: + "testfile" + string => "/tmp/03_immutable.txt"; +} + +body fsattrs set_immutable +{ + immutable => "true"; +} + +bundle agent init +{ + files: + "$(global.testfile)" + content => "I'm immutable"; + "/tmp/content.txt" + content => "But agent can override"; + + commands: + "chattr +i $(global.testfile)" + contain => in_shell; +} + +bundle agent test +{ + meta: + "description" -> { "CFE-1840", "ENT-10961" } + string => "Test that agent can override the immutable bit on a file while using the copy_from attribute"; + + "test_skip_unsupported" + string => "hpux|aix|solaris|windows"; + + files: + "$(global.testfile)" + fsattrs => set_immutable, + copy_from => local_dcp("/tmp/content.txt"); +} + +bundle agent check +{ + vars: + "expected" + string => "But agent can override"; + "actual" + string => readfile("$(global.testfile)"); + + classes: + "ok" + expression => strcmp("$(actual)", "$(expected)"); + + reports: + ok:: + "$(this.promise_filename) Pass"; + !ok:: + "$(this.promise_filename) FAIL"; + any:: + "Expected: '$(expected)', actual: '$(actual)'"; +} + +bundle agent destroy +{ + commands: + "chattr -i $(global.testfile)" + contain => in_shell, + handle => "is mutable"; + + files: + "$(global.testfile)" + delete => tidy, + depends_on => { "is mutable" }; + "$(global.testfile).cfsaved" + delete => tidy; + "/tmp/content.txt" + delete => tidy; +} diff --git a/tests/acceptance/10_files/unsafe/04_immutable.cf b/tests/acceptance/10_files/unsafe/04_immutable.cf new file mode 100644 index 0000000000..00979f7f7b --- /dev/null +++ b/tests/acceptance/10_files/unsafe/04_immutable.cf @@ -0,0 +1,77 @@ +############################################################################## +# +# Test that agent can override the immutable bit when using delete +# attribute +# +############################################################################## + +body common control +{ + inputs => { "../../default.cf.sub" }; + bundlesequence => { default("$(this.promise_filename)") }; + version => "1.0"; +} + +bundle agent global +{ + vars: + "testfile" + string => "/tmp/04_immutable.txt"; +} + +body fsattrs set_immutable +{ + immutable => "true"; +} + +bundle agent init +{ + files: + "$(global.testfile)" + content => "I'm immutable"; + + commands: + "chattr +i $(global.testfile)" + contain => in_shell; +} + +bundle agent test +{ + meta: + "description" -> { "CFE-1840", "ENT-10961" } + string => "Test that agent can override the immutable bit on a file while using the delete attribute"; + + "test_skip_unsupported" + string => "hpux|aix|solaris|windows"; + + files: + "$(global.testfile)" + fsattrs => set_immutable, + delete => tidy; +} + +bundle agent check +{ + classes: + "ok" + expression => not(fileexists("$(global.testfile)")); + + reports: + ok:: + "$(this.promise_filename) Pass"; + !ok:: + "$(this.promise_filename) FAIL"; +} + +bundle agent destroy +{ + commands: + "chattr -i $(global.testfile)" + contain => in_shell, + handle => "is mutable"; + + files: + "$(global.testfile)" + delete => tidy, + depends_on => { "is mutable" }; +} diff --git a/tests/acceptance/10_files/unsafe/05_immutable.cf b/tests/acceptance/10_files/unsafe/05_immutable.cf new file mode 100644 index 0000000000..020058f9c5 --- /dev/null +++ b/tests/acceptance/10_files/unsafe/05_immutable.cf @@ -0,0 +1,93 @@ +############################################################################## +# +# Test that agent can override the immutable bit when using edit_line +# attribute +# +############################################################################## + +body common control +{ + inputs => { "../../default.cf.sub" }; + bundlesequence => { default("$(this.promise_filename)") }; + version => "1.0"; +} + +bundle agent global +{ + vars: + "testfile" + string => "/tmp/05_immutable.txt"; +} + +body fsattrs set_immutable +{ + immutable => "true"; +} + +bundle agent init +{ + files: + "$(global.testfile)" + content => "I'm immutable"; + + commands: + "chattr +i $(global.testfile)" + contain => in_shell; +} + +bundle edit_line insert_line +{ + insert_lines: + "But agent can override"; +} + +bundle agent test +{ + meta: + "description" -> { "CFE-1840", "ENT-10961" } + string => "Test that agent can override the immutable bit on a file while using the edit_line attribute"; + + "test_skip_unsupported" + string => "hpux|aix|solaris|windows"; + + files: + "$(global.testfile)" + fsattrs => set_immutable, + edit_line => insert_line; +} + +bundle agent check +{ + vars: + "expected" + string => "I'm immutable$(const.n)But agent can override$(const.n)"; + "actual" + string => readfile("$(global.testfile)"); + + classes: + "ok" + expression => strcmp("$(actual)", "$(expected)"); + + reports: + ok:: + "$(this.promise_filename) Pass"; + !ok:: + "$(this.promise_filename) FAIL"; + any:: + "Expected:$(const.n)'$(expected)',$(const.n)actual:$(const.n)'$(actual)'"; +} + +bundle agent destroy +{ + commands: + "chattr -i $(global.testfile)" + contain => in_shell, + handle => "is mutable"; + + files: + "$(global.testfile)" + delete => tidy, + depends_on => { "is mutable" }; + "$(global.testfile).cf-before-edit" + delete => tidy; +} diff --git a/tests/acceptance/10_files/unsafe/06_immutable.cf b/tests/acceptance/10_files/unsafe/06_immutable.cf new file mode 100644 index 0000000000..497dfb2450 --- /dev/null +++ b/tests/acceptance/10_files/unsafe/06_immutable.cf @@ -0,0 +1,93 @@ +############################################################################## +# +# Test that agent can override the immutable bit when using edit_xml attribute +# +############################################################################## + +body common control +{ + inputs => { "../../default.cf.sub" }; + bundlesequence => { default("$(this.promise_filename)") }; + version => "1.0"; +} + +bundle agent global +{ + vars: + "testfile" + string => "/tmp/06_immutable.txt"; +} + +body fsattrs set_immutable +{ + immutable => "true"; +} + +bundle agent init +{ + files: + "$(global.testfile)" + content => 'I\'m immutable'; + + commands: + "chattr +i $(global.testfile)" + contain => in_shell; +} + +bundle edit_xml edit_foo +{ + set_text: + "But agent can override" + select_xpath => "/foo"; +} + +bundle agent test +{ + meta: + "description" -> { "CFE-1840", "ENT-10961" } + string => "Test that agent can override the immutable bit on a file while using the edit_xml attribute"; + + "test_skip_unsupported" + string => "hpux|aix|solaris|windows"; + + files: + "$(global.testfile)" + fsattrs => set_immutable, + edit_xml => edit_foo; +} + +bundle agent check +{ + vars: + "expected" + string => '$(const.n)But agent can override$(const.n)'; + "actual" + string => readfile("$(global.testfile)"); + + classes: + "ok" + expression => strcmp("$(actual)", "$(expected)"); + + reports: + ok:: + "$(this.promise_filename) Pass"; + !ok:: + "$(this.promise_filename) FAIL"; + any:: + "Expected: '$(expected)', actual: '$(actual)'"; +} + +bundle agent destroy +{ + commands: + "chattr -i $(global.testfile)" + contain => in_shell, + handle => "is mutable"; + + files: + "$(global.testfile)" + delete => tidy, + depends_on => { "is mutable" }; + "$(global.testfile).cf-before-edit" + delete => tidy; +} diff --git a/tests/acceptance/10_files/unsafe/07_immutable.cf b/tests/acceptance/10_files/unsafe/07_immutable.cf new file mode 100644 index 0000000000..446a433563 --- /dev/null +++ b/tests/acceptance/10_files/unsafe/07_immutable.cf @@ -0,0 +1,85 @@ +############################################################################## +# +# Test that agent can override the immutable bit when using perms attribute +# +############################################################################## + +body common control +{ + inputs => { "../../default.cf.sub" }; + bundlesequence => { default("$(this.promise_filename)") }; + version => "1.0"; +} + +bundle agent global +{ + vars: + "testfile" + string => "/tmp/07_immutable.txt"; +} + +body fsattrs set_immutable +{ + immutable => "true"; +} + +bundle agent init +{ + files: + "$(global.testfile)" + create => "true", + perms => m("600"); + + commands: + "chattr +i $(global.testfile)" + contain => in_shell; +} + +bundle agent test +{ + meta: + "description" -> { "CFE-1840", "ENT-10961" } + string => "Test that agent can override the immutable bit on a file while using the perms attribute"; + + "test_skip_unsupported" + string => "hpux|aix|solaris|windows"; + + files: + "$(global.testfile)" + fsattrs => set_immutable, + perms => m("644"); +} + +bundle agent check +{ + vars: + "expected" + string => "644"; + "actual" + string => string_tail(filestat("$(global.testfile)", "modeoct"), "3"); + + classes: + "ok" + expression => strcmp("$(actual)", "$(expected)"); + + reports: + ok:: + "$(this.promise_filename) Pass"; + !ok:: + "$(this.promise_filename) FAIL"; + any:: + "Expected: '$(expected)', actual: '$(actual)'"; +} + +bundle agent destroy +{ + commands: + "chattr -i $(global.testfile)" + contain => in_shell, + handle => "is mutable"; + + files: + "$(global.testfile)" + delete => tidy, + depends_on => { "is mutable" }; +} diff --git a/tests/acceptance/10_files/unsafe/08_immutable.cf b/tests/acceptance/10_files/unsafe/08_immutable.cf new file mode 100644 index 0000000000..dc50046a29 --- /dev/null +++ b/tests/acceptance/10_files/unsafe/08_immutable.cf @@ -0,0 +1,82 @@ +############################################################################## +# +# Test that agent can override the immutable bit when using touch attribute +# +############################################################################## + +body common control +{ + inputs => { "../../default.cf.sub" }; + bundlesequence => { default("$(this.promise_filename)") }; + version => "1.0"; +} + +bundle agent global +{ + vars: + "testfile" + string => "/tmp/08_immutable.txt"; +} + +body fsattrs set_immutable +{ + immutable => "true"; +} + +bundle agent init +{ + commands: + "touch -d @0 $(global.testfile)" + contain => in_shell; + "chattr +i $(global.testfile)" + contain => in_shell; +} + +bundle agent test +{ + meta: + "description" -> { "CFE-1840", "ENT-10961" } + string => "Test that agent can override the immutable bit on a file while using the touch attribute"; + + "test_skip_unsupported" + string => "hpux|aix|solaris|windows"; + + files: + "$(global.testfile)" + fsattrs => set_immutable, + touch => "true"; +} + +bundle agent check +{ + vars: + "not_expected" + string => "0"; + "actual" + string => filestat("$(global.testfile)", "mtime"); + + classes: + "ok" + expression => not(strcmp("$(actual)", "$(not_expected)")); + + reports: + ok:: + "$(this.promise_filename) Pass"; + !ok:: + "$(this.promise_filename) FAIL"; + any:: + "Not expecting $(not_expected), got $(actual)"; +} + +bundle agent destroy +{ + commands: + "chattr -i $(global.testfile)" + contain => in_shell, + handle => "is mutable"; + + files: + "$(global.testfile)" + delete => tidy, + depends_on => { "is mutable" }; +} diff --git a/tests/acceptance/10_files/unsafe/09_immutable.cf b/tests/acceptance/10_files/unsafe/09_immutable.cf new file mode 100644 index 0000000000..5aa825751f --- /dev/null +++ b/tests/acceptance/10_files/unsafe/09_immutable.cf @@ -0,0 +1,94 @@ +############################################################################## +# +# Test that agent can override the immutable bit when using edit_template +# attribute with mustache +# +############################################################################## + +body common control +{ + inputs => { "../../default.cf.sub" }; + bundlesequence => { default("$(this.promise_filename)") }; + version => "1.0"; +} + +bundle agent global +{ + vars: + "testfile" + string => "/tmp/09_immutable.txt"; +} + +body fsattrs set_immutable +{ + immutable => "true"; +} + +bundle agent init +{ + files: + "$(global.testfile)" + create => "true", + content => "Hello olehermanse!"; + + commands: + "chattr +i $(global.testfile)" + contain => in_shell; +} + +bundle agent test +{ + meta: + "description" -> { "CFE-1840", "ENT-10961" } + string => "Test that agent can override the immutable bit on a file while using the edit_template attribute with mustache"; + + "test_skip_unsupported" + string => "hpux|aix|solaris|windows"; + + vars: + "object" + data => '{ "user": "larsewi" }'; + + files: + "$(global.testfile)" + fsattrs => set_immutable, + template_method => "inline_mustache", + edit_template_string => "Hello {{{user}}}!", + template_data => @(object); +} + +bundle agent check +{ + vars: + "expected" + string => "Hello larsewi!"; + "actual" + string => readfile("$(global.testfile)"); + + classes: + "ok" + expression => strcmp("$(actual)", "$(expected)"); + + reports: + ok:: + "$(this.promise_filename) Pass"; + !ok:: + "$(this.promise_filename) FAIL"; + any:: + "Expecting '$(expected)', got '$(actual)'"; +} + +bundle agent destroy +{ + commands: + "chattr -i $(global.testfile)" + contain => in_shell, + handle => "is mutable"; + + files: + "$(global.testfile)" + delete => tidy, + depends_on => { "is mutable" }; + "$(global.testfile).cf-before-edit" + delete => tidy; +} diff --git a/tests/acceptance/10_files/unsafe/10_immutable.cf b/tests/acceptance/10_files/unsafe/10_immutable.cf new file mode 100644 index 0000000000..9e588d7232 --- /dev/null +++ b/tests/acceptance/10_files/unsafe/10_immutable.cf @@ -0,0 +1,98 @@ +############################################################################## +# +# Test that agent can override the immutable bit when using acl attribute +# +############################################################################## + +body common control +{ + inputs => { "../../default.cf.sub" }; + bundlesequence => { default("$(this.promise_filename)") }; + version => "1.0"; +} + +bundle agent global +{ + vars: + "testfile" + string => "/tmp/10_immutable.txt"; +} + +body fsattrs set_immutable +{ + immutable => "true"; +} + +body acl acl_user_root_rw +{ + acl_method => "append"; + aces => { "user:root:rw" }; +} + +body acl acl_user_root_rwx +{ + acl_method => "append"; + aces => { "user:root:rwx" }; +} + +bundle agent init +{ + files: + "$(global.testfile)" + create => "true", + acl => acl_user_root_rw; + + commands: + "chattr +i $(global.testfile)" + contain => in_shell; +} + +bundle agent test +{ + meta: + "description" -> { "CFE-1840", "ENT-10961" } + string => "Test that agent can override the immutable bit on a file while using the acl attribute"; + + "test_skip_unsupported" + string => "hpux|aix|solaris|windows"; + + files: + "$(global.testfile)" + fsattrs => set_immutable, + acl => acl_user_root_rwx; + +} + +bundle agent check +{ + vars: + "expected" + string => ".*user:root:rwx.*"; + "acls" + slist => getacls("$(global.testfile)", "access"); + + classes: + "ok" + expression => some("$(expected)", "acls"); + + reports: + ok:: + "$(this.promise_filename) Pass"; + !ok:: + "$(this.promise_filename) FAIL"; + any:: + "Expecting output matching '$(expected)', got '$(acls)'"; +} + +bundle agent destroy +{ + commands: + "chattr -i $(global.testfile)" + contain => in_shell, + handle => "is mutable"; + + files: + "$(global.testfile)" + delete => tidy, + depends_on => { "is mutable" }; +} diff --git a/tests/acceptance/10_files/unsafe/11_immutable.cf b/tests/acceptance/10_files/unsafe/11_immutable.cf new file mode 100644 index 0000000000..6ae49ebb67 --- /dev/null +++ b/tests/acceptance/10_files/unsafe/11_immutable.cf @@ -0,0 +1,104 @@ +############################################################################## +# +# Test that agent can override the immutable bit when using transformer +# attribute +# +############################################################################## + +body common control +{ + inputs => { "../../default.cf.sub" }; + bundlesequence => { default("$(this.promise_filename)") }; + version => "1.0"; +} + +bundle agent global +{ + vars: + "testfile" + string => "/tmp/11_immutable.txt"; +} + +body fsattrs set_immutable +{ + immutable => "true"; +} + +bundle agent init +{ + files: + "$(global.testfile)" + create => "true"; + + commands: + "chattr +i $(global.testfile)" + contain => in_shell; +} + +bundle agent test +{ + meta: + "description" -> { "CFE-1840", "ENT-10961" } + string => "Test that agent can override the immutable bit on a file while using the transformer attribute"; + + "test_skip_unsupported" + string => "hpux|aix|solaris|windows"; + + vars: + "gzip_path" + string => ifelse( + isexecutable("/bin/gzip"), + "/bin/gzip", + "/usr/bin/gzip" + ); + + files: + "$(global.testfile)" + fsattrs => set_immutable, + transformer => "$(gzip_path) $(this.promiser)"; + +} + +bundle agent check +{ + classes: + "original_exists" + expression => fileexists("$(global.testfile)"); + "transformed_exists" + expression => fileexists("$(global.testfile).gz"); + "ok" + expression => "!original_exists&transformed_exists"; + + reports: + ok:: + "$(this.promise_filename) Pass"; + !ok:: + "$(this.promise_filename) FAIL"; + any:: + "Expected file '$(global.testfile)' to not exists $(with)" + with => ifelse("!original_exists", "and it does not", "but it does"); + "Expected file '$(global.testfile).gz' to exists $(with)" + with => ifelse("transformed_exists", "and it does", "but it does not"); +} + +bundle agent destroy +{ + commands: + "chattr -i $(global.testfile)" + contain => in_shell, + handle => "is mutable 1", + if => fileexists("$(global.testfile)"); + "chattr -i $(global.testfile).gz" + contain => in_shell, + handle => "is mutable 2", + if => fileexists("$(global.testfile).gz"); + + files: + "$(global.testfile)" + delete => tidy, + depends_on => { "is mutable 1" }; + files: + "$(global.testfile).gz" + delete => tidy, + depends_on => { "is mutable 2" }; +} diff --git a/tests/acceptance/10_files/unsafe/12_immutable.cf b/tests/acceptance/10_files/unsafe/12_immutable.cf new file mode 100644 index 0000000000..a1546db22b --- /dev/null +++ b/tests/acceptance/10_files/unsafe/12_immutable.cf @@ -0,0 +1,99 @@ +############################################################################## +# +# Test that agent can override the immutable bit when using rename attribute +# +############################################################################## + +body common control +{ + inputs => { "../../default.cf.sub" }; + bundlesequence => { default("$(this.promise_filename)") }; + version => "1.0"; +} + +bundle agent global +{ + vars: + "testfile" + string => "/tmp/12_immutable.txt"; +} + +body fsattrs set_immutable +{ + immutable => "true"; +} + +bundle agent init +{ + files: + "$(global.testfile)" + create => "true"; + + commands: + "chattr +i $(global.testfile)" + contain => in_shell; +} + +body rename nuke +{ + disable => "true"; + disable_suffix => ".nuked"; +} + +bundle agent test +{ + meta: + "description" -> { "CFE-1840", "ENT-10961" } + string => "Test that agent can override the immutable bit on a file while using the rename attribute"; + + "test_skip_unsupported" + string => "hpux|aix|solaris|windows"; + + files: + "$(global.testfile)" + fsattrs => set_immutable, + rename => nuke; +} + +bundle agent check +{ + classes: + "original_exists" + expression => fileexists("$(global.testfile)"); + "nuked_exists" + expression => fileexists("$(global.testfile).nuked"); + "ok" + expression => "!original_exists&nuked_exists"; + + reports: + ok:: + "$(this.promise_filename) Pass"; + !ok:: + "$(this.promise_filename) FAIL"; + any:: + "Expected file '$(global.testfile)' to not exists $(with)" + with => ifelse("!original_exists", "and it does not", "but it does"); + "Expected file '$(global.testfile).nuked' to exists $(with)" + with => ifelse("nuked_exists", "and it does", "but it does not"); +} + +bundle agent destroy +{ + commands: + "chattr -i $(global.testfile)" + contain => in_shell, + handle => "orig is mutable", + if => fileexists("$(global.testfile)"); + "chattr -i $(global.testfile).nuked" + contain => in_shell, + handle => "nuked is mutable", + if => fileexists("$(global.testfile).nuked"); + + files: + "$(global.testfile)" + delete => tidy, + depends_on => { "orig is mutable" }; + "$(global.testfile).nuked" + delete => tidy, + depends_on => { "nuked is mutable" }; +}