Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 18 additions & 17 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/openfang-api/src/openai_compat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ fn convert_messages(oai_messages: &[OaiMessage]) -> Vec<Message> {
provider_msg_id: None,
role,
content,
source: None,
})
})
.collect()
Expand Down
2 changes: 2 additions & 0 deletions crates/openfang-api/src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,8 @@ pub fn inject_attachments_into_session(
_ => openfang_memory::session::Session {
id: entry.session_id,
agent_id,
user_id: openfang_memory::session::default_user_id(),
parent_session_id: None,
messages: Vec::new(),
context_window_tokens: 0,
label: None,
Expand Down
1 change: 1 addition & 0 deletions crates/openfang-kernel/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,4 @@ libc = "0.2"
[dev-dependencies]
tokio-test = { workspace = true }
tempfile = { workspace = true }
rusqlite = { workspace = true }
125 changes: 123 additions & 2 deletions crates/openfang-kernel/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,52 @@ pub struct AuthManager {

impl AuthManager {
/// Create a new AuthManager from kernel user configuration.
///
/// Thin wrapper over [`Self::new_with_default`] for callers that do not
/// have a persistent default-user UUID to bind. Each user gets a freshly
/// generated `UserId`.
pub fn new(user_configs: &[UserConfig]) -> Self {
Self::new_with_default(user_configs, None)
}

/// Like [`Self::new`] but accepts an explicit persistent default-user UUID.
///
/// When `default_user_id` is `Some(uid)`, the config user marked
/// `is_default = true` (or, if none is marked, the first user in the list)
/// is registered under `uid` instead of a freshly-generated UUID. This
/// binds the user's display name and channel bindings to the kernel's
/// persistent default-session bucket, so unattributed traffic is
/// attributed to a real person rather than the nil-UUID anonymous bucket.
pub fn new_with_default(user_configs: &[UserConfig], default_user_id: Option<UserId>) -> Self {
let manager = Self {
users: DashMap::new(),
channel_index: DashMap::new(),
};

for config in user_configs {
let user_id = UserId::new();
// Determine which config index claims the default user identity.
// Priority: an explicit `is_default = true` block, else the first
// user. We only need a `Some` here when the caller actually has a
// persistent UUID to bind, otherwise every user gets a fresh ID.
let default_index: Option<usize> = if default_user_id.is_some() {
user_configs
.iter()
.position(|c| c.is_default)
.or(if user_configs.is_empty() {
None
} else {
Some(0)
})
} else {
None
};

for (idx, config) in user_configs.iter().enumerate() {
let user_id = if Some(idx) == default_index {
// SAFETY: default_index is only Some when default_user_id is Some.
default_user_id.unwrap()
} else {
UserId::new()
};
let role = UserRole::from_str_role(&config.role);
let identity = UserIdentity {
id: user_id,
Expand All @@ -131,6 +169,7 @@ impl AuthManager {
user = %config.name,
role = %role,
bindings = config.channel_bindings.len(),
default = (Some(idx) == default_index),
"Registered user"
);
}
Expand Down Expand Up @@ -205,6 +244,7 @@ mod tests {
m
},
api_key_hash: None,
is_default: false,
},
UserConfig {
name: "Guest".to_string(),
Expand All @@ -215,12 +255,14 @@ mod tests {
m
},
api_key_hash: None,
is_default: false,
},
UserConfig {
name: "ReadOnly".to_string(),
role: "viewer".to_string(),
channel_bindings: HashMap::new(),
api_key_hash: None,
is_default: false,
},
]
}
Expand Down Expand Up @@ -304,6 +346,85 @@ mod tests {
assert_eq!(manager.user_count(), 0);
}

#[test]
fn test_new_with_default_binds_is_default_user() {
// When one [[users]] entry sets `is_default = true`, the manager must
// register that user under the persistent UUID (not a fresh one).
let default_uid = UserId::new();
let configs = vec![
UserConfig {
name: "Alice".to_string(),
role: "user".to_string(),
channel_bindings: HashMap::new(),
api_key_hash: None,
is_default: false,
},
UserConfig {
name: "Owner".to_string(),
role: "owner".to_string(),
channel_bindings: HashMap::new(),
api_key_hash: None,
is_default: true,
},
];

let manager = AuthManager::new_with_default(&configs, Some(default_uid));
// Owner's name resolves to the persistent UUID.
let owner = manager.get_user(default_uid).expect("owner registered");
assert_eq!(owner.name, "Owner");
assert_eq!(owner.role, UserRole::Owner);
}

#[test]
fn test_new_with_default_falls_back_to_first_user() {
// When no entry sets `is_default`, the first user in the list inherits
// the persistent UUID. This matches the single-user-install default
// where there is exactly one `[[users]]` block.
let default_uid = UserId::new();
let configs = vec![
UserConfig {
name: "Primary".to_string(),
role: "owner".to_string(),
channel_bindings: HashMap::new(),
api_key_hash: None,
is_default: false,
},
UserConfig {
name: "Secondary".to_string(),
role: "user".to_string(),
channel_bindings: HashMap::new(),
api_key_hash: None,
is_default: false,
},
];

let manager = AuthManager::new_with_default(&configs, Some(default_uid));
let primary = manager.get_user(default_uid).expect("primary registered");
assert_eq!(primary.name, "Primary");
}

#[test]
fn test_new_with_default_none_keeps_fresh_uuids() {
// Pass `None` (e.g. from a non-kernel test harness): no user is
// bound to the persistent UUID; each entry gets a fresh `UserId`.
let configs = test_configs();
let manager = AuthManager::new_with_default(&configs, None);
assert_eq!(manager.user_count(), 3);
// None of the users can be looked up under the nil UUID by accident.
assert!(manager.get_user(UserId(uuid::Uuid::nil())).is_none());
}

#[test]
fn test_new_with_default_empty_config_is_noop() {
// Empty [[users]] config: the default UUID has nowhere to bind, so
// the manager comes up disabled. This matches a fresh install with
// no user config block.
let default_uid = UserId::new();
let manager = AuthManager::new_with_default(&[], Some(default_uid));
assert!(!manager.is_enabled());
assert_eq!(manager.user_count(), 0);
}

#[test]
fn test_role_parsing() {
assert_eq!(UserRole::from_str_role("owner"), UserRole::Owner);
Expand Down
Loading
Loading