mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-05 08:23:53 +08:00
Merge pull request #2490 from DeusData/fix/workspace-manifest-resolved-entries
fix(workspace): classify manifest entries in their resolved form
This commit is contained in:
+59
-10
@@ -776,6 +776,43 @@ bool cbm_workspace_manifest_is_approved(const char *cache_dir, const char *proje
|
||||
return found;
|
||||
}
|
||||
|
||||
/* Resolve a manifest entry to the directory it names — the form the policy is
|
||||
* defined over. The file holds the entry as a person wrote it, and a spelling
|
||||
* can reach a directory through a link or through `..` segments; the directory
|
||||
* that would be indexed is the resolved one, so that is the one classified, at
|
||||
* approval and again at use. Only an absolute entry is accepted: the manifest
|
||||
* is read by a long-lived process whose working directory is unrelated to the
|
||||
* project, so a relative spelling has no base it could honestly be resolved
|
||||
* against. Returns NULL on success, otherwise the reason the entry is unusable.
|
||||
* out must hold WS_LINE_MAX bytes. */
|
||||
static const char *ws_manifest_entry_resolve(const char *entry, char *out, size_t out_sz) {
|
||||
if (ws_volume_prefix_len(entry) == 0) {
|
||||
return "must be an absolute path";
|
||||
}
|
||||
if (!cbm_canonical_path(entry, out, out_sz)) {
|
||||
return "must name an existing directory";
|
||||
}
|
||||
if (!cbm_is_dir(out)) {
|
||||
return "is not a directory";
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* "requested path <entry>: <reason>", with the resolved form shown when it
|
||||
* differs from what was written: the person typed one and the policy judged
|
||||
* the other. */
|
||||
static void ws_manifest_entry_refusal(char *err, size_t err_sz, const char *entry,
|
||||
const char *resolved, const char *reason) {
|
||||
if (!err || err_sz == 0) {
|
||||
return;
|
||||
}
|
||||
if (resolved && strcmp(resolved, entry) != 0) {
|
||||
snprintf(err, err_sz, "requested path %s (resolves to %s): %s", entry, resolved, reason);
|
||||
} else {
|
||||
snprintf(err, err_sz, "requested path %s: %s", entry, reason);
|
||||
}
|
||||
}
|
||||
|
||||
bool cbm_workspace_manifest_approve(const char *cache_dir, const char *home_dir,
|
||||
const char *project_root, char *err, size_t err_sz) {
|
||||
if (err && err_sz) {
|
||||
@@ -800,14 +837,19 @@ bool cbm_workspace_manifest_approve(const char *cache_dir, const char *home_dir,
|
||||
}
|
||||
|
||||
/* Approving a manifest must not become a way around the breadth policy: every
|
||||
* requested entry has to stand on its own as an indexing root. */
|
||||
* requested entry has to stand on its own as an indexing root, judged by the
|
||||
* directory it resolves to. */
|
||||
for (int i = 0; i < m.count; i++) {
|
||||
cbm_ws_verdict_t v = cbm_workspace_classify_root(m.entries[i], home_dir, cache_dir);
|
||||
char resolved[WS_LINE_MAX];
|
||||
const char *unusable = ws_manifest_entry_resolve(m.entries[i], resolved, sizeof(resolved));
|
||||
if (unusable) {
|
||||
ws_manifest_entry_refusal(err, err_sz, m.entries[i], NULL, unusable);
|
||||
return false;
|
||||
}
|
||||
cbm_ws_verdict_t v = cbm_workspace_classify_root(resolved, home_dir, cache_dir);
|
||||
if (v != CBM_WS_ALLOW) {
|
||||
if (err) {
|
||||
snprintf(err, err_sz, "requested path %s: %s", m.entries[i],
|
||||
cbm_workspace_verdict_reason(v));
|
||||
}
|
||||
ws_manifest_entry_refusal(err, err_sz, m.entries[i], resolved,
|
||||
cbm_workspace_verdict_reason(v));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -850,12 +892,19 @@ bool cbm_workspace_manifest_allows(const char *cache_dir, const char *home_dir,
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < m.count; i++) {
|
||||
/* Re-classify at use time as well as at approval time: the credential list
|
||||
* may have grown since, and a stored approval must not outrank it. */
|
||||
if (cbm_workspace_classify_root(m.entries[i], home_dir, cache_dir) != CBM_WS_ALLOW) {
|
||||
/* Resolve and re-classify at use time as well as at approval time: the
|
||||
* credential list may have grown since, and a link may lead somewhere
|
||||
* else than it did when the person approved it. A stored approval must
|
||||
* not outrank either. An entry that no longer resolves matches nothing,
|
||||
* and the other entries of the manifest are consulted as before. */
|
||||
char resolved[WS_LINE_MAX];
|
||||
if (ws_manifest_entry_resolve(m.entries[i], resolved, sizeof(resolved)) != NULL) {
|
||||
continue;
|
||||
}
|
||||
if (cbm_path_within_root(m.entries[i], candidate)) {
|
||||
if (cbm_workspace_classify_root(resolved, home_dir, cache_dir) != CBM_WS_ALLOW) {
|
||||
continue;
|
||||
}
|
||||
if (cbm_path_within_root(resolved, candidate)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,12 +145,19 @@ bool cbm_workspace_manifest_read(const char *project_root, cbm_ws_manifest_t *ou
|
||||
bool cbm_workspace_manifest_is_approved(const char *cache_dir, const char *project_root,
|
||||
const cbm_ws_manifest_t *manifest);
|
||||
|
||||
/* Record approval for the manifest currently on disk. Refuses when a requested
|
||||
* entry would not be allowable as a root on its own — approving a manifest must
|
||||
* not become a way around the breadth policy. */
|
||||
/* Record approval for the manifest currently on disk. Each entry is judged in
|
||||
* its resolved form (links followed, `..` collapsed), since that is the
|
||||
* directory that would be indexed; an entry that is relative, does not exist
|
||||
* or is not a directory is refused, as is one that would not be allowable as a
|
||||
* root on its own — approving a manifest must not become a way around the
|
||||
* breadth policy. err names the entry as written and, when it differs, the
|
||||
* directory it resolves to. */
|
||||
bool cbm_workspace_manifest_approve(const char *cache_dir, const char *home_dir,
|
||||
const char *project_root, char *err, size_t err_sz);
|
||||
|
||||
/* True when candidate is at or below an APPROVED manifest entry of project_root. */
|
||||
/* True when candidate is at or below an APPROVED manifest entry of project_root.
|
||||
* Entries are resolved and classified again here: approval binds to the
|
||||
* manifest text, and where an entry leads is read at the time it is used. An
|
||||
* entry that no longer resolves matches nothing. */
|
||||
bool cbm_workspace_manifest_allows(const char *cache_dir, const char *home_dir,
|
||||
const char *project_root, const char *candidate);
|
||||
|
||||
+371
-13
@@ -334,13 +334,29 @@ TEST(ws_manifest_rejects_control_characters) {
|
||||
/* THE property the design rests on: a manifest grants nothing until a person
|
||||
* approves it, and editing it lapses that approval rather than inheriting it. */
|
||||
TEST(ws_manifest_approval_is_keyed_to_content) {
|
||||
char *base = th_mktempdir("cbm_ws_m3");
|
||||
char *cache = th_mktempdir("cbm_ws_m3c");
|
||||
ASSERT(base != NULL);
|
||||
ASSERT(cache != NULL);
|
||||
/* th_mktempdir returns one static buffer: copy each result at once. */
|
||||
char base[256];
|
||||
char cache[256];
|
||||
char *created = th_mktempdir("cbm_ws_m3");
|
||||
ASSERT(created != NULL);
|
||||
snprintf(base, sizeof(base), "%s", created);
|
||||
created = th_mktempdir("cbm_ws_m3c");
|
||||
ASSERT(created != NULL);
|
||||
snprintf(cache, sizeof(cache), "%s", created);
|
||||
|
||||
/* Entries must name existing directories. */
|
||||
char sdk[1024];
|
||||
char protos[1024];
|
||||
snprintf(sdk, sizeof(sdk), "%s/sdk", base);
|
||||
snprintf(protos, sizeof(protos), "%s/protos", base);
|
||||
ASSERT_EQ(cbm_mkdir(sdk), 0);
|
||||
ASSERT_EQ(cbm_mkdir(protos), 0);
|
||||
|
||||
char path[1024];
|
||||
char body[4096];
|
||||
snprintf(path, sizeof(path), "%s/%s", base, CBM_WS_MANIFEST_NAME);
|
||||
ws_write(path, "/opt/sdk\n");
|
||||
snprintf(body, sizeof(body), "%s\n", sdk);
|
||||
ws_write(path, body);
|
||||
|
||||
cbm_ws_manifest_t before;
|
||||
ASSERT_TRUE(cbm_workspace_manifest_read(base, &before));
|
||||
@@ -352,7 +368,8 @@ TEST(ws_manifest_approval_is_keyed_to_content) {
|
||||
ASSERT_TRUE(cbm_workspace_manifest_is_approved(cache, base, &before));
|
||||
|
||||
/* Widen the requests, as a `git pull` would. Approval must lapse. */
|
||||
ws_write(path, "/opt/sdk\n/srv/protos\n");
|
||||
snprintf(body, sizeof(body), "%s\n%s\n", sdk, protos);
|
||||
ws_write(path, body);
|
||||
cbm_ws_manifest_t after;
|
||||
ASSERT_TRUE(cbm_workspace_manifest_read(base, &after));
|
||||
ASSERT_TRUE(strcmp(before.digest, after.digest) != 0);
|
||||
@@ -363,18 +380,35 @@ TEST(ws_manifest_approval_is_keyed_to_content) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Approving a manifest must not become a route around the breadth policy. */
|
||||
/* Approving a manifest must not become a route around the breadth policy. The
|
||||
* entry has to exist, so the overbroad directory is "/etc" on POSIX and, on
|
||||
* Windows, where one component below the drive is already an ordinary
|
||||
* workspace, the drive root of the temporary directory. */
|
||||
TEST(ws_manifest_approval_refuses_overbroad_requests) {
|
||||
char *base = th_mktempdir("cbm_ws_m4");
|
||||
char *cache = th_mktempdir("cbm_ws_m4c");
|
||||
ASSERT(base != NULL);
|
||||
ASSERT(cache != NULL);
|
||||
char base[256];
|
||||
char cache[256];
|
||||
char *created = th_mktempdir("cbm_ws_m4");
|
||||
ASSERT(created != NULL);
|
||||
snprintf(base, sizeof(base), "%s", created);
|
||||
created = th_mktempdir("cbm_ws_m4c");
|
||||
ASSERT(created != NULL);
|
||||
snprintf(cache, sizeof(cache), "%s", created);
|
||||
|
||||
char path[1024];
|
||||
char body[1024];
|
||||
const char *refusal = NULL;
|
||||
snprintf(path, sizeof(path), "%s/%s", base, CBM_WS_MANIFEST_NAME);
|
||||
ws_write(path, "/etc\n");
|
||||
#ifdef _WIN32
|
||||
snprintf(body, sizeof(body), "%.3s\n", base);
|
||||
refusal = cbm_workspace_verdict_reason(CBM_WS_DENY_ABSOLUTE);
|
||||
#else
|
||||
snprintf(body, sizeof(body), "/etc\n");
|
||||
refusal = cbm_workspace_verdict_reason(CBM_WS_DENY_TOO_SHALLOW);
|
||||
#endif
|
||||
ws_write(path, body);
|
||||
char err[1024];
|
||||
ASSERT_FALSE(cbm_workspace_manifest_approve(cache, HOME, base, err, sizeof(err)));
|
||||
ASSERT_TRUE(strstr(err, "too broad") != NULL);
|
||||
ASSERT_TRUE(strstr(err, refusal) != NULL);
|
||||
th_cleanup(base);
|
||||
th_cleanup(cache);
|
||||
PASS();
|
||||
@@ -463,6 +497,323 @@ TEST(ws_linked_home_classified_as_home) {
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ── Manifest entries are judged in their resolved form ──────────────────── */
|
||||
|
||||
enum { WS_T_PATH = 4096 };
|
||||
|
||||
/* th_mktempdir hands back one static buffer, so copy at once — and in canonical
|
||||
* form, because the policy compares resolved spellings and /tmp is a link on
|
||||
* macOS. */
|
||||
static bool ws_tempdir_canonical(const char *prefix, char *out, size_t out_sz) {
|
||||
char *created = th_mktempdir(prefix);
|
||||
return created && cbm_canonical_path(created, out, out_sz) == 1;
|
||||
}
|
||||
|
||||
static bool ws_join(char *out, size_t out_sz, const char *a, const char *b) {
|
||||
int n = snprintf(out, out_sz, "%s/%s", a, b);
|
||||
return n > 0 && (size_t)n < out_sz;
|
||||
}
|
||||
|
||||
/* Create <parent>/<name> and return it in canonical form. */
|
||||
static bool ws_mkdir_canonical(const char *parent, const char *name, char *out, size_t out_sz) {
|
||||
char made[WS_T_PATH];
|
||||
if (!ws_join(made, sizeof(made), parent, name) || cbm_mkdir(made) != 0) {
|
||||
return false;
|
||||
}
|
||||
return cbm_canonical_path(made, out, out_sz) == 1;
|
||||
}
|
||||
|
||||
static void ws_manifest_write(const char *base, const char *body) {
|
||||
char path[WS_T_PATH];
|
||||
if (ws_join(path, sizeof(path), base, CBM_WS_MANIFEST_NAME)) {
|
||||
ws_write(path, body);
|
||||
}
|
||||
}
|
||||
|
||||
static void ws_manifest_write_line(const char *base, const char *entry) {
|
||||
char body[WS_T_PATH];
|
||||
int n = snprintf(body, sizeof(body), "%s\n", entry);
|
||||
if (n > 0 && (size_t)n < sizeof(body)) {
|
||||
ws_manifest_write(base, body);
|
||||
}
|
||||
}
|
||||
|
||||
/* Put the manifest on record as approved by writing the store line directly,
|
||||
* in the form cbm_workspace_manifest_approve writes ("<digest> <project>").
|
||||
* This is the record an approval leaves behind when the entry was judged by
|
||||
* its spelling, and the use-time check must stand on its own against it. */
|
||||
static bool ws_manifest_record_approval(const char *cache, const char *base) {
|
||||
cbm_ws_manifest_t m;
|
||||
if (!cbm_workspace_manifest_read(base, &m) || !m.present) {
|
||||
return false;
|
||||
}
|
||||
char store[WS_T_PATH];
|
||||
char line[WS_T_PATH];
|
||||
if (!ws_join(store, sizeof(store), cache, "approved_manifests")) {
|
||||
return false;
|
||||
}
|
||||
int n = snprintf(line, sizeof(line), "%s %s\n", m.digest, base);
|
||||
if (n <= 0 || (size_t)n >= sizeof(line)) {
|
||||
return false;
|
||||
}
|
||||
ws_write(store, line);
|
||||
return cbm_workspace_manifest_is_approved(cache, base, &m);
|
||||
}
|
||||
|
||||
/* The directory a manifest entry names is the one that would be indexed, so
|
||||
* that is the form the policy judges — at approval and again at use. Spelled
|
||||
* through `..`, an entry reads as an ordinary path several components deep
|
||||
* while naming the home directory or the volume root. */
|
||||
TEST(ws_manifest_entry_classified_in_resolved_form) {
|
||||
char base[WS_T_PATH];
|
||||
char cache[WS_T_PATH];
|
||||
ASSERT_TRUE(ws_tempdir_canonical("cbm_ws_m5", base, sizeof(base)));
|
||||
ASSERT_TRUE(ws_tempdir_canonical("cbm_ws_m5c", cache, sizeof(cache)));
|
||||
|
||||
char home[WS_T_PATH];
|
||||
char src[WS_T_PATH];
|
||||
char home_sub[WS_T_PATH];
|
||||
ASSERT_TRUE(ws_mkdir_canonical(base, "home", home, sizeof(home)));
|
||||
ASSERT_TRUE(ws_mkdir_canonical(base, "src", src, sizeof(src)));
|
||||
ASSERT_TRUE(ws_mkdir_canonical(home, "notes", home_sub, sizeof(home_sub)));
|
||||
|
||||
char entry[WS_T_PATH];
|
||||
char expected[WS_T_PATH];
|
||||
char err[1024];
|
||||
|
||||
/* "<base>/src/../home" is the home directory. */
|
||||
ASSERT_TRUE(ws_join(entry, sizeof(entry), src, "../home"));
|
||||
ws_manifest_write_line(base, entry);
|
||||
ASSERT_FALSE(cbm_workspace_manifest_approve(cache, home, base, err, sizeof(err)));
|
||||
ASSERT_NOT_NULL(strstr(err, entry));
|
||||
ASSERT(snprintf(expected, sizeof(expected), "(resolves to %s)", home) > 0);
|
||||
ASSERT_NOT_NULL(strstr(err, expected));
|
||||
ASSERT_NOT_NULL(strstr(err, cbm_workspace_verdict_reason(CBM_WS_DENY_SENSITIVE)));
|
||||
|
||||
/* An approval already on record for this spelling does not outrank the
|
||||
* use-time check. */
|
||||
ASSERT_TRUE(ws_manifest_record_approval(cache, base));
|
||||
ASSERT_FALSE(cbm_workspace_manifest_allows(cache, home, base, home_sub));
|
||||
|
||||
/* Enough `..` segments reach the volume root from anywhere. */
|
||||
ASSERT_TRUE(ws_join(entry, sizeof(entry), base, "../../../../../../../../../../../.."));
|
||||
ws_manifest_write_line(base, entry);
|
||||
ASSERT_FALSE(cbm_workspace_manifest_approve(cache, home, base, err, sizeof(err)));
|
||||
ASSERT_NOT_NULL(strstr(err, entry));
|
||||
ASSERT_NOT_NULL(strstr(err, cbm_workspace_verdict_reason(CBM_WS_DENY_ABSOLUTE)));
|
||||
|
||||
th_cleanup(base);
|
||||
th_cleanup(cache);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* An entry that does not resolve to a directory is refused at approval, naming
|
||||
* the entry; at use it simply matches nothing, and the other entries of the
|
||||
* same manifest keep working. */
|
||||
TEST(ws_manifest_unresolvable_entry_refused) {
|
||||
char base[WS_T_PATH];
|
||||
char cache[WS_T_PATH];
|
||||
ASSERT_TRUE(ws_tempdir_canonical("cbm_ws_m6", base, sizeof(base)));
|
||||
ASSERT_TRUE(ws_tempdir_canonical("cbm_ws_m6c", cache, sizeof(cache)));
|
||||
|
||||
char entry[WS_T_PATH];
|
||||
char err[1024];
|
||||
|
||||
/* Missing. */
|
||||
ASSERT_TRUE(ws_join(entry, sizeof(entry), base, "missing"));
|
||||
ws_manifest_write_line(base, entry);
|
||||
ASSERT_FALSE(cbm_workspace_manifest_approve(cache, HOME, base, err, sizeof(err)));
|
||||
ASSERT_NOT_NULL(strstr(err, entry));
|
||||
ASSERT_NOT_NULL(strstr(err, "must name an existing directory"));
|
||||
|
||||
/* Present, but a file. */
|
||||
ASSERT_TRUE(ws_join(entry, sizeof(entry), base, "notes.txt"));
|
||||
ws_write(entry, "not a directory\n");
|
||||
ws_manifest_write_line(base, entry);
|
||||
ASSERT_FALSE(cbm_workspace_manifest_approve(cache, HOME, base, err, sizeof(err)));
|
||||
ASSERT_NOT_NULL(strstr(err, entry));
|
||||
ASSERT_NOT_NULL(strstr(err, "is not a directory"));
|
||||
|
||||
/* Relative: the file is read by a long-lived process whose working
|
||||
* directory is unrelated to the project, so there is no base to resolve
|
||||
* it against. */
|
||||
ws_manifest_write_line(base, "src");
|
||||
ASSERT_FALSE(cbm_workspace_manifest_approve(cache, HOME, base, err, sizeof(err)));
|
||||
ASSERT_NOT_NULL(strstr(err, "requested path src:"));
|
||||
ASSERT_NOT_NULL(strstr(err, "must be an absolute path"));
|
||||
|
||||
/* At use: a directory that disappeared after approval matches nothing and
|
||||
* does not take the rest of the manifest with it. */
|
||||
char present[WS_T_PATH];
|
||||
char present_sub[WS_T_PATH];
|
||||
char body[WS_T_PATH];
|
||||
ASSERT_TRUE(ws_mkdir_canonical(base, "present", present, sizeof(present)));
|
||||
ASSERT_TRUE(ws_mkdir_canonical(present, "inc", present_sub, sizeof(present_sub)));
|
||||
ASSERT_TRUE(ws_join(entry, sizeof(entry), base, "gone"));
|
||||
ASSERT(snprintf(body, sizeof(body), "%s\n%s\n", entry, present) > 0);
|
||||
ws_manifest_write(base, body);
|
||||
ASSERT_TRUE(ws_manifest_record_approval(cache, base));
|
||||
ASSERT_TRUE(cbm_workspace_manifest_allows(cache, HOME, base, present_sub));
|
||||
ASSERT_FALSE(cbm_workspace_manifest_allows(cache, HOME, base, base));
|
||||
|
||||
th_cleanup(base);
|
||||
th_cleanup(cache);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The ordinary shapes keep their result: an absolute existing directory, an
|
||||
* entry equal to the project root, an entry outside every root in the grant
|
||||
* store (the manifest is consulted on its own), and on POSIX a link to an
|
||||
* ordinary directory. */
|
||||
TEST(ws_manifest_ordinary_entries_unchanged) {
|
||||
char base[WS_T_PATH];
|
||||
char cache[WS_T_PATH];
|
||||
ASSERT_TRUE(ws_tempdir_canonical("cbm_ws_m7", base, sizeof(base)));
|
||||
ASSERT_TRUE(ws_tempdir_canonical("cbm_ws_m7c", cache, sizeof(cache)));
|
||||
|
||||
char sdk[WS_T_PATH];
|
||||
char sdk_inc[WS_T_PATH];
|
||||
char granted[WS_T_PATH];
|
||||
ASSERT_TRUE(ws_mkdir_canonical(base, "sdk", sdk, sizeof(sdk)));
|
||||
ASSERT_TRUE(ws_mkdir_canonical(sdk, "inc", sdk_inc, sizeof(sdk_inc)));
|
||||
ASSERT_TRUE(ws_mkdir_canonical(base, "granted", granted, sizeof(granted)));
|
||||
|
||||
char err[1024];
|
||||
/* The grant store names a root the sdk is not under. */
|
||||
ASSERT_TRUE(cbm_workspace_grant_add(cache, HOME, granted, false, err, sizeof(err)));
|
||||
|
||||
ws_manifest_write_line(base, sdk);
|
||||
ASSERT_TRUE(cbm_workspace_manifest_approve(cache, HOME, base, err, sizeof(err)));
|
||||
ASSERT_STR_EQ(err, "");
|
||||
ASSERT_TRUE(cbm_workspace_manifest_allows(cache, HOME, base, sdk_inc));
|
||||
ASSERT_FALSE(cbm_workspace_manifest_allows(cache, HOME, base, granted));
|
||||
|
||||
/* Equal to the project root. */
|
||||
ws_manifest_write_line(base, base);
|
||||
ASSERT_TRUE(cbm_workspace_manifest_approve(cache, HOME, base, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_manifest_allows(cache, HOME, base, base));
|
||||
ASSERT_TRUE(cbm_workspace_manifest_allows(cache, HOME, base, sdk_inc));
|
||||
|
||||
#ifndef _WIN32
|
||||
/* A link to an ordinary directory is that directory. Windows is not
|
||||
* covered here: symlink(2) has no counterpart in the test helpers, and
|
||||
* CreateSymbolicLinkW needs a privilege the CI runner does not hold; the
|
||||
* resolved-form rules themselves are exercised on every platform by the
|
||||
* `..` cases above. */
|
||||
char alias[WS_T_PATH];
|
||||
ASSERT_TRUE(ws_join(alias, sizeof(alias), base, "alias"));
|
||||
ASSERT_EQ(symlink(sdk, alias), 0);
|
||||
ws_manifest_write_line(base, alias);
|
||||
ASSERT_TRUE(cbm_workspace_manifest_approve(cache, HOME, base, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_manifest_allows(cache, HOME, base, sdk_inc));
|
||||
#endif
|
||||
|
||||
th_cleanup(base);
|
||||
th_cleanup(cache);
|
||||
PASS();
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
/* A link is judged by where it leads. Windows is not covered: symlink(2) has
|
||||
* no counterpart in the test helpers and CreateSymbolicLinkW needs a privilege
|
||||
* the CI runner does not hold; the resolved-form rules are exercised on every
|
||||
* platform by ws_manifest_entry_classified_in_resolved_form. */
|
||||
TEST(ws_manifest_link_to_sensitive_directory_refused) {
|
||||
char base[WS_T_PATH];
|
||||
char cache[WS_T_PATH];
|
||||
ASSERT_TRUE(ws_tempdir_canonical("cbm_ws_m8", base, sizeof(base)));
|
||||
ASSERT_TRUE(ws_tempdir_canonical("cbm_ws_m8c", cache, sizeof(cache)));
|
||||
|
||||
char home[WS_T_PATH];
|
||||
char home_sub[WS_T_PATH];
|
||||
char keys[WS_T_PATH];
|
||||
char keys_sub[WS_T_PATH];
|
||||
ASSERT_TRUE(ws_mkdir_canonical(base, "home", home, sizeof(home)));
|
||||
ASSERT_TRUE(ws_mkdir_canonical(home, "notes", home_sub, sizeof(home_sub)));
|
||||
ASSERT_TRUE(ws_mkdir_canonical(base, ".ssh", keys, sizeof(keys)));
|
||||
ASSERT_TRUE(ws_mkdir_canonical(keys, "hosts", keys_sub, sizeof(keys_sub)));
|
||||
|
||||
char to_home[WS_T_PATH];
|
||||
char to_keys[WS_T_PATH];
|
||||
ASSERT_TRUE(ws_join(to_home, sizeof(to_home), base, "to-home"));
|
||||
ASSERT_TRUE(ws_join(to_keys, sizeof(to_keys), base, "to-keys"));
|
||||
ASSERT_EQ(symlink(home, to_home), 0);
|
||||
ASSERT_EQ(symlink(keys, to_keys), 0);
|
||||
|
||||
char expected[WS_T_PATH];
|
||||
char err[1024];
|
||||
|
||||
ws_manifest_write_line(base, to_home);
|
||||
ASSERT_FALSE(cbm_workspace_manifest_approve(cache, home, base, err, sizeof(err)));
|
||||
ASSERT_NOT_NULL(strstr(err, to_home));
|
||||
ASSERT(snprintf(expected, sizeof(expected), "(resolves to %s)", home) > 0);
|
||||
ASSERT_NOT_NULL(strstr(err, expected));
|
||||
ASSERT_NOT_NULL(strstr(err, cbm_workspace_verdict_reason(CBM_WS_DENY_SENSITIVE)));
|
||||
|
||||
ws_manifest_write_line(base, to_keys);
|
||||
ASSERT_FALSE(cbm_workspace_manifest_approve(cache, home, base, err, sizeof(err)));
|
||||
ASSERT_NOT_NULL(strstr(err, to_keys));
|
||||
ASSERT(snprintf(expected, sizeof(expected), "(resolves to %s)", keys) > 0);
|
||||
ASSERT_NOT_NULL(strstr(err, expected));
|
||||
ASSERT_NOT_NULL(strstr(err, cbm_workspace_verdict_reason(CBM_WS_DENY_SENSITIVE)));
|
||||
|
||||
/* On record as approved: the use-time check still refuses both. */
|
||||
char body[WS_T_PATH];
|
||||
ASSERT(snprintf(body, sizeof(body), "%s\n%s\n", to_home, to_keys) > 0);
|
||||
ws_manifest_write(base, body);
|
||||
ASSERT_TRUE(ws_manifest_record_approval(cache, base));
|
||||
ASSERT_FALSE(cbm_workspace_manifest_allows(cache, home, base, home_sub));
|
||||
ASSERT_FALSE(cbm_workspace_manifest_allows(cache, home, base, keys_sub));
|
||||
|
||||
th_cleanup(base);
|
||||
th_cleanup(cache);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Approval binds to the manifest text; where a link leads is read again each
|
||||
* time the manifest is used. A link approved while it led to an ordinary
|
||||
* directory and later pointed at the home directory grants nothing there.
|
||||
* POSIX only, for the reason given at the test above. */
|
||||
TEST(ws_manifest_link_retarget_is_seen_at_use) {
|
||||
char base[WS_T_PATH];
|
||||
char cache[WS_T_PATH];
|
||||
ASSERT_TRUE(ws_tempdir_canonical("cbm_ws_m9", base, sizeof(base)));
|
||||
ASSERT_TRUE(ws_tempdir_canonical("cbm_ws_m9c", cache, sizeof(cache)));
|
||||
|
||||
char shared[WS_T_PATH];
|
||||
char shared_sub[WS_T_PATH];
|
||||
char home[WS_T_PATH];
|
||||
char home_sub[WS_T_PATH];
|
||||
ASSERT_TRUE(ws_mkdir_canonical(base, "shared", shared, sizeof(shared)));
|
||||
ASSERT_TRUE(ws_mkdir_canonical(shared, "proto", shared_sub, sizeof(shared_sub)));
|
||||
ASSERT_TRUE(ws_mkdir_canonical(base, "home", home, sizeof(home)));
|
||||
ASSERT_TRUE(ws_mkdir_canonical(home, "notes", home_sub, sizeof(home_sub)));
|
||||
|
||||
char extra[WS_T_PATH];
|
||||
ASSERT_TRUE(ws_join(extra, sizeof(extra), base, "extra"));
|
||||
ASSERT_EQ(symlink(shared, extra), 0);
|
||||
|
||||
char err[1024];
|
||||
ws_manifest_write_line(base, extra);
|
||||
ASSERT_TRUE(cbm_workspace_manifest_approve(cache, home, base, err, sizeof(err)));
|
||||
ASSERT_TRUE(cbm_workspace_manifest_allows(cache, home, base, shared_sub));
|
||||
|
||||
/* Same text, new target. */
|
||||
ASSERT_EQ(unlink(extra), 0);
|
||||
ASSERT_EQ(symlink(home, extra), 0);
|
||||
cbm_ws_manifest_t m;
|
||||
ASSERT_TRUE(cbm_workspace_manifest_read(base, &m));
|
||||
ASSERT_TRUE(cbm_workspace_manifest_is_approved(cache, base, &m));
|
||||
|
||||
ASSERT_FALSE(cbm_workspace_manifest_allows(cache, home, base, home_sub));
|
||||
ASSERT_FALSE(cbm_workspace_manifest_allows(cache, home, base, shared_sub));
|
||||
|
||||
th_cleanup(base);
|
||||
th_cleanup(cache);
|
||||
PASS();
|
||||
}
|
||||
#endif /* _WIN32 */
|
||||
|
||||
SUITE(workspace) {
|
||||
RUN_TEST(ws_manifest_absent_is_not_an_error);
|
||||
RUN_TEST(ws_manifest_parses_entries_and_skips_comments);
|
||||
@@ -486,4 +837,11 @@ SUITE(workspace) {
|
||||
RUN_TEST(ws_every_verdict_has_a_reason);
|
||||
RUN_TEST(ws_home_dir_is_resolved);
|
||||
RUN_TEST(ws_linked_home_classified_as_home);
|
||||
RUN_TEST(ws_manifest_entry_classified_in_resolved_form);
|
||||
RUN_TEST(ws_manifest_unresolvable_entry_refused);
|
||||
RUN_TEST(ws_manifest_ordinary_entries_unchanged);
|
||||
#ifndef _WIN32
|
||||
RUN_TEST(ws_manifest_link_to_sensitive_directory_refused);
|
||||
RUN_TEST(ws_manifest_link_retarget_is_seen_at_use);
|
||||
#endif
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user