Skip to content

Conversation

@ByteBaker
Copy link
Contributor

@ByteBaker ByteBaker commented Sep 1, 2025

User description

So far the OpenAPI descriptions for endpoint handlers was directly derived from the doc comments, leaving little control. This also leaked any dev-specific information into API spec.

Now the summary and description are explicitly populated for all the endpoints, detaching them from doc comments.

Also update response body annotations wherever necessary

Also bump

  • utoipa: 4.0.0 -> 5.0.0
  • utoipa-swagger-ui: 4.0.0 -> 9.0.0

PR Type

Documentation, Enhancement


Description

  • Add explicit OpenAPI summaries/descriptions

  • Correct response body schemas to Object/()

  • Update params/types for accuracy

  • Improve endpoint examples and metadata


Diagram Walkthrough

flowchart LR
  alerts["Alerts endpoints"] -- "summaries + schemas" --> openapi["OpenAPI docs"]
  roles_groups["Roles/Groups/Users/Resources"] -- "summaries + schemas" --> openapi
  metrics_prom["Metrics/PromQL"] -- "summaries + schemas" --> openapi
  org["Organizations"] -- "summaries + schemas" --> openapi
  search["Search APIs"] -- "summaries + schemas" --> openapi
  logs["Logs ingestion"] -- "summaries + schemas" --> openapi
  dashboards["Dashboards & Reports"] -- "summaries + schemas" --> openapi
  folders["Folders"] -- "summaries + schemas" --> openapi
  ai["AI Prompts"] -- "schemas for errors" --> openapi
  clusters["Clusters"] -- "add description" --> openapi
Loading

File Walkthrough

Relevant files
Documentation
12 files
fga.rs
Add summaries and fix response schemas for RBAC endpoints
+197/-38
mod.rs
Document Prometheus APIs and correct response bodies         
+36/-18 
org.rs
Enhance organization endpoints docs and schemas                   
+43/-11 
mod.rs
Add alert API summaries; fix IDs and bodies                           
+41/-23 
deprecated.rs
Mark deprecated alert APIs with summaries and schema fixes
+46/-17 
folders.rs
Add folder API docs; adjust response body types                   
+32/-18 
mod.rs
Improve search API docs; use Object and () schemas             
+31/-23 
ingest.rs
Add ingestion endpoint summaries; fix response schemas     
+33/-9   
reports.rs
Document reports APIs; switch responses to ()/Object         
+49/-15 
mod.rs
Add dashboard API summaries and correct responses               
+29/-13 
prompt.rs
Use Object for error responses; import Object                       
+11/-10 
mod.rs
Add clusters listing endpoint description                               
+4/-0     
Additional files
51 files
Cargo.toml +2/-2     
organization.rs +5/-3     
action.rs +16/-2   
alert.rs +1/-0     
mod.rs +3/-2     
cluster.rs +5/-4     
mod.rs +14/-4   
mod.rs +18/-5   
mod.rs +19/-5   
mod.rs +27/-7   
destinations.rs +3/-2     
folder.rs +3/-2     
components.rs +9/-8     
mod.rs +1/-1     
search.rs +1/-1     
stream.rs +7/-3     
mod.rs +2/-1     
mod.rs +3/-1     
requests.rs +1/-0     
responses.rs +1/-0     
reports.rs +1/-0     
action.rs +51/-20 
operations.rs +6/-6     
chat.rs +6/-5     
destinations.rs +32/-10 
templates.rs +34/-10 
billings.rs +20/-19 
marketing.rs +3/-2     
org_usage.rs +2/-2     
mod.rs +16/-6   
mod.rs +2/-2     
mod.rs +41/-11 
mod.rs +33/-4   
mod.rs +16/-0   
loki.rs +5/-0     
ingest.rs +12/-4   
settings.rs +12/-4   
pipeline.rs +23/-11 
mod.rs +16/-8   
ingest.rs +21/-6   
multi_streams.rs +12/-10 
saved_view.rs +17/-11 
search_inspector.rs +8/-3     
search_job.rs +39/-19 
search_stream.rs +4/-2     
mod.rs +23/-6   
mod.rs +5/-1     
mod.rs +10/-0   
mod.rs +39/-21 
mod.rs +9/-5     
mod.rs +46/-8   

@github-actions github-actions bot added the ☢️ Bug Something isn't working label Sep 1, 2025
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Greptile Summary

This PR significantly improves OpenAPI documentation quality by upgrading utoipa from v4.0.0 to v5.0.0 and utoipa-swagger-ui from v4.0.0 to v9.0.0, while systematically adding explicit summary and description fields to all HTTP endpoint handlers. The changes address a fundamental issue where OpenAPI descriptions were previously auto-generated from Rust doc comments, which could leak development-specific information into the public API specification and provided limited control over documentation quality.

The PR touches over 60 files across the entire HTTP API surface, adding comprehensive endpoint descriptions that explain functionality, use cases, parameters, and business logic in user-friendly terms. Response body annotations have been standardized, replacing specific types like HttpResponse with more appropriate generic types (Object for JSON responses, () for empty responses) to improve OpenAPI schema accuracy.

Additionally, the PR adds ToSchema derive macros and schema annotations (#[schema(value_type = ...)]) to numerous data structures throughout the codebase. These annotations ensure that complex internal types like Ksuid, OrdF64, and DateTime are properly represented as simple strings and numbers in the generated API specification, rather than exposing internal Rust type complexity to API consumers.

The changes maintain complete backward compatibility in terms of actual API behavior while dramatically improving the quality and professionalism of the generated OpenAPI documentation. This represents a comprehensive effort to create clean, controlled API documentation that's suitable for external consumption and tooling integration.

Confidence score: 3/5

  • This PR requires careful review due to potential runtime issues and logical errors in several files
  • Score reflects concerns about duplicate code, type mismatches, and unsafe operations that could cause production problems
  • Pay close attention to organization/settings.rs, clusters/mod.rs, metrics/ingest.rs, actions/action.rs, and folders.rs for critical issues

61 files reviewed, 14 comments

Edit Code Review Bot Settings | Greptile

("name" = String, Path, description = "Function name"),
),
responses(
(status = 200, description = "Success", content_type = "application/json", body = FunctionList),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Response body type mismatch: the endpoint returns pipeline dependencies but body type is FunctionList, which contains Transform objects rather than pipeline dependency information

#[utoipa::path(
context_path = "/api",
tag = "Roles",
operation_id = "GetRoLesUsers",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syntax: Typo in operation_id: "GetRoLesUsers" should be "GetRoleUsers"

Suggested change
operation_id = "GetRoLesUsers",
operation_id = "GetRoleUsers",

("org_id" = String, Path, description = "Organization name")
),
responses(
(status = StatusCode::OK, description = "Prompt rolled back to default", body = PromptResponse),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Response body type inconsistent: returns PromptResponse but endpoint returns NoContent on success (line 206)

Suggested change
(status = StatusCode::OK, description = "Prompt rolled back to default", body = PromptResponse),
(status = StatusCode::NO_CONTENT, description = "Prompt rolled back to default"),

(status = 200, description = "Success", content_type = "application/json", body =
HttpResponse), (status = 400, description = "Error", content_type = "application/json",
body = HttpResponse), )
request_body(content = Template, description = "Template data", content_type = "application/json"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: GET endpoints should not specify request_body. Remove this line.

Suggested change
request_body(content = Template, description = "Template data", content_type = "application/json"),
responses(

("org_id" = String, Path, description = "Organization name (must be meta org)"),
),
responses(
(status = 200, description = "Success", content_type = "application/json", body = DomainManagementResponse),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Response body type references 'DomainManagementResponse' but this type isn't imported or visible in this file

),
request_body(content = DomainManagementRequest, description = "Domain management configuration", content_type = "application/json"),
responses(
(status = 200, description = "Success", content_type = "application/json", body = DomainOperationResponse),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Response body type references 'DomainOperationResponse' but this type isn't imported or visible in this file

@github-actions
Copy link
Contributor

github-actions bot commented Sep 1, 2025

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Inconsistent Operation IDs

Multiple endpoints use duplicate or inconsistent operation_ids (e.g., both create and list roles use CreateRoles/ListRoles in multiple cfg variants, and GetRoLesUsers appears with inconsistent casing). This may generate conflicting or confusing OpenAPI paths/operations. Verify uniqueness and consistent naming across cfg(feature) variants.

    context_path = "/api",
    tag = "Roles",
    operation_id = "CreateRoles",
    summary = "Create custom role",
    description = "Creates a new custom role with specified permissions and capabilities. This endpoint is only available with enterprise features enabled and will return a forbidden error in the community edition.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
    ),
    request_body(content = UserRoleRequest, description = "UserRoleRequest", content_type = "application/json"),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Object),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[post("/{org_id}/roles")]
