mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 09:50:21 +08:00
feat: add user groups and inherited access policies
This commit is contained in:
@@ -3,9 +3,11 @@ use chrono::{DateTime, TimeZone, Utc};
|
||||
use sqlx::{sqlite::SqliteRow, QueryBuilder, Row, Sqlite};
|
||||
|
||||
use super::types::{
|
||||
LdapAuthUserProvisioningOutcome, StoredUserAuthRecord, StoredUserExportRow,
|
||||
normalize_user_group_name, LdapAuthUserProvisioningOutcome, StoredUserAuthRecord,
|
||||
StoredUserExportRow, StoredUserGroup, StoredUserGroupMember, StoredUserGroupMembership,
|
||||
StoredUserOAuthLinkSummary, StoredUserPreferenceRecord, StoredUserSessionRecord,
|
||||
StoredUserSummary, UserExportListQuery, UserExportSummary, UserReadRepository,
|
||||
StoredUserSummary, UpsertUserGroupRecord, UserExportListQuery, UserExportSummary,
|
||||
UserReadRepository,
|
||||
};
|
||||
use crate::driver::sqlite::SqlitePool;
|
||||
use crate::error::SqlResultExt;
|
||||
@@ -32,9 +34,13 @@ SELECT
|
||||
role,
|
||||
auth_source,
|
||||
allowed_providers,
|
||||
allowed_providers_mode,
|
||||
allowed_api_formats,
|
||||
allowed_api_formats_mode,
|
||||
allowed_models,
|
||||
allowed_models_mode,
|
||||
rate_limit,
|
||||
rate_limit_mode,
|
||||
model_capability_settings,
|
||||
is_active
|
||||
FROM users
|
||||
@@ -50,8 +56,11 @@ SELECT
|
||||
role,
|
||||
auth_source,
|
||||
allowed_providers,
|
||||
allowed_providers_mode,
|
||||
allowed_api_formats,
|
||||
allowed_api_formats_mode,
|
||||
allowed_models,
|
||||
allowed_models_mode,
|
||||
is_active,
|
||||
is_deleted,
|
||||
created_at,
|
||||
@@ -69,8 +78,11 @@ SELECT
|
||||
users.role AS role,
|
||||
users.auth_source AS auth_source,
|
||||
users.allowed_providers AS allowed_providers,
|
||||
users.allowed_providers_mode AS allowed_providers_mode,
|
||||
users.allowed_api_formats AS allowed_api_formats,
|
||||
users.allowed_api_formats_mode AS allowed_api_formats_mode,
|
||||
users.allowed_models AS allowed_models,
|
||||
users.allowed_models_mode AS allowed_models_mode,
|
||||
users.is_active AS is_active,
|
||||
users.is_deleted AS is_deleted,
|
||||
users.created_at AS created_at,
|
||||
@@ -130,6 +142,40 @@ SELECT
|
||||
FROM user_sessions
|
||||
"#;
|
||||
|
||||
const USER_GROUP_COLUMNS: &str = r#"
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
normalized_name,
|
||||
description,
|
||||
priority,
|
||||
allowed_providers,
|
||||
allowed_providers_mode,
|
||||
allowed_api_formats,
|
||||
allowed_api_formats_mode,
|
||||
allowed_models,
|
||||
allowed_models_mode,
|
||||
rate_limit,
|
||||
rate_limit_mode,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM user_groups
|
||||
"#;
|
||||
|
||||
const USER_GROUP_MEMBER_COLUMNS: &str = r#"
|
||||
SELECT
|
||||
user_group_members.group_id,
|
||||
users.id AS user_id,
|
||||
users.username,
|
||||
users.email,
|
||||
users.role,
|
||||
users.is_active,
|
||||
users.is_deleted,
|
||||
user_group_members.created_at
|
||||
FROM user_group_members
|
||||
JOIN users ON users.id = user_group_members.user_id
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SqliteUserReadRepository {
|
||||
pool: SqlitePool,
|
||||
@@ -163,6 +209,22 @@ impl SqliteUserReadRepository {
|
||||
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||
rows.iter().map(map_user_auth_row).collect()
|
||||
}
|
||||
|
||||
async fn fetch_group_rows(
|
||||
&self,
|
||||
mut builder: QueryBuilder<'_, Sqlite>,
|
||||
) -> Result<Vec<StoredUserGroup>, DataLayerError> {
|
||||
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||
rows.iter().map(map_user_group_row).collect()
|
||||
}
|
||||
|
||||
async fn fetch_group_member_rows(
|
||||
&self,
|
||||
mut builder: QueryBuilder<'_, Sqlite>,
|
||||
) -> Result<Vec<StoredUserGroupMember>, DataLayerError> {
|
||||
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||
rows.iter().map(map_user_group_member_row).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -224,6 +286,16 @@ impl UserReadRepository for SqliteUserReadRepository {
|
||||
if let Some(is_active) = query.is_active {
|
||||
builder.push(" AND is_active = ").push_bind(is_active);
|
||||
}
|
||||
if let Some(group_id) = query
|
||||
.group_id
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
builder.push(" AND id IN (SELECT user_id FROM user_group_members WHERE group_id = ");
|
||||
builder.push_bind(group_id);
|
||||
builder.push(")");
|
||||
}
|
||||
if let Some(search) = query
|
||||
.search
|
||||
.as_deref()
|
||||
@@ -296,6 +368,285 @@ WHERE is_deleted = 0
|
||||
self.fetch_export_rows(builder).await
|
||||
}
|
||||
|
||||
async fn list_user_groups(&self) -> Result<Vec<StoredUserGroup>, DataLayerError> {
|
||||
let mut builder = QueryBuilder::<Sqlite>::new(USER_GROUP_COLUMNS);
|
||||
builder.push(" ORDER BY priority DESC, name ASC, id ASC");
|
||||
self.fetch_group_rows(builder).await
|
||||
}
|
||||
|
||||
async fn find_user_group_by_id(
|
||||
&self,
|
||||
group_id: &str,
|
||||
) -> Result<Option<StoredUserGroup>, DataLayerError> {
|
||||
let mut builder = QueryBuilder::<Sqlite>::new(USER_GROUP_COLUMNS);
|
||||
builder
|
||||
.push(" WHERE id = ")
|
||||
.push_bind(group_id)
|
||||
.push(" LIMIT 1");
|
||||
Ok(self.fetch_group_rows(builder).await?.into_iter().next())
|
||||
}
|
||||
|
||||
async fn list_user_groups_by_ids(
|
||||
&self,
|
||||
group_ids: &[String],
|
||||
) -> Result<Vec<StoredUserGroup>, DataLayerError> {
|
||||
if group_ids.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut builder = QueryBuilder::<Sqlite>::new(USER_GROUP_COLUMNS);
|
||||
builder.push(" WHERE id IN (");
|
||||
{
|
||||
let mut separated = builder.separated(", ");
|
||||
for group_id in group_ids {
|
||||
separated.push_bind(group_id);
|
||||
}
|
||||
}
|
||||
builder.push(") ORDER BY priority DESC, name ASC, id ASC");
|
||||
self.fetch_group_rows(builder).await
|
||||
}
|
||||
|
||||
async fn create_user_group(
|
||||
&self,
|
||||
record: UpsertUserGroupRecord,
|
||||
) -> Result<Option<StoredUserGroup>, DataLayerError> {
|
||||
let now = current_unix_secs();
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let name = normalize_user_group_name(&record.name);
|
||||
let normalized_name = name.to_ascii_lowercase();
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO user_groups (
|
||||
id, name, normalized_name, description, priority,
|
||||
allowed_providers, allowed_providers_mode,
|
||||
allowed_api_formats, allowed_api_formats_mode,
|
||||
allowed_models, allowed_models_mode,
|
||||
rate_limit, rate_limit_mode, created_at, updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"#,
|
||||
)
|
||||
.bind(&id)
|
||||
.bind(name)
|
||||
.bind(normalized_name)
|
||||
.bind(record.description)
|
||||
.bind(record.priority)
|
||||
.bind(json_string_from_option_vec(
|
||||
record.allowed_providers.as_ref(),
|
||||
))
|
||||
.bind(record.allowed_providers_mode)
|
||||
.bind(json_string_from_option_vec(
|
||||
record.allowed_api_formats.as_ref(),
|
||||
))
|
||||
.bind(record.allowed_api_formats_mode)
|
||||
.bind(json_string_from_option_vec(record.allowed_models.as_ref()))
|
||||
.bind(record.allowed_models_mode)
|
||||
.bind(record.rate_limit)
|
||||
.bind(record.rate_limit_mode)
|
||||
.bind(now)
|
||||
.bind(now)
|
||||
.execute(&self.pool)
|
||||
.await;
|
||||
match result {
|
||||
Ok(_) => self.find_user_group_by_id(&id).await,
|
||||
Err(sqlx::Error::Database(err)) if err.is_unique_violation() => Err(
|
||||
DataLayerError::InvalidInput("duplicate user group name".to_string()),
|
||||
),
|
||||
Err(err) => Err(err).map_sql_err(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_user_group(
|
||||
&self,
|
||||
group_id: &str,
|
||||
record: UpsertUserGroupRecord,
|
||||
) -> Result<Option<StoredUserGroup>, DataLayerError> {
|
||||
let now = current_unix_secs();
|
||||
let name = normalize_user_group_name(&record.name);
|
||||
let normalized_name = name.to_ascii_lowercase();
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
UPDATE user_groups
|
||||
SET name = ?,
|
||||
normalized_name = ?,
|
||||
description = ?,
|
||||
priority = ?,
|
||||
allowed_providers = ?,
|
||||
allowed_providers_mode = ?,
|
||||
allowed_api_formats = ?,
|
||||
allowed_api_formats_mode = ?,
|
||||
allowed_models = ?,
|
||||
allowed_models_mode = ?,
|
||||
rate_limit = ?,
|
||||
rate_limit_mode = ?,
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
"#,
|
||||
)
|
||||
.bind(name)
|
||||
.bind(normalized_name)
|
||||
.bind(record.description)
|
||||
.bind(record.priority)
|
||||
.bind(json_string_from_option_vec(
|
||||
record.allowed_providers.as_ref(),
|
||||
))
|
||||
.bind(record.allowed_providers_mode)
|
||||
.bind(json_string_from_option_vec(
|
||||
record.allowed_api_formats.as_ref(),
|
||||
))
|
||||
.bind(record.allowed_api_formats_mode)
|
||||
.bind(json_string_from_option_vec(record.allowed_models.as_ref()))
|
||||
.bind(record.allowed_models_mode)
|
||||
.bind(record.rate_limit)
|
||||
.bind(record.rate_limit_mode)
|
||||
.bind(now)
|
||||
.bind(group_id)
|
||||
.execute(&self.pool)
|
||||
.await;
|
||||
match result {
|
||||
Ok(result) if result.rows_affected() == 0 => Ok(None),
|
||||
Ok(_) => self.find_user_group_by_id(group_id).await,
|
||||
Err(sqlx::Error::Database(err)) if err.is_unique_violation() => Err(
|
||||
DataLayerError::InvalidInput("duplicate user group name".to_string()),
|
||||
),
|
||||
Err(err) => Err(err).map_sql_err(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_user_group(&self, group_id: &str) -> Result<bool, DataLayerError> {
|
||||
let result = sqlx::query("DELETE FROM user_groups WHERE id = ?")
|
||||
.bind(group_id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
|
||||
async fn list_user_group_members(
|
||||
&self,
|
||||
group_id: &str,
|
||||
) -> Result<Vec<StoredUserGroupMember>, DataLayerError> {
|
||||
let mut builder = QueryBuilder::<Sqlite>::new(USER_GROUP_MEMBER_COLUMNS);
|
||||
builder
|
||||
.push(" WHERE user_group_members.group_id = ")
|
||||
.push_bind(group_id)
|
||||
.push(" ORDER BY users.username ASC, users.id ASC");
|
||||
self.fetch_group_member_rows(builder).await
|
||||
}
|
||||
|
||||
async fn replace_user_group_members(
|
||||
&self,
|
||||
group_id: &str,
|
||||
user_ids: &[String],
|
||||
) -> Result<Vec<StoredUserGroupMember>, DataLayerError> {
|
||||
let mut tx = self.pool.begin().await.map_sql_err()?;
|
||||
sqlx::query("DELETE FROM user_group_members WHERE group_id = ?")
|
||||
.bind(group_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
let now = current_unix_secs();
|
||||
for user_id in normalized_ids(user_ids) {
|
||||
sqlx::query(
|
||||
"INSERT OR IGNORE INTO user_group_members (group_id, user_id, created_at) VALUES (?, ?, ?)",
|
||||
)
|
||||
.bind(group_id)
|
||||
.bind(user_id)
|
||||
.bind(now)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
}
|
||||
tx.commit().await.map_sql_err()?;
|
||||
self.list_user_group_members(group_id).await
|
||||
}
|
||||
|
||||
async fn list_user_groups_for_user(
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> Result<Vec<StoredUserGroup>, DataLayerError> {
|
||||
let mut builder = QueryBuilder::<Sqlite>::new(USER_GROUP_COLUMNS);
|
||||
builder
|
||||
.push(" WHERE id IN (SELECT group_id FROM user_group_members WHERE user_id = ")
|
||||
.push_bind(user_id)
|
||||
.push(") ORDER BY priority DESC, name ASC, id ASC");
|
||||
self.fetch_group_rows(builder).await
|
||||
}
|
||||
|
||||
async fn list_user_group_memberships_by_user_ids(
|
||||
&self,
|
||||
user_ids: &[String],
|
||||
) -> Result<Vec<StoredUserGroupMembership>, DataLayerError> {
|
||||
if user_ids.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut builder = QueryBuilder::<Sqlite>::new(
|
||||
r#"
|
||||
SELECT
|
||||
user_group_members.user_id,
|
||||
user_groups.id AS group_id,
|
||||
user_groups.name AS group_name,
|
||||
user_groups.priority AS group_priority,
|
||||
user_group_members.created_at
|
||||
FROM user_group_members
|
||||
JOIN user_groups ON user_groups.id = user_group_members.group_id
|
||||
WHERE user_group_members.user_id IN (
|
||||
"#,
|
||||
);
|
||||
{
|
||||
let mut separated = builder.separated(", ");
|
||||
for user_id in user_ids {
|
||||
separated.push_bind(user_id);
|
||||
}
|
||||
}
|
||||
builder.push(") ORDER BY user_group_members.user_id ASC, user_groups.priority DESC, user_groups.name ASC, user_groups.id ASC");
|
||||
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||
rows.iter().map(map_user_group_membership_row).collect()
|
||||
}
|
||||
|
||||
async fn replace_user_groups_for_user(
|
||||
&self,
|
||||
user_id: &str,
|
||||
group_ids: &[String],
|
||||
) -> Result<Vec<StoredUserGroup>, DataLayerError> {
|
||||
let mut tx = self.pool.begin().await.map_sql_err()?;
|
||||
sqlx::query("DELETE FROM user_group_members WHERE user_id = ?")
|
||||
.bind(user_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
let now = current_unix_secs();
|
||||
for group_id in normalized_ids(group_ids) {
|
||||
sqlx::query(
|
||||
"INSERT OR IGNORE INTO user_group_members (group_id, user_id, created_at) VALUES (?, ?, ?)",
|
||||
)
|
||||
.bind(group_id)
|
||||
.bind(user_id)
|
||||
.bind(now)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
}
|
||||
tx.commit().await.map_sql_err()?;
|
||||
self.list_user_groups_for_user(user_id).await
|
||||
}
|
||||
|
||||
async fn add_user_to_group(
|
||||
&self,
|
||||
group_id: &str,
|
||||
user_id: &str,
|
||||
) -> Result<bool, DataLayerError> {
|
||||
let result = sqlx::query(
|
||||
"INSERT OR IGNORE INTO user_group_members (group_id, user_id, created_at) VALUES (?, ?, ?)",
|
||||
)
|
||||
.bind(group_id)
|
||||
.bind(user_id)
|
||||
.bind(current_unix_secs())
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
|
||||
async fn find_user_auth_by_id(
|
||||
&self,
|
||||
user_id: &str,
|
||||
@@ -453,9 +804,10 @@ WHERE provider_type = ?
|
||||
r#"
|
||||
INSERT INTO users (
|
||||
id, email, email_verified, username, password_hash, role, auth_source,
|
||||
allowed_providers_mode, allowed_api_formats_mode, allowed_models_mode, rate_limit_mode,
|
||||
is_active, is_deleted, created_at, updated_at, last_login_at
|
||||
)
|
||||
VALUES (?, ?, 1, ?, NULL, 'user', 'oauth', 1, 0, ?, ?, ?)
|
||||
VALUES (?, ?, 1, ?, NULL, 'user', 'oauth', 'inherit', 'inherit', 'inherit', 'inherit', 1, 0, ?, ?, ?)
|
||||
"#,
|
||||
)
|
||||
.bind(&user_id)
|
||||
@@ -675,14 +1027,38 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
rate_limit: Option<i32>,
|
||||
is_active: Option<bool>,
|
||||
) -> Result<Option<StoredUserAuthRecord>, DataLayerError> {
|
||||
let allowed_providers_mode = if allowed_providers.is_some() {
|
||||
"specific"
|
||||
} else {
|
||||
"unrestricted"
|
||||
};
|
||||
let allowed_api_formats_mode = if allowed_api_formats.is_some() {
|
||||
"specific"
|
||||
} else {
|
||||
"unrestricted"
|
||||
};
|
||||
let allowed_models_mode = if allowed_models.is_some() {
|
||||
"specific"
|
||||
} else {
|
||||
"unrestricted"
|
||||
};
|
||||
let rate_limit_mode = if rate_limit.is_some() {
|
||||
"custom"
|
||||
} else {
|
||||
"system"
|
||||
};
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
UPDATE users
|
||||
SET role = CASE WHEN ? THEN COALESCE(?, role) ELSE role END,
|
||||
allowed_providers = CASE WHEN ? THEN ? ELSE allowed_providers END,
|
||||
allowed_providers_mode = CASE WHEN ? THEN ? ELSE allowed_providers_mode END,
|
||||
allowed_api_formats = CASE WHEN ? THEN ? ELSE allowed_api_formats END,
|
||||
allowed_api_formats_mode = CASE WHEN ? THEN ? ELSE allowed_api_formats_mode END,
|
||||
allowed_models = CASE WHEN ? THEN ? ELSE allowed_models END,
|
||||
allowed_models_mode = CASE WHEN ? THEN ? ELSE allowed_models_mode END,
|
||||
rate_limit = CASE WHEN ? THEN ? ELSE rate_limit END,
|
||||
rate_limit_mode = CASE WHEN ? THEN ? ELSE rate_limit_mode END,
|
||||
is_active = CASE WHEN ? THEN ? ELSE is_active END,
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
@@ -695,18 +1071,26 @@ WHERE id = ?
|
||||
allowed_providers,
|
||||
"users.allowed_providers",
|
||||
)?)
|
||||
.bind(allowed_providers_present)
|
||||
.bind(allowed_providers_mode)
|
||||
.bind(allowed_api_formats_present)
|
||||
.bind(optional_string_list_json(
|
||||
allowed_api_formats,
|
||||
"users.allowed_api_formats",
|
||||
)?)
|
||||
.bind(allowed_api_formats_present)
|
||||
.bind(allowed_api_formats_mode)
|
||||
.bind(allowed_models_present)
|
||||
.bind(optional_string_list_json(
|
||||
allowed_models,
|
||||
"users.allowed_models",
|
||||
)?)
|
||||
.bind(allowed_models_present)
|
||||
.bind(allowed_models_mode)
|
||||
.bind(rate_limit_present)
|
||||
.bind(rate_limit)
|
||||
.bind(rate_limit_present)
|
||||
.bind(rate_limit_mode)
|
||||
.bind(is_active.is_some())
|
||||
.bind(is_active)
|
||||
.bind(chrono::Utc::now().timestamp())
|
||||
@@ -720,6 +1104,44 @@ WHERE id = ?
|
||||
self.find_user_auth_by_id(user_id).await
|
||||
}
|
||||
|
||||
async fn update_local_auth_user_policy_modes(
|
||||
&self,
|
||||
user_id: &str,
|
||||
allowed_providers_mode: Option<String>,
|
||||
allowed_api_formats_mode: Option<String>,
|
||||
allowed_models_mode: Option<String>,
|
||||
rate_limit_mode: Option<String>,
|
||||
) -> Result<Option<StoredUserAuthRecord>, DataLayerError> {
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
UPDATE users
|
||||
SET allowed_providers_mode = CASE WHEN ? THEN ? ELSE allowed_providers_mode END,
|
||||
allowed_api_formats_mode = CASE WHEN ? THEN ? ELSE allowed_api_formats_mode END,
|
||||
allowed_models_mode = CASE WHEN ? THEN ? ELSE allowed_models_mode END,
|
||||
rate_limit_mode = CASE WHEN ? THEN ? ELSE rate_limit_mode END,
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
"#,
|
||||
)
|
||||
.bind(allowed_providers_mode.is_some())
|
||||
.bind(allowed_providers_mode)
|
||||
.bind(allowed_api_formats_mode.is_some())
|
||||
.bind(allowed_api_formats_mode)
|
||||
.bind(allowed_models_mode.is_some())
|
||||
.bind(allowed_models_mode)
|
||||
.bind(rate_limit_mode.is_some())
|
||||
.bind(rate_limit_mode)
|
||||
.bind(chrono::Utc::now().timestamp())
|
||||
.bind(user_id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
if result.rows_affected() == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
self.find_user_auth_by_id(user_id).await
|
||||
}
|
||||
|
||||
async fn update_user_model_capability_settings(
|
||||
&self,
|
||||
user_id: &str,
|
||||
@@ -751,18 +1173,29 @@ WHERE id = ?
|
||||
username: String,
|
||||
password_hash: String,
|
||||
) -> Result<Option<StoredUserAuthRecord>, DataLayerError> {
|
||||
self.create_local_auth_user_with_settings(
|
||||
email,
|
||||
email_verified,
|
||||
username,
|
||||
password_hash,
|
||||
"user".to_string(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
let user_id = uuid::Uuid::new_v4().to_string();
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO users (
|
||||
id, email, email_verified, username, password_hash, role, auth_source,
|
||||
allowed_providers_mode, allowed_api_formats_mode, allowed_models_mode, rate_limit_mode,
|
||||
is_active, is_deleted, created_at, updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, 'user', 'local', 'inherit', 'inherit', 'inherit', 'inherit', 1, 0, ?, ?)
|
||||
"#,
|
||||
)
|
||||
.bind(&user_id)
|
||||
.bind(email)
|
||||
.bind(email_verified)
|
||||
.bind(username)
|
||||
.bind(password_hash)
|
||||
.bind(now)
|
||||
.bind(now)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
self.find_user_auth_by_id(&user_id).await
|
||||
}
|
||||
|
||||
async fn create_local_auth_user_with_settings(
|
||||
@@ -779,14 +1212,37 @@ WHERE id = ?
|
||||
) -> Result<Option<StoredUserAuthRecord>, DataLayerError> {
|
||||
let user_id = uuid::Uuid::new_v4().to_string();
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
let allowed_providers_mode = if allowed_providers.is_some() {
|
||||
"specific"
|
||||
} else {
|
||||
"unrestricted"
|
||||
};
|
||||
let allowed_api_formats_mode = if allowed_api_formats.is_some() {
|
||||
"specific"
|
||||
} else {
|
||||
"unrestricted"
|
||||
};
|
||||
let allowed_models_mode = if allowed_models.is_some() {
|
||||
"specific"
|
||||
} else {
|
||||
"unrestricted"
|
||||
};
|
||||
let rate_limit_mode = if rate_limit.is_some() {
|
||||
"custom"
|
||||
} else {
|
||||
"system"
|
||||
};
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO users (
|
||||
id, email, email_verified, username, password_hash, role, auth_source,
|
||||
allowed_providers, allowed_api_formats, allowed_models, rate_limit,
|
||||
allowed_providers, allowed_providers_mode,
|
||||
allowed_api_formats, allowed_api_formats_mode,
|
||||
allowed_models, allowed_models_mode,
|
||||
rate_limit, rate_limit_mode,
|
||||
is_active, is_deleted, created_at, updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'local', ?, ?, ?, ?, 1, 0, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'local', ?, ?, ?, ?, ?, ?, ?, ?, 1, 0, ?, ?)
|
||||
"#,
|
||||
)
|
||||
.bind(&user_id)
|
||||
@@ -799,15 +1255,19 @@ VALUES (?, ?, ?, ?, ?, ?, 'local', ?, ?, ?, ?, 1, 0, ?, ?)
|
||||
allowed_providers,
|
||||
"users.allowed_providers",
|
||||
)?)
|
||||
.bind(allowed_providers_mode)
|
||||
.bind(optional_string_list_json(
|
||||
allowed_api_formats,
|
||||
"users.allowed_api_formats",
|
||||
)?)
|
||||
.bind(allowed_api_formats_mode)
|
||||
.bind(optional_string_list_json(
|
||||
allowed_models,
|
||||
"users.allowed_models",
|
||||
)?)
|
||||
.bind(allowed_models_mode)
|
||||
.bind(rate_limit)
|
||||
.bind(rate_limit_mode)
|
||||
.bind(now)
|
||||
.bind(now)
|
||||
.execute(&self.pool)
|
||||
@@ -1166,6 +1626,24 @@ fn optional_string_list_json(
|
||||
.transpose()
|
||||
}
|
||||
|
||||
fn json_string_from_option_vec(value: Option<&Vec<String>>) -> Option<String> {
|
||||
value.and_then(|items| serde_json::to_string(items).ok())
|
||||
}
|
||||
|
||||
fn normalized_ids(values: &[String]) -> Vec<String> {
|
||||
values
|
||||
.iter()
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
.collect::<std::collections::BTreeSet<_>>()
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn current_unix_secs() -> i64 {
|
||||
chrono::Utc::now().timestamp()
|
||||
}
|
||||
|
||||
fn optional_json_string(
|
||||
value: Option<serde_json::Value>,
|
||||
field_name: &str,
|
||||
@@ -1382,6 +1860,14 @@ fn map_user_export_row(row: &SqliteRow) -> Result<StoredUserExportRow, DataLayer
|
||||
)?,
|
||||
row.try_get("is_active").map_sql_err()?,
|
||||
)
|
||||
.and_then(|record| {
|
||||
record.with_policy_modes(
|
||||
row.try_get("allowed_providers_mode").map_sql_err()?,
|
||||
row.try_get("allowed_api_formats_mode").map_sql_err()?,
|
||||
row.try_get("allowed_models_mode").map_sql_err()?,
|
||||
row.try_get("rate_limit_mode").map_sql_err()?,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn map_user_auth_row(row: &SqliteRow) -> Result<StoredUserAuthRecord, DataLayerError> {
|
||||
@@ -1410,6 +1896,67 @@ fn map_user_auth_row(row: &SqliteRow) -> Result<StoredUserAuthRecord, DataLayerE
|
||||
optional_datetime_from_unix_secs(row.try_get("created_at").map_sql_err()?),
|
||||
optional_datetime_from_unix_secs(row.try_get("last_login_at").map_sql_err()?),
|
||||
)
|
||||
.and_then(|record| {
|
||||
record.with_policy_modes(
|
||||
row.try_get("allowed_providers_mode").map_sql_err()?,
|
||||
row.try_get("allowed_api_formats_mode").map_sql_err()?,
|
||||
row.try_get("allowed_models_mode").map_sql_err()?,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn map_user_group_row(row: &SqliteRow) -> Result<StoredUserGroup, DataLayerError> {
|
||||
StoredUserGroup::new(
|
||||
row.try_get("id").map_sql_err()?,
|
||||
row.try_get("name").map_sql_err()?,
|
||||
row.try_get("normalized_name").map_sql_err()?,
|
||||
row.try_get("description").map_sql_err()?,
|
||||
row.try_get("priority").map_sql_err()?,
|
||||
optional_json_from_string(
|
||||
row.try_get("allowed_providers").map_sql_err()?,
|
||||
"user_groups.allowed_providers",
|
||||
)?,
|
||||
row.try_get("allowed_providers_mode").map_sql_err()?,
|
||||
optional_json_from_string(
|
||||
row.try_get("allowed_api_formats").map_sql_err()?,
|
||||
"user_groups.allowed_api_formats",
|
||||
)?,
|
||||
row.try_get("allowed_api_formats_mode").map_sql_err()?,
|
||||
optional_json_from_string(
|
||||
row.try_get("allowed_models").map_sql_err()?,
|
||||
"user_groups.allowed_models",
|
||||
)?,
|
||||
row.try_get("allowed_models_mode").map_sql_err()?,
|
||||
row.try_get("rate_limit").map_sql_err()?,
|
||||
row.try_get("rate_limit_mode").map_sql_err()?,
|
||||
optional_datetime_from_unix_secs(row.try_get("created_at").map_sql_err()?),
|
||||
optional_datetime_from_unix_secs(row.try_get("updated_at").map_sql_err()?),
|
||||
)
|
||||
}
|
||||
|
||||
fn map_user_group_member_row(row: &SqliteRow) -> Result<StoredUserGroupMember, DataLayerError> {
|
||||
Ok(StoredUserGroupMember {
|
||||
group_id: row.try_get("group_id").map_sql_err()?,
|
||||
user_id: row.try_get("user_id").map_sql_err()?,
|
||||
username: row.try_get("username").map_sql_err()?,
|
||||
email: row.try_get("email").map_sql_err()?,
|
||||
role: row.try_get("role").map_sql_err()?,
|
||||
is_active: row.try_get("is_active").map_sql_err()?,
|
||||
is_deleted: row.try_get("is_deleted").map_sql_err()?,
|
||||
created_at: optional_datetime_from_unix_secs(row.try_get("created_at").map_sql_err()?),
|
||||
})
|
||||
}
|
||||
|
||||
fn map_user_group_membership_row(
|
||||
row: &SqliteRow,
|
||||
) -> Result<StoredUserGroupMembership, DataLayerError> {
|
||||
Ok(StoredUserGroupMembership {
|
||||
user_id: row.try_get("user_id").map_sql_err()?,
|
||||
group_id: row.try_get("group_id").map_sql_err()?,
|
||||
group_name: row.try_get("group_name").map_sql_err()?,
|
||||
group_priority: row.try_get("group_priority").map_sql_err()?,
|
||||
created_at: optional_datetime_from_unix_secs(row.try_get("created_at").map_sql_err()?),
|
||||
})
|
||||
}
|
||||
|
||||
fn map_oauth_link_summary_row(
|
||||
@@ -1560,6 +2107,7 @@ INSERT INTO users (
|
||||
role: Some("user".to_string()),
|
||||
is_active: Some(true),
|
||||
search: None,
|
||||
group_id: None,
|
||||
})
|
||||
.await
|
||||
.expect("export page should load");
|
||||
|
||||
Reference in New Issue
Block a user