Compare commits
3 Commits
v1.682.0
...
claude/iss
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d304a36ea | ||
|
|
e9b83341e9 | ||
|
|
f4820f1417 |
49
backend/.sqlx/query-b0ca6573362ecc8d31b9ff35780897c03f9508ee061a6a5f47c2a4259f039b5f.json
generated
Normal file
49
backend/.sqlx/query-b0ca6573362ecc8d31b9ff35780897c03f9508ee061a6a5f47c2a4259f039b5f.json
generated
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, changed_by, changed_at, change_type, member_affected\n FROM group_permission_history\n WHERE workspace_id = $1 AND group_name = $2\n ORDER BY changed_at DESC\n LIMIT $3 OFFSET $4",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "changed_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "changed_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "change_type",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "member_affected",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "b0ca6573362ecc8d31b9ff35780897c03f9508ee061a6a5f47c2a4259f039b5f"
|
||||
}
|
||||
18
backend/.sqlx/query-ba9f782bdd2349ff8ca87a3f6cd924c41f1ffa2fc05980023d7a99eadbf55a38.json
generated
Normal file
18
backend/.sqlx/query-ba9f782bdd2349ff8ca87a3f6cd924c41f1ffa2fc05980023d7a99eadbf55a38.json
generated
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO folder_permission_history\n (workspace_id, folder_name, changed_by, change_type, owner_affected)\n VALUES ($1, $2, $3, $4, $5)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ba9f782bdd2349ff8ca87a3f6cd924c41f1ffa2fc05980023d7a99eadbf55a38"
|
||||
}
|
||||
49
backend/.sqlx/query-d0e01f07a46823fc9d6d81f4911537e904edd7c8d466d1338f82ecace0fe8436.json
generated
Normal file
49
backend/.sqlx/query-d0e01f07a46823fc9d6d81f4911537e904edd7c8d466d1338f82ecace0fe8436.json
generated
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, changed_by, changed_at, change_type, owner_affected\n FROM folder_permission_history\n WHERE workspace_id = $1 AND folder_name = $2\n ORDER BY changed_at DESC\n LIMIT $3 OFFSET $4",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "changed_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "changed_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "change_type",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "owner_affected",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "d0e01f07a46823fc9d6d81f4911537e904edd7c8d466d1338f82ecace0fe8436"
|
||||
}
|
||||
18
backend/.sqlx/query-f95358255e55d68dd453d173e23ab3cb1f1c2ba5b1cfc78f706b0b014f045477.json
generated
Normal file
18
backend/.sqlx/query-f95358255e55d68dd453d173e23ab3cb1f1c2ba5b1cfc78f706b0b014f045477.json
generated
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO group_permission_history\n (workspace_id, group_name, changed_by, change_type, member_affected)\n VALUES ($1, $2, $3, $4, $5)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f95358255e55d68dd453d173e23ab3cb1f1c2ba5b1cfc78f706b0b014f045477"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Add down migration script here
|
||||
DROP INDEX IF EXISTS idx_group_perm_history_workspace_group;
|
||||
DROP TABLE IF EXISTS group_permission_history;
|
||||
|
||||
DROP INDEX IF EXISTS idx_folder_perm_history_workspace_folder;
|
||||
DROP TABLE IF EXISTS folder_permission_history;
|
||||
@@ -0,0 +1,31 @@
|
||||
-- Add up migration script here
|
||||
|
||||
-- Folder permission changes history
|
||||
CREATE TABLE IF NOT EXISTS folder_permission_history (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
workspace_id VARCHAR(50) NOT NULL,
|
||||
folder_name VARCHAR(255) NOT NULL,
|
||||
changed_by VARCHAR(50) NOT NULL,
|
||||
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
change_type VARCHAR(50) NOT NULL,
|
||||
owner_affected VARCHAR(100),
|
||||
FOREIGN KEY (workspace_id, folder_name) REFERENCES folder(workspace_id, name) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_folder_perm_history_workspace_folder
|
||||
ON folder_permission_history(workspace_id, folder_name, changed_at DESC);
|
||||
|
||||
-- Group permission changes history
|
||||
CREATE TABLE IF NOT EXISTS group_permission_history (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
workspace_id VARCHAR(50) NOT NULL,
|
||||
group_name VARCHAR(255) NOT NULL,
|
||||
changed_by VARCHAR(50) NOT NULL,
|
||||
changed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
change_type VARCHAR(50) NOT NULL,
|
||||
member_affected VARCHAR(100),
|
||||
FOREIGN KEY (workspace_id, group_name) REFERENCES group_(workspace_id, name) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_group_perm_history_workspace_group
|
||||
ON group_permission_history(workspace_id, group_name, changed_at DESC);
|
||||
@@ -11995,6 +11995,40 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/groups_history/get/{name}:
|
||||
get:
|
||||
summary: get group permission history
|
||||
operationId: getGroupPermissionHistory
|
||||
tags:
|
||||
- group
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Name"
|
||||
- $ref: "#/components/parameters/Page"
|
||||
- $ref: "#/components/parameters/PerPage"
|
||||
responses:
|
||||
"200":
|
||||
description: group permission history
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
changed_by:
|
||||
type: string
|
||||
changed_at:
|
||||
type: string
|
||||
format: date-time
|
||||
change_type:
|
||||
type: string
|
||||
member_affected:
|
||||
type: string
|
||||
nullable: true
|
||||
|
||||
/w/{workspace}/folders/list:
|
||||
get:
|
||||
summary: list folders
|
||||
@@ -12258,6 +12292,40 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/folders_history/get/{name}:
|
||||
get:
|
||||
summary: get folder permission history
|
||||
operationId: getFolderPermissionHistory
|
||||
tags:
|
||||
- folder
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Name"
|
||||
- $ref: "#/components/parameters/Page"
|
||||
- $ref: "#/components/parameters/PerPage"
|
||||
responses:
|
||||
"200":
|
||||
description: folder permission history
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
changed_by:
|
||||
type: string
|
||||
changed_at:
|
||||
type: string
|
||||
format: date-time
|
||||
change_type:
|
||||
type: string
|
||||
owner_affected:
|
||||
type: string
|
||||
nullable: true
|
||||
|
||||
/workers/list:
|
||||
get:
|
||||
summary: list workers
|
||||
|
||||
68
backend/windmill-api/src/folder_history.rs
Normal file
68
backend/windmill-api/src/folder_history.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Author: Ruben Fiszel
|
||||
* Copyright: Windmill Labs, Inc 2022
|
||||
* This file and its contents are licensed under the AGPLv3 License.
|
||||
* Please see the included NOTICE for copyright information and
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
use crate::db::ApiAuthed;
|
||||
use axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use windmill_common::{
|
||||
db::UserDB,
|
||||
error::JsonResult,
|
||||
utils::{paginate, Pagination},
|
||||
};
|
||||
|
||||
use serde::Serialize;
|
||||
use sqlx::FromRow;
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
Router::new().route("/get/:name", get(get_folder_permission_history))
|
||||
}
|
||||
|
||||
#[derive(Serialize, FromRow)]
|
||||
pub struct FolderPermissionChange {
|
||||
pub id: i64,
|
||||
pub changed_by: String,
|
||||
pub changed_at: chrono::DateTime<chrono::Utc>,
|
||||
pub change_type: String,
|
||||
pub owner_affected: Option<String>,
|
||||
}
|
||||
|
||||
async fn get_folder_permission_history(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, name)): Path<(String, String)>,
|
||||
Query(pagination): Query<Pagination>,
|
||||
) -> JsonResult<Vec<FolderPermissionChange>> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
// Check if user is owner of the folder
|
||||
crate::folders::require_is_owner(&authed, &name)?;
|
||||
|
||||
let (per_page, offset) = paginate(pagination);
|
||||
|
||||
let history = sqlx::query_as!(
|
||||
FolderPermissionChange,
|
||||
"SELECT id, changed_by, changed_at, change_type, owner_affected
|
||||
FROM folder_permission_history
|
||||
WHERE workspace_id = $1 AND folder_name = $2
|
||||
ORDER BY changed_at DESC
|
||||
LIMIT $3 OFFSET $4",
|
||||
w_id,
|
||||
name,
|
||||
per_page as i64,
|
||||
offset as i64
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(axum::Json(history))
|
||||
}
|
||||
@@ -329,6 +329,10 @@ async fn update_folder(
|
||||
|
||||
sqlb.set("edited_at", "now()");
|
||||
|
||||
// Track whether permission-related fields are being updated
|
||||
let owners_changed = ng.owners.is_some();
|
||||
let extra_perms_changed = ng.extra_perms.is_some();
|
||||
|
||||
if !authed.is_admin {
|
||||
let prefixed_username = format!("u/{}", authed.username);
|
||||
if ng.owners.as_ref().is_some_and(|x| {
|
||||
@@ -415,6 +419,21 @@ async fn update_folder(
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Log permission changes if owners or extra_perms were updated
|
||||
let should_log = owners_changed || extra_perms_changed;
|
||||
if should_log {
|
||||
log_folder_permission_change(
|
||||
&mut *tx,
|
||||
&w_id,
|
||||
&name,
|
||||
&authed.username,
|
||||
"update_permissions",
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
@@ -672,6 +691,17 @@ async fn add_owner(
|
||||
Some([("owner", owner.as_str())].into()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
log_folder_permission_change(
|
||||
&mut *tx,
|
||||
&w_id,
|
||||
&name,
|
||||
&authed.username,
|
||||
"add_owner",
|
||||
Some(&owner),
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
webhook.send_message(
|
||||
@@ -725,6 +755,17 @@ async fn remove_owner(
|
||||
Some([("owner", owner.as_str())].into()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
log_folder_permission_change(
|
||||
&mut *tx,
|
||||
&w_id,
|
||||
&name,
|
||||
&authed.username,
|
||||
"remove_owner",
|
||||
Some(&owner),
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
webhook.send_message(
|
||||
@@ -734,3 +775,26 @@ async fn remove_owner(
|
||||
|
||||
Ok(format!("Removed {} to folder {}", owner, name))
|
||||
}
|
||||
|
||||
pub async fn log_folder_permission_change<'c, E: sqlx::Executor<'c, Database = Postgres>>(
|
||||
db: E,
|
||||
workspace_id: &str,
|
||||
folder_name: &str,
|
||||
changed_by: &str,
|
||||
change_type: &str,
|
||||
owner_affected: Option<&str>,
|
||||
) -> Result<()> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO folder_permission_history
|
||||
(workspace_id, folder_name, changed_by, change_type, owner_affected)
|
||||
VALUES ($1, $2, $3, $4, $5)",
|
||||
workspace_id,
|
||||
folder_name,
|
||||
changed_by,
|
||||
change_type,
|
||||
owner_affected
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ async fn add_granular_acl(
|
||||
"UPDATE {kind} SET extra_perms = jsonb_set(extra_perms, $1, to_jsonb($2), \
|
||||
true) WHERE {identifier} = $3 AND workspace_id = $4 RETURNING extra_perms"
|
||||
))
|
||||
.bind(vec![owner])
|
||||
.bind(vec![owner.clone()])
|
||||
.bind(write.unwrap_or(false))
|
||||
.bind(path)
|
||||
.bind(&w_id)
|
||||
@@ -124,6 +124,30 @@ async fn add_granular_acl(
|
||||
.await?;
|
||||
|
||||
let _ = not_found_if_none(obj_o, &kind, &path)?;
|
||||
|
||||
// Log permission changes for folders and groups
|
||||
if kind == "folder" {
|
||||
crate::folders::log_folder_permission_change(
|
||||
&mut *tx,
|
||||
&w_id,
|
||||
path,
|
||||
&authed.username,
|
||||
"update_extra_perms",
|
||||
Some(&owner),
|
||||
)
|
||||
.await?;
|
||||
} else if kind == "group_" {
|
||||
crate::groups::log_group_permission_change(
|
||||
&mut *tx,
|
||||
&w_id,
|
||||
path,
|
||||
&authed.username,
|
||||
"update_extra_perms",
|
||||
Some(&owner),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
match kind {
|
||||
@@ -229,13 +253,37 @@ async fn remove_granular_acl(
|
||||
"UPDATE {kind} SET extra_perms = extra_perms - $1 WHERE {identifier} = $2 AND \
|
||||
workspace_id = $3 RETURNING extra_perms"
|
||||
))
|
||||
.bind(owner)
|
||||
.bind(&owner)
|
||||
.bind(path)
|
||||
.bind(&w_id)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let _ = not_found_if_none(obj_o, &kind, &path)?;
|
||||
|
||||
// Log permission changes for folders and groups
|
||||
if kind == "folder" {
|
||||
crate::folders::log_folder_permission_change(
|
||||
&mut *tx,
|
||||
&w_id,
|
||||
path,
|
||||
&authed.username,
|
||||
"remove_extra_perms",
|
||||
Some(&owner),
|
||||
)
|
||||
.await?;
|
||||
} else if kind == "group_" {
|
||||
crate::groups::log_group_permission_change(
|
||||
&mut *tx,
|
||||
&w_id,
|
||||
path,
|
||||
&authed.username,
|
||||
"remove_extra_perms",
|
||||
Some(&owner),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
match kind {
|
||||
|
||||
73
backend/windmill-api/src/group_history.rs
Normal file
73
backend/windmill-api/src/group_history.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Author: Ruben Fiszel
|
||||
* Copyright: Windmill Labs, Inc 2022
|
||||
* This file and its contents are licensed under the AGPLv3 License.
|
||||
* Please see the included NOTICE for copyright information and
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
use crate::db::{ApiAuthed, DB};
|
||||
use axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use windmill_common::{
|
||||
db::UserDB,
|
||||
error::{Error, JsonResult},
|
||||
utils::{paginate, Pagination},
|
||||
};
|
||||
|
||||
use serde::Serialize;
|
||||
use sqlx::FromRow;
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
Router::new().route("/get/:name", get(get_group_permission_history))
|
||||
}
|
||||
|
||||
#[derive(Serialize, FromRow)]
|
||||
pub struct GroupPermissionChange {
|
||||
pub id: i64,
|
||||
pub changed_by: String,
|
||||
pub changed_at: chrono::DateTime<chrono::Utc>,
|
||||
pub change_type: String,
|
||||
pub member_affected: Option<String>,
|
||||
}
|
||||
|
||||
async fn get_group_permission_history(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, name)): Path<(String, String)>,
|
||||
Query(pagination): Query<Pagination>,
|
||||
) -> JsonResult<Vec<GroupPermissionChange>> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
// Only workspace admins can view group permission history
|
||||
if !authed.is_admin {
|
||||
return Err(Error::NotAuthorized(
|
||||
"Only workspace administrators can view group permission history".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let (per_page, offset) = paginate(pagination);
|
||||
|
||||
let history = sqlx::query_as!(
|
||||
GroupPermissionChange,
|
||||
"SELECT id, changed_by, changed_at, change_type, member_affected
|
||||
FROM group_permission_history
|
||||
WHERE workspace_id = $1 AND group_name = $2
|
||||
ORDER BY changed_at DESC
|
||||
LIMIT $3 OFFSET $4",
|
||||
w_id,
|
||||
name,
|
||||
per_page as i64,
|
||||
offset as i64
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(axum::Json(history))
|
||||
}
|
||||
@@ -534,6 +534,17 @@ async fn update_group(
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
log_group_permission_change(
|
||||
&mut *tx,
|
||||
&w_id,
|
||||
&name,
|
||||
&authed.username,
|
||||
"update_summary",
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
@@ -583,6 +594,17 @@ async fn add_user(
|
||||
Some([("user", user_username.as_str())].into()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
log_group_permission_change(
|
||||
&mut *tx,
|
||||
&w_id,
|
||||
&name,
|
||||
&authed.username,
|
||||
"add_member",
|
||||
Some(&user_username),
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
@@ -843,6 +865,16 @@ async fn remove_user(
|
||||
)
|
||||
.await?;
|
||||
|
||||
log_group_permission_change(
|
||||
&mut *tx,
|
||||
&w_id,
|
||||
&name,
|
||||
&authed.username,
|
||||
"remove_member",
|
||||
Some(&user_username),
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
@@ -973,3 +1005,26 @@ async fn overwrite_igroups() -> JsonResult<String> {
|
||||
"This feature is only available in the enterprise version".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn log_group_permission_change<'c, E: sqlx::Executor<'c, Database = Postgres>>(
|
||||
db: E,
|
||||
workspace_id: &str,
|
||||
group_name: &str,
|
||||
changed_by: &str,
|
||||
change_type: &str,
|
||||
member_affected: Option<&str>,
|
||||
) -> Result<()> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO group_permission_history
|
||||
(workspace_id, group_name, changed_by, change_type, member_affected)
|
||||
VALUES ($1, $2, $3, $4, $5)",
|
||||
workspace_id,
|
||||
group_name,
|
||||
changed_by,
|
||||
change_type,
|
||||
member_affected
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -90,8 +90,10 @@ mod favorite;
|
||||
mod flow_conversations;
|
||||
pub mod flows;
|
||||
mod folders;
|
||||
mod folder_history;
|
||||
mod granular_acls;
|
||||
mod groups;
|
||||
mod group_history;
|
||||
#[cfg(feature = "private")]
|
||||
pub mod indexer_ee;
|
||||
mod indexer_oss;
|
||||
@@ -450,7 +452,9 @@ pub async fn run_server(
|
||||
flow_conversations::workspaced_service(),
|
||||
)
|
||||
.nest("/folders", folders::workspaced_service())
|
||||
.nest("/folders_history", folder_history::workspaced_service())
|
||||
.nest("/groups", groups::workspaced_service())
|
||||
.nest("/groups_history", group_history::workspaced_service())
|
||||
.nest("/inputs", inputs::workspaced_service())
|
||||
.nest("/job_metrics", job_metrics::workspaced_service())
|
||||
.nest("/job_helpers", job_helpers_service)
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import Select from './select/Select.svelte'
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
import TextInput from './text_input/TextInput.svelte'
|
||||
import PermissionHistory from './PermissionHistory.svelte'
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
@@ -441,4 +442,19 @@
|
||||
{/if}
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
{#if can_write}
|
||||
<PermissionHistory
|
||||
{name}
|
||||
kind="folder"
|
||||
fetchHistory={async (workspace, folderName, page, perPage) => {
|
||||
return await FolderService.getFolderPermissionHistory({
|
||||
workspace,
|
||||
name: folderName,
|
||||
page,
|
||||
perPage
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
import TextInput from './text_input/TextInput.svelte'
|
||||
import { Trash } from 'lucide-svelte'
|
||||
import PermissionHistory from './PermissionHistory.svelte'
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
@@ -311,4 +312,19 @@
|
||||
</div>
|
||||
{/if}
|
||||
</Label>
|
||||
|
||||
{#if $userStore?.is_admin}
|
||||
<PermissionHistory
|
||||
{name}
|
||||
kind="group"
|
||||
fetchHistory={async (workspace, groupName, page, perPage) => {
|
||||
return await GroupService.getGroupPermissionHistory({
|
||||
workspace,
|
||||
name: groupName,
|
||||
page,
|
||||
perPage
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
99
frontend/src/lib/components/PermissionHistory.svelte
Normal file
99
frontend/src/lib/components/PermissionHistory.svelte
Normal file
@@ -0,0 +1,99 @@
|
||||
<script lang="ts">
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { untrack } from 'svelte'
|
||||
import TableCustom from './TableCustom.svelte'
|
||||
import Skeleton from './common/skeleton/Skeleton.svelte'
|
||||
import Label from './Label.svelte'
|
||||
|
||||
interface PermissionChange {
|
||||
id?: number
|
||||
changed_by?: string
|
||||
changed_at?: string
|
||||
change_type?: string
|
||||
owner_affected?: string | null
|
||||
member_affected?: string | null
|
||||
}
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
kind: 'folder' | 'group'
|
||||
fetchHistory: (
|
||||
workspace: string,
|
||||
name: string,
|
||||
page: number,
|
||||
perPage: number
|
||||
) => Promise<PermissionChange[]>
|
||||
}
|
||||
|
||||
let { name, kind, fetchHistory }: Props = $props()
|
||||
let history: PermissionChange[] | undefined = $state(undefined)
|
||||
let loading = $state(false)
|
||||
let page = $state(1)
|
||||
let perPage = $state(10)
|
||||
|
||||
async function loadHistory() {
|
||||
if (!$workspaceStore) return
|
||||
loading = true
|
||||
try {
|
||||
history = await fetchHistory($workspaceStore, name, page, perPage)
|
||||
} catch (e) {
|
||||
console.error('Failed to load permission history:', e)
|
||||
history = []
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
function formatChangeType(changeType: string): string {
|
||||
return changeType
|
||||
.split('_')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
$effect.pre(() => {
|
||||
if ($workspaceStore && name) {
|
||||
untrack(() => {
|
||||
loadHistory()
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<Label label="Permission History">
|
||||
{#if loading || history === undefined}
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each new Array(3) as _}
|
||||
<Skeleton layout={[[4], 0.7]} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else if history.length === 0}
|
||||
<p class="text-primary text-sm">No permission changes recorded yet</p>
|
||||
{:else}
|
||||
<TableCustom>
|
||||
<tr slot="header-row">
|
||||
<th>Changed By</th>
|
||||
<th>Change Type</th>
|
||||
<th>{kind === 'folder' ? 'Owner Affected' : 'Member Affected'}</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
{#snippet body()}
|
||||
<tbody>
|
||||
{#each history as change}
|
||||
<tr>
|
||||
<td>{change.changed_by ?? '-'}</td>
|
||||
<td>{change.change_type ? formatChangeType(change.change_type) : '-'}</td>
|
||||
<td>{change.owner_affected ?? change.member_affected ?? '-'}</td>
|
||||
<td class="text-xs">{change.changed_at ? formatDate(change.changed_at) : '-'}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
{/snippet}
|
||||
</TableCustom>
|
||||
{/if}
|
||||
</Label>
|
||||
Reference in New Issue
Block a user