pub async fn create_role(
    _org_id: web::Path<String>,
    _role_id: web::Json<UserRoleRequest>,
) -> Result<HttpResponse, Error> {
    Ok(MetaHttpResponse::forbidden("Not Supported"))
}

#[cfg(feature = "enterprise")]
/// DeleteRole
///
/// #{"ratelimit_module":"Roles", "ratelimit_module_operation":"delete"}#
#[utoipa::path(
    context_path = "/api",
    tag = "Roles",
    operation_id = "DeleteRole",
    summary = "Delete custom role",
    description = "Permanently removes a custom role from the organization. Users and groups assigned to this role will lose the associated permissions. Standard predefined roles cannot be deleted. Requires enterprise features to be enabled.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
        ("role_id" = String, Path, description = "Role Id"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Object),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[delete("/{org_id}/roles/{role_id}")]
pub async fn delete_role(path: web::Path<(String, String)>) -> Result<HttpResponse, Error> {
    let (org_id, role_name) = path.into_inner();

    match o2_openfga::authorizer::roles::delete_role(&org_id, &role_name).await {
        Ok(_) => Ok(MetaHttpResponse::ok(
            serde_json::json!({"successful": "true"}),
        )),
        Err(err) => Ok(MetaHttpResponse::internal_error(err)),
    }
}

#[cfg(not(feature = "enterprise"))]
#[utoipa::path(
    context_path = "/api",
    tag = "Roles",
    operation_id = "DeleteRole",
    summary = "Delete custom role",
    description = "Permanently removes a custom role from the organization. This endpoint is only available with enterprise features enabled and will return a forbidden error in the community edition.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
        ("role_id" = String, Path, description = "Role Id"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Object),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[delete("/{org_id}/roles/{role_id}")]
pub async fn delete_role(_path: web::Path<(String, String)>) -> Result<HttpResponse, Error> {
    Ok(MetaHttpResponse::forbidden("Not Supported"))
}

#[cfg(feature = "enterprise")]
/// ListRoles
///
/// #{"ratelimit_module":"Roles", "ratelimit_module_operation":"list"}#
#[utoipa::path(
    context_path = "/api",
    tag = "Roles",
    operation_id = "ListRoles",
    summary = "List organization roles",
    description = "Retrieves a list of all roles available in the organization, including both standard predefined roles and custom roles. Users will only see roles they have permissions to view when role-based access control is active. Requires enterprise features to be enabled.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Vec<String>),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[get("/{org_id}/roles")]
pub async fn get_roles(org_id: web::Path<String>, req: HttpRequest) -> Result<HttpResponse, Error> {
    let org_id = org_id.into_inner();
    let mut permitted;
    // Get List of allowed objects

    let user_id = req.headers().get("user_id").unwrap();
    match crate::handler::http::auth::validator::list_objects_for_user(
        &org_id,
        user_id.to_str().unwrap(),
        "GET",
        "role",
    )
    .await
    {
        Ok(list) => {
            permitted = list;
        }
        Err(e) => {
            return Ok(MetaHttpResponse::forbidden(e.to_string()));
        }
    }
    // Get List of allowed objects ends

    if let Some(mut local_permitted) = permitted {
        let prefix = "role:";
        for value in local_permitted.iter_mut() {
            if value.starts_with(prefix) {
                *value = value.strip_prefix(prefix).unwrap().to_string();
            }
            let role_prefix = format!("{org_id}/");
            if value.starts_with(&role_prefix) {
                *value = value.strip_prefix(&role_prefix).unwrap().to_string()
            }
        }
        permitted = Some(local_permitted);
    }

    match o2_openfga::authorizer::roles::get_all_roles(&org_id, permitted).await {
        Ok(res) => Ok(HttpResponse::Ok().json(res)),
        Err(err) => Ok(MetaHttpResponse::internal_error(err)),
    }
}

#[cfg(not(feature = "enterprise"))]
#[utoipa::path(
    context_path = "/api",
    tag = "Roles",
    operation_id = "ListRoles",
    summary = "List organization roles",
    description = "Retrieves a list of all roles available in the organization. This endpoint is only available with enterprise features enabled and will return a forbidden error in the community edition.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Vec<String>),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[get("/{org_id}/roles")]
pub async fn get_roles(
    _org_id: web::Path<String>,
    _req: HttpRequest,
) -> Result<HttpResponse, Error> {
    Ok(MetaHttpResponse::forbidden("Not Supported"))
}

#[cfg(feature = "enterprise")]
/// UpdateRoles
///
/// #{"ratelimit_module":"Roles", "ratelimit_module_operation":"update"}#
#[utoipa::path(
    context_path = "/api",
    tag = "Roles",
    operation_id = "UpdateRoles",
    summary = "Update role permissions",
    description = "Updates an existing role by adding or removing permissions and users. Allows modification of role capabilities and user assignments to maintain proper access control. Standard roles cannot be modified. Requires enterprise features to be enabled.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
        ("role_id" = String, Path, description = "Role Id"),
    ),
    request_body(content = Object, description = "RoleRequest", content_type = "application/json"),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Object),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[put("/{org_id}/roles/{role_id}")]
