mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 08:09:27 +00:00
fix(profiles): stop losing the first upload to a new profiles stream under RBAC (#15956)
This commit is contained in:
@@ -389,6 +389,7 @@ where
|
||||
StreamType::Logs => RpStreamType::Logs,
|
||||
StreamType::Metrics => RpStreamType::Metrics,
|
||||
StreamType::Traces => RpStreamType::Traces,
|
||||
StreamType::Profiles => RpStreamType::Profiles,
|
||||
StreamType::EnrichmentTables => RpStreamType::EnrichmentTables,
|
||||
_ => RpStreamType::Logs,
|
||||
}),
|
||||
|
||||
@@ -33,7 +33,7 @@ use o2_enterprise::enterprise::{
|
||||
use o2_openfga::{
|
||||
authorizer::authz::{
|
||||
add_tuple_for_pipeline, get_add_user_to_org_tuples, get_org_creation_tuples,
|
||||
get_ownership_all_org_tuple, get_ownership_tuple, update_tuples,
|
||||
get_ownership_all_org_tuple, get_ownership_tuple, get_parent_tuple, update_tuples,
|
||||
},
|
||||
meta::mapping::{
|
||||
LOGS_INSIGHTS_KEY, LOGS_PATTERN_KEY, NON_OWNING_ORG, OFGA_MODELS, OFGAModel,
|
||||
@@ -80,6 +80,7 @@ struct PendingMigrations {
|
||||
fleet: bool,
|
||||
public_dashboards: bool,
|
||||
missing_ownership: bool,
|
||||
profiles: bool,
|
||||
}
|
||||
|
||||
pub async fn init() -> Result<(), anyhow::Error> {
|
||||
@@ -343,6 +344,14 @@ pub async fn init() -> Result<(), anyhow::Error> {
|
||||
&mut tuples,
|
||||
);
|
||||
}
|
||||
if pending.profiles {
|
||||
get_parent_tuple(
|
||||
&format!("_all_{org_name}"),
|
||||
"stream",
|
||||
&format!("profiles:_all_{org_name}"),
|
||||
&mut tuples,
|
||||
);
|
||||
}
|
||||
}
|
||||
run_folder_and_data_migrations(&pending).await;
|
||||
}
|
||||
@@ -563,6 +572,9 @@ fn all_org_ownership_keys(pending: &PendingMigrations) -> Vec<&'static str> {
|
||||
if pending.public_dashboards {
|
||||
keys.push("public_dashboards");
|
||||
}
|
||||
if pending.profiles {
|
||||
keys.push("profiles");
|
||||
}
|
||||
keys
|
||||
}
|
||||
|
||||
@@ -749,6 +761,11 @@ fn pending_migrations(latest: &str, existing: &str) -> PendingMigrations {
|
||||
log::info!("[OFGA:Local] public dashboards permissions migration needed");
|
||||
pending.public_dashboards = true;
|
||||
}
|
||||
let v0_0_61 = version_compare::Version::from("0.0.61").unwrap();
|
||||
if meta_version >= v0_0_61 && existing_model_version < v0_0_61 {
|
||||
log::info!("[OFGA:Local] profiles stream permissions migration needed");
|
||||
pending.profiles = true;
|
||||
}
|
||||
|
||||
pending
|
||||
}
|
||||
@@ -855,4 +872,16 @@ mod tests {
|
||||
};
|
||||
assert_eq!(all_org_ownership_keys(&pending), vec!["public_dashboards"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profiles_back_fill_runs_once_across_0_0_61() {
|
||||
assert!(pending_migrations("0.0.61", "0.0.60").profiles);
|
||||
assert!(!pending_migrations("0.0.61", "0.0.61").profiles);
|
||||
assert!(!pending_migrations("0.0.60", "0.0.59").profiles);
|
||||
assert_eq!(
|
||||
all_org_ownership_keys(&pending_migrations("0.0.61", "0.0.60")),
|
||||
vec!["profiles"]
|
||||
);
|
||||
assert!(OFGA_MODELS.contains_key("profiles"));
|
||||
}
|
||||
}
|
||||
|
||||
+58
-23
@@ -18,13 +18,10 @@ use common::meta::authz::Authz;
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub async fn set_ownership(org_id: &str, obj_type: &str, obj: Authz) {
|
||||
if o2_openfga::config::get_config().enabled {
|
||||
use o2_openfga::{authorizer, meta::mapping::OFGA_MODELS};
|
||||
use o2_openfga::authorizer;
|
||||
|
||||
let obj_str = format!("{}:{}", OFGA_MODELS.get(obj_type).unwrap().key, obj.obj_id);
|
||||
let parent_type = if obj.parent_type.is_empty() {
|
||||
""
|
||||
} else {
|
||||
OFGA_MODELS.get(obj.parent_type.as_str()).unwrap().key
|
||||
let Some((obj_str, parent_type)) = ofga_object(obj_type, &obj) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Default folder is already created for new organizations. This also
|
||||
@@ -55,30 +52,20 @@ pub fn ownership_tuples(
|
||||
obj_type: &str,
|
||||
obj: &Authz,
|
||||
) -> Vec<o2_openfga::authorizer::authz::TupleKey> {
|
||||
use o2_openfga::{authorizer, meta::mapping::OFGA_MODELS};
|
||||
|
||||
let obj_str = format!("{}:{}", OFGA_MODELS.get(obj_type).unwrap().key, obj.obj_id);
|
||||
let parent_type = if obj.parent_type.is_empty() {
|
||||
""
|
||||
} else {
|
||||
OFGA_MODELS.get(obj.parent_type.as_str()).unwrap().key
|
||||
let Some((obj_str, parent_type)) = ofga_object(obj_type, obj) else {
|
||||
return vec![];
|
||||
};
|
||||
authorizer::authz::ownership_tuples(org_id, &obj_str, &obj.parent, parent_type)
|
||||
o2_openfga::authorizer::authz::ownership_tuples(org_id, &obj_str, &obj.parent, parent_type)
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub async fn remove_ownership(org_id: &str, obj_type: &str, obj: Authz) {
|
||||
if o2_openfga::config::get_config().enabled {
|
||||
use o2_openfga::{authorizer, meta::mapping::OFGA_MODELS};
|
||||
|
||||
let obj_str = format!("{}:{}", OFGA_MODELS.get(obj_type).unwrap().key, obj.obj_id);
|
||||
let parent_type = if obj.parent_type.is_empty() {
|
||||
""
|
||||
} else {
|
||||
OFGA_MODELS.get(obj.parent_type.as_str()).unwrap().key
|
||||
let Some((obj_str, parent_type)) = ofga_object(obj_type, &obj) else {
|
||||
return;
|
||||
};
|
||||
|
||||
authorizer::authz::remove_ownership(org_id, &obj_str, &obj.parent, parent_type).await;
|
||||
o2_openfga::authorizer::authz::remove_ownership(org_id, &obj_str, &obj.parent, parent_type)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,3 +126,51 @@ pub async fn list_objects_for_user(
|
||||
) -> anyhow::Result<Option<Vec<String>>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// OpenFGA object and parent type of `obj`; `None` when either type is unmapped.
|
||||
#[cfg(feature = "enterprise")]
|
||||
fn ofga_object(obj_type: &str, obj: &Authz) -> Option<(String, &'static str)> {
|
||||
use o2_openfga::meta::mapping::OFGA_MODELS;
|
||||
|
||||
// Unmapped types must not panic: ingestion calls this after the schema is saved.
|
||||
let Some(model) = OFGA_MODELS.get(obj_type) else {
|
||||
log::error!(
|
||||
"[OFGA] no model for type {obj_type}, ownership of {} not tracked",
|
||||
obj.obj_id
|
||||
);
|
||||
return None;
|
||||
};
|
||||
let parent_type = if obj.parent_type.is_empty() {
|
||||
""
|
||||
} else if let Some(parent) = OFGA_MODELS.get(obj.parent_type.as_str()) {
|
||||
parent.key
|
||||
} else {
|
||||
log::error!(
|
||||
"[OFGA] no model for parent type {}, ownership of {obj_type}:{} not tracked",
|
||||
obj.parent_type,
|
||||
obj.obj_id
|
||||
);
|
||||
return None;
|
||||
};
|
||||
Some((format!("{}:{}", model.key, obj.obj_id), parent_type))
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "enterprise"))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_ofga_object_skips_unmapped_types() {
|
||||
assert!(ofga_object("no_such_type", &Authz::new("s")).is_none());
|
||||
|
||||
let mut obj = Authz::new("s");
|
||||
obj.parent_type = "no_such_type".to_string();
|
||||
obj.parent = "p".to_string();
|
||||
assert!(ofga_object("logs", &obj).is_none());
|
||||
|
||||
assert_eq!(
|
||||
ofga_object("logs", &Authz::new("s")),
|
||||
Some(("logs:s".to_string(), ""))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ vi.mock("@/composables/useStreams", () => ({
|
||||
],
|
||||
},
|
||||
traces: { list: [{ name: "svc-a" }] },
|
||||
profiles: { list: [{ name: "cpu-prof" }, { name: "mem-prof" }] },
|
||||
index: { list: [{ name: "users" }] },
|
||||
enrichment_tables: { list: [{ name: "geo" }] },
|
||||
metadata: { list: [{ name: "meta" }] },
|
||||
@@ -466,6 +467,14 @@ const SAVED_GRANTS_RESOURCE_CATALOG = [
|
||||
top_level: false,
|
||||
order: 3,
|
||||
},
|
||||
{
|
||||
key: "profiles",
|
||||
display_name: "Profiles",
|
||||
has_entities: true,
|
||||
parent: "stream",
|
||||
top_level: false,
|
||||
order: 80,
|
||||
},
|
||||
{ key: "dfolder", display_name: "Dash Folders", has_entities: true, top_level: true, order: 4 },
|
||||
{
|
||||
key: "dashboard",
|
||||
@@ -1805,6 +1814,7 @@ describe("EditRole - module navigation", () => {
|
||||
"index",
|
||||
"logs",
|
||||
"metrics",
|
||||
"profiles",
|
||||
"traces",
|
||||
]);
|
||||
});
|
||||
@@ -1819,6 +1829,7 @@ describe("EditRole - module navigation", () => {
|
||||
"index",
|
||||
"logs",
|
||||
"metrics",
|
||||
"profiles",
|
||||
"traces",
|
||||
]);
|
||||
});
|
||||
@@ -2385,11 +2396,17 @@ async function loadStreamType(wrapper, type) {
|
||||
}
|
||||
|
||||
describe("EditRole - loaded rows through the tree [characterization]", () => {
|
||||
it("getStreamsTypes builds four type rows", async () => {
|
||||
it("getStreamsTypes builds five type rows", async () => {
|
||||
const wrapper = await mountEditRole();
|
||||
const stream = await loadResource(wrapper, "stream");
|
||||
|
||||
expect(stream.entities.map((e) => e.name)).toEqual(["logs", "traces", "metrics", "index"]);
|
||||
expect(stream.entities.map((e) => e.name)).toEqual([
|
||||
"logs",
|
||||
"traces",
|
||||
"metrics",
|
||||
"profiles",
|
||||
"index",
|
||||
]);
|
||||
expect(stream.entities[0]).toMatchObject({
|
||||
name: "logs",
|
||||
type: "Type",
|
||||
@@ -3078,6 +3095,15 @@ describe("EditRole savedGrants - updateRolePermissions expansion", () => {
|
||||
expect(rows.find((r) => r.name === "sys").permission.AllowGet.value).toBe(false);
|
||||
});
|
||||
|
||||
it("expands the stream tree and ticks a per-stream profiles row", async () => {
|
||||
const wrapper = await mountWithSaved([{ object: "profiles:cpu-prof", permission: "AllowGet" }]);
|
||||
const rows = wrapper.vm.heavyResourceEntities["profiles"] ?? [];
|
||||
const cpu = rows.find((r) => r.name === "cpu-prof");
|
||||
expect(cpu.resourceName).toBe("profiles");
|
||||
expect(cpu.permission.AllowGet.value).toBe(true);
|
||||
expect(rows.find((r) => r.name === "mem-prof").permission.AllowGet.value).toBe(false);
|
||||
});
|
||||
|
||||
it("expands dfolder and its folder to tick a folder-scoped dashboard grant", async () => {
|
||||
const wrapper = await mountWithSaved([
|
||||
{ object: "dashboard:default/d1", permission: "AllowGet" },
|
||||
@@ -3629,6 +3655,7 @@ describe("EditRole - all modules", () => {
|
||||
"index",
|
||||
"logs",
|
||||
"metrics",
|
||||
"profiles",
|
||||
"traces",
|
||||
]);
|
||||
expect(wrapper.find('[data-test^="edit-role-module-pane-bulk-"]').exists()).toBe(false);
|
||||
|
||||
@@ -90,6 +90,7 @@ const GROUP_OF: Record<string, ModuleGroupId> = {
|
||||
logs: "data",
|
||||
metrics: "data",
|
||||
traces: "data",
|
||||
profiles: "data",
|
||||
index: "data",
|
||||
metadata: "data",
|
||||
enrichment_table: "data",
|
||||
|
||||
@@ -10,6 +10,7 @@ const getStreamsMock = vi.hoisted(() =>
|
||||
logs: { list: [{ name: "app" }, { name: "sys" }] },
|
||||
metrics: { list: [{ name: "cpu" }] },
|
||||
traces: { list: [{ name: "svc-a" }] },
|
||||
profiles: { list: [{ name: "cpu-prof" }] },
|
||||
index: { list: [{ name: "users" }] },
|
||||
enrichment_tables: { list: [{ name: "geo" }] },
|
||||
metadata: { list: [{ name: "meta" }] },
|
||||
@@ -334,7 +335,7 @@ describe("useRoleEntityLoaders - getResourceEntities dispatch", () => {
|
||||
});
|
||||
|
||||
describe("useRoleEntityLoaders - stream loaders", () => {
|
||||
it("getStreamsTypes builds four type rows without a service call", async () => {
|
||||
it("getStreamsTypes builds five type rows without a service call", async () => {
|
||||
const { getStreamsTypes, updateResourceResource } = setup();
|
||||
getStreamsMock.mockClear();
|
||||
|
||||
@@ -345,6 +346,7 @@ describe("useRoleEntityLoaders - stream loaders", () => {
|
||||
"logs",
|
||||
"traces",
|
||||
"metrics",
|
||||
"profiles",
|
||||
"index",
|
||||
]);
|
||||
expect(updateResourceResource.mock.calls[0]).toEqual([
|
||||
@@ -356,6 +358,9 @@ describe("useRoleEntityLoaders - stream loaders", () => {
|
||||
"name",
|
||||
]);
|
||||
expect(updateResourceResource.mock.calls[3][3]).toEqual([
|
||||
{ stream_type: "profiles", name: "menu.profiles" },
|
||||
]);
|
||||
expect(updateResourceResource.mock.calls[4][3]).toEqual([
|
||||
{ stream_type: "index", name: "iam.indices" },
|
||||
]);
|
||||
});
|
||||
@@ -374,26 +379,30 @@ describe("useRoleEntityLoaders - stream loaders", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("getMetrics, getTraces and getIndexStreams each ask for their own stream type", async () => {
|
||||
const { getMetrics, getTraces, getIndexStreams, updateEntityEntities } = setup();
|
||||
it("getMetrics, getTraces, getProfiles and getIndexStreams each ask for their own stream type", async () => {
|
||||
const { getMetrics, getTraces, getProfiles, getIndexStreams, updateEntityEntities } = setup();
|
||||
getStreamsMock.mockClear();
|
||||
|
||||
const metrics = row({ name: "metrics", childName: "metrics" });
|
||||
const traces = row({ name: "traces", childName: "traces" });
|
||||
const profiles = row({ name: "profiles", childName: "profiles" });
|
||||
const index = row({ name: "index", childName: "index" });
|
||||
|
||||
await getMetrics(metrics as any);
|
||||
await getTraces(traces as any);
|
||||
await getProfiles(profiles as any);
|
||||
await getIndexStreams(index as any);
|
||||
|
||||
expect(getStreamsMock.mock.calls).toEqual([
|
||||
["metrics", false],
|
||||
["traces", false],
|
||||
["profiles", false],
|
||||
["index", false],
|
||||
]);
|
||||
expect(updateEntityEntities.mock.calls.map((call) => call[2])).toEqual([
|
||||
[{ name: "cpu" }],
|
||||
[{ name: "svc-a" }],
|
||||
[{ name: "cpu-prof" }],
|
||||
[{ name: "users" }],
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -96,6 +96,7 @@ export const useRoleEntityLoaders = (deps: LoaderDeps) => {
|
||||
logs: getLogs,
|
||||
metrics: getMetrics,
|
||||
traces: getTraces,
|
||||
profiles: getProfiles,
|
||||
index: getIndexStreams,
|
||||
alert: getAlerts,
|
||||
template: getTemplates,
|
||||
@@ -571,6 +572,16 @@ export const useRoleEntityLoaders = (deps: LoaderDeps) => {
|
||||
});
|
||||
};
|
||||
|
||||
const getProfiles = async (resource: Resource | Entity) => {
|
||||
const profiles: any = await getStreams("profiles", false);
|
||||
|
||||
updateEntityEntities(resource, ["name"], profiles.list);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
resolve(true);
|
||||
});
|
||||
};
|
||||
|
||||
const getMetadataStreams = async () => {
|
||||
const metadata: any = await getStreams("metadata", false);
|
||||
|
||||
@@ -586,6 +597,7 @@ export const useRoleEntityLoaders = (deps: LoaderDeps) => {
|
||||
{ stream_type: "logs", name: t("common.logs") },
|
||||
{ stream_type: "traces", name: t("common.traces") },
|
||||
{ stream_type: "metrics", name: t("common.metrics") },
|
||||
{ stream_type: "profiles", name: t("menu.profiles") },
|
||||
{ stream_type: "index", name: t("iam.indices") },
|
||||
];
|
||||
|
||||
@@ -810,6 +822,7 @@ export const useRoleEntityLoaders = (deps: LoaderDeps) => {
|
||||
getIndexStreams,
|
||||
getMetrics,
|
||||
getTraces,
|
||||
getProfiles,
|
||||
getMetadataStreams,
|
||||
getStreamsTypes,
|
||||
getReportFolders,
|
||||
|
||||
@@ -141,6 +141,7 @@ export const useRoleJsonView = (deps: JsonViewDeps) => {
|
||||
resource === "logs" ||
|
||||
resource === "metrics" ||
|
||||
resource === "traces" ||
|
||||
resource === "profiles" ||
|
||||
resource === "index"
|
||||
) {
|
||||
resourceDetails = resourceMapper.value["stream"].entities.find(
|
||||
@@ -211,6 +212,7 @@ export const useRoleJsonView = (deps: JsonViewDeps) => {
|
||||
resource === "logs" ||
|
||||
resource === "metrics" ||
|
||||
resource === "traces" ||
|
||||
resource === "profiles" ||
|
||||
resource === "index"
|
||||
) {
|
||||
resourceDetails = resourceMapper.value["stream"].entities.find(
|
||||
|
||||
@@ -264,7 +264,7 @@ describe("useRolePermissionRows - updateEntityEntities", () => {
|
||||
it("mirrors the heavy stream types into heavyResourceEntities and nothing else", () => {
|
||||
const { updateEntityEntities, heavyResourceEntities } = setup();
|
||||
const node = (name: string) => ({ name, childName: name, entities: [] as any[] });
|
||||
const nodes = ["logs", "metrics", "traces", "index", "dfolder"].map(node);
|
||||
const nodes = ["logs", "metrics", "traces", "profiles", "index", "dfolder"].map(node);
|
||||
|
||||
nodes.forEach((n) => updateEntityEntities(n as any, ["name"], [{ name: n.name + "-1" }]));
|
||||
|
||||
@@ -272,6 +272,7 @@ describe("useRolePermissionRows - updateEntityEntities", () => {
|
||||
"index",
|
||||
"logs",
|
||||
"metrics",
|
||||
"profiles",
|
||||
"traces",
|
||||
]);
|
||||
expect(heavyResourceEntities.value.logs.map((e: any) => e.name)).toEqual(["logs-1"]);
|
||||
|
||||
@@ -126,6 +126,7 @@ export const useRolePermissionRows = (deps: RowDeps) => {
|
||||
entity.name === "logs" ||
|
||||
entity.name === "metrics" ||
|
||||
entity.name === "traces" ||
|
||||
entity.name === "profiles" ||
|
||||
entity.name === "index"
|
||||
) {
|
||||
heavyResourceEntities.value[entity.name] = [...entities];
|
||||
|
||||
@@ -226,6 +226,7 @@ export const useSavedGrantExpansion = (deps: ExpansionDeps) => {
|
||||
resource === "logs" ||
|
||||
resource === "metrics" ||
|
||||
resource === "traces" ||
|
||||
resource === "profiles" ||
|
||||
resource === "index"
|
||||
) {
|
||||
const streamResource = resourceMapper["stream"].entities.find(
|
||||
|
||||
Reference in New Issue
Block a user