mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
feat: remove the okta tool (#506)
This commit is contained in:
@@ -12,7 +12,6 @@ exclude = [
|
||||
"tools-src/google-drive",
|
||||
"tools-src/google-sheets",
|
||||
"tools-src/google-slides",
|
||||
"tools-src/okta",
|
||||
"tools-src/slack",
|
||||
"tools-src/telegram",
|
||||
]
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"name": "okta",
|
||||
"display_name": "Okta",
|
||||
"kind": "tool",
|
||||
"version": "0.1.0",
|
||||
"description": "Okta SSO for user profile, app catalog, and SSO launch links",
|
||||
"keywords": ["sso", "identity", "authentication", "okta"],
|
||||
|
||||
"source": {
|
||||
"dir": "tools-src/okta",
|
||||
"capabilities": "okta-tool.capabilities.json",
|
||||
"crate_name": "okta-tool"
|
||||
},
|
||||
|
||||
"artifacts": {
|
||||
"wasm32-wasip2": {
|
||||
"url": "https://github.com/nearai/ironclaw/releases/latest/download/okta-wasm32-wasip2.tar.gz",
|
||||
"sha256": null
|
||||
}
|
||||
},
|
||||
|
||||
"auth_summary": {
|
||||
"method": "oauth",
|
||||
"provider": "Okta",
|
||||
"secrets": ["okta_oauth_token"],
|
||||
"shared_auth": null,
|
||||
"setup_url": "https://developer.okta.com/docs/guides/implement-oauth-for-okta/main/"
|
||||
},
|
||||
|
||||
"tags": ["identity"]
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
[package]
|
||||
name = "okta-tool"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Okta SSO tool for IronClaw (WASM component) — user profile, app catalog, and SSO launch links"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
wit-bindgen = "=0.36"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
lto = true
|
||||
strip = true
|
||||
codegen-units = 1
|
||||
|
||||
[workspace]
|
||||
@@ -1,105 +0,0 @@
|
||||
{
|
||||
"http": {
|
||||
"allowlist": [
|
||||
{
|
||||
"host": "*.okta.com",
|
||||
"path_prefix": "/api/v1/",
|
||||
"methods": ["GET", "POST", "PUT"]
|
||||
},
|
||||
{
|
||||
"host": "*.okta.com",
|
||||
"path_prefix": "/idp/myaccount/",
|
||||
"methods": ["GET", "PUT"]
|
||||
},
|
||||
{
|
||||
"host": "*.okta.com",
|
||||
"path_prefix": "/oauth2/v1/",
|
||||
"methods": ["POST"]
|
||||
},
|
||||
{
|
||||
"host": "*.oktapreview.com",
|
||||
"path_prefix": "/api/v1/",
|
||||
"methods": ["GET", "POST", "PUT"]
|
||||
},
|
||||
{
|
||||
"host": "*.oktapreview.com",
|
||||
"path_prefix": "/idp/myaccount/",
|
||||
"methods": ["GET", "PUT"]
|
||||
},
|
||||
{
|
||||
"host": "*.oktapreview.com",
|
||||
"path_prefix": "/oauth2/v1/",
|
||||
"methods": ["POST"]
|
||||
},
|
||||
{
|
||||
"host": "*.okta-emea.com",
|
||||
"path_prefix": "/api/v1/",
|
||||
"methods": ["GET", "POST", "PUT"]
|
||||
},
|
||||
{
|
||||
"host": "*.okta-emea.com",
|
||||
"path_prefix": "/idp/myaccount/",
|
||||
"methods": ["GET", "PUT"]
|
||||
},
|
||||
{
|
||||
"host": "*.okta-emea.com",
|
||||
"path_prefix": "/oauth2/v1/",
|
||||
"methods": ["POST"]
|
||||
}
|
||||
],
|
||||
"credentials": {
|
||||
"okta_oauth_token": {
|
||||
"secret_name": "okta_oauth_token",
|
||||
"location": { "type": "bearer" },
|
||||
"host_patterns": ["*.okta.com", "*.oktapreview.com", "*.okta-emea.com"]
|
||||
}
|
||||
},
|
||||
"rate_limit": {
|
||||
"requests_per_minute": 30,
|
||||
"requests_per_hour": 500
|
||||
},
|
||||
"timeout_secs": 30
|
||||
},
|
||||
"workspace": {
|
||||
"allowed_prefixes": ["okta/"]
|
||||
},
|
||||
"secrets": {
|
||||
"allowed_names": ["okta_oauth_token"]
|
||||
},
|
||||
"auth": {
|
||||
"secret_name": "okta_oauth_token",
|
||||
"display_name": "Okta",
|
||||
"oauth": {
|
||||
"authorization_url": "https://{okta_domain}/oauth2/v1/authorize",
|
||||
"token_url": "https://{okta_domain}/oauth2/v1/token",
|
||||
"client_id_env": "OKTA_OAUTH_CLIENT_ID",
|
||||
"client_secret_env": "OKTA_OAUTH_CLIENT_SECRET",
|
||||
"scopes": [
|
||||
"openid",
|
||||
"profile",
|
||||
"email",
|
||||
"offline_access",
|
||||
"okta.users.read.self",
|
||||
"okta.users.manage.self",
|
||||
"okta.apps.read"
|
||||
],
|
||||
"use_pkce": true
|
||||
},
|
||||
"instructions": "1. In your Okta Admin Console, go to Applications > Create App Integration\n2. Select 'OIDC - OpenID Connect', then 'Web Application'\n3. Set Sign-in redirect URI to http://localhost:9876/callback (through :9886)\n4. Under Okta API Scopes, grant: okta.users.read.self, okta.users.manage.self, okta.apps.read\n5. Copy the Client ID and Client Secret\n6. IMPORTANT: You must use the Org Authorization Server (not a custom one)\n7. Store your Okta domain in workspace at 'okta/domain' (e.g., 'mycompany.okta.com')\n8. For custom domains, add them to okta-tool.capabilities.json allowlist",
|
||||
"setup_url": "https://developer.okta.com/docs/guides/implement-oauth-for-okta/main/",
|
||||
"token_hint": "OAuth2 access token (JWT)",
|
||||
"env_var": "OKTA_OAUTH_TOKEN"
|
||||
},
|
||||
"setup": {
|
||||
"required_secrets": [
|
||||
{
|
||||
"name": "okta_oauth_client_id",
|
||||
"prompt": "Okta OAuth Client ID"
|
||||
},
|
||||
{
|
||||
"name": "okta_oauth_client_secret",
|
||||
"prompt": "Okta OAuth Client Secret"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
use crate::near::agent::host;
|
||||
use crate::types::*;
|
||||
|
||||
const WORKSPACE_DOMAIN_PATH: &str = "okta/domain";
|
||||
|
||||
/// Read the configured Okta domain from workspace, or return a helpful error.
|
||||
fn get_domain() -> Result<String, String> {
|
||||
host::workspace_read(WORKSPACE_DOMAIN_PATH).ok_or_else(|| {
|
||||
"Okta domain not configured. Write your Okta domain to workspace path 'okta/domain' \
|
||||
using the memory_write tool (e.g., memory_write with path='okta/domain' and \
|
||||
content='mycompany.okta.com')."
|
||||
.to_string()
|
||||
})
|
||||
}
|
||||
|
||||
/// Build the base URL for the Okta Management API.
|
||||
fn management_base(domain: &str) -> String {
|
||||
format!("https://{}/api/v1", domain)
|
||||
}
|
||||
|
||||
/// Make an Okta API call.
|
||||
fn okta_api_call(method: &str, url: &str, body: Option<&str>) -> Result<String, String> {
|
||||
let headers = if body.is_some() {
|
||||
r#"{"Content-Type": "application/json", "Accept": "application/json"}"#
|
||||
} else {
|
||||
r#"{"Accept": "application/json"}"#
|
||||
};
|
||||
|
||||
let body_bytes = body.map(|b| b.as_bytes().to_vec());
|
||||
|
||||
host::log(
|
||||
host::LogLevel::Debug,
|
||||
&format!("Okta API: {} {}", method, url),
|
||||
);
|
||||
|
||||
let response = host::http_request(method, url, headers, body_bytes.as_deref(), None)?;
|
||||
|
||||
if response.status < 200 || response.status >= 300 {
|
||||
let body_text = String::from_utf8_lossy(&response.body);
|
||||
// Try to extract Okta's error summary for a better message.
|
||||
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&body_text) {
|
||||
if let Some(summary) = parsed["errorSummary"].as_str() {
|
||||
return Err(format!("Okta API error ({}): {}", response.status, summary));
|
||||
}
|
||||
}
|
||||
return Err(format!(
|
||||
"Okta API returned status {}: {}",
|
||||
response.status, body_text
|
||||
));
|
||||
}
|
||||
|
||||
String::from_utf8(response.body).map_err(|e| format!("Invalid UTF-8: {}", e))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Action implementations
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// GET /api/v1/users/me
|
||||
pub fn get_profile() -> Result<String, String> {
|
||||
let domain = get_domain()?;
|
||||
let url = format!("{}/users/me", management_base(&domain));
|
||||
let response = okta_api_call("GET", &url, None)?;
|
||||
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(&response).map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
let profile = parse_user_profile(&parsed)?;
|
||||
serde_json::to_string(&profile).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// POST /api/v1/users/me (partial update via Management API)
|
||||
pub fn update_profile(fields: &serde_json::Value) -> Result<String, String> {
|
||||
let domain = get_domain()?;
|
||||
let url = format!("{}/users/me", management_base(&domain));
|
||||
|
||||
// Wrap fields under "profile" key for Okta's expected format.
|
||||
let payload = serde_json::json!({ "profile": fields });
|
||||
let body = serde_json::to_string(&payload).map_err(|e| e.to_string())?;
|
||||
|
||||
let response = okta_api_call("POST", &url, Some(&body))?;
|
||||
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(&response).map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
let profile = parse_user_profile(&parsed)?;
|
||||
let result = UpdateProfileResult {
|
||||
success: true,
|
||||
profile,
|
||||
};
|
||||
serde_json::to_string(&result).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// GET /api/v1/users/me/appLinks
|
||||
pub fn list_apps() -> Result<String, String> {
|
||||
let domain = get_domain()?;
|
||||
let url = format!("{}/users/me/appLinks", management_base(&domain));
|
||||
let response = okta_api_call("GET", &url, None)?;
|
||||
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(&response).map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
let apps = parse_app_links(&parsed)?;
|
||||
let count = apps.len();
|
||||
let result = ListAppsResult { apps, count };
|
||||
serde_json::to_string(&result).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Search apps by label (case-insensitive substring match).
|
||||
pub fn search_apps(query: &str) -> Result<String, String> {
|
||||
let domain = get_domain()?;
|
||||
let url = format!("{}/users/me/appLinks", management_base(&domain));
|
||||
let response = okta_api_call("GET", &url, None)?;
|
||||
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(&response).map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
let all_apps = parse_app_links(&parsed)?;
|
||||
let query_lower = query.to_lowercase();
|
||||
|
||||
let apps: Vec<AppLink> = all_apps
|
||||
.into_iter()
|
||||
.filter(|app| {
|
||||
app.label.to_lowercase().contains(&query_lower)
|
||||
|| app.app_name.to_lowercase().contains(&query_lower)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let count = apps.len();
|
||||
let result = ListAppsResult { apps, count };
|
||||
serde_json::to_string(&result).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Find an app by ID or label and return its SSO launch link.
|
||||
pub fn get_app_sso_link(app: &str) -> Result<String, String> {
|
||||
let domain = get_domain()?;
|
||||
let url = format!("{}/users/me/appLinks", management_base(&domain));
|
||||
let response = okta_api_call("GET", &url, None)?;
|
||||
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(&response).map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
let all_apps = parse_app_links(&parsed)?;
|
||||
let app_lower = app.to_lowercase();
|
||||
|
||||
// Try exact ID match first, then case-insensitive label match.
|
||||
let found = all_apps
|
||||
.iter()
|
||||
.find(|a| a.app_instance_id == app)
|
||||
.or_else(|| {
|
||||
all_apps
|
||||
.iter()
|
||||
.find(|a| a.label.to_lowercase() == app_lower)
|
||||
})
|
||||
.or_else(|| {
|
||||
all_apps
|
||||
.iter()
|
||||
.find(|a| a.label.to_lowercase().contains(&app_lower))
|
||||
});
|
||||
|
||||
match found {
|
||||
Some(app_link) => {
|
||||
let result = AppSsoLinkResult {
|
||||
label: app_link.label.clone(),
|
||||
link_url: app_link.link_url.clone(),
|
||||
app_instance_id: app_link.app_instance_id.clone(),
|
||||
app_name: app_link.app_name.clone(),
|
||||
};
|
||||
serde_json::to_string(&result).map_err(|e| e.to_string())
|
||||
}
|
||||
None => {
|
||||
let available: Vec<String> = all_apps.iter().map(|a| a.label.clone()).collect();
|
||||
Err(format!(
|
||||
"App '{}' not found. Available apps: {}",
|
||||
app,
|
||||
available.join(", ")
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// GET /idp/myaccount/organization
|
||||
pub fn get_org_info() -> Result<String, String> {
|
||||
let domain = get_domain()?;
|
||||
let url = format!("https://{}/idp/myaccount/organization", domain);
|
||||
|
||||
// MyAccount API requires the okta-version header.
|
||||
let response = okta_api_call_with_headers(
|
||||
"GET",
|
||||
&url,
|
||||
None,
|
||||
r#"{"Accept": "application/json; okta-version=1.0.0"}"#,
|
||||
)?;
|
||||
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(&response).map_err(|e| format!("Failed to parse response: {}", e))?;
|
||||
|
||||
let result = OrgInfo {
|
||||
id: parsed["id"].as_str().unwrap_or("").to_string(),
|
||||
name: parsed["name"].as_str().unwrap_or("").to_string(),
|
||||
subdomain: parsed["subdomain"].as_str().map(|s| s.to_string()),
|
||||
website: parsed["website"].as_str().map(|s| s.to_string()),
|
||||
support_phone: parsed["supportPhoneNumber"].as_str().map(|s| s.to_string()),
|
||||
technical_contact: parsed["technicalContact"].as_str().map(|s| s.to_string()),
|
||||
};
|
||||
serde_json::to_string(&result).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Like `okta_api_call` but with custom headers (for MyAccount API versioning).
|
||||
fn okta_api_call_with_headers(
|
||||
method: &str,
|
||||
url: &str,
|
||||
body: Option<&str>,
|
||||
headers: &str,
|
||||
) -> Result<String, String> {
|
||||
let body_bytes = body.map(|b| b.as_bytes().to_vec());
|
||||
|
||||
host::log(
|
||||
host::LogLevel::Debug,
|
||||
&format!("Okta API: {} {}", method, url),
|
||||
);
|
||||
|
||||
let response = host::http_request(method, url, headers, body_bytes.as_deref(), None)?;
|
||||
|
||||
if response.status < 200 || response.status >= 300 {
|
||||
let body_text = String::from_utf8_lossy(&response.body);
|
||||
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&body_text) {
|
||||
if let Some(summary) = parsed["errorSummary"].as_str() {
|
||||
return Err(format!("Okta API error ({}): {}", response.status, summary));
|
||||
}
|
||||
}
|
||||
return Err(format!(
|
||||
"Okta API returned status {}: {}",
|
||||
response.status, body_text
|
||||
));
|
||||
}
|
||||
|
||||
String::from_utf8(response.body).map_err(|e| format!("Invalid UTF-8: {}", e))
|
||||
}
|
||||
|
||||
fn parse_user_profile(v: &serde_json::Value) -> Result<UserProfile, String> {
|
||||
let p = &v["profile"];
|
||||
Ok(UserProfile {
|
||||
id: v["id"].as_str().unwrap_or("").to_string(),
|
||||
status: v["status"].as_str().unwrap_or("").to_string(),
|
||||
first_name: p["firstName"].as_str().unwrap_or("").to_string(),
|
||||
last_name: p["lastName"].as_str().unwrap_or("").to_string(),
|
||||
email: p["email"].as_str().unwrap_or("").to_string(),
|
||||
login: p["login"].as_str().unwrap_or("").to_string(),
|
||||
mobile_phone: p["mobilePhone"].as_str().map(|s| s.to_string()),
|
||||
display_name: p["displayName"].as_str().map(|s| s.to_string()),
|
||||
nick_name: p["nickName"].as_str().map(|s| s.to_string()),
|
||||
title: p["title"].as_str().map(|s| s.to_string()),
|
||||
department: p["department"].as_str().map(|s| s.to_string()),
|
||||
organization: p["organization"].as_str().map(|s| s.to_string()),
|
||||
timezone: p["timezone"].as_str().map(|s| s.to_string()),
|
||||
locale: p["locale"].as_str().map(|s| s.to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_app_links(v: &serde_json::Value) -> Result<Vec<AppLink>, String> {
|
||||
let arr = v
|
||||
.as_array()
|
||||
.ok_or_else(|| "Expected array of app links from Okta".to_string())?;
|
||||
|
||||
Ok(arr
|
||||
.iter()
|
||||
.map(|a| AppLink {
|
||||
app_instance_id: a["appInstanceId"].as_str().unwrap_or("").to_string(),
|
||||
label: a["label"].as_str().unwrap_or("").to_string(),
|
||||
link_url: a["linkUrl"].as_str().unwrap_or("").to_string(),
|
||||
logo_url: a["logoUrl"].as_str().map(|s| s.to_string()),
|
||||
app_name: a["appName"].as_str().unwrap_or("").to_string(),
|
||||
hidden: a["hidden"].as_bool().unwrap_or(false),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
//! Okta WASM Tool for IronClaw.
|
||||
//!
|
||||
//! Provides user profile management, SSO app catalog browsing, and
|
||||
//! launch links for all applications under Okta single sign-on.
|
||||
//!
|
||||
//! # Setup
|
||||
//!
|
||||
//! 1. Configure OAuth2 with PKCE (see capabilities.json instructions)
|
||||
//! 2. Write your Okta domain to workspace: `memory_write(path="okta/domain", content="mycompany.okta.com")`
|
||||
//! 3. All actions read the domain from workspace automatically
|
||||
//!
|
||||
//! # Capabilities Required
|
||||
//!
|
||||
//! - HTTP: `*.okta.com/api/v1/*`, `*.okta.com/idp/myaccount/*` (GET, POST, PUT)
|
||||
//! - Secrets: `okta_oauth_token` (injected as Bearer token)
|
||||
//! - Workspace: `okta/` prefix (read-only, for domain config)
|
||||
//!
|
||||
//! # Supported Actions
|
||||
//!
|
||||
//! - `get_profile`: Fetch the current user's profile
|
||||
//! - `update_profile`: Update profile fields
|
||||
//! - `list_apps`: List all SSO apps assigned to the user
|
||||
//! - `search_apps`: Search apps by name
|
||||
//! - `get_app_sso_link`: Get the SSO launch URL for a specific app
|
||||
//! - `get_org_info`: Get organization details
|
||||
|
||||
mod api;
|
||||
mod types;
|
||||
|
||||
use types::OktaAction;
|
||||
|
||||
wit_bindgen::generate!({
|
||||
world: "sandboxed-tool",
|
||||
path: "../../wit/tool.wit",
|
||||
});
|
||||
|
||||
struct OktaTool;
|
||||
|
||||
impl exports::near::agent::tool::Guest for OktaTool {
|
||||
fn execute(req: exports::near::agent::tool::Request) -> exports::near::agent::tool::Response {
|
||||
match execute_inner(&req.params) {
|
||||
Ok(result) => exports::near::agent::tool::Response {
|
||||
output: Some(result),
|
||||
error: None,
|
||||
},
|
||||
Err(e) => exports::near::agent::tool::Response {
|
||||
output: None,
|
||||
error: Some(e),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn schema() -> String {
|
||||
r#"{
|
||||
"type": "object",
|
||||
"required": ["action"],
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["get_profile", "update_profile", "list_apps", "search_apps", "get_app_sso_link", "get_org_info"],
|
||||
"description": "The Okta operation to perform"
|
||||
},
|
||||
"fields": {
|
||||
"type": "object",
|
||||
"description": "Profile fields to update (e.g., firstName, lastName, email, mobilePhone, displayName, title, department). Required for: update_profile"
|
||||
},
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Case-insensitive search query to match against app labels and names. Required for: search_apps"
|
||||
},
|
||||
"app": {
|
||||
"type": "string",
|
||||
"description": "App instance ID (e.g., '0oa1xxx') or app label (e.g., 'Google Workspace'). Required for: get_app_sso_link"
|
||||
}
|
||||
}
|
||||
}"#
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn description() -> String {
|
||||
"Okta SSO tool for managing your profile and accessing all applications under \
|
||||
single sign-on. Supports viewing/updating your Okta profile, listing all assigned \
|
||||
SSO apps, searching apps by name, and getting direct SSO launch links. Requires \
|
||||
Okta domain in workspace at 'okta/domain' and an OAuth token with \
|
||||
okta.users.read.self, okta.users.manage.self, and okta.apps.read scopes."
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_inner(params: &str) -> Result<String, String> {
|
||||
if !crate::near::agent::host::secret_exists("okta_oauth_token") {
|
||||
return Err(
|
||||
"Okta OAuth token not configured. Please add the 'okta_oauth_token' secret \
|
||||
via OAuth2 flow or set the OKTA_OAUTH_TOKEN environment variable."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let action: OktaAction =
|
||||
serde_json::from_str(params).map_err(|e| format!("Invalid parameters: {}", e))?;
|
||||
|
||||
crate::near::agent::host::log(
|
||||
crate::near::agent::host::LogLevel::Info,
|
||||
&format!("Executing Okta action: {:?}", action),
|
||||
);
|
||||
|
||||
match action {
|
||||
OktaAction::GetProfile => api::get_profile(),
|
||||
OktaAction::UpdateProfile { fields } => api::update_profile(&fields),
|
||||
OktaAction::ListApps => api::list_apps(),
|
||||
OktaAction::SearchApps { query } => api::search_apps(&query),
|
||||
OktaAction::GetAppSsoLink { app } => api::get_app_sso_link(&app),
|
||||
OktaAction::GetOrgInfo => api::get_org_info(),
|
||||
}
|
||||
}
|
||||
|
||||
export!(OktaTool);
|
||||
@@ -1,119 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Input parameters for the Okta tool.
|
||||
///
|
||||
/// Actions map to Okta Management API (/api/v1/) and MyAccount API (/idp/myaccount/).
|
||||
/// The tool reads the Okta domain from workspace at `okta/domain`.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "action", rename_all = "snake_case")]
|
||||
pub enum OktaAction {
|
||||
/// Get the current user's Okta profile.
|
||||
GetProfile,
|
||||
|
||||
/// Update fields on the current user's profile (partial update).
|
||||
UpdateProfile {
|
||||
/// Key-value pairs of profile fields to update.
|
||||
/// Common fields: firstName, lastName, email, mobilePhone, displayName,
|
||||
/// nickName, title, department, organization.
|
||||
fields: serde_json::Value,
|
||||
},
|
||||
|
||||
/// List all SSO applications assigned to the current user.
|
||||
ListApps,
|
||||
|
||||
/// Search assigned apps by name (case-insensitive substring match).
|
||||
SearchApps {
|
||||
/// Search query to match against app labels.
|
||||
query: String,
|
||||
},
|
||||
|
||||
/// Get the SSO launch link for a specific app by its instance ID or label.
|
||||
GetAppSsoLink {
|
||||
/// App instance ID (e.g., "0oa1xxx") or app label to search for.
|
||||
app: String,
|
||||
},
|
||||
|
||||
/// Get information about the Okta organization.
|
||||
GetOrgInfo,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Response types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// User profile from Okta.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct UserProfile {
|
||||
pub id: String,
|
||||
pub status: String,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub email: String,
|
||||
pub login: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mobile_phone: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub display_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub nick_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub department: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub organization: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub timezone: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub locale: Option<String>,
|
||||
}
|
||||
|
||||
/// Result of a profile update.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct UpdateProfileResult {
|
||||
pub success: bool,
|
||||
pub profile: UserProfile,
|
||||
}
|
||||
|
||||
/// An SSO app link (chiclet) assigned to the user.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AppLink {
|
||||
pub app_instance_id: String,
|
||||
pub label: String,
|
||||
pub link_url: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub logo_url: Option<String>,
|
||||
pub app_name: String,
|
||||
pub hidden: bool,
|
||||
}
|
||||
|
||||
/// Result of listing or searching apps.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ListAppsResult {
|
||||
pub apps: Vec<AppLink>,
|
||||
pub count: usize,
|
||||
}
|
||||
|
||||
/// SSO launch link for a specific app.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AppSsoLinkResult {
|
||||
pub label: String,
|
||||
pub link_url: String,
|
||||
pub app_instance_id: String,
|
||||
pub app_name: String,
|
||||
}
|
||||
|
||||
/// Okta organization info.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct OrgInfo {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub subdomain: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub website: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub support_phone: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub technical_contact: Option<String>,
|
||||
}
|
||||
Reference in New Issue
Block a user