pub async fn update_role(
    path: web::Path<(String, String)>,
    update_role: web::Json<RoleRequest>,
) -> Result<HttpResponse, Error> {
    let (org_id, role_id) = path.into_inner();
    let update_role = update_role.into_inner();

    match o2_openfga::authorizer::roles::update_role(
        &org_id,
        &role_id,
        update_role.add,
        update_role.remove,
        update_role.add_users,
        update_role.remove_users,
    )
    .await
    {
        Ok(_) => Ok(MetaHttpResponse::ok("Role updated successfully")),
        Err(err) => Ok(MetaHttpResponse::internal_error(err)),
    }
}

#[cfg(not(feature = "enterprise"))]
#[utoipa::path(
    context_path = "/api",
    tag = "Roles",
    operation_id = "UpdateRoles",
    summary = "Update role permissions",
    description = "Updates an existing role by adding or removing permissions and users. This endpoint is only available with enterprise features enabled and will return a forbidden error in the community edition.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
        ("role_id" = String, Path, description = "Role Id"),
    ),
    request_body(content = Object, description = "RoleRequest", content_type = "application/json"),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Object),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[post("/{org_id}/roles/{role_id}")]
pub async fn update_role(
    _path: web::Path<(String, String)>,
    _permissions: web::Json<String>,
) -> Result<HttpResponse, actix_web::Error> {
    Ok(MetaHttpResponse::forbidden("Not Supported"))
}

#[cfg(feature = "enterprise")]
/// GetResourcePermission
///
/// #{"ratelimit_module":"Roles", "ratelimit_module_operation":"get"}#
#[utoipa::path(
    context_path = "/api",
    tag = "Roles",
    operation_id = "GetResourcePermission",
    summary = "Get role permissions for resource",
    description = "Retrieves detailed permissions that a specific role has on a particular resource type. Useful for understanding access control capabilities and auditing role assignments. Requires enterprise features to be enabled.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
        ("role_id" = String, Path, description = "Role Id"),
        ("resource" = String, Path, description = "resource"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Vec<Object>),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[get("/{org_id}/roles/{role_id}/permissions/{resource}")]
pub async fn get_role_permissions(
    path: web::Path<(String, String, String)>,
) -> Result<HttpResponse, Error> {
    let (org_id, role_id, resource) = path.into_inner();
    match o2_openfga::authorizer::roles::get_role_permissions(&org_id, &role_id, &resource).await {
        Ok(res) => Ok(HttpResponse::Ok().json(res)),
        Err(err) => Ok(MetaHttpResponse::internal_error(err)),
    }
}

#[cfg(not(feature = "enterprise"))]
#[utoipa::path(
    context_path = "/api",
    tag = "Roles",
    operation_id = "GetResourcePermission",
    summary = "Get role permissions for resource",
    description = "Retrieves detailed permissions that a specific role has on a particular resource type. This endpoint is only available with enterprise features enabled and will return a forbidden error in the community edition.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
        ("role_id" = String, Path, description = "Role Id"),
        ("resource" = String, Path, description = "resource"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Vec<Object>),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[get("/{org_id}/roles/{role_id}/permissions/{resource}")]
pub async fn get_role_permissions(
    _path: web::Path<(String, String, String)>,
) -> Result<HttpResponse, Error> {
    Ok(MetaHttpResponse::forbidden("Not Supported"))
}

#[cfg(feature = "enterprise")]
/// GetRoleUsers
///
/// #{"ratelimit_module":"Roles", "ratelimit_module_operation":"list"}#
#[utoipa::path(
    context_path = "/api",
    tag = "Roles",
    operation_id = "GetRoleUsers",
    summary = "Get users assigned to role",
    description = "Retrieves a list of all users who are currently assigned to a specific role. Useful for role management, auditing user permissions, and understanding access control assignments. Requires enterprise features to be enabled.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
        ("role_id" = String, Path, description = "Role Id"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Vec<String>),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[get("/{org_id}/roles/{role_id}/users")]
pub async fn get_users_with_role(path: web::Path<(String, String)>) -> Result<HttpResponse, Error> {
    let (org_id, role_id) = path.into_inner();
    match o2_openfga::authorizer::roles::get_users_with_role(&org_id, &role_id).await {
        Ok(res) => Ok(HttpResponse::Ok().json(res)),
        Err(err) => Ok(MetaHttpResponse::internal_error(err)),
    }
}

#[cfg(not(feature = "enterprise"))]
#[utoipa::path(
    context_path = "/api",
    tag = "Roles",
    operation_id = "GetRoLesUsers",
    summary = "Get users assigned to role",
    description = "Retrieves a list of all users who are currently assigned to a specific role. This endpoint is only available with enterprise features enabled and will return a forbidden error in the community edition.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
        ("role_id" = String, Path, description = "Role Id"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Vec<String>),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
Schema Downgrade Risk

Many responses changed from concrete types (e.g., HttpResponse, O2EntityAuthorization, IngestionResponse, RoleRequest) to Object or (). Ensure these reflect actual runtime response bodies; otherwise, clients lose strong typing and accurate schemas in generated SDKs.

    ),
    request_body(content = UserRoleRequest, description = "UserRoleRequest", content_type = "application/json"),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Object),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[post("/{org_id}/roles")]
pub async fn create_role(
    _org_id: web::Path<String>,
    _role_id: web::Json<UserRoleRequest>,
) -> Result<HttpResponse, Error> {
    Ok(MetaHttpResponse::forbidden("Not Supported"))
}

#[cfg(feature = "enterprise")]
/// DeleteRole
///
/// #{"ratelimit_module":"Roles", "ratelimit_module_operation":"delete"}#
#[utoipa::path(
    context_path = "/api",
    tag = "Roles",
    operation_id = "DeleteRole",
    summary = "Delete custom role",
    description = "Permanently removes a custom role from the organization. Users and groups assigned to this role will lose the associated permissions. Standard predefined roles cannot be deleted. Requires enterprise features to be enabled.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
        ("role_id" = String, Path, description = "Role Id"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Object),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[delete("/{org_id}/roles/{role_id}")]
pub async fn delete_role(path: web::Path<(String, String)>) -> Result<HttpResponse, Error> {
    let (org_id, role_name) = path.into_inner();

    match o2_openfga::authorizer::roles::delete_role(&org_id, &role_name).await {
        Ok(_) => Ok(MetaHttpResponse::ok(
            serde_json::json!({"successful": "true"}),
        )),
        Err(err) => Ok(MetaHttpResponse::internal_error(err)),
    }
}

#[cfg(not(feature = "enterprise"))]
#[utoipa::path(
    context_path = "/api",
    tag = "Roles",
    operation_id = "DeleteRole",
    summary = "Delete custom role",
    description = "Permanently removes a custom role from the organization. This endpoint is only available with enterprise features enabled and will return a forbidden error in the community edition.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
        ("role_id" = String, Path, description = "Role Id"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Object),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[delete("/{org_id}/roles/{role_id}")]
pub async fn delete_role(_path: web::Path<(String, String)>) -> Result<HttpResponse, Error> {
    Ok(MetaHttpResponse::forbidden("Not Supported"))
}

#[cfg(feature = "enterprise")]
/// ListRoles
///
/// #{"ratelimit_module":"Roles", "ratelimit_module_operation":"list"}#
#[utoipa::path(
    context_path = "/api",
    tag = "Roles",
    operation_id = "ListRoles",
    summary = "List organization roles",
    description = "Retrieves a list of all roles available in the organization, including both standard predefined roles and custom roles. Users will only see roles they have permissions to view when role-based access control is active. Requires enterprise features to be enabled.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Vec<String>),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[get("/{org_id}/roles")]
pub async fn get_roles(org_id: web::Path<String>, req: HttpRequest) -> Result<HttpResponse, Error> {
    let org_id = org_id.into_inner();
    let mut permitted;
    // Get List of allowed objects

    let user_id = req.headers().get("user_id").unwrap();
    match crate::handler::http::auth::validator::list_objects_for_user(
        &org_id,
        user_id.to_str().unwrap(),
        "GET",
        "role",
    )
    .await
    {
        Ok(list) => {
            permitted = list;
        }
        Err(e) => {
            return Ok(MetaHttpResponse::forbidden(e.to_string()));
        }
    }
    // Get List of allowed objects ends

    if let Some(mut local_permitted) = permitted {
        let prefix = "role:";
        for value in local_permitted.iter_mut() {
            if value.starts_with(prefix) {
                *value = value.strip_prefix(prefix).unwrap().to_string();
            }
            let role_prefix = format!("{org_id}/");
            if value.starts_with(&role_prefix) {
                *value = value.strip_prefix(&role_prefix).unwrap().to_string()
            }
        }
        permitted = Some(local_permitted);
    }

    match o2_openfga::authorizer::roles::get_all_roles(&org_id, permitted).await {
        Ok(res) => Ok(HttpResponse::Ok().json(res)),
        Err(err) => Ok(MetaHttpResponse::internal_error(err)),
    }
}

#[cfg(not(feature = "enterprise"))]
#[utoipa::path(
    context_path = "/api",
    tag = "Roles",
    operation_id = "ListRoles",
    summary = "List organization roles",
    description = "Retrieves a list of all roles available in the organization. This endpoint is only available with enterprise features enabled and will return a forbidden error in the community edition.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Vec<String>),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[get("/{org_id}/roles")]
pub async fn get_roles(
    _org_id: web::Path<String>,
    _req: HttpRequest,
) -> Result<HttpResponse, Error> {
    Ok(MetaHttpResponse::forbidden("Not Supported"))
}

#[cfg(feature = "enterprise")]
/// UpdateRoles
///
/// #{"ratelimit_module":"Roles", "ratelimit_module_operation":"update"}#
#[utoipa::path(
    context_path = "/api",
    tag = "Roles",
    operation_id = "UpdateRoles",
    summary = "Update role permissions",
    description = "Updates an existing role by adding or removing permissions and users. Allows modification of role capabilities and user assignments to maintain proper access control. Standard roles cannot be modified. Requires enterprise features to be enabled.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
        ("role_id" = String, Path, description = "Role Id"),
    ),
    request_body(content = Object, description = "RoleRequest", content_type = "application/json"),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Object),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[put("/{org_id}/roles/{role_id}")]
pub async fn update_role(
    path: web::Path<(String, String)>,
    update_role: web::Json<RoleRequest>,
) -> Result<HttpResponse, Error> {
    let (org_id, role_id) = path.into_inner();
    let update_role = update_role.into_inner();

    match o2_openfga::authorizer::roles::update_role(
        &org_id,
        &role_id,
        update_role.add,
        update_role.remove,
        update_role.add_users,
        update_role.remove_users,
    )
    .await
    {
        Ok(_) => Ok(MetaHttpResponse::ok("Role updated successfully")),
        Err(err) => Ok(MetaHttpResponse::internal_error(err)),
    }
}

#[cfg(not(feature = "enterprise"))]
#[utoipa::path(
    context_path = "/api",
    tag = "Roles",
    operation_id = "UpdateRoles",
    summary = "Update role permissions",
    description = "Updates an existing role by adding or removing permissions and users. This endpoint is only available with enterprise features enabled and will return a forbidden error in the community edition.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
        ("role_id" = String, Path, description = "Role Id"),
    ),
    request_body(content = Object, description = "RoleRequest", content_type = "application/json"),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Object),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[post("/{org_id}/roles/{role_id}")]
pub async fn update_role(
    _path: web::Path<(String, String)>,
    _permissions: web::Json<String>,
) -> Result<HttpResponse, actix_web::Error> {
    Ok(MetaHttpResponse::forbidden("Not Supported"))
}

#[cfg(feature = "enterprise")]
/// GetResourcePermission
///
/// #{"ratelimit_module":"Roles", "ratelimit_module_operation":"get"}#
#[utoipa::path(
    context_path = "/api",
    tag = "Roles",
    operation_id = "GetResourcePermission",
    summary = "Get role permissions for resource",
    description = "Retrieves detailed permissions that a specific role has on a particular resource type. Useful for understanding access control capabilities and auditing role assignments. Requires enterprise features to be enabled.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
        ("role_id" = String, Path, description = "Role Id"),
        ("resource" = String, Path, description = "resource"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Vec<Object>),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[get("/{org_id}/roles/{role_id}/permissions/{resource}")]
pub async fn get_role_permissions(
    path: web::Path<(String, String, String)>,
) -> Result<HttpResponse, Error> {
    let (org_id, role_id, resource) = path.into_inner();
    match o2_openfga::authorizer::roles::get_role_permissions(&org_id, &role_id, &resource).await {
        Ok(res) => Ok(HttpResponse::Ok().json(res)),
        Err(err) => Ok(MetaHttpResponse::internal_error(err)),
    }
}

#[cfg(not(feature = "enterprise"))]
#[utoipa::path(
    context_path = "/api",
    tag = "Roles",
    operation_id = "GetResourcePermission",
    summary = "Get role permissions for resource",
    description = "Retrieves detailed permissions that a specific role has on a particular resource type. This endpoint is only available with enterprise features enabled and will return a forbidden error in the community edition.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
        ("role_id" = String, Path, description = "Role Id"),
        ("resource" = String, Path, description = "resource"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Vec<Object>),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[get("/{org_id}/roles/{role_id}/permissions/{resource}")]
pub async fn get_role_permissions(
    _path: web::Path<(String, String, String)>,
) -> Result<HttpResponse, Error> {
    Ok(MetaHttpResponse::forbidden("Not Supported"))
}

#[cfg(feature = "enterprise")]
/// GetRoleUsers
///
/// #{"ratelimit_module":"Roles", "ratelimit_module_operation":"list"}#
#[utoipa::path(
    context_path = "/api",
    tag = "Roles",
    operation_id = "GetRoleUsers",
    summary = "Get users assigned to role",
    description = "Retrieves a list of all users who are currently assigned to a specific role. Useful for role management, auditing user permissions, and understanding access control assignments. Requires enterprise features to be enabled.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
        ("role_id" = String, Path, description = "Role Id"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Vec<String>),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[get("/{org_id}/roles/{role_id}/users")]
pub async fn get_users_with_role(path: web::Path<(String, String)>) -> Result<HttpResponse, Error> {
    let (org_id, role_id) = path.into_inner();
    match o2_openfga::authorizer::roles::get_users_with_role(&org_id, &role_id).await {
        Ok(res) => Ok(HttpResponse::Ok().json(res)),
        Err(err) => Ok(MetaHttpResponse::internal_error(err)),
    }
}

#[cfg(not(feature = "enterprise"))]
#[utoipa::path(
    context_path = "/api",
    tag = "Roles",
    operation_id = "GetRoLesUsers",
    summary = "Get users assigned to role",
    description = "Retrieves a list of all users who are currently assigned to a specific role. This endpoint is only available with enterprise features enabled and will return a forbidden error in the community edition.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
        ("role_id" = String, Path, description = "Role Id"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Vec<String>),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[get("/{org_id}/roles/{role_id}/users")]
pub async fn get_users_with_role(
    _path: web::Path<(String, String)>,
) -> Result<HttpResponse, Error> {
    Ok(MetaHttpResponse::forbidden("Not Supported"))
}

#[cfg(feature = "enterprise")]
/// GetUserRoles
///
/// #{"ratelimit_module":"Users", "ratelimit_module_operation":"get"}#
#[utoipa::path(
    context_path = "/api",
    tag = "Users",
    operation_id = "GetUserRoles",
    summary = "Get roles for user",
    description = "Retrieves all roles assigned to a specific user in the organization. Shows both directly assigned roles and roles inherited through group membership. Requires enterprise features to be enabled.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
        ("user_email" = String, Path, description = "User email address"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Vec<String>),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[get("/{org_id}/users/{user_email}/roles")]
pub async fn get_roles_for_user(path: web::Path<(String, String)>) -> Result<HttpResponse, Error> {
    let (org_id, user_email) = path.into_inner();
    let res = o2_openfga::authorizer::roles::get_roles_for_org_user(&org_id, &user_email).await;

    Ok(HttpResponse::Ok().json(res))
}

#[cfg(not(feature = "enterprise"))]
#[utoipa::path(
    context_path = "/api",
    tag = "Users",
    operation_id = "GetUserRoles",
    summary = "Get roles for user",
    description = "Retrieves all roles assigned to a specific user in the organization. This endpoint is only available with enterprise features enabled and will return a forbidden error in the community edition.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
        ("user_email" = String, Path, description = "User email address"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Vec<String>),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[get("/{org_id}/users/{user_email}/roles")]
pub async fn get_roles_for_user(_path: web::Path<(String, String)>) -> Result<HttpResponse, Error> {
    Ok(MetaHttpResponse::forbidden("Not Supported"))
}

#[cfg(feature = "enterprise")]
/// GetUserGroups
///
/// #{"ratelimit_module":"Users", "ratelimit_module_operation":"get"}#
#[utoipa::path(
    context_path = "/api",
    tag = "Users",
    operation_id = "GetUserGroups",
    summary = "Get groups for user",
    description = "Retrieves all groups that a specific user belongs to in the organization. Shows group memberships which determine inherited roles and permissions. Requires enterprise features to be enabled.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
        ("user_email" = String, Path, description = "User email address"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Vec<String>),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[get("/{org_id}/users/{user_email}/groups")]
pub async fn get_groups_for_user(path: web::Path<(String, String)>) -> Result<HttpResponse, Error> {
    let (org_id, user_email) = path.into_inner();
    match o2_openfga::authorizer::groups::get_groups_for_org_user(&org_id, &user_email).await {
        Ok(res) => Ok(HttpResponse::Ok().json(res)),
        Err(err) => Ok(MetaHttpResponse::internal_error(err)),
    }
}

#[cfg(not(feature = "enterprise"))]
#[utoipa::path(
    context_path = "/api",
    tag = "Users",
    operation_id = "GetUserGroups",
    summary = "Get groups for user",
    description = "Retrieves all groups that a specific user belongs to in the organization. This endpoint is only available with enterprise features enabled and will return a forbidden error in the community edition.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
        ("user_email" = String, Path, description = "User email address"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Vec<String>),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[get("/{org_id}/users/{user_email}/groups")]
pub async fn get_groups_for_user(
    _path: web::Path<(String, String)>,
) -> Result<HttpResponse, Error> {
    Ok(MetaHttpResponse::forbidden("Not Supported"))
}

#[cfg(feature = "enterprise")]
/// CreateGroup
///
/// #{"ratelimit_module":"Groups", "ratelimit_module_operation":"create"}#
#[utoipa::path(
    context_path = "/api",
    tag = "Groups",
    operation_id = "CreateGroup",
    summary = "Create user group",
    description = "Creates a new user group with specified users and roles. Groups allow efficient management of permissions by assigning roles to groups instead of individual users. Requires enterprise features to be enabled.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
    ),
    request_body(content = UserGroup, description = "UserGroup", content_type = "application/json"),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Object),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[post("/{org_id}/groups")]
pub async fn create_group(
    org_id: web::Path<String>,
    user_group: web::Json<UserGroup>,
) -> Result<HttpResponse, Error> {
    use crate::handler::http::auth::jwt::format_role_name_only;

    let org_id = org_id.into_inner();
    let mut user_grp = user_group.into_inner();
    user_grp.name = format_role_name_only(user_grp.name.trim());

    match o2_openfga::authorizer::groups::create_group(
        &org_id,
        &user_grp.name,
        user_grp.users.unwrap_or_default(),
    )
    .await
    {
        Ok(_) => Ok(MetaHttpResponse::ok("Group created successfully")),
        Err(err) => Ok(MetaHttpResponse::internal_error(err)),
    }
}

#[cfg(not(feature = "enterprise"))]
#[utoipa::path(
    context_path = "/api",
    tag = "Groups",
    operation_id = "CreateGroup",
    summary = "Create user group",
    description = "Creates a new user group with specified users and roles. This endpoint is only available with enterprise features enabled and will return a forbidden error in the community edition.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
    ),
    request_body(content = UserGroup, description = "UserGroup", content_type = "application/json"),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Object),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[post("/{org_id}/groups")]
pub async fn create_group(
    _org_id: web::Path<String>,
    _user_group: web::Json<UserGroup>,
) -> Result<HttpResponse, Error> {
    Ok(MetaHttpResponse::forbidden("Not Supported"))
}

#[cfg(feature = "enterprise")]
/// UpdateGroup
///
/// #{"ratelimit_module":"Groups", "ratelimit_module_operation":"update"}#
#[utoipa::path(
    context_path = "/api",
    tag = "Groups",
    operation_id = "UpdateGroup",
    summary = "Update user group",
    description = "Updates an existing user group by adding or removing users and roles. Allows dynamic management of group membership and permissions to maintain proper access control. Requires enterprise features to be enabled.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
        ("group_name" = String, Path, description = "Group name"),
    ),
    request_body(content = UserGroupRequest, description = "UserGroupRequest", content_type = "application/json"),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Object),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[put("/{org_id}/groups/{group_name}")]
pub async fn update_group(
    path: web::Path<(String, String)>,
    user_group: web::Json<UserGroupRequest>,
) -> Result<HttpResponse, Error> {
    let (org_id, group_name) = path.into_inner();
    let user_grp = user_group.into_inner();

    match o2_openfga::authorizer::groups::update_group(
        &org_id,
        &group_name,
        user_grp.add_users,
        user_grp.remove_users,
        user_grp.add_roles,
        user_grp.remove_roles,
    )
    .await
    {
        Ok(_) => Ok(MetaHttpResponse::ok("Group updated successfully")),
        Err(err) => Ok(MetaHttpResponse::internal_error(err)),
    }
}

#[cfg(not(feature = "enterprise"))]
#[utoipa::path(
    context_path = "/api",
    tag = "Groups",
    operation_id = "UpdateGroup",
    summary = "Update user group",
    description = "Updates an existing user group by adding or removing users and roles. This endpoint is only available with enterprise features enabled and will return a forbidden error in the community edition.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
        ("group_name" = String, Path, description = "Group name"),
    ),
    request_body(content = UserGroupRequest, description = "UserGroupRequest", content_type = "application/json"),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Object),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[put("/{org_id}/groups/{group_name}")]
pub async fn update_group(
    _path: web::Path<(String, String)>,
    _user_group: web::Json<UserGroupRequest>,
) -> Result<HttpResponse, Error> {
    Ok(MetaHttpResponse::forbidden("Not Supported"))
}

#[cfg(feature = "enterprise")]
/// ListGroups
///
/// #{"ratelimit_module":"Groups", "ratelimit_module_operation":"list"}#
#[utoipa::path(
    context_path = "/api",
    tag = "Groups",
    operation_id = "ListGroups",
    summary = "List organization groups",
    description = "Retrieves a list of all user groups in the organization. Users will only see groups they have permissions to view when role-based access control is active. Useful for managing group-based permissions and understanding organizational structure. Requires enterprise features to be enabled.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Vec<String>),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[get("/{org_id}/groups")]
pub async fn get_groups(path: web::Path<String>, req: HttpRequest) -> Result<HttpResponse, Error> {
    let org_id = path.into_inner();

    let mut permitted;
    // Get List of allowed objects

    let user_id = req.headers().get("user_id").unwrap();
    match crate::handler::http::auth::validator::list_objects_for_user(
        &org_id,
        user_id.to_str().unwrap(),
        "GET",
        "group",
    )
    .await
    {
        Ok(list) => {
            permitted = list;
        }
        Err(e) => {
            return Ok(crate::common::meta::http::HttpResponse::forbidden(
                e.to_string(),
            ));
        }
    }
    // Get List of allowed objects ends

    if let Some(mut local_permitted) = permitted {
        let prefix = "group:";
        for value in local_permitted.iter_mut() {
            if value.starts_with(prefix) {
                *value = value.strip_prefix(prefix).unwrap().to_string();
            }
            let group_prefix = format!("{org_id}/");
            if value.starts_with(&group_prefix) {
                *value = value.strip_prefix(&group_prefix).unwrap().to_string()
            }
        }
        permitted = Some(local_permitted);
    }

    match o2_openfga::authorizer::groups::get_all_groups(&org_id, permitted).await {
        Ok(res) => Ok(HttpResponse::Ok().json(res)),
        Err(err) => Ok(MetaHttpResponse::internal_error(err)),
    }
}

#[cfg(not(feature = "enterprise"))]
#[utoipa::path(
    context_path = "/api",
    tag = "Groups",
    operation_id = "ListGroups",
    summary = "List organization groups",
    description = "Retrieves a list of all user groups in the organization. This endpoint is only available with enterprise features enabled and will return a forbidden error in the community edition.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Vec<String>),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[get("/{org_id}/groups")]
pub async fn get_groups(_path: web::Path<String>) -> Result<HttpResponse, Error> {
    Ok(MetaHttpResponse::forbidden("Not Supported"))
}

#[cfg(feature = "enterprise")]
/// GetGroup
///
/// #{"ratelimit_module":"Groups", "ratelimit_module_operation":"get"}#
#[utoipa::path(
    context_path = "/api",
    tag = "Groups",
    operation_id = "GetGroup",
    summary = "Get group details",
    description = "Retrieves detailed information about a specific user group including its members, assigned roles, and configuration. Useful for understanding group composition and permissions. Requires enterprise features to be enabled.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = UserGroup),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[get("/{org_id}/groups/{group_name}")]
pub async fn get_group_details(path: web::Path<(String, String)>) -> Result<HttpResponse, Error> {
    let (org_id, group_name) = path.into_inner();

    match o2_openfga::authorizer::groups::get_group_details(&org_id, &group_name).await {
        Ok(res) => Ok(HttpResponse::Ok().json(res)),
        Err(err) => Ok(MetaHttpResponse::internal_error(err)),
    }
}

#[cfg(not(feature = "enterprise"))]
#[utoipa::path(
    context_path = "/api",
    tag = "Groups",
    operation_id = "GetGroup",
    summary = "Get group details",
    description = "Retrieves detailed information about a specific user group including its members, assigned roles, and configuration. This endpoint is only available with enterprise features enabled and will return a forbidden error in the community edition.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = UserGroup),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[get("/{org_id}/groups/{group_name}")]
pub async fn get_group_details(_path: web::Path<(String, String)>) -> Result<HttpResponse, Error> {
    Ok(MetaHttpResponse::forbidden("Not Supported"))
}

#[cfg(feature = "enterprise")]
/// GetResources
///
/// #{"ratelimit_module":"Resources", "ratelimit_module_operation":"list"}#
#[utoipa::path(
    context_path = "/api",
    tag = "Resources",
    operation_id = "GetResources",
    summary = "Get available resources",
    description = "Retrieves a list of all available resource types that can be used in role and permission assignments. Shows the resource hierarchy and available permission levels for fine-grained access control. Requires enterprise features to be enabled.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Vec<Object>),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[get("/{org_id}/resources")]
pub async fn get_resources(_org_id: web::Path<String>) -> Result<HttpResponse, Error> {
    #[cfg(feature = "cloud")]
    use o2_openfga::meta::mapping::NON_CLOUD_RESOURCE_KEYS;
    use o2_openfga::meta::mapping::Resource;
    let resources = o2_openfga::meta::mapping::OFGA_MODELS
        .values()
        .collect::<Vec<&Resource>>();
    #[cfg(feature = "cloud")]
    let resources = resources
        .into_iter()
        .filter(|r| !NON_CLOUD_RESOURCE_KEYS.contains(&r.key))
        .collect::<Vec<&Resource>>();
    Ok(HttpResponse::Ok().json(resources))
}

#[cfg(not(feature = "enterprise"))]
#[utoipa::path(
    context_path = "/api",
    tag = "Resources",
    operation_id = "GetResources",
    summary = "Get available resources",
    description = "Retrieves a list of all available resource types that can be used in role and permission assignments. This endpoint is only available with enterprise features enabled and will return a forbidden error in the community edition.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Vec<Object>),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[get("/{org_id}/resources")]
pub async fn get_resources(_org_id: web::Path<String>) -> Result<HttpResponse, Error> {
    Ok(MetaHttpResponse::forbidden("Not Supported"))
}

#[cfg(feature = "enterprise")]
/// DeleteGroup
///
/// #{"ratelimit_module":"Groups", "ratelimit_module_operation":"delete"}#
#[utoipa::path(
    context_path = "/api",
    tag = "Groups",
    operation_id = "DeleteGroup",
    summary = "Delete user group",
    description = "Permanently removes a user group from the organization. Users in the group will lose group-based permissions but retain any directly assigned roles. This action cannot be undone. Requires enterprise features to be enabled.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
        ("group_name" = String, Path, description = "Group name"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Object),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
)]
#[delete("/{org_id}/groups/{group_name}")]
pub async fn delete_group(path: web::Path<(String, String)>) -> Result<HttpResponse, Error> {
    let (org_id, group_name) = path.into_inner();

    match o2_openfga::authorizer::groups::delete_group(&org_id, &group_name).await {
        Ok(_) => Ok(MetaHttpResponse::ok(
            serde_json::json!({"successful": "true"}),
        )),
        Err(err) => Ok(MetaHttpResponse::internal_error(err)),
    }
}

#[cfg(not(feature = "enterprise"))]
#[utoipa::path(
    context_path = "/api",
    tag = "Groups",
    operation_id = "DeleteGroup",
    summary = "Delete user group",
    description = "Permanently removes a user group from the organization. This endpoint is only available with enterprise features enabled and will return a forbidden error in the community edition.",
    security(
        ("Authorization"= [])
    ),
    params(
        ("org_id" = String, Path, description = "Organization name"),
        ("group_name" = String, Path, description = "Group name"),
    ),
    responses(
        (status = 200, description = "Success", content_type = "application/json", body = Object),
        (status = 500, description = "Failure", content_type = "application/json", body = ()),
    )
Missing Import Cleanup

New import of config::meta::folder::Folder is added but not used in the shown hunks. If unused in the file, remove to avoid warnings and maintain cleanliness.

use actix_web::{HttpRequest, HttpResponse, Responder, delete, get, post, put, web};
use config::meta::folder::Folder;

use crate::{
    common::meta::http::HttpResponse as MetaHttpResponse,
    handler::http::models::folders::{
        CreateFolderRequestBody, CreateFolderResponseBody, FolderType, GetFolderResponseBody,
        ListFoldersResponseBody, UpdateFolderRequestBody,
    },
    service::folders::{self, FolderError},
};

@github-actions
Copy link
Contributor

github-actions bot commented Sep 1, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Remove invalid request bodies

The DELETE and GET endpoints declare a request_body which is not used and can break
OpenAPI validity for these methods. Remove the body from these endpoints to match
actual behavior and avoid client generation issues.

src/handler/http/request/actions/action.rs [83]

-request_body(content = Template, description = "Template data", content_type ="application/json"),
+// Remove the request_body line entirely for DELETE/GET endpoints.
Suggestion importance[1-10]: 8

__

Why: DELETE/GET endpoints shouldn't declare a request body; keeping request_body can mislead OpenAPI clients. Removing it improves spec correctness.

Medium
Align parameter type with handler

The path handlers still use Ksuid for alert_id while the OpenAPI params now declare
it as String. This type mismatch will generate incorrect OpenAPI and may confuse
clients. Align the OpenAPI param type with the handler by using Ksuid to reflect the
actual path variable type.

src/handler/http/request/alerts/mod.rs [189-196]

 params(
     ("org_id" = String, Path, description = "Organization name"),
-    ("alert_id" = String, Path, description = "Alert ID"),
+    ("alert_id" = Ksuid, Path, description = "Alert ID"),
     ("folder" = Option<String>, Query, description = "Folder ID (Required if RBAC enabled)"),
   ),
Suggestion importance[1-10]: 7

__

Why: The handlers still use Ksuid in function signatures while the OpenAPI params changed to String; aligning to Ksuid avoids schema-runtime mismatch and improves API correctness.

Medium
Align path parameter names

The path mapping uses {ksuid} elsewhere while this endpoint declares action_id,
causing parameter mismatch and incorrect binding in generated specs/clients. Align
the parameter name to ksuid in both params and route, or adjust the route to
{action_id} consistently.

src/handler/http/request/actions/action.rs [176-179]

 params(
     ("org_id" = String, Path, description = "Organization name"),
-    ("action_id" = String, Path, description = "Action ID"),
+    ("ksuid" = String, Path, description = "Action ID"),
 ),
Suggestion importance[1-10]: 7

__

Why: The endpoint path uses {action_id} but other endpoints use {ksuid}; aligning names avoids confusion, though it's not strictly a bug if this route indeed expects action_id.

Medium
General
Restore typed schema for query

Replacing the schema of query with a generic Object loses the strong OpenAPI schema
for Query, degrading API docs and client generation. Keep the original typed
reference so consumers know the expected structure. If partial masking is needed,
use #[schema(value_type = Query)] or remove the override entirely.

src/config/src/meta/search.rs [57-59]

 #[derive(Clone, Debug, Default, Serialize, Deserialize, ToSchema)]
 #[schema(as = SearchRequest)]
 pub struct Request {
-    #[schema(value_type = Object)]
+    // Preserve concrete schema so docs and clients are accurate
     pub query: Query,
     #[serde(default)]
     pub encoding: RequestEncoding,
Suggestion importance[1-10]: 7

__

Why: Changing query to #[schema(value_type = Object)] (line 57) weakens OpenAPI typing; reverting to Query improves docs and client generation without affecting runtime.

Medium
Use concrete request schema

Declaring the request body as a generic Object removes field-level validation and
documentation for the expected payload. Reference the concrete request model used by
the handler (e.g., ShortenUrlRequest) so OpenAPI correctly documents required fields
and types, improving client generation and validation.

src/handler/http/request/short_url/mod.rs [40-41]

 request_body(
-    content = Object,
+    content = ShortenUrlRequest,
     description = "The original URL to shorten",
     content_type = "application/json",
     example = json!({
         "url": "https://example.com/dashboard?id=123"
     }),
 ),
Suggestion importance[1-10]: 7

__

Why: Replacing content = Object (line 40) with ShortenUrlRequest restores precise request schema and validation in OpenAPI, enhancing client usability.

Medium
Replace unit type response bodies

Using an empty tuple () as a response body in Utoipa generates an invalid schema for
OpenAPI tools. Replace it with Object for empty JSON objects or remove body to
indicate no content, matching actual responses.

src/handler/http/request/stream/mod.rs [66-67]

-(status = 400, description = "Failure", content_type = "application/json", body = ()),
+(status = 400, description = "Failure", content_type = "application/json", body = Object),
Suggestion importance[1-10]: 6

__

Why: Using () as a response body can produce awkward or invalid schemas; switching to Object yields clearer OpenAPI docs, a moderate quality improvement.

Low
Use concrete response types in specs

Returning Object for success and () for errors in OpenAPI where the actual handlers
likely return JSON bodies or plain text can mislead clients and break codegen. Use
concrete response schemas or at least consistent String/specific types that match
the handler responses.

src/handler/http/request/dashboards/mod.rs [297-301]

 responses(
-    (status = StatusCode::OK, description = "Success", body = Object),
-    (status = StatusCode::NOT_FOUND, description = "NotFound", body = ()),
-    (status = StatusCode::INTERNAL_SERVER_ERROR, description = "Error", body = ()),
+    (status = StatusCode::OK, description = "Success", body = String),
+    (status = StatusCode::NOT_FOUND, description = "NotFound", body = String),
+    (status = StatusCode::INTERNAL_SERVER_ERROR, description = "Error", body = String),
 ),
Suggestion importance[1-10]: 5

__

Why: Replacing Object/() with String may improve client generation, but without verifying handlers actually return String this is speculative; moderate impact on API clarity but uncertain accuracy.

Low
Remove unnecessary Object import

Importing Object from utoipa::openapi at module level can shadow or conflict with
other Object usages and is unnecessary for attribute macros. Remove this import and
refer to Object only within utoipa attributes, where it's resolved by the macro.
This avoids potential name collisions in the file's namespace.

src/handler/http/request/ai/chat.rs [23]

-use utoipa::openapi::Object;
+// Removed: use utoipa::openapi::Object;
Suggestion importance[1-10]: 5

__

Why: The import use utoipa::openapi::Object; at line 23 is only used in utoipa attributes and can be omitted to avoid namespace clutter; however, it's not incorrect, just a minor cleanup.

Low

@ByteBaker ByteBaker force-pushed the feat/openapi-description-fix branch 4 times, most recently from 3fedb10 to 4b67762 Compare September 1, 2025 08:34
@ByteBaker
Copy link
Contributor Author

@greptileai

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Greptile Summary

This PR comprehensively overhauls the OpenAPI documentation generation across the entire OpenObserve codebase. The changes upgrade utoipa from 4.0.0 to 5.0.0 and utoipa-swagger-ui from 4.0.0 to 9.0.0, enabling better control over API documentation. The core improvement detaches OpenAPI endpoint descriptions from Rust doc comments by adding explicit summary and description fields to all endpoint annotations. This prevents development-specific information from leaking into the public API specification.

The PR systematically updates response body schemas across hundreds of endpoints, replacing specific internal types (like HttpResponse, SearchResponse, Pipeline) with generic OpenAPI-friendly types (Object for successful JSON responses and () for empty responses). Schema annotations are added to custom types like Ksuid, OrdF64, and DateTime<Utc> fields to ensure proper OpenAPI type representation. Complex internal structures are simplified to generic Object types in the documentation while maintaining type safety in the actual implementation.

The changes span all major functional areas including alerts, dashboards, search APIs, user management, organization settings, metrics ingestion, PromQL endpoints, streaming APIs, and enterprise features like RBAC. Each endpoint now has professional, user-facing documentation with clear explanations of functionality, use cases, and expected behavior. The upgrade also required updating HTTP method enumeration logic in the OpenAPI info function to work with the new utoipa 5.0.0 API structure.

Confidence score: 4/5

  • This PR requires careful review due to extensive OpenAPI documentation changes across the entire codebase and potential breaking changes from major version upgrades
  • Score reflects the comprehensive nature of changes affecting API documentation generation and the need to verify that all endpoints maintain correct schema representations
  • Pay close attention to response body schema changes and endpoint parameter annotations, particularly in critical endpoints like authentication, billing, and data ingestion

69 files reviewed, 4 comments

Edit Code Review Bot Settings | Greptile

description = "Ingests metrics data using OpenTelemetry Protocol (OTLP) format. Supports both Protocol Buffers and JSON \
content types for OTLP metrics ingestion. This is the standard endpoint for OpenTelemetry SDK and \
collector integrations to send metrics data.",
request_body(content = String, description = "ExportMetricsServiceRequest", content_type = "application/x-protobuf"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syntax: Missing params section for org_id parameter - all other similar endpoints in the PR include this parameter documentation

Suggested change
request_body(content = String, description = "ExportMetricsServiceRequest", content_type = "application/x-protobuf"),
params(
("org_id" = String, Path, description = "Organization name"),
),
request_body(content = String, description = "ExportMetricsServiceRequest", content_type = "application/x-protobuf"),

Comment on lines 105 to +111
(status = 200, description = "Success", content_type = "application/json", body =
HttpResponse), (status = 400, description = "Error", content_type = "application/json",
body = HttpResponse), )
Object), (status = 400, description = "Error", content_type = "application/json",
body = ()), )
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syntax: Formatting issue: response body definition is split across multiple lines inconsistently

Suggested change
(status = 200, description = "Success", content_type = "application/json", body =
HttpResponse), (status = 400, description = "Error", content_type = "application/json",
body = HttpResponse), )
Object), (status = 400, description = "Error", content_type = "application/json",
body = ()), )
(status = 200, description = "Success", content_type = "application/json", body = Object),
(status = 400, description = "Error", content_type = "application/json", body = ()),

/// #{"ratelimit_module":"Reports", "ratelimit_module_operation":"update"}#
#[utoipa::path(
context_path = "/api",
tag = "Report",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syntax: Tag inconsistency: should be 'Reports' to match other endpoints in this file

Suggested change
tag = "Report",
tag = "Reports",

Comment on lines 127 to +135
(status = 200, description = "Success", content_type = "application/json", body =
HttpResponse), (status = 400, description = "Error", content_type = "application/json",
body = HttpResponse), )
Object), (status = 400, description = "Error", content_type = "application/json",
body = ()), )
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syntax: Formatting issue: response body definition is split across multiple lines inconsistently

Suggested change
(status = 200, description = "Success", content_type = "application/json", body =
HttpResponse), (status = 400, description = "Error", content_type = "application/json",
body = HttpResponse), )
Object), (status = 400, description = "Error", content_type = "application/json",
body = ()), )
(status = 200, description = "Success", content_type = "application/json", body = Object),
(status = 400, description = "Error", content_type = "application/json", body = ()),

@ByteBaker ByteBaker force-pushed the feat/openapi-description-fix branch from 4b67762 to 4803d49 Compare September 1, 2025 09:09
@ByteBaker ByteBaker force-pushed the feat/openapi-description-fix branch from 4803d49 to 1f8bd2f Compare September 1, 2025 10:40
@ByteBaker ByteBaker force-pushed the feat/openapi-description-fix branch 4 times, most recently from 9a557a5 to d9624b0 Compare September 1, 2025 17:21
So far the OpenAPI descriptions for endpoint handlers was directly
derived from the doc comments, leaving little control. This also
leaked any dev-specific information into API spec.

Now the summary and description are explicitly populated for all
the endpoints, detaching them from doc comments.

Also update response body annotations wherever necessary

Also bump
- utoipa: 4.0.0 -> 5.0.0
- utoipa-swagger-ui: 4.0.0 -> 9.0.0
@ByteBaker ByteBaker force-pushed the feat/openapi-description-fix branch from d9624b0 to c081351 Compare September 1, 2025 17:21
@ByteBaker ByteBaker merged commit d87d346 into main Sep 1, 2025
29 checks passed
@ByteBaker ByteBaker deleted the feat/openapi-description-fix branch September 1, 2025 18:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

☢️ Bug Something isn't working Review effort 3/5

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants