diff --git a/Component-CLI.md b/Component-CLI.md index bcbed81..1abacae 100644 --- a/Component-CLI.md +++ b/Component-CLI.md @@ -61,6 +61,17 @@ scadalink template update --file scadalink template delete scadalink template validate scadalink template diff +scadalink template attribute add --template-id --name --data-type [--default-value ] [--tag-path ] +scadalink template attribute update --template-id --name [--data-type ] [--default-value ] [--tag-path ] +scadalink template attribute delete --template-id --name +scadalink template alarm add --template-id --name --trigger-attribute --condition --setpoint [--severity ] [--notification-list ] +scadalink template alarm update --template-id --name [--condition ] [--setpoint ] [--severity ] [--notification-list ] +scadalink template alarm delete --template-id --name +scadalink template script add --template-id --name --trigger-type [--trigger-attribute ] [--interval ] --code +scadalink template script update --template-id --name [--trigger-type ] [--trigger-attribute ] [--interval ] [--code ] +scadalink template script delete --template-id --name +scadalink template composition add --template-id --module-template-id --instance-name +scadalink template composition delete --template-id --instance-name ``` ### Instance Commands @@ -69,6 +80,7 @@ scadalink instance list [--site ] [--area ] [--format json|table] scadalink instance get [--format json|table] scadalink instance create --template --site --code [--area ] scadalink instance set-overrides --file +scadalink instance set-bindings --bindings scadalink instance bind-connections --file scadalink instance assign-area --area scadalink instance enable @@ -85,6 +97,7 @@ scadalink site update --file scadalink site delete scadalink site area list scadalink site area create --name [--parent ] +scadalink site area update --name [--new-name ] [--parent ] scadalink site area delete --name ``` @@ -122,7 +135,7 @@ scadalink notification get [--format json|table] scadalink notification create --file scadalink notification update --file scadalink notification delete -scadalink notification smtp get [--format json|table] +scadalink notification smtp list [--format json|table] scadalink notification smtp update --file ``` @@ -130,12 +143,17 @@ scadalink notification smtp update --file ``` scadalink security api-key list [--format json|table] scadalink security api-key create --name +scadalink security api-key update [--name ] [--enabled ] scadalink security api-key enable scadalink security api-key disable scadalink security api-key delete scadalink security role-mapping list [--format json|table] scadalink security role-mapping create --group --role [--site ] +scadalink security role-mapping update --id [--group ] [--role ] scadalink security role-mapping delete --group --role +scadalink security scope-rule list [--role-mapping-id ] [--format json|table] +scadalink security scope-rule add --role-mapping-id --site-id +scadalink security scope-rule delete --id ``` ### Audit Log Commands @@ -147,6 +165,35 @@ scadalink audit-log query [--user ] [--entity-type ] [--from [--format json|table] +scadalink health event-log --site-identifier [--from ] [--to ] [--search ] [--page ] [--page-size ] [--format json|table] +scadalink health parked-messages --site-identifier [--page ] [--page-size ] [--format json|table] +``` + +### Shared Script Commands +``` +scadalink shared-script list [--format json|table] +scadalink shared-script get --id [--format json|table] +scadalink shared-script create --name --code +scadalink shared-script update --id [--name ] [--code ] +scadalink shared-script delete --id +``` + +### Database Connection Commands +``` +scadalink db-connection list [--format json|table] +scadalink db-connection get --id [--format json|table] +scadalink db-connection create --name --connection-string [--provider ] +scadalink db-connection update --id [--name ] [--connection-string ] [--provider ] +scadalink db-connection delete --id +``` + +### Inbound API Method Commands +``` +scadalink api-method list [--format json|table] +scadalink api-method get --id [--format json|table] +scadalink api-method create --name --code [--description ] +scadalink api-method update --id [--name ] [--code ] [--description ] +scadalink api-method delete --id ``` ## Configuration diff --git a/Component-ManagementService.md b/Component-ManagementService.md index 62653ed..2879752 100644 --- a/Component-ManagementService.md +++ b/Component-ManagementService.md @@ -45,12 +45,19 @@ The ManagementActor registers itself with `ClusterClientReceptionist` at startup - **ValidateTemplate**: Run on-demand pre-deployment validation (flattening, naming collisions, script compilation). - **GetTemplateDiff**: Compare deployed vs. template-derived configuration for an instance. +### Template Members + +- **AddTemplateAttribute** / **UpdateTemplateAttribute** / **DeleteTemplateAttribute**: Manage attributes on a template. +- **AddTemplateAlarm** / **UpdateTemplateAlarm** / **DeleteTemplateAlarm**: Manage alarm definitions on a template. +- **AddTemplateScript** / **UpdateTemplateScript** / **DeleteTemplateScript**: Manage scripts on a template. +- **AddTemplateComposition** / **DeleteTemplateComposition**: Manage feature module compositions on a template. + ### Instances - **ListInstances** / **GetInstance**: Query instances, with filtering by site and area. - **CreateInstance**: Create a new instance from a template. - **UpdateInstanceOverrides**: Set attribute overrides on an instance. -- **BindDataConnections**: Bind data connections to instance attributes. +- **SetInstanceBindings** / **BindDataConnections**: Bind data connections to instance attributes. - **AssignArea**: Assign an instance to an area. - **EnableInstance** / **DisableInstance** / **DeleteInstance**: Instance lifecycle commands. @@ -85,25 +92,47 @@ The ManagementActor registers itself with `ClusterClientReceptionist` at startup ### Security (LDAP & API Keys) -- **ListApiKeys** / **CreateApiKey** / **EnableApiKey** / **DisableApiKey** / **DeleteApiKey**: Manage API keys. +- **ListApiKeys** / **CreateApiKey** / **UpdateApiKey** / **EnableApiKey** / **DisableApiKey** / **DeleteApiKey**: Manage API keys. - **ListRoleMappings** / **CreateRoleMapping** / **UpdateRoleMapping** / **DeleteRoleMapping**: Manage LDAP group-to-role mappings. +- **ListScopeRules** / **AddScopeRule** / **DeleteScopeRule**: Manage site scope rules on role mappings. ### Audit Log - **QueryAuditLog**: Query audit log entries with filtering by entity type, user, date range, etc. +### Shared Scripts + +- **ListSharedScripts** / **GetSharedScript**: Query shared script definitions. +- **CreateSharedScript** / **UpdateSharedScript** / **DeleteSharedScript**: Manage shared scripts. + +### Database Connections + +- **ListDatabaseConnections** / **GetDatabaseConnection**: Query database connection definitions. +- **CreateDatabaseConnection** / **UpdateDatabaseConnection** / **DeleteDatabaseConnection**: Manage database connections. + +### Inbound API Methods + +- **ListApiMethods** / **GetApiMethod**: Query inbound API method definitions. +- **CreateApiMethod** / **UpdateApiMethod** / **DeleteApiMethod**: Manage inbound API methods. + ### Health - **GetHealthSummary**: Query current health status of all sites. - **GetSiteHealth**: Query detailed health for a specific site. +### Remote Queries + +- **QuerySiteEventLog**: Query site event log entries from a remote site (routed via communication layer). Supports date range, keyword search, and pagination. +- **QueryParkedMessages**: Query parked (dead-letter) messages at a remote site (routed via communication layer). Supports pagination. + ## Authorization Every incoming message carries the authenticated user's identity and roles. The ManagementActor enforces the same role-based authorization rules as the Central UI: -- **Admin** role required for: site management, area management, API key management, role mapping management, system configuration. -- **Design** role required for: template authoring, shared scripts, external system definitions, database connection definitions, notification lists, inbound API method definitions. -- **Deployment** role required for: instance management, deployments, debug view, parked message management, site event log viewing. Site scoping is enforced for site-scoped Deployment users. +- **Admin** role required for: site management, area management, API key management, role mapping management, scope rule management, system configuration. +- **Design** role required for: template authoring (including template member management: attributes, alarms, scripts, compositions), shared scripts, external system definitions, database connection definitions, notification lists, inbound API method definitions. +- **Deployment** role required for: instance management, deployments, debug view, parked message queries, site event log queries. Site scoping is enforced for site-scoped Deployment users. +- **Read-only access** (any authenticated role): health summary, health site, site event log queries, parked message queries. Unauthorized commands receive an `Unauthorized` response message. Failed authorization attempts are not audit logged (consistent with existing behavior). @@ -120,7 +149,8 @@ The ManagementActor receives the following services and repositories via DI (inj - `INotificationRepository` — Notification lists and SMTP config. - `ISecurityRepository` — API keys and LDAP role mappings. - `IInboundApiRepository` — Inbound API method definitions. -- `ICentralUiRepository` — UI-related queries (shared scripts, database connections). +- `ISharedScriptRepository` / `SharedScriptService` — Shared script definitions. +- `IDatabaseConnectionRepository` — Database connection definitions. - `ICentralHealthAggregator` — Health status aggregation. - `CommunicationService` — Central-site communication for deployment and remote queries. diff --git a/src/ScadaLink.CLI/Commands/ApiMethodCommands.cs b/src/ScadaLink.CLI/Commands/ApiMethodCommands.cs new file mode 100644 index 0000000..b6860c2 --- /dev/null +++ b/src/ScadaLink.CLI/Commands/ApiMethodCommands.cs @@ -0,0 +1,118 @@ +using System.CommandLine; +using System.CommandLine.Parsing; +using ScadaLink.Commons.Messages.Management; + +namespace ScadaLink.CLI.Commands; + +public static class ApiMethodCommands +{ + public static Command Build(Option contactPointsOption, Option formatOption) + { + var command = new Command("api-method") { Description = "Manage inbound API methods" }; + + command.Add(BuildList(contactPointsOption, formatOption)); + command.Add(BuildGet(contactPointsOption, formatOption)); + command.Add(BuildCreate(contactPointsOption, formatOption)); + command.Add(BuildUpdate(contactPointsOption, formatOption)); + command.Add(BuildDelete(contactPointsOption, formatOption)); + + return command; + } + + private static Command BuildList(Option contactPointsOption, Option formatOption) + { + var cmd = new Command("list") { Description = "List all API methods" }; + cmd.SetAction(async (ParseResult result) => + { + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, new ListApiMethodsCommand()); + }); + return cmd; + } + + private static Command BuildGet(Option contactPointsOption, Option formatOption) + { + var idOption = new Option("--id") { Description = "API method ID", Required = true }; + var cmd = new Command("get") { Description = "Get an API method by ID" }; + cmd.Add(idOption); + cmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(idOption); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, new GetApiMethodCommand(id)); + }); + return cmd; + } + + private static Command BuildCreate(Option contactPointsOption, Option formatOption) + { + var nameOption = new Option("--name") { Description = "Method name", Required = true }; + var scriptOption = new Option("--script") { Description = "Script code", Required = true }; + var timeoutOption = new Option("--timeout") { Description = "Timeout in seconds" }; + timeoutOption.DefaultValueFactory = _ => 30; + var parametersOption = new Option("--parameters") { Description = "Parameter definitions JSON" }; + var returnDefOption = new Option("--return-def") { Description = "Return type definition" }; + + var cmd = new Command("create") { Description = "Create an API method" }; + cmd.Add(nameOption); + cmd.Add(scriptOption); + cmd.Add(timeoutOption); + cmd.Add(parametersOption); + cmd.Add(returnDefOption); + cmd.SetAction(async (ParseResult result) => + { + var name = result.GetValue(nameOption)!; + var script = result.GetValue(scriptOption)!; + var timeout = result.GetValue(timeoutOption); + var parameters = result.GetValue(parametersOption); + var returnDef = result.GetValue(returnDefOption); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, + new CreateApiMethodCommand(name, script, timeout, parameters, returnDef)); + }); + return cmd; + } + + private static Command BuildUpdate(Option contactPointsOption, Option formatOption) + { + var idOption = new Option("--id") { Description = "API method ID", Required = true }; + var scriptOption = new Option("--script") { Description = "Script code", Required = true }; + var timeoutOption = new Option("--timeout") { Description = "Timeout in seconds" }; + timeoutOption.DefaultValueFactory = _ => 30; + var parametersOption = new Option("--parameters") { Description = "Parameter definitions JSON" }; + var returnDefOption = new Option("--return-def") { Description = "Return type definition" }; + + var cmd = new Command("update") { Description = "Update an API method" }; + cmd.Add(idOption); + cmd.Add(scriptOption); + cmd.Add(timeoutOption); + cmd.Add(parametersOption); + cmd.Add(returnDefOption); + cmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(idOption); + var script = result.GetValue(scriptOption)!; + var timeout = result.GetValue(timeoutOption); + var parameters = result.GetValue(parametersOption); + var returnDef = result.GetValue(returnDefOption); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, + new UpdateApiMethodCommand(id, script, timeout, parameters, returnDef)); + }); + return cmd; + } + + private static Command BuildDelete(Option contactPointsOption, Option formatOption) + { + var idOption = new Option("--id") { Description = "API method ID", Required = true }; + var cmd = new Command("delete") { Description = "Delete an API method" }; + cmd.Add(idOption); + cmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(idOption); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, new DeleteApiMethodCommand(id)); + }); + return cmd; + } +} diff --git a/src/ScadaLink.CLI/Commands/DataConnectionCommands.cs b/src/ScadaLink.CLI/Commands/DataConnectionCommands.cs index cb2387d..3a55476 100644 --- a/src/ScadaLink.CLI/Commands/DataConnectionCommands.cs +++ b/src/ScadaLink.CLI/Commands/DataConnectionCommands.cs @@ -11,13 +11,69 @@ public static class DataConnectionCommands var command = new Command("data-connection") { Description = "Manage data connections" }; command.Add(BuildList(contactPointsOption, formatOption)); + command.Add(BuildGet(contactPointsOption, formatOption)); command.Add(BuildCreate(contactPointsOption, formatOption)); + command.Add(BuildUpdate(contactPointsOption, formatOption)); command.Add(BuildDelete(contactPointsOption, formatOption)); command.Add(BuildAssign(contactPointsOption, formatOption)); + command.Add(BuildUnassign(contactPointsOption, formatOption)); return command; } + private static Command BuildGet(Option contactPointsOption, Option formatOption) + { + var idOption = new Option("--id") { Description = "Data connection ID", Required = true }; + var cmd = new Command("get") { Description = "Get a data connection by ID" }; + cmd.Add(idOption); + cmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(idOption); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, new GetDataConnectionCommand(id)); + }); + return cmd; + } + + private static Command BuildUpdate(Option contactPointsOption, Option formatOption) + { + var idOption = new Option("--id") { Description = "Data connection ID", Required = true }; + var nameOption = new Option("--name") { Description = "Connection name", Required = true }; + var protocolOption = new Option("--protocol") { Description = "Protocol", Required = true }; + var configOption = new Option("--configuration") { Description = "Configuration JSON" }; + + var cmd = new Command("update") { Description = "Update a data connection" }; + cmd.Add(idOption); + cmd.Add(nameOption); + cmd.Add(protocolOption); + cmd.Add(configOption); + cmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(idOption); + var name = result.GetValue(nameOption)!; + var protocol = result.GetValue(protocolOption)!; + var config = result.GetValue(configOption); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, + new UpdateDataConnectionCommand(id, name, protocol, config)); + }); + return cmd; + } + + private static Command BuildUnassign(Option contactPointsOption, Option formatOption) + { + var idOption = new Option("--assignment-id") { Description = "Assignment ID", Required = true }; + var cmd = new Command("unassign") { Description = "Unassign a data connection from a site" }; + cmd.Add(idOption); + cmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(idOption); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, new UnassignDataConnectionFromSiteCommand(id)); + }); + return cmd; + } + private static Command BuildList(Option contactPointsOption, Option formatOption) { var cmd = new Command("list") { Description = "List all data connections" }; diff --git a/src/ScadaLink.CLI/Commands/DbConnectionCommands.cs b/src/ScadaLink.CLI/Commands/DbConnectionCommands.cs new file mode 100644 index 0000000..27b5400 --- /dev/null +++ b/src/ScadaLink.CLI/Commands/DbConnectionCommands.cs @@ -0,0 +1,101 @@ +using System.CommandLine; +using System.CommandLine.Parsing; +using ScadaLink.Commons.Messages.Management; + +namespace ScadaLink.CLI.Commands; + +public static class DbConnectionCommands +{ + public static Command Build(Option contactPointsOption, Option formatOption) + { + var command = new Command("db-connection") { Description = "Manage database connections" }; + + command.Add(BuildList(contactPointsOption, formatOption)); + command.Add(BuildGet(contactPointsOption, formatOption)); + command.Add(BuildCreate(contactPointsOption, formatOption)); + command.Add(BuildUpdate(contactPointsOption, formatOption)); + command.Add(BuildDelete(contactPointsOption, formatOption)); + + return command; + } + + private static Command BuildList(Option contactPointsOption, Option formatOption) + { + var cmd = new Command("list") { Description = "List all database connections" }; + cmd.SetAction(async (ParseResult result) => + { + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, new ListDatabaseConnectionsCommand()); + }); + return cmd; + } + + private static Command BuildGet(Option contactPointsOption, Option formatOption) + { + var idOption = new Option("--id") { Description = "Database connection ID", Required = true }; + var cmd = new Command("get") { Description = "Get a database connection by ID" }; + cmd.Add(idOption); + cmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(idOption); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, new GetDatabaseConnectionCommand(id)); + }); + return cmd; + } + + private static Command BuildCreate(Option contactPointsOption, Option formatOption) + { + var nameOption = new Option("--name") { Description = "Connection name", Required = true }; + var connStrOption = new Option("--connection-string") { Description = "Connection string", Required = true }; + + var cmd = new Command("create") { Description = "Create a database connection" }; + cmd.Add(nameOption); + cmd.Add(connStrOption); + cmd.SetAction(async (ParseResult result) => + { + var name = result.GetValue(nameOption)!; + var connStr = result.GetValue(connStrOption)!; + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, + new CreateDatabaseConnectionDefCommand(name, connStr)); + }); + return cmd; + } + + private static Command BuildUpdate(Option contactPointsOption, Option formatOption) + { + var idOption = new Option("--id") { Description = "Database connection ID", Required = true }; + var nameOption = new Option("--name") { Description = "Connection name", Required = true }; + var connStrOption = new Option("--connection-string") { Description = "Connection string", Required = true }; + + var cmd = new Command("update") { Description = "Update a database connection" }; + cmd.Add(idOption); + cmd.Add(nameOption); + cmd.Add(connStrOption); + cmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(idOption); + var name = result.GetValue(nameOption)!; + var connStr = result.GetValue(connStrOption)!; + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, + new UpdateDatabaseConnectionDefCommand(id, name, connStr)); + }); + return cmd; + } + + private static Command BuildDelete(Option contactPointsOption, Option formatOption) + { + var idOption = new Option("--id") { Description = "Database connection ID", Required = true }; + var cmd = new Command("delete") { Description = "Delete a database connection" }; + cmd.Add(idOption); + cmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(idOption); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, new DeleteDatabaseConnectionDefCommand(id)); + }); + return cmd; + } +} diff --git a/src/ScadaLink.CLI/Commands/ExternalSystemCommands.cs b/src/ScadaLink.CLI/Commands/ExternalSystemCommands.cs index fbb443a..a2f35b2 100644 --- a/src/ScadaLink.CLI/Commands/ExternalSystemCommands.cs +++ b/src/ScadaLink.CLI/Commands/ExternalSystemCommands.cs @@ -11,12 +11,56 @@ public static class ExternalSystemCommands var command = new Command("external-system") { Description = "Manage external systems" }; command.Add(BuildList(contactPointsOption, formatOption)); + command.Add(BuildGet(contactPointsOption, formatOption)); command.Add(BuildCreate(contactPointsOption, formatOption)); + command.Add(BuildUpdate(contactPointsOption, formatOption)); command.Add(BuildDelete(contactPointsOption, formatOption)); return command; } + private static Command BuildGet(Option contactPointsOption, Option formatOption) + { + var idOption = new Option("--id") { Description = "External system ID", Required = true }; + var cmd = new Command("get") { Description = "Get an external system by ID" }; + cmd.Add(idOption); + cmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(idOption); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, new GetExternalSystemCommand(id)); + }); + return cmd; + } + + private static Command BuildUpdate(Option contactPointsOption, Option formatOption) + { + var idOption = new Option("--id") { Description = "External system ID", Required = true }; + var nameOption = new Option("--name") { Description = "System name", Required = true }; + var urlOption = new Option("--endpoint-url") { Description = "Endpoint URL", Required = true }; + var authTypeOption = new Option("--auth-type") { Description = "Auth type", Required = true }; + var authConfigOption = new Option("--auth-config") { Description = "Auth configuration JSON" }; + + var cmd = new Command("update") { Description = "Update an external system" }; + cmd.Add(idOption); + cmd.Add(nameOption); + cmd.Add(urlOption); + cmd.Add(authTypeOption); + cmd.Add(authConfigOption); + cmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(idOption); + var name = result.GetValue(nameOption)!; + var url = result.GetValue(urlOption)!; + var authType = result.GetValue(authTypeOption)!; + var authConfig = result.GetValue(authConfigOption); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, + new UpdateExternalSystemCommand(id, name, url, authType, authConfig)); + }); + return cmd; + } + private static Command BuildList(Option contactPointsOption, Option formatOption) { var cmd = new Command("list") { Description = "List all external systems" }; diff --git a/src/ScadaLink.CLI/Commands/HealthCommands.cs b/src/ScadaLink.CLI/Commands/HealthCommands.cs index dc71a40..0ced68b 100644 --- a/src/ScadaLink.CLI/Commands/HealthCommands.cs +++ b/src/ScadaLink.CLI/Commands/HealthCommands.cs @@ -12,6 +12,8 @@ public static class HealthCommands command.Add(BuildSummary(contactPointsOption, formatOption)); command.Add(BuildSite(contactPointsOption, formatOption)); + command.Add(BuildEventLog(contactPointsOption, formatOption)); + command.Add(BuildParkedMessages(contactPointsOption, formatOption)); return command; } @@ -40,4 +42,67 @@ public static class HealthCommands }); return cmd; } + + private static Command BuildEventLog(Option contactPointsOption, Option formatOption) + { + var siteOption = new Option("--site") { Description = "Site identifier", Required = true }; + var eventTypeOption = new Option("--event-type") { Description = "Filter by event type" }; + var severityOption = new Option("--severity") { Description = "Filter by severity" }; + var keywordOption = new Option("--keyword") { Description = "Keyword search" }; + var fromOption = new Option("--from") { Description = "Start date (ISO 8601)" }; + var toOption = new Option("--to") { Description = "End date (ISO 8601)" }; + var pageOption = new Option("--page") { Description = "Page number" }; + pageOption.DefaultValueFactory = _ => 1; + var pageSizeOption = new Option("--page-size") { Description = "Page size" }; + pageSizeOption.DefaultValueFactory = _ => 50; + + var cmd = new Command("event-log") { Description = "Query site event logs" }; + cmd.Add(siteOption); + cmd.Add(eventTypeOption); + cmd.Add(severityOption); + cmd.Add(keywordOption); + cmd.Add(fromOption); + cmd.Add(toOption); + cmd.Add(pageOption); + cmd.Add(pageSizeOption); + cmd.SetAction(async (ParseResult result) => + { + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, + new QueryEventLogsCommand( + result.GetValue(siteOption)!, + result.GetValue(eventTypeOption), + result.GetValue(severityOption), + result.GetValue(keywordOption), + result.GetValue(fromOption), + result.GetValue(toOption), + result.GetValue(pageOption), + result.GetValue(pageSizeOption))); + }); + return cmd; + } + + private static Command BuildParkedMessages(Option contactPointsOption, Option formatOption) + { + var siteOption = new Option("--site") { Description = "Site identifier", Required = true }; + var pageOption = new Option("--page") { Description = "Page number" }; + pageOption.DefaultValueFactory = _ => 1; + var pageSizeOption = new Option("--page-size") { Description = "Page size" }; + pageSizeOption.DefaultValueFactory = _ => 50; + + var cmd = new Command("parked-messages") { Description = "Query parked messages at a site" }; + cmd.Add(siteOption); + cmd.Add(pageOption); + cmd.Add(pageSizeOption); + cmd.SetAction(async (ParseResult result) => + { + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, + new QueryParkedMessagesCommand( + result.GetValue(siteOption)!, + result.GetValue(pageOption), + result.GetValue(pageSizeOption))); + }); + return cmd; + } } diff --git a/src/ScadaLink.CLI/Commands/InstanceCommands.cs b/src/ScadaLink.CLI/Commands/InstanceCommands.cs index a842853..42b0c8c 100644 --- a/src/ScadaLink.CLI/Commands/InstanceCommands.cs +++ b/src/ScadaLink.CLI/Commands/InstanceCommands.cs @@ -11,7 +11,9 @@ public static class InstanceCommands var command = new Command("instance") { Description = "Manage instances" }; command.Add(BuildList(contactPointsOption, formatOption)); + command.Add(BuildGet(contactPointsOption, formatOption)); command.Add(BuildCreate(contactPointsOption, formatOption)); + command.Add(BuildSetBindings(contactPointsOption, formatOption)); command.Add(BuildDeploy(contactPointsOption, formatOption)); command.Add(BuildEnable(contactPointsOption, formatOption)); command.Add(BuildDisable(contactPointsOption, formatOption)); @@ -20,6 +22,43 @@ public static class InstanceCommands return command; } + private static Command BuildGet(Option contactPointsOption, Option formatOption) + { + var idOption = new Option("--id") { Description = "Instance ID", Required = true }; + var cmd = new Command("get") { Description = "Get an instance by ID" }; + cmd.Add(idOption); + cmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(idOption); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, new GetInstanceCommand(id)); + }); + return cmd; + } + + private static Command BuildSetBindings(Option contactPointsOption, Option formatOption) + { + var idOption = new Option("--id") { Description = "Instance ID", Required = true }; + var bindingsOption = new Option("--bindings") { Description = "JSON array of [attributeName, dataConnectionId] pairs", Required = true }; + + var cmd = new Command("set-bindings") { Description = "Set data connection bindings for an instance" }; + cmd.Add(idOption); + cmd.Add(bindingsOption); + cmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(idOption); + var bindingsJson = result.GetValue(bindingsOption)!; + var pairs = System.Text.Json.JsonSerializer.Deserialize>>(bindingsJson) + ?? throw new InvalidOperationException("Invalid bindings JSON"); + var bindings = pairs.Select(p => + (p[0].ToString()!, int.Parse(p[1].ToString()!))).ToList(); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, + new SetConnectionBindingsCommand(id, bindings)); + }); + return cmd; + } + private static Command BuildList(Option contactPointsOption, Option formatOption) { var siteIdOption = new Option("--site-id") { Description = "Filter by site ID" }; diff --git a/src/ScadaLink.CLI/Commands/NotificationCommands.cs b/src/ScadaLink.CLI/Commands/NotificationCommands.cs index 4076eea..54962e4 100644 --- a/src/ScadaLink.CLI/Commands/NotificationCommands.cs +++ b/src/ScadaLink.CLI/Commands/NotificationCommands.cs @@ -11,12 +11,91 @@ public static class NotificationCommands var command = new Command("notification") { Description = "Manage notification lists" }; command.Add(BuildList(contactPointsOption, formatOption)); + command.Add(BuildGet(contactPointsOption, formatOption)); command.Add(BuildCreate(contactPointsOption, formatOption)); + command.Add(BuildUpdate(contactPointsOption, formatOption)); command.Add(BuildDelete(contactPointsOption, formatOption)); + command.Add(BuildSmtp(contactPointsOption, formatOption)); return command; } + private static Command BuildGet(Option contactPointsOption, Option formatOption) + { + var idOption = new Option("--id") { Description = "Notification list ID", Required = true }; + var cmd = new Command("get") { Description = "Get a notification list by ID" }; + cmd.Add(idOption); + cmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(idOption); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, new GetNotificationListCommand(id)); + }); + return cmd; + } + + private static Command BuildUpdate(Option contactPointsOption, Option formatOption) + { + var idOption = new Option("--id") { Description = "Notification list ID", Required = true }; + var nameOption = new Option("--name") { Description = "List name", Required = true }; + var emailsOption = new Option("--emails") { Description = "Comma-separated recipient emails", Required = true }; + + var cmd = new Command("update") { Description = "Update a notification list" }; + cmd.Add(idOption); + cmd.Add(nameOption); + cmd.Add(emailsOption); + cmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(idOption); + var name = result.GetValue(nameOption)!; + var emailsRaw = result.GetValue(emailsOption)!; + var emails = emailsRaw.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList(); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, + new UpdateNotificationListCommand(id, name, emails)); + }); + return cmd; + } + + private static Command BuildSmtp(Option contactPointsOption, Option formatOption) + { + var group = new Command("smtp") { Description = "Manage SMTP configuration" }; + + var listCmd = new Command("list") { Description = "List SMTP configurations" }; + listCmd.SetAction(async (ParseResult result) => + { + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, new ListSmtpConfigsCommand()); + }); + group.Add(listCmd); + + var idOption = new Option("--id") { Description = "SMTP config ID", Required = true }; + var serverOption = new Option("--server") { Description = "SMTP server", Required = true }; + var portOption = new Option("--port") { Description = "SMTP port", Required = true }; + var authModeOption = new Option("--auth-mode") { Description = "Auth mode", Required = true }; + var fromOption = new Option("--from-address") { Description = "From email address", Required = true }; + var updateCmd = new Command("update") { Description = "Update SMTP configuration" }; + updateCmd.Add(idOption); + updateCmd.Add(serverOption); + updateCmd.Add(portOption); + updateCmd.Add(authModeOption); + updateCmd.Add(fromOption); + updateCmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(idOption); + var server = result.GetValue(serverOption)!; + var port = result.GetValue(portOption); + var authMode = result.GetValue(authModeOption)!; + var from = result.GetValue(fromOption)!; + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, + new UpdateSmtpConfigCommand(id, server, port, authMode, from)); + }); + group.Add(updateCmd); + + return group; + } + private static Command BuildList(Option contactPointsOption, Option formatOption) { var cmd = new Command("list") { Description = "List all notification lists" }; diff --git a/src/ScadaLink.CLI/Commands/SecurityCommands.cs b/src/ScadaLink.CLI/Commands/SecurityCommands.cs index 44d6819..1628820 100644 --- a/src/ScadaLink.CLI/Commands/SecurityCommands.cs +++ b/src/ScadaLink.CLI/Commands/SecurityCommands.cs @@ -12,6 +12,7 @@ public static class SecurityCommands command.Add(BuildApiKey(contactPointsOption, formatOption)); command.Add(BuildRoleMapping(contactPointsOption, formatOption)); + command.Add(BuildScopeRule(contactPointsOption, formatOption)); return command; } @@ -50,6 +51,20 @@ public static class SecurityCommands }); group.Add(deleteCmd); + var updateIdOption = new Option("--id") { Description = "API key ID", Required = true }; + var enabledOption = new Option("--enabled") { Description = "Enable or disable", Required = true }; + var updateCmd = new Command("update") { Description = "Enable or disable an API key" }; + updateCmd.Add(updateIdOption); + updateCmd.Add(enabledOption); + updateCmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(updateIdOption); + var enabled = result.GetValue(enabledOption); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, new UpdateApiKeyCommand(id, enabled)); + }); + group.Add(updateCmd); + return group; } @@ -91,6 +106,67 @@ public static class SecurityCommands }); group.Add(deleteCmd); + var updateIdOption = new Option("--id") { Description = "Mapping ID", Required = true }; + var updateLdapGroupOption = new Option("--ldap-group") { Description = "LDAP group name", Required = true }; + var updateRoleOption = new Option("--role") { Description = "Role name", Required = true }; + var updateCmd = new Command("update") { Description = "Update a role mapping" }; + updateCmd.Add(updateIdOption); + updateCmd.Add(updateLdapGroupOption); + updateCmd.Add(updateRoleOption); + updateCmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(updateIdOption); + var ldapGroup = result.GetValue(updateLdapGroupOption)!; + var role = result.GetValue(updateRoleOption)!; + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, + new UpdateRoleMappingCommand(id, ldapGroup, role)); + }); + group.Add(updateCmd); + + return group; + } + + private static Command BuildScopeRule(Option contactPointsOption, Option formatOption) + { + var group = new Command("scope-rule") { Description = "Manage LDAP scope rules" }; + + var mappingIdOption = new Option("--mapping-id") { Description = "Role mapping ID", Required = true }; + var listCmd = new Command("list") { Description = "List scope rules for a mapping" }; + listCmd.Add(mappingIdOption); + listCmd.SetAction(async (ParseResult result) => + { + var mappingId = result.GetValue(mappingIdOption); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, new ListScopeRulesCommand(mappingId)); + }); + group.Add(listCmd); + + var addMappingIdOption = new Option("--mapping-id") { Description = "Role mapping ID", Required = true }; + var siteIdOption = new Option("--site-id") { Description = "Site ID", Required = true }; + var addCmd = new Command("add") { Description = "Add a scope rule" }; + addCmd.Add(addMappingIdOption); + addCmd.Add(siteIdOption); + addCmd.SetAction(async (ParseResult result) => + { + var mappingId = result.GetValue(addMappingIdOption); + var siteId = result.GetValue(siteIdOption); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, new AddScopeRuleCommand(mappingId, siteId)); + }); + group.Add(addCmd); + + var deleteIdOption = new Option("--id") { Description = "Scope rule ID", Required = true }; + var deleteCmd = new Command("delete") { Description = "Delete a scope rule" }; + deleteCmd.Add(deleteIdOption); + deleteCmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(deleteIdOption); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, new DeleteScopeRuleCommand(id)); + }); + group.Add(deleteCmd); + return group; } } diff --git a/src/ScadaLink.CLI/Commands/SharedScriptCommands.cs b/src/ScadaLink.CLI/Commands/SharedScriptCommands.cs new file mode 100644 index 0000000..bac1a0f --- /dev/null +++ b/src/ScadaLink.CLI/Commands/SharedScriptCommands.cs @@ -0,0 +1,113 @@ +using System.CommandLine; +using System.CommandLine.Parsing; +using ScadaLink.Commons.Messages.Management; + +namespace ScadaLink.CLI.Commands; + +public static class SharedScriptCommands +{ + public static Command Build(Option contactPointsOption, Option formatOption) + { + var command = new Command("shared-script") { Description = "Manage shared scripts" }; + + command.Add(BuildList(contactPointsOption, formatOption)); + command.Add(BuildGet(contactPointsOption, formatOption)); + command.Add(BuildCreate(contactPointsOption, formatOption)); + command.Add(BuildUpdate(contactPointsOption, formatOption)); + command.Add(BuildDelete(contactPointsOption, formatOption)); + + return command; + } + + private static Command BuildList(Option contactPointsOption, Option formatOption) + { + var cmd = new Command("list") { Description = "List all shared scripts" }; + cmd.SetAction(async (ParseResult result) => + { + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, new ListSharedScriptsCommand()); + }); + return cmd; + } + + private static Command BuildGet(Option contactPointsOption, Option formatOption) + { + var idOption = new Option("--id") { Description = "Shared script ID", Required = true }; + var cmd = new Command("get") { Description = "Get a shared script by ID" }; + cmd.Add(idOption); + cmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(idOption); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, new GetSharedScriptCommand(id)); + }); + return cmd; + } + + private static Command BuildCreate(Option contactPointsOption, Option formatOption) + { + var nameOption = new Option("--name") { Description = "Script name", Required = true }; + var codeOption = new Option("--code") { Description = "Script code", Required = true }; + var parametersOption = new Option("--parameters") { Description = "Parameter definitions JSON" }; + var returnDefOption = new Option("--return-def") { Description = "Return type definition" }; + + var cmd = new Command("create") { Description = "Create a shared script" }; + cmd.Add(nameOption); + cmd.Add(codeOption); + cmd.Add(parametersOption); + cmd.Add(returnDefOption); + cmd.SetAction(async (ParseResult result) => + { + var name = result.GetValue(nameOption)!; + var code = result.GetValue(codeOption)!; + var parameters = result.GetValue(parametersOption); + var returnDef = result.GetValue(returnDefOption); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, + new CreateSharedScriptCommand(name, code, parameters, returnDef)); + }); + return cmd; + } + + private static Command BuildUpdate(Option contactPointsOption, Option formatOption) + { + var idOption = new Option("--id") { Description = "Shared script ID", Required = true }; + var nameOption = new Option("--name") { Description = "Script name", Required = true }; + var codeOption = new Option("--code") { Description = "Script code", Required = true }; + var parametersOption = new Option("--parameters") { Description = "Parameter definitions JSON" }; + var returnDefOption = new Option("--return-def") { Description = "Return type definition" }; + + var cmd = new Command("update") { Description = "Update a shared script" }; + cmd.Add(idOption); + cmd.Add(nameOption); + cmd.Add(codeOption); + cmd.Add(parametersOption); + cmd.Add(returnDefOption); + cmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(idOption); + var name = result.GetValue(nameOption)!; + var code = result.GetValue(codeOption)!; + var parameters = result.GetValue(parametersOption); + var returnDef = result.GetValue(returnDefOption); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, + new UpdateSharedScriptCommand(id, name, code, parameters, returnDef)); + }); + return cmd; + } + + private static Command BuildDelete(Option contactPointsOption, Option formatOption) + { + var idOption = new Option("--id") { Description = "Shared script ID", Required = true }; + var cmd = new Command("delete") { Description = "Delete a shared script" }; + cmd.Add(idOption); + cmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(idOption); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, new DeleteSharedScriptCommand(id)); + }); + return cmd; + } +} diff --git a/src/ScadaLink.CLI/Commands/SiteCommands.cs b/src/ScadaLink.CLI/Commands/SiteCommands.cs index fab0f45..1cd51f4 100644 --- a/src/ScadaLink.CLI/Commands/SiteCommands.cs +++ b/src/ScadaLink.CLI/Commands/SiteCommands.cs @@ -11,14 +11,30 @@ public static class SiteCommands var command = new Command("site") { Description = "Manage sites" }; command.Add(BuildList(contactPointsOption, formatOption)); + command.Add(BuildGet(contactPointsOption, formatOption)); command.Add(BuildCreate(contactPointsOption, formatOption)); command.Add(BuildUpdate(contactPointsOption, formatOption)); command.Add(BuildDelete(contactPointsOption, formatOption)); command.Add(BuildDeployArtifacts(contactPointsOption, formatOption)); + command.Add(BuildArea(contactPointsOption, formatOption)); return command; } + private static Command BuildGet(Option contactPointsOption, Option formatOption) + { + var idOption = new Option("--id") { Description = "Site ID", Required = true }; + var cmd = new Command("get") { Description = "Get a site by ID" }; + cmd.Add(idOption); + cmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(idOption); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, new GetSiteCommand(id)); + }); + return cmd; + } + private static Command BuildList(Option contactPointsOption, Option formatOption) { var cmd = new Command("list") { Description = "List all sites" }; @@ -100,6 +116,67 @@ public static class SiteCommands return cmd; } + private static Command BuildArea(Option contactPointsOption, Option formatOption) + { + var group = new Command("area") { Description = "Manage areas" }; + + var siteIdOption = new Option("--site-id") { Description = "Site ID", Required = true }; + var listCmd = new Command("list") { Description = "List areas for a site" }; + listCmd.Add(siteIdOption); + listCmd.SetAction(async (ParseResult result) => + { + var siteId = result.GetValue(siteIdOption); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, new ListAreasCommand(siteId)); + }); + group.Add(listCmd); + + var createSiteIdOption = new Option("--site-id") { Description = "Site ID", Required = true }; + var nameOption = new Option("--name") { Description = "Area name", Required = true }; + var parentOption = new Option("--parent-id") { Description = "Parent area ID" }; + var createCmd = new Command("create") { Description = "Create an area" }; + createCmd.Add(createSiteIdOption); + createCmd.Add(nameOption); + createCmd.Add(parentOption); + createCmd.SetAction(async (ParseResult result) => + { + var siteId = result.GetValue(createSiteIdOption); + var name = result.GetValue(nameOption)!; + var parentId = result.GetValue(parentOption); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, + new CreateAreaCommand(siteId, name, parentId)); + }); + group.Add(createCmd); + + var updateIdOption = new Option("--id") { Description = "Area ID", Required = true }; + var updateNameOption = new Option("--name") { Description = "New area name", Required = true }; + var updateCmd = new Command("update") { Description = "Update an area" }; + updateCmd.Add(updateIdOption); + updateCmd.Add(updateNameOption); + updateCmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(updateIdOption); + var name = result.GetValue(updateNameOption)!; + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, new UpdateAreaCommand(id, name)); + }); + group.Add(updateCmd); + + var deleteIdOption = new Option("--id") { Description = "Area ID", Required = true }; + var deleteCmd = new Command("delete") { Description = "Delete an area" }; + deleteCmd.Add(deleteIdOption); + deleteCmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(deleteIdOption); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, new DeleteAreaCommand(id)); + }); + group.Add(deleteCmd); + + return group; + } + private static Command BuildDeployArtifacts(Option contactPointsOption, Option formatOption) { var siteIdOption = new Option("--site-id") { Description = "Target site ID (all sites if omitted)" }; diff --git a/src/ScadaLink.CLI/Commands/TemplateCommands.cs b/src/ScadaLink.CLI/Commands/TemplateCommands.cs index 847f580..07684bf 100644 --- a/src/ScadaLink.CLI/Commands/TemplateCommands.cs +++ b/src/ScadaLink.CLI/Commands/TemplateCommands.cs @@ -13,7 +13,13 @@ public static class TemplateCommands command.Add(BuildList(contactPointsOption, formatOption)); command.Add(BuildGet(contactPointsOption, formatOption)); command.Add(BuildCreate(contactPointsOption, formatOption)); + command.Add(BuildUpdate(contactPointsOption, formatOption)); + command.Add(BuildValidate(contactPointsOption, formatOption)); command.Add(BuildDelete(contactPointsOption, formatOption)); + command.Add(BuildAttribute(contactPointsOption, formatOption)); + command.Add(BuildAlarm(contactPointsOption, formatOption)); + command.Add(BuildScript(contactPointsOption, formatOption)); + command.Add(BuildComposition(contactPointsOption, formatOption)); return command; } @@ -65,6 +71,45 @@ public static class TemplateCommands return cmd; } + private static Command BuildUpdate(Option contactPointsOption, Option formatOption) + { + var idOption = new Option("--id") { Description = "Template ID", Required = true }; + var nameOption = new Option("--name") { Description = "Template name", Required = true }; + var descOption = new Option("--description") { Description = "Template description" }; + var parentOption = new Option("--parent-id") { Description = "Parent template ID" }; + + var cmd = new Command("update") { Description = "Update a template" }; + cmd.Add(idOption); + cmd.Add(nameOption); + cmd.Add(descOption); + cmd.Add(parentOption); + cmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(idOption); + var name = result.GetValue(nameOption)!; + var desc = result.GetValue(descOption); + var parentId = result.GetValue(parentOption); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, + new UpdateTemplateCommand(id, name, desc, parentId)); + }); + return cmd; + } + + private static Command BuildValidate(Option contactPointsOption, Option formatOption) + { + var idOption = new Option("--id") { Description = "Template ID", Required = true }; + var cmd = new Command("validate") { Description = "Validate a template" }; + cmd.Add(idOption); + cmd.SetAction(async (ParseResult result) => + { + var id = result.GetValue(idOption); + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, new ValidateTemplateCommand(id)); + }); + return cmd; + } + private static Command BuildDelete(Option contactPointsOption, Option formatOption) { var idOption = new Option("--id") { Description = "Template ID", Required = true }; @@ -78,4 +123,281 @@ public static class TemplateCommands }); return cmd; } + + private static Command BuildAttribute(Option contactPointsOption, Option formatOption) + { + var group = new Command("attribute") { Description = "Manage template attributes" }; + + var templateIdOption = new Option("--template-id") { Description = "Template ID", Required = true }; + var nameOption = new Option("--name") { Description = "Attribute name", Required = true }; + var dataTypeOption = new Option("--data-type") { Description = "Data type", Required = true }; + var valueOption = new Option("--value") { Description = "Default value" }; + var descOption = new Option("--description") { Description = "Description" }; + var sourceOption = new Option("--data-source") { Description = "Data source reference" }; + var lockedOption = new Option("--locked") { Description = "Lock status" }; + lockedOption.DefaultValueFactory = _ => false; + + var addCmd = new Command("add") { Description = "Add an attribute to a template" }; + addCmd.Add(templateIdOption); + addCmd.Add(nameOption); + addCmd.Add(dataTypeOption); + addCmd.Add(valueOption); + addCmd.Add(descOption); + addCmd.Add(sourceOption); + addCmd.Add(lockedOption); + addCmd.SetAction(async (ParseResult result) => + { + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, + new AddTemplateAttributeCommand( + result.GetValue(templateIdOption), + result.GetValue(nameOption)!, + result.GetValue(dataTypeOption)!, + result.GetValue(valueOption), + result.GetValue(descOption), + result.GetValue(sourceOption), + result.GetValue(lockedOption))); + }); + group.Add(addCmd); + + var updateIdOption = new Option("--id") { Description = "Attribute ID", Required = true }; + var updateNameOption = new Option("--name") { Description = "Attribute name", Required = true }; + var updateDataTypeOption = new Option("--data-type") { Description = "Data type", Required = true }; + var updateValueOption = new Option("--value") { Description = "Default value" }; + var updateDescOption = new Option("--description") { Description = "Description" }; + var updateSourceOption = new Option("--data-source") { Description = "Data source reference" }; + var updateLockedOption = new Option("--locked") { Description = "Lock status" }; + updateLockedOption.DefaultValueFactory = _ => false; + + var updateCmd = new Command("update") { Description = "Update a template attribute" }; + updateCmd.Add(updateIdOption); + updateCmd.Add(updateNameOption); + updateCmd.Add(updateDataTypeOption); + updateCmd.Add(updateValueOption); + updateCmd.Add(updateDescOption); + updateCmd.Add(updateSourceOption); + updateCmd.Add(updateLockedOption); + updateCmd.SetAction(async (ParseResult result) => + { + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, + new UpdateTemplateAttributeCommand( + result.GetValue(updateIdOption), + result.GetValue(updateNameOption)!, + result.GetValue(updateDataTypeOption)!, + result.GetValue(updateValueOption), + result.GetValue(updateDescOption), + result.GetValue(updateSourceOption), + result.GetValue(updateLockedOption))); + }); + group.Add(updateCmd); + + var deleteIdOption = new Option("--id") { Description = "Attribute ID", Required = true }; + var deleteCmd = new Command("delete") { Description = "Delete a template attribute" }; + deleteCmd.Add(deleteIdOption); + deleteCmd.SetAction(async (ParseResult result) => + { + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, + new DeleteTemplateAttributeCommand(result.GetValue(deleteIdOption))); + }); + group.Add(deleteCmd); + + return group; + } + + private static Command BuildAlarm(Option contactPointsOption, Option formatOption) + { + var group = new Command("alarm") { Description = "Manage template alarms" }; + + var templateIdOption = new Option("--template-id") { Description = "Template ID", Required = true }; + var nameOption = new Option("--name") { Description = "Alarm name", Required = true }; + var triggerTypeOption = new Option("--trigger-type") { Description = "Trigger type", Required = true }; + var priorityOption = new Option("--priority") { Description = "Alarm priority", Required = true }; + var descOption = new Option("--description") { Description = "Description" }; + var triggerConfigOption = new Option("--trigger-config") { Description = "Trigger configuration JSON" }; + var lockedOption = new Option("--locked") { Description = "Lock status" }; + lockedOption.DefaultValueFactory = _ => false; + + var addCmd = new Command("add") { Description = "Add an alarm to a template" }; + addCmd.Add(templateIdOption); + addCmd.Add(nameOption); + addCmd.Add(triggerTypeOption); + addCmd.Add(priorityOption); + addCmd.Add(descOption); + addCmd.Add(triggerConfigOption); + addCmd.Add(lockedOption); + addCmd.SetAction(async (ParseResult result) => + { + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, + new AddTemplateAlarmCommand( + result.GetValue(templateIdOption), + result.GetValue(nameOption)!, + result.GetValue(triggerTypeOption)!, + result.GetValue(priorityOption)!, + result.GetValue(descOption), + result.GetValue(triggerConfigOption), + result.GetValue(lockedOption))); + }); + group.Add(addCmd); + + var updateIdOption = new Option("--id") { Description = "Alarm ID", Required = true }; + var updateNameOption = new Option("--name") { Description = "Alarm name", Required = true }; + var updateTriggerTypeOption = new Option("--trigger-type") { Description = "Trigger type", Required = true }; + var updatePriorityOption = new Option("--priority") { Description = "Alarm priority", Required = true }; + var updateDescOption = new Option("--description") { Description = "Description" }; + var updateTriggerConfigOption = new Option("--trigger-config") { Description = "Trigger configuration JSON" }; + var updateLockedOption = new Option("--locked") { Description = "Lock status" }; + updateLockedOption.DefaultValueFactory = _ => false; + + var updateCmd = new Command("update") { Description = "Update a template alarm" }; + updateCmd.Add(updateIdOption); + updateCmd.Add(updateNameOption); + updateCmd.Add(updateTriggerTypeOption); + updateCmd.Add(updatePriorityOption); + updateCmd.Add(updateDescOption); + updateCmd.Add(updateTriggerConfigOption); + updateCmd.Add(updateLockedOption); + updateCmd.SetAction(async (ParseResult result) => + { + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, + new UpdateTemplateAlarmCommand( + result.GetValue(updateIdOption), + result.GetValue(updateNameOption)!, + result.GetValue(updateTriggerTypeOption)!, + result.GetValue(updatePriorityOption)!, + result.GetValue(updateDescOption), + result.GetValue(updateTriggerConfigOption), + result.GetValue(updateLockedOption))); + }); + group.Add(updateCmd); + + var deleteIdOption = new Option("--id") { Description = "Alarm ID", Required = true }; + var deleteCmd = new Command("delete") { Description = "Delete a template alarm" }; + deleteCmd.Add(deleteIdOption); + deleteCmd.SetAction(async (ParseResult result) => + { + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, + new DeleteTemplateAlarmCommand(result.GetValue(deleteIdOption))); + }); + group.Add(deleteCmd); + + return group; + } + + private static Command BuildScript(Option contactPointsOption, Option formatOption) + { + var group = new Command("script") { Description = "Manage template scripts" }; + + var templateIdOption = new Option("--template-id") { Description = "Template ID", Required = true }; + var nameOption = new Option("--name") { Description = "Script name", Required = true }; + var codeOption = new Option("--code") { Description = "Script code", Required = true }; + var triggerTypeOption = new Option("--trigger-type") { Description = "Trigger type", Required = true }; + var triggerConfigOption = new Option("--trigger-config") { Description = "Trigger configuration JSON" }; + var lockedOption = new Option("--locked") { Description = "Lock status" }; + lockedOption.DefaultValueFactory = _ => false; + + var addCmd = new Command("add") { Description = "Add a script to a template" }; + addCmd.Add(templateIdOption); + addCmd.Add(nameOption); + addCmd.Add(codeOption); + addCmd.Add(triggerTypeOption); + addCmd.Add(triggerConfigOption); + addCmd.Add(lockedOption); + addCmd.SetAction(async (ParseResult result) => + { + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, + new AddTemplateScriptCommand( + result.GetValue(templateIdOption), + result.GetValue(nameOption)!, + result.GetValue(codeOption)!, + result.GetValue(triggerTypeOption)!, + result.GetValue(triggerConfigOption), + result.GetValue(lockedOption))); + }); + group.Add(addCmd); + + var updateIdOption = new Option("--id") { Description = "Script ID", Required = true }; + var updateNameOption = new Option("--name") { Description = "Script name", Required = true }; + var updateCodeOption = new Option("--code") { Description = "Script code", Required = true }; + var updateTriggerTypeOption = new Option("--trigger-type") { Description = "Trigger type", Required = true }; + var updateTriggerConfigOption = new Option("--trigger-config") { Description = "Trigger configuration JSON" }; + var updateLockedOption = new Option("--locked") { Description = "Lock status" }; + updateLockedOption.DefaultValueFactory = _ => false; + + var updateCmd = new Command("update") { Description = "Update a template script" }; + updateCmd.Add(updateIdOption); + updateCmd.Add(updateNameOption); + updateCmd.Add(updateCodeOption); + updateCmd.Add(updateTriggerTypeOption); + updateCmd.Add(updateTriggerConfigOption); + updateCmd.Add(updateLockedOption); + updateCmd.SetAction(async (ParseResult result) => + { + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, + new UpdateTemplateScriptCommand( + result.GetValue(updateIdOption), + result.GetValue(updateNameOption)!, + result.GetValue(updateCodeOption)!, + result.GetValue(updateTriggerTypeOption)!, + result.GetValue(updateTriggerConfigOption), + result.GetValue(updateLockedOption))); + }); + group.Add(updateCmd); + + var deleteIdOption = new Option("--id") { Description = "Script ID", Required = true }; + var deleteCmd = new Command("delete") { Description = "Delete a template script" }; + deleteCmd.Add(deleteIdOption); + deleteCmd.SetAction(async (ParseResult result) => + { + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, + new DeleteTemplateScriptCommand(result.GetValue(deleteIdOption))); + }); + group.Add(deleteCmd); + + return group; + } + + private static Command BuildComposition(Option contactPointsOption, Option formatOption) + { + var group = new Command("composition") { Description = "Manage template compositions" }; + + var templateIdOption = new Option("--template-id") { Description = "Template ID", Required = true }; + var instanceNameOption = new Option("--instance-name") { Description = "Composed instance name", Required = true }; + var composedTemplateIdOption = new Option("--composed-template-id") { Description = "Composed template ID", Required = true }; + + var addCmd = new Command("add") { Description = "Add a composition to a template" }; + addCmd.Add(templateIdOption); + addCmd.Add(instanceNameOption); + addCmd.Add(composedTemplateIdOption); + addCmd.SetAction(async (ParseResult result) => + { + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, + new AddTemplateCompositionCommand( + result.GetValue(templateIdOption), + result.GetValue(instanceNameOption)!, + result.GetValue(composedTemplateIdOption))); + }); + group.Add(addCmd); + + var deleteIdOption = new Option("--id") { Description = "Composition ID", Required = true }; + var deleteCmd = new Command("delete") { Description = "Delete a template composition" }; + deleteCmd.Add(deleteIdOption); + deleteCmd.SetAction(async (ParseResult result) => + { + return await CommandHelpers.ExecuteCommandAsync( + result, contactPointsOption, formatOption, + new DeleteTemplateCompositionCommand(result.GetValue(deleteIdOption))); + }); + group.Add(deleteCmd); + + return group; + } } diff --git a/src/ScadaLink.CLI/Program.cs b/src/ScadaLink.CLI/Program.cs index 6fc2ccc..143f1c0 100644 --- a/src/ScadaLink.CLI/Program.cs +++ b/src/ScadaLink.CLI/Program.cs @@ -26,6 +26,9 @@ rootCommand.Add(NotificationCommands.Build(contactPointsOption, formatOption)); rootCommand.Add(SecurityCommands.Build(contactPointsOption, formatOption)); rootCommand.Add(AuditLogCommands.Build(contactPointsOption, formatOption)); rootCommand.Add(HealthCommands.Build(contactPointsOption, formatOption)); +rootCommand.Add(SharedScriptCommands.Build(contactPointsOption, formatOption)); +rootCommand.Add(DbConnectionCommands.Build(contactPointsOption, formatOption)); +rootCommand.Add(ApiMethodCommands.Build(contactPointsOption, formatOption)); rootCommand.SetAction(_ => { diff --git a/src/ScadaLink.CLI/README.md b/src/ScadaLink.CLI/README.md index 295d129..93b1a4e 100644 --- a/src/ScadaLink.CLI/README.md +++ b/src/ScadaLink.CLI/README.md @@ -119,6 +119,21 @@ scadalink --contact-points template create --name [--description | `--description` | no | Template description | | `--parent-id` | no | Parent template ID for inheritance | +#### `template update` + +Update an existing template's name, description, or parent. + +```sh +scadalink --contact-points template update --id [--name ] [--description ] [--parent-id ] +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--id` | yes | Template ID | +| `--name` | no | Updated template name | +| `--description` | no | Updated description | +| `--parent-id` | no | Updated parent template ID | + #### `template delete` Delete a template by ID. @@ -131,10 +146,201 @@ scadalink --contact-points template delete --id |--------|----------|-------------| | `--id` | yes | Template ID | +#### `template validate` + +Run pre-deployment validation on a template (flattening, naming collisions, script compilation). + +```sh +scadalink --contact-points template validate --id +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--id` | yes | Template ID | + +#### `template attribute add` + +Add an attribute to a template. + +```sh +scadalink --contact-points template attribute add --template-id --name --data-type [--default-value ] [--tag-path ] +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--template-id` | yes | Template ID | +| `--name` | yes | Attribute name | +| `--data-type` | yes | Attribute data type (e.g. `Float`, `Int`, `String`, `Bool`) | +| `--default-value` | no | Default value | +| `--tag-path` | no | Data connection tag path | + +#### `template attribute update` + +Update an attribute on a template. + +```sh +scadalink --contact-points template attribute update --template-id --name [--data-type ] [--default-value ] [--tag-path ] +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--template-id` | yes | Template ID | +| `--name` | yes | Attribute name to update | +| `--data-type` | no | Updated data type | +| `--default-value` | no | Updated default value | +| `--tag-path` | no | Updated tag path | + +#### `template attribute delete` + +Remove an attribute from a template. + +```sh +scadalink --contact-points template attribute delete --template-id --name +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--template-id` | yes | Template ID | +| `--name` | yes | Attribute name to delete | + +#### `template alarm add` + +Add an alarm definition to a template. + +```sh +scadalink --contact-points template alarm add --template-id --name --trigger-attribute --condition --setpoint [--severity ] [--notification-list ] +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--template-id` | yes | Template ID | +| `--name` | yes | Alarm name | +| `--trigger-attribute` | yes | Attribute that triggers the alarm | +| `--condition` | yes | Trigger condition (e.g. `GreaterThan`, `LessThan`, `Equal`) | +| `--setpoint` | yes | Setpoint value | +| `--severity` | no | Alarm severity (default: `Warning`) | +| `--notification-list` | no | Notification list name to notify on alarm | + +#### `template alarm update` + +Update an alarm definition on a template. + +```sh +scadalink --contact-points template alarm update --template-id --name [--condition ] [--setpoint ] [--severity ] [--notification-list ] +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--template-id` | yes | Template ID | +| `--name` | yes | Alarm name to update | +| `--condition` | no | Updated trigger condition | +| `--setpoint` | no | Updated setpoint value | +| `--severity` | no | Updated severity | +| `--notification-list` | no | Updated notification list name | + +#### `template alarm delete` + +Remove an alarm definition from a template. + +```sh +scadalink --contact-points template alarm delete --template-id --name +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--template-id` | yes | Template ID | +| `--name` | yes | Alarm name to delete | + +#### `template script add` + +Add a script to a template. + +```sh +scadalink --contact-points template script add --template-id --name --trigger-type [--trigger-attribute ] [--interval ] --code +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--template-id` | yes | Template ID | +| `--name` | yes | Script name | +| `--trigger-type` | yes | Trigger type: `OnChange`, `Periodic`, `OnAlarm` | +| `--trigger-attribute` | no | Attribute name for `OnChange` trigger | +| `--interval` | no | Interval in milliseconds for `Periodic` trigger | +| `--code` | yes | Script source code (or `@filepath` to read from file) | + +#### `template script update` + +Update a script on a template. + +```sh +scadalink --contact-points template script update --template-id --name [--trigger-type ] [--trigger-attribute ] [--interval ] [--code ] +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--template-id` | yes | Template ID | +| `--name` | yes | Script name to update | +| `--trigger-type` | no | Updated trigger type | +| `--trigger-attribute` | no | Updated trigger attribute | +| `--interval` | no | Updated interval | +| `--code` | no | Updated script source code (or `@filepath`) | + +#### `template script delete` + +Remove a script from a template. + +```sh +scadalink --contact-points template script delete --template-id --name +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--template-id` | yes | Template ID | +| `--name` | yes | Script name to delete | + +#### `template composition add` + +Add a feature module composition to a template. + +```sh +scadalink --contact-points template composition add --template-id --module-template-id --instance-name +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--template-id` | yes | Target template ID | +| `--module-template-id` | yes | Feature module template ID to compose | +| `--instance-name` | yes | Instance name for the composed module (used in path-qualified addressing) | + +#### `template composition delete` + +Remove a feature module composition from a template. + +```sh +scadalink --contact-points template composition delete --template-id --instance-name +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--template-id` | yes | Target template ID | +| `--instance-name` | yes | Instance name of the composed module to remove | + --- ### `instance` — Manage instances +#### `instance get` + +Get a single instance by ID. + +```sh +scadalink --contact-points instance get --id +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--id` | yes | Instance ID | + #### `instance list` List instances, with optional filters. @@ -212,10 +418,35 @@ scadalink --contact-points instance delete --id |--------|----------|-------------| | `--id` | yes | Instance ID | +#### `instance set-bindings` + +Set data connection bindings for an instance's attributes. + +```sh +scadalink --contact-points instance set-bindings --id --bindings +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--id` | yes | Instance ID | +| `--bindings` | yes | JSON string mapping attribute names to data connection IDs (e.g. `{"attr1": 1, "attr2": 2}`) | + --- ### `site` — Manage sites +#### `site get` + +Get a single site by ID. + +```sh +scadalink --contact-points site get --id +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--id` | yes | Site ID | + #### `site list` List all registered sites. @@ -262,6 +493,58 @@ scadalink --contact-points site deploy-artifacts [--site-id ] |--------|----------|-------------| | `--site-id` | no | Target site ID; omit to deploy to all sites | +#### `site area list` + +List all areas for a site. + +```sh +scadalink --contact-points site area list --site-id +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--site-id` | yes | Site ID | + +#### `site area create` + +Create an area within a site. + +```sh +scadalink --contact-points site area create --site-id --name [--parent-area-id ] +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--site-id` | yes | Site ID | +| `--name` | yes | Area name | +| `--parent-area-id` | no | Parent area ID for nested areas | + +#### `site area update` + +Update an area's name or parent. + +```sh +scadalink --contact-points site area update --id [--name ] [--parent-area-id ] +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--id` | yes | Area ID | +| `--name` | no | Updated area name | +| `--parent-area-id` | no | Updated parent area ID | + +#### `site area delete` + +Delete an area. Fails if any instances are assigned to it. + +```sh +scadalink --contact-points site area delete --id +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--id` | yes | Area ID | + --- ### `deploy` — Deployment operations @@ -309,6 +592,18 @@ scadalink --contact-points deploy status [--instance-id ] [--status < ### `data-connection` — Manage data connections +#### `data-connection get` + +Get a single data connection by ID. + +```sh +scadalink --contact-points data-connection get --id +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--id` | yes | Data connection ID | + #### `data-connection list` List all configured data connections. @@ -331,6 +626,21 @@ scadalink --contact-points data-connection create --name --protoc | `--protocol` | yes | Protocol identifier (e.g. `OpcUa`) | | `--configuration` | no | Protocol-specific configuration as a JSON string | +#### `data-connection update` + +Update a data connection definition. + +```sh +scadalink --contact-points data-connection update --id [--name ] [--protocol ] [--configuration ] +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--id` | yes | Data connection ID | +| `--name` | no | Updated connection name | +| `--protocol` | no | Updated protocol identifier | +| `--configuration` | no | Updated protocol-specific configuration as a JSON string | + #### `data-connection delete` Delete a data connection. @@ -356,10 +666,35 @@ scadalink --contact-points data-connection assign --connection-id -- | `--connection-id` | yes | Data connection ID | | `--site-id` | yes | Site ID | +#### `data-connection unassign` + +Remove a data connection assignment from a site. + +```sh +scadalink --contact-points data-connection unassign --connection-id --site-id +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--connection-id` | yes | Data connection ID | +| `--site-id` | yes | Site ID | + --- ### `external-system` — Manage external HTTP systems +#### `external-system get` + +Get a single external system definition by ID. + +```sh +scadalink --contact-points external-system get --id +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--id` | yes | External system ID | + #### `external-system list` List all external system definitions. @@ -383,6 +718,22 @@ scadalink --contact-points external-system create --name --endpoi | `--auth-type` | yes | Authentication type: `ApiKey` or `BasicAuth` | | `--auth-config` | no | Auth credentials as a JSON string | +#### `external-system update` + +Update an external system definition. + +```sh +scadalink --contact-points external-system update --id [--name ] [--endpoint-url ] [--auth-type ] [--auth-config ] +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--id` | yes | External system ID | +| `--name` | no | Updated display name | +| `--endpoint-url` | no | Updated base URL | +| `--auth-type` | no | Updated authentication type | +| `--auth-config` | no | Updated auth credentials as a JSON string | + #### `external-system delete` Delete an external system definition. @@ -399,6 +750,18 @@ scadalink --contact-points external-system delete --id ### `notification` — Manage notification lists +#### `notification get` + +Get a single notification list by ID. + +```sh +scadalink --contact-points notification get --id +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--id` | yes | Notification list ID | + #### `notification list` List all notification lists. @@ -420,6 +783,20 @@ scadalink --contact-points notification create --name --emails notification update --id [--name ] [--emails ] +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--id` | yes | Notification list ID | +| `--name` | no | Updated list name | +| `--emails` | no | Updated comma-separated list of recipient email addresses | + #### `notification delete` Delete a notification list. @@ -432,6 +809,31 @@ scadalink --contact-points notification delete --id |--------|----------|-------------| | `--id` | yes | Notification list ID | +#### `notification smtp list` + +Show the current SMTP configuration. + +```sh +scadalink --contact-points notification smtp list +``` + +#### `notification smtp update` + +Update the SMTP configuration. + +```sh +scadalink --contact-points notification smtp update --host --port --auth-type [--username ] [--password ] [--from-address ] +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--host` | yes | SMTP server hostname | +| `--port` | yes | SMTP server port | +| `--auth-type` | yes | Authentication type: `OAuth2` or `Basic` | +| `--username` | no | SMTP username (for Basic auth) | +| `--password` | no | SMTP password (for Basic auth) | +| `--from-address` | no | Sender email address | + --- ### `security` — Security settings @@ -456,6 +858,20 @@ scadalink --contact-points security api-key create --name |--------|----------|-------------| | `--name` | yes | Descriptive label for the key | +#### `security api-key update` + +Update an API key's name or enabled status. + +```sh +scadalink --contact-points security api-key update --id [--name ] [--enabled ] +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--id` | yes | API key ID | +| `--name` | no | Updated label | +| `--enabled` | no | Enable or disable the key (`true` or `false`) | + #### `security api-key delete` Revoke and delete an API key. @@ -489,6 +905,20 @@ scadalink --contact-points security role-mapping create --ldap-group security role-mapping update --id [--ldap-group ] [--role ] +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--id` | yes | Mapping ID | +| `--ldap-group` | no | Updated LDAP group distinguished name or CN | +| `--role` | no | Updated ScadaLink role | + #### `security role-mapping delete` Remove an LDAP role mapping. @@ -501,6 +931,43 @@ scadalink --contact-points security role-mapping delete --id |--------|----------|-------------| | `--id` | yes | Mapping ID | +#### `security scope-rule list` + +List all site scope rules for role mappings. + +```sh +scadalink --contact-points security scope-rule list [--role-mapping-id ] +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--role-mapping-id` | no | Filter by role mapping ID | + +#### `security scope-rule add` + +Add a site scope rule to a role mapping, restricting it to a specific site. + +```sh +scadalink --contact-points security scope-rule add --role-mapping-id --site-id +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--role-mapping-id` | yes | Role mapping ID | +| `--site-id` | yes | Site ID to scope the mapping to | + +#### `security scope-rule delete` + +Remove a site scope rule from a role mapping. + +```sh +scadalink --contact-points security scope-rule delete --id +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--id` | yes | Scope rule ID | + --- ### `health` — Health monitoring @@ -525,6 +992,37 @@ scadalink --contact-points health site --identifier |--------|----------|-------------| | `--identifier` | yes | Site identifier (e.g. `site-a`) | +#### `health event-log` + +Query the site event log for a specific site. Events are fetched remotely from the site's local SQLite store. + +```sh +scadalink --contact-points health event-log --site-identifier [--from ] [--to ] [--search ] [--page ] [--page-size ] +``` + +| Option | Required | Default | Description | +|--------|----------|---------|-------------| +| `--site-identifier` | yes | — | Site identifier | +| `--from` | no | — | Start timestamp in ISO 8601 format | +| `--to` | no | — | End timestamp in ISO 8601 format | +| `--search` | no | — | Keyword search term | +| `--page` | no | `1` | Page number | +| `--page-size` | no | `50` | Results per page | + +#### `health parked-messages` + +Query parked (dead-letter) messages at a specific site. + +```sh +scadalink --contact-points health parked-messages --site-identifier [--page ] [--page-size ] +``` + +| Option | Required | Default | Description | +|--------|----------|---------|-------------| +| `--site-identifier` | yes | — | Site identifier | +| `--page` | no | `1` | Page number | +| `--page-size` | no | `50` | Results per page | + --- ### `audit-log` — Audit log queries @@ -549,6 +1047,199 @@ scadalink --contact-points audit-log query [options] --- +### `shared-script` — Manage shared scripts + +#### `shared-script list` + +List all shared script definitions. + +```sh +scadalink --contact-points shared-script list +``` + +#### `shared-script get` + +Get a single shared script by ID. + +```sh +scadalink --contact-points shared-script get --id +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--id` | yes | Shared script ID | + +#### `shared-script create` + +Create a new shared script. + +```sh +scadalink --contact-points shared-script create --name --code +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--name` | yes | Shared script name | +| `--code` | yes | Script source code (or `@filepath` to read from file) | + +#### `shared-script update` + +Update a shared script's name or code. + +```sh +scadalink --contact-points shared-script update --id [--name ] [--code ] +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--id` | yes | Shared script ID | +| `--name` | no | Updated script name | +| `--code` | no | Updated script source code (or `@filepath`) | + +#### `shared-script delete` + +Delete a shared script. + +```sh +scadalink --contact-points shared-script delete --id +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--id` | yes | Shared script ID | + +--- + +### `db-connection` — Manage database connections + +#### `db-connection list` + +List all database connection definitions. + +```sh +scadalink --contact-points db-connection list +``` + +#### `db-connection get` + +Get a single database connection by ID. + +```sh +scadalink --contact-points db-connection get --id +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--id` | yes | Database connection ID | + +#### `db-connection create` + +Create a new database connection definition. + +```sh +scadalink --contact-points db-connection create --name --connection-string [--provider ] +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--name` | yes | Connection name | +| `--connection-string` | yes | Database connection string | +| `--provider` | no | Database provider (default: `SqlServer`) | + +#### `db-connection update` + +Update a database connection definition. + +```sh +scadalink --contact-points db-connection update --id [--name ] [--connection-string ] [--provider ] +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--id` | yes | Database connection ID | +| `--name` | no | Updated connection name | +| `--connection-string` | no | Updated connection string | +| `--provider` | no | Updated database provider | + +#### `db-connection delete` + +Delete a database connection definition. + +```sh +scadalink --contact-points db-connection delete --id +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--id` | yes | Database connection ID | + +--- + +### `api-method` — Manage inbound API methods + +#### `api-method list` + +List all inbound API method definitions. + +```sh +scadalink --contact-points api-method list +``` + +#### `api-method get` + +Get a single inbound API method by ID. + +```sh +scadalink --contact-points api-method get --id +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--id` | yes | API method ID | + +#### `api-method create` + +Create a new inbound API method. + +```sh +scadalink --contact-points api-method create --name --code [--description ] +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--name` | yes | Method name (used as the URL path segment in `POST /api/{methodName}`) | +| `--code` | yes | Script source code implementing the method (or `@filepath` to read from file) | +| `--description` | no | Method description | + +#### `api-method update` + +Update an inbound API method. + +```sh +scadalink --contact-points api-method update --id [--name ] [--code ] [--description ] +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--id` | yes | API method ID | +| `--name` | no | Updated method name | +| `--code` | no | Updated script source code (or `@filepath`) | +| `--description` | no | Updated description | + +#### `api-method delete` + +Delete an inbound API method. + +```sh +scadalink --contact-points api-method delete --id +``` + +| Option | Required | Description | +|--------|----------|-------------| +| `--id` | yes | API method ID | + +--- + ## Architecture Notes The CLI connects to the Central cluster using Akka.NET's `ClusterClient`. It does not join the cluster — it contacts the `ClusterClientReceptionist` on one of the configured Central nodes and sends commands to the `ManagementActor` at path `/user/management`. diff --git a/src/ScadaLink.Commons/Messages/Management/DatabaseConnectionDefCommands.cs b/src/ScadaLink.Commons/Messages/Management/DatabaseConnectionDefCommands.cs new file mode 100644 index 0000000..a958d9d --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Management/DatabaseConnectionDefCommands.cs @@ -0,0 +1,7 @@ +namespace ScadaLink.Commons.Messages.Management; + +public record ListDatabaseConnectionsCommand; +public record GetDatabaseConnectionCommand(int DatabaseConnectionId); +public record CreateDatabaseConnectionDefCommand(string Name, string ConnectionString); +public record UpdateDatabaseConnectionDefCommand(int DatabaseConnectionId, string Name, string ConnectionString); +public record DeleteDatabaseConnectionDefCommand(int DatabaseConnectionId); diff --git a/src/ScadaLink.Commons/Messages/Management/InboundApiCommands.cs b/src/ScadaLink.Commons/Messages/Management/InboundApiCommands.cs new file mode 100644 index 0000000..5358168 --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Management/InboundApiCommands.cs @@ -0,0 +1,7 @@ +namespace ScadaLink.Commons.Messages.Management; + +public record ListApiMethodsCommand; +public record GetApiMethodCommand(int ApiMethodId); +public record CreateApiMethodCommand(string Name, string Script, int TimeoutSeconds, string? ParameterDefinitions, string? ReturnDefinition); +public record UpdateApiMethodCommand(int ApiMethodId, string Script, int TimeoutSeconds, string? ParameterDefinitions, string? ReturnDefinition); +public record DeleteApiMethodCommand(int ApiMethodId); diff --git a/src/ScadaLink.Commons/Messages/Management/RemoteQueryCommands.cs b/src/ScadaLink.Commons/Messages/Management/RemoteQueryCommands.cs new file mode 100644 index 0000000..c50e48d --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Management/RemoteQueryCommands.cs @@ -0,0 +1,4 @@ +namespace ScadaLink.Commons.Messages.Management; + +public record QueryEventLogsCommand(string SiteIdentifier, string? EventType = null, string? Severity = null, string? Keyword = null, DateTimeOffset? From = null, DateTimeOffset? To = null, int Page = 1, int PageSize = 50); +public record QueryParkedMessagesCommand(string SiteIdentifier, int Page = 1, int PageSize = 50); diff --git a/src/ScadaLink.Commons/Messages/Management/SecurityCommands.cs b/src/ScadaLink.Commons/Messages/Management/SecurityCommands.cs index c89b6c4..9870128 100644 --- a/src/ScadaLink.Commons/Messages/Management/SecurityCommands.cs +++ b/src/ScadaLink.Commons/Messages/Management/SecurityCommands.cs @@ -7,3 +7,7 @@ public record ListRoleMappingsCommand; public record CreateRoleMappingCommand(string LdapGroupName, string Role); public record UpdateRoleMappingCommand(int MappingId, string LdapGroupName, string Role); public record DeleteRoleMappingCommand(int MappingId); +public record UpdateApiKeyCommand(int ApiKeyId, bool IsEnabled); +public record ListScopeRulesCommand(int MappingId); +public record AddScopeRuleCommand(int MappingId, int SiteId); +public record DeleteScopeRuleCommand(int ScopeRuleId); diff --git a/src/ScadaLink.Commons/Messages/Management/SharedScriptCommands.cs b/src/ScadaLink.Commons/Messages/Management/SharedScriptCommands.cs new file mode 100644 index 0000000..e4db794 --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Management/SharedScriptCommands.cs @@ -0,0 +1,7 @@ +namespace ScadaLink.Commons.Messages.Management; + +public record ListSharedScriptsCommand; +public record GetSharedScriptCommand(int SharedScriptId); +public record CreateSharedScriptCommand(string Name, string Code, string? ParameterDefinitions, string? ReturnDefinition); +public record UpdateSharedScriptCommand(int SharedScriptId, string Name, string Code, string? ParameterDefinitions, string? ReturnDefinition); +public record DeleteSharedScriptCommand(int SharedScriptId); diff --git a/src/ScadaLink.Commons/Messages/Management/SiteCommands.cs b/src/ScadaLink.Commons/Messages/Management/SiteCommands.cs index 629cf82..572eef0 100644 --- a/src/ScadaLink.Commons/Messages/Management/SiteCommands.cs +++ b/src/ScadaLink.Commons/Messages/Management/SiteCommands.cs @@ -8,3 +8,4 @@ public record DeleteSiteCommand(int SiteId); public record ListAreasCommand(int SiteId); public record CreateAreaCommand(int SiteId, string Name, int? ParentAreaId); public record DeleteAreaCommand(int AreaId); +public record UpdateAreaCommand(int AreaId, string Name); diff --git a/src/ScadaLink.Commons/Messages/Management/TemplateCommands.cs b/src/ScadaLink.Commons/Messages/Management/TemplateCommands.cs index 7ffd940..7195476 100644 --- a/src/ScadaLink.Commons/Messages/Management/TemplateCommands.cs +++ b/src/ScadaLink.Commons/Messages/Management/TemplateCommands.cs @@ -6,3 +6,16 @@ public record CreateTemplateCommand(string Name, string? Description, int? Paren public record UpdateTemplateCommand(int TemplateId, string Name, string? Description, int? ParentTemplateId); public record DeleteTemplateCommand(int TemplateId); public record ValidateTemplateCommand(int TemplateId); + +// Template member operations +public record AddTemplateAttributeCommand(int TemplateId, string Name, string DataType, string? Value, string? Description, string? DataSourceReference, bool IsLocked); +public record UpdateTemplateAttributeCommand(int AttributeId, string Name, string DataType, string? Value, string? Description, string? DataSourceReference, bool IsLocked); +public record DeleteTemplateAttributeCommand(int AttributeId); +public record AddTemplateAlarmCommand(int TemplateId, string Name, string TriggerType, int PriorityLevel, string? Description, string? TriggerConfiguration, bool IsLocked); +public record UpdateTemplateAlarmCommand(int AlarmId, string Name, string TriggerType, int PriorityLevel, string? Description, string? TriggerConfiguration, bool IsLocked); +public record DeleteTemplateAlarmCommand(int AlarmId); +public record AddTemplateScriptCommand(int TemplateId, string Name, string Code, string? TriggerType, string? TriggerConfiguration, bool IsLocked); +public record UpdateTemplateScriptCommand(int ScriptId, string Name, string Code, string? TriggerType, string? TriggerConfiguration, bool IsLocked); +public record DeleteTemplateScriptCommand(int ScriptId); +public record AddTemplateCompositionCommand(int TemplateId, string InstanceName, int ComposedTemplateId); +public record DeleteTemplateCompositionCommand(int CompositionId); diff --git a/src/ScadaLink.ManagementService/ManagementActor.cs b/src/ScadaLink.ManagementService/ManagementActor.cs index 2d09834..d6e51f6 100644 --- a/src/ScadaLink.ManagementService/ManagementActor.cs +++ b/src/ScadaLink.ManagementService/ManagementActor.cs @@ -6,11 +6,14 @@ using Microsoft.Extensions.Logging; using ScadaLink.Commons.Entities.ExternalSystems; using ScadaLink.Commons.Entities.InboundApi; using ScadaLink.Commons.Entities.Instances; +using ScadaLink.Commons.Entities.Scripts; +using ScadaLink.Commons.Entities.Templates; using ScadaLink.Commons.Entities.Notifications; using ScadaLink.Commons.Entities.Security; using ScadaLink.Commons.Entities.Sites; using ScadaLink.Commons.Interfaces.Repositories; using ScadaLink.Commons.Messages.Management; +using ScadaLink.Commons.Messages.RemoteQuery; using ScadaLink.DeploymentManager; using ScadaLink.HealthMonitoring; using ScadaLink.Communication; @@ -76,7 +79,9 @@ public class ManagementActor : ReceiveActor CreateSiteCommand or UpdateSiteCommand or DeleteSiteCommand or ListRoleMappingsCommand or CreateRoleMappingCommand or UpdateRoleMappingCommand or DeleteRoleMappingCommand - or ListApiKeysCommand or CreateApiKeyCommand or DeleteApiKeyCommand => "Admin", + or ListApiKeysCommand or CreateApiKeyCommand or DeleteApiKeyCommand + or UpdateApiKeyCommand + or ListScopeRulesCommand or AddScopeRuleCommand or DeleteScopeRuleCommand => "Admin", // Design operations CreateAreaCommand or DeleteAreaCommand @@ -90,7 +95,15 @@ public class ManagementActor : ReceiveActor or CreateDataConnectionCommand or UpdateDataConnectionCommand or DeleteDataConnectionCommand or AssignDataConnectionToSiteCommand - or UnassignDataConnectionFromSiteCommand => "Design", + or UnassignDataConnectionFromSiteCommand + or AddTemplateAttributeCommand or UpdateTemplateAttributeCommand or DeleteTemplateAttributeCommand + or AddTemplateAlarmCommand or UpdateTemplateAlarmCommand or DeleteTemplateAlarmCommand + or AddTemplateScriptCommand or UpdateTemplateScriptCommand or DeleteTemplateScriptCommand + or AddTemplateCompositionCommand or DeleteTemplateCompositionCommand + or CreateSharedScriptCommand or UpdateSharedScriptCommand or DeleteSharedScriptCommand + or CreateDatabaseConnectionDefCommand or UpdateDatabaseConnectionDefCommand or DeleteDatabaseConnectionDefCommand + or CreateApiMethodCommand or UpdateApiMethodCommand or DeleteApiMethodCommand + or UpdateAreaCommand => "Design", // Deployment operations CreateInstanceCommand or MgmtDeployInstanceCommand or MgmtEnableInstanceCommand @@ -114,6 +127,19 @@ public class ManagementActor : ReceiveActor DeleteTemplateCommand cmd => await HandleDeleteTemplate(sp, cmd, user), ValidateTemplateCommand cmd => await HandleValidateTemplate(sp, cmd), + // Template members + AddTemplateAttributeCommand cmd => await HandleAddAttribute(sp, cmd, user), + UpdateTemplateAttributeCommand cmd => await HandleUpdateAttribute(sp, cmd, user), + DeleteTemplateAttributeCommand cmd => await HandleDeleteAttribute(sp, cmd, user), + AddTemplateAlarmCommand cmd => await HandleAddAlarm(sp, cmd, user), + UpdateTemplateAlarmCommand cmd => await HandleUpdateAlarm(sp, cmd, user), + DeleteTemplateAlarmCommand cmd => await HandleDeleteAlarm(sp, cmd, user), + AddTemplateScriptCommand cmd => await HandleAddScript(sp, cmd, user), + UpdateTemplateScriptCommand cmd => await HandleUpdateScript(sp, cmd, user), + DeleteTemplateScriptCommand cmd => await HandleDeleteScript(sp, cmd, user), + AddTemplateCompositionCommand cmd => await HandleAddComposition(sp, cmd, user), + DeleteTemplateCompositionCommand cmd => await HandleDeleteComposition(sp, cmd, user), + // Instances ListInstancesCommand cmd => await HandleListInstances(sp, cmd), GetInstanceCommand cmd => await HandleGetInstance(sp, cmd), @@ -133,6 +159,7 @@ public class ManagementActor : ReceiveActor ListAreasCommand cmd => await HandleListAreas(sp, cmd), CreateAreaCommand cmd => await HandleCreateArea(sp, cmd), DeleteAreaCommand cmd => await HandleDeleteArea(sp, cmd), + UpdateAreaCommand cmd => await HandleUpdateArea(sp, cmd), // Data Connections ListDataConnectionsCommand => await HandleListDataConnections(sp), @@ -159,6 +186,27 @@ public class ManagementActor : ReceiveActor ListSmtpConfigsCommand => await HandleListSmtpConfigs(sp), UpdateSmtpConfigCommand cmd => await HandleUpdateSmtpConfig(sp, cmd), + // Shared Scripts + ListSharedScriptsCommand => await HandleListSharedScripts(sp), + GetSharedScriptCommand cmd => await HandleGetSharedScript(sp, cmd), + CreateSharedScriptCommand cmd => await HandleCreateSharedScript(sp, cmd, user), + UpdateSharedScriptCommand cmd => await HandleUpdateSharedScript(sp, cmd, user), + DeleteSharedScriptCommand cmd => await HandleDeleteSharedScript(sp, cmd, user), + + // Database Connections (External System) + ListDatabaseConnectionsCommand => await HandleListDatabaseConnections(sp), + GetDatabaseConnectionCommand cmd => await HandleGetDatabaseConnection(sp, cmd), + CreateDatabaseConnectionDefCommand cmd => await HandleCreateDatabaseConnection(sp, cmd), + UpdateDatabaseConnectionDefCommand cmd => await HandleUpdateDatabaseConnection(sp, cmd), + DeleteDatabaseConnectionDefCommand cmd => await HandleDeleteDatabaseConnection(sp, cmd), + + // Inbound API Methods + ListApiMethodsCommand => await HandleListApiMethods(sp), + GetApiMethodCommand cmd => await HandleGetApiMethod(sp, cmd), + CreateApiMethodCommand cmd => await HandleCreateApiMethod(sp, cmd), + UpdateApiMethodCommand cmd => await HandleUpdateApiMethod(sp, cmd), + DeleteApiMethodCommand cmd => await HandleDeleteApiMethod(sp, cmd), + // Security ListRoleMappingsCommand => await HandleListRoleMappings(sp), CreateRoleMappingCommand cmd => await HandleCreateRoleMapping(sp, cmd), @@ -167,6 +215,10 @@ public class ManagementActor : ReceiveActor ListApiKeysCommand => await HandleListApiKeys(sp), CreateApiKeyCommand cmd => await HandleCreateApiKey(sp, cmd), DeleteApiKeyCommand cmd => await HandleDeleteApiKey(sp, cmd), + UpdateApiKeyCommand cmd => await HandleUpdateApiKey(sp, cmd), + ListScopeRulesCommand cmd => await HandleListScopeRules(sp, cmd), + AddScopeRuleCommand cmd => await HandleAddScopeRule(sp, cmd), + DeleteScopeRuleCommand cmd => await HandleDeleteScopeRule(sp, cmd), // Deployments MgmtDeployArtifactsCommand cmd => await HandleDeployArtifacts(sp, cmd, user), @@ -179,6 +231,10 @@ public class ManagementActor : ReceiveActor GetHealthSummaryCommand => HandleGetHealthSummary(sp), GetSiteHealthCommand cmd => HandleGetSiteHealth(sp, cmd), + // Remote Queries + QueryEventLogsCommand cmd => await HandleQueryEventLogs(sp, cmd), + QueryParkedMessagesCommand cmd => await HandleQueryParkedMessages(sp, cmd), + _ => throw new NotSupportedException($"Unknown command type: {command.GetType().Name}") }; } @@ -705,4 +761,348 @@ public class ManagementActor : ReceiveActor var aggregator = sp.GetRequiredService(); return aggregator.GetSiteState(cmd.SiteIdentifier); } + + // ======================================================================== + // Template member handlers + // ======================================================================== + + private static async Task HandleAddAttribute(IServiceProvider sp, AddTemplateAttributeCommand cmd, string user) + { + var svc = sp.GetRequiredService(); + var attr = new TemplateAttribute(cmd.Name) + { + DataType = Enum.Parse(cmd.DataType, ignoreCase: true), + Value = cmd.Value, + Description = cmd.Description, + DataSourceReference = cmd.DataSourceReference, + IsLocked = cmd.IsLocked + }; + var result = await svc.AddAttributeAsync(cmd.TemplateId, attr, user); + return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error); + } + + private static async Task HandleUpdateAttribute(IServiceProvider sp, UpdateTemplateAttributeCommand cmd, string user) + { + var svc = sp.GetRequiredService(); + var attr = new TemplateAttribute(cmd.Name) + { + DataType = Enum.Parse(cmd.DataType, ignoreCase: true), + Value = cmd.Value, + Description = cmd.Description, + DataSourceReference = cmd.DataSourceReference, + IsLocked = cmd.IsLocked + }; + var result = await svc.UpdateAttributeAsync(cmd.AttributeId, attr, user); + return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error); + } + + private static async Task HandleDeleteAttribute(IServiceProvider sp, DeleteTemplateAttributeCommand cmd, string user) + { + var svc = sp.GetRequiredService(); + var result = await svc.DeleteAttributeAsync(cmd.AttributeId, user); + return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error); + } + + private static async Task HandleAddAlarm(IServiceProvider sp, AddTemplateAlarmCommand cmd, string user) + { + var svc = sp.GetRequiredService(); + var alarm = new TemplateAlarm(cmd.Name) + { + TriggerType = Enum.Parse(cmd.TriggerType, ignoreCase: true), + PriorityLevel = cmd.PriorityLevel, + Description = cmd.Description, + TriggerConfiguration = cmd.TriggerConfiguration, + IsLocked = cmd.IsLocked + }; + var result = await svc.AddAlarmAsync(cmd.TemplateId, alarm, user); + return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error); + } + + private static async Task HandleUpdateAlarm(IServiceProvider sp, UpdateTemplateAlarmCommand cmd, string user) + { + var svc = sp.GetRequiredService(); + var alarm = new TemplateAlarm(cmd.Name) + { + TriggerType = Enum.Parse(cmd.TriggerType, ignoreCase: true), + PriorityLevel = cmd.PriorityLevel, + Description = cmd.Description, + TriggerConfiguration = cmd.TriggerConfiguration, + IsLocked = cmd.IsLocked + }; + var result = await svc.UpdateAlarmAsync(cmd.AlarmId, alarm, user); + return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error); + } + + private static async Task HandleDeleteAlarm(IServiceProvider sp, DeleteTemplateAlarmCommand cmd, string user) + { + var svc = sp.GetRequiredService(); + var result = await svc.DeleteAlarmAsync(cmd.AlarmId, user); + return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error); + } + + private static async Task HandleAddScript(IServiceProvider sp, AddTemplateScriptCommand cmd, string user) + { + var svc = sp.GetRequiredService(); + var script = new TemplateScript(cmd.Name, cmd.Code) + { + TriggerType = cmd.TriggerType, + TriggerConfiguration = cmd.TriggerConfiguration, + IsLocked = cmd.IsLocked + }; + var result = await svc.AddScriptAsync(cmd.TemplateId, script, user); + return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error); + } + + private static async Task HandleUpdateScript(IServiceProvider sp, UpdateTemplateScriptCommand cmd, string user) + { + var svc = sp.GetRequiredService(); + var script = new TemplateScript(cmd.Name, cmd.Code) + { + TriggerType = cmd.TriggerType, + TriggerConfiguration = cmd.TriggerConfiguration, + IsLocked = cmd.IsLocked + }; + var result = await svc.UpdateScriptAsync(cmd.ScriptId, script, user); + return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error); + } + + private static async Task HandleDeleteScript(IServiceProvider sp, DeleteTemplateScriptCommand cmd, string user) + { + var svc = sp.GetRequiredService(); + var result = await svc.DeleteScriptAsync(cmd.ScriptId, user); + return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error); + } + + private static async Task HandleAddComposition(IServiceProvider sp, AddTemplateCompositionCommand cmd, string user) + { + var svc = sp.GetRequiredService(); + var result = await svc.AddCompositionAsync(cmd.TemplateId, cmd.ComposedTemplateId, cmd.InstanceName, user); + return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error); + } + + private static async Task HandleDeleteComposition(IServiceProvider sp, DeleteTemplateCompositionCommand cmd, string user) + { + var svc = sp.GetRequiredService(); + var result = await svc.DeleteCompositionAsync(cmd.CompositionId, user); + return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error); + } + + // ======================================================================== + // Shared Script handlers + // ======================================================================== + + private static async Task HandleListSharedScripts(IServiceProvider sp) + { + var svc = sp.GetRequiredService(); + return await svc.GetAllSharedScriptsAsync(); + } + + private static async Task HandleGetSharedScript(IServiceProvider sp, GetSharedScriptCommand cmd) + { + var svc = sp.GetRequiredService(); + return await svc.GetSharedScriptByIdAsync(cmd.SharedScriptId); + } + + private static async Task HandleCreateSharedScript(IServiceProvider sp, CreateSharedScriptCommand cmd, string user) + { + var svc = sp.GetRequiredService(); + var result = await svc.CreateSharedScriptAsync(cmd.Name, cmd.Code, cmd.ParameterDefinitions, cmd.ReturnDefinition, user); + return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error); + } + + private static async Task HandleUpdateSharedScript(IServiceProvider sp, UpdateSharedScriptCommand cmd, string user) + { + var svc = sp.GetRequiredService(); + var result = await svc.UpdateSharedScriptAsync(cmd.SharedScriptId, cmd.Code, cmd.ParameterDefinitions, cmd.ReturnDefinition, user); + return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error); + } + + private static async Task HandleDeleteSharedScript(IServiceProvider sp, DeleteSharedScriptCommand cmd, string user) + { + var svc = sp.GetRequiredService(); + var result = await svc.DeleteSharedScriptAsync(cmd.SharedScriptId, user); + return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error); + } + + // ======================================================================== + // Database Connection Definition handlers + // ======================================================================== + + private static async Task HandleListDatabaseConnections(IServiceProvider sp) + { + var repo = sp.GetRequiredService(); + return await repo.GetAllDatabaseConnectionsAsync(); + } + + private static async Task HandleGetDatabaseConnection(IServiceProvider sp, GetDatabaseConnectionCommand cmd) + { + var repo = sp.GetRequiredService(); + return await repo.GetDatabaseConnectionByIdAsync(cmd.DatabaseConnectionId); + } + + private static async Task HandleCreateDatabaseConnection(IServiceProvider sp, CreateDatabaseConnectionDefCommand cmd) + { + var repo = sp.GetRequiredService(); + var def = new DatabaseConnectionDefinition(cmd.Name, cmd.ConnectionString); + await repo.AddDatabaseConnectionAsync(def); + await repo.SaveChangesAsync(); + return def; + } + + private static async Task HandleUpdateDatabaseConnection(IServiceProvider sp, UpdateDatabaseConnectionDefCommand cmd) + { + var repo = sp.GetRequiredService(); + var def = await repo.GetDatabaseConnectionByIdAsync(cmd.DatabaseConnectionId) + ?? throw new InvalidOperationException($"DatabaseConnection with ID {cmd.DatabaseConnectionId} not found."); + def.Name = cmd.Name; + def.ConnectionString = cmd.ConnectionString; + await repo.UpdateDatabaseConnectionAsync(def); + await repo.SaveChangesAsync(); + return def; + } + + private static async Task HandleDeleteDatabaseConnection(IServiceProvider sp, DeleteDatabaseConnectionDefCommand cmd) + { + var repo = sp.GetRequiredService(); + await repo.DeleteDatabaseConnectionAsync(cmd.DatabaseConnectionId); + await repo.SaveChangesAsync(); + return true; + } + + // ======================================================================== + // Inbound API Method handlers + // ======================================================================== + + private static async Task HandleListApiMethods(IServiceProvider sp) + { + var repo = sp.GetRequiredService(); + return await repo.GetAllApiMethodsAsync(); + } + + private static async Task HandleGetApiMethod(IServiceProvider sp, GetApiMethodCommand cmd) + { + var repo = sp.GetRequiredService(); + return await repo.GetApiMethodByIdAsync(cmd.ApiMethodId); + } + + private static async Task HandleCreateApiMethod(IServiceProvider sp, CreateApiMethodCommand cmd) + { + var repo = sp.GetRequiredService(); + var method = new ApiMethod(cmd.Name, cmd.Script) + { + TimeoutSeconds = cmd.TimeoutSeconds, + ParameterDefinitions = cmd.ParameterDefinitions, + ReturnDefinition = cmd.ReturnDefinition + }; + await repo.AddApiMethodAsync(method); + await repo.SaveChangesAsync(); + return method; + } + + private static async Task HandleUpdateApiMethod(IServiceProvider sp, UpdateApiMethodCommand cmd) + { + var repo = sp.GetRequiredService(); + var method = await repo.GetApiMethodByIdAsync(cmd.ApiMethodId) + ?? throw new InvalidOperationException($"ApiMethod with ID {cmd.ApiMethodId} not found."); + method.Script = cmd.Script; + method.TimeoutSeconds = cmd.TimeoutSeconds; + method.ParameterDefinitions = cmd.ParameterDefinitions; + method.ReturnDefinition = cmd.ReturnDefinition; + await repo.UpdateApiMethodAsync(method); + await repo.SaveChangesAsync(); + return method; + } + + private static async Task HandleDeleteApiMethod(IServiceProvider sp, DeleteApiMethodCommand cmd) + { + var repo = sp.GetRequiredService(); + await repo.DeleteApiMethodAsync(cmd.ApiMethodId); + await repo.SaveChangesAsync(); + return true; + } + + // ======================================================================== + // Additional Security handlers (API key update, scope rules) + // ======================================================================== + + private static async Task HandleUpdateApiKey(IServiceProvider sp, UpdateApiKeyCommand cmd) + { + var repo = sp.GetRequiredService(); + var key = await repo.GetApiKeyByIdAsync(cmd.ApiKeyId) + ?? throw new InvalidOperationException($"ApiKey with ID {cmd.ApiKeyId} not found."); + key.IsEnabled = cmd.IsEnabled; + await repo.UpdateApiKeyAsync(key); + await repo.SaveChangesAsync(); + return key; + } + + private static async Task HandleListScopeRules(IServiceProvider sp, ListScopeRulesCommand cmd) + { + var repo = sp.GetRequiredService(); + return await repo.GetScopeRulesForMappingAsync(cmd.MappingId); + } + + private static async Task HandleAddScopeRule(IServiceProvider sp, AddScopeRuleCommand cmd) + { + var repo = sp.GetRequiredService(); + var rule = new SiteScopeRule { LdapGroupMappingId = cmd.MappingId, SiteId = cmd.SiteId }; + await repo.AddScopeRuleAsync(rule); + await repo.SaveChangesAsync(); + return rule; + } + + private static async Task HandleDeleteScopeRule(IServiceProvider sp, DeleteScopeRuleCommand cmd) + { + var repo = sp.GetRequiredService(); + await repo.DeleteScopeRuleAsync(cmd.ScopeRuleId); + await repo.SaveChangesAsync(); + return true; + } + + // ======================================================================== + // Area update handler + // ======================================================================== + + private static async Task HandleUpdateArea(IServiceProvider sp, UpdateAreaCommand cmd) + { + var repo = sp.GetRequiredService(); + var area = await repo.GetAreaByIdAsync(cmd.AreaId) + ?? throw new InvalidOperationException($"Area with ID {cmd.AreaId} not found."); + area.Name = cmd.Name; + await repo.UpdateAreaAsync(area); + await repo.SaveChangesAsync(); + return area; + } + + // ======================================================================== + // Remote Query handlers + // ======================================================================== + + private static async Task HandleQueryEventLogs(IServiceProvider sp, QueryEventLogsCommand cmd) + { + var commService = sp.GetRequiredService(); + var request = new EventLogQueryRequest( + Guid.NewGuid().ToString("N"), + cmd.SiteIdentifier, + cmd.From, cmd.To, + cmd.EventType, cmd.Severity, + null, // InstanceId + cmd.Keyword, + null, // ContinuationToken + cmd.PageSize, + DateTimeOffset.UtcNow); + return await commService.QueryEventLogsAsync(cmd.SiteIdentifier, request); + } + + private static async Task HandleQueryParkedMessages(IServiceProvider sp, QueryParkedMessagesCommand cmd) + { + var commService = sp.GetRequiredService(); + var request = new ParkedMessageQueryRequest( + Guid.NewGuid().ToString("N"), + cmd.SiteIdentifier, + cmd.Page, + cmd.PageSize, + DateTimeOffset.UtcNow); + return await commService.QueryParkedMessagesAsync(cmd.SiteIdentifier, request); + } } diff --git a/tests/ScadaLink.ManagementService.Tests/ManagementActorTests.cs b/tests/ScadaLink.ManagementService.Tests/ManagementActorTests.cs index 1411032..999832c 100644 --- a/tests/ScadaLink.ManagementService.Tests/ManagementActorTests.cs +++ b/tests/ScadaLink.ManagementService.Tests/ManagementActorTests.cs @@ -11,6 +11,7 @@ using ScadaLink.Commons.Interfaces.Services; using ScadaLink.Commons.Messages.Management; using ScadaLink.Commons.Types; using ScadaLink.ManagementService; +using ScadaLink.TemplateEngine; using ScadaLink.TemplateEngine.Services; namespace ScadaLink.ManagementService.Tests; @@ -148,10 +149,9 @@ public class ManagementActorTests : TestKit, IDisposable actor.Tell(envelope); var response = ExpectMsg(TimeSpan.FromSeconds(5)); - var data = Assert.IsAssignableFrom>(response.Data); - Assert.Equal(2, data.Count); - Assert.Equal("PumpTemplate", data[0].Name); - Assert.Equal("ValveTemplate", data[1].Name); + Assert.NotNull(response.JsonData); + Assert.Contains("PumpTemplate", response.JsonData); + Assert.Contains("ValveTemplate", response.JsonData); } // ======================================================================== @@ -192,8 +192,8 @@ public class ManagementActorTests : TestKit, IDisposable var response = ExpectMsg(TimeSpan.FromSeconds(5)); Assert.Equal(envelope.CorrelationId, response.CorrelationId); - var instance = Assert.IsType(response.Data); - Assert.Equal("Pump1", instance.UniqueName); + Assert.NotNull(response.JsonData); + Assert.Contains("Pump1", response.JsonData); } // ======================================================================== @@ -297,4 +297,185 @@ public class ManagementActorTests : TestKit, IDisposable var response = ExpectMsg(TimeSpan.FromSeconds(5)); Assert.Equal(envelope.CorrelationId, response.CorrelationId); } + + // ======================================================================== + // New command authorization tests + // ======================================================================== + + [Fact] + public void SharedScriptCreate_WithAdminRole_ReturnsUnauthorized() + { + var actor = CreateActor(); + var envelope = Envelope(new CreateSharedScriptCommand("Script1", "code", null, null), "Admin"); + + actor.Tell(envelope); + + var response = ExpectMsg(TimeSpan.FromSeconds(5)); + Assert.Contains("Design", response.Message); + } + + [Fact] + public void DatabaseConnectionCreate_WithDeploymentRole_ReturnsUnauthorized() + { + var actor = CreateActor(); + var envelope = Envelope(new CreateDatabaseConnectionDefCommand("DB1", "Server=test"), "Deployment"); + + actor.Tell(envelope); + + var response = ExpectMsg(TimeSpan.FromSeconds(5)); + Assert.Contains("Design", response.Message); + } + + [Fact] + public void ApiMethodCreate_WithAdminRole_ReturnsUnauthorized() + { + var actor = CreateActor(); + var envelope = Envelope(new CreateApiMethodCommand("Method1", "code", 30, null, null), "Admin"); + + actor.Tell(envelope); + + var response = ExpectMsg(TimeSpan.FromSeconds(5)); + Assert.Contains("Design", response.Message); + } + + [Fact] + public void AddTemplateAttribute_WithDeploymentRole_ReturnsUnauthorized() + { + var actor = CreateActor(); + var envelope = Envelope(new AddTemplateAttributeCommand(1, "Attr1", "Float", null, null, null, false), "Deployment"); + + actor.Tell(envelope); + + var response = ExpectMsg(TimeSpan.FromSeconds(5)); + Assert.Contains("Design", response.Message); + } + + [Fact] + public void UpdateApiKey_WithDesignRole_ReturnsUnauthorized() + { + var actor = CreateActor(); + var envelope = Envelope(new UpdateApiKeyCommand(1, true), "Design"); + + actor.Tell(envelope); + + var response = ExpectMsg(TimeSpan.FromSeconds(5)); + Assert.Contains("Admin", response.Message); + } + + [Fact] + public void AddScopeRule_WithDesignRole_ReturnsUnauthorized() + { + var actor = CreateActor(); + var envelope = Envelope(new AddScopeRuleCommand(1, 1), "Design"); + + actor.Tell(envelope); + + var response = ExpectMsg(TimeSpan.FromSeconds(5)); + Assert.Contains("Admin", response.Message); + } + + [Fact] + public void UpdateArea_WithAdminRole_ReturnsUnauthorized() + { + var actor = CreateActor(); + var envelope = Envelope(new UpdateAreaCommand(1, "NewName"), "Admin"); + + actor.Tell(envelope); + + var response = ExpectMsg(TimeSpan.FromSeconds(5)); + Assert.Contains("Design", response.Message); + } + + // ======================================================================== + // New command read-only query tests (no role required) + // ======================================================================== + + [Fact] + public void ListSharedScripts_WithNoRoles_ReturnsSuccess() + { + _templateRepo.GetAllSharedScriptsAsync(Arg.Any()) + .Returns(new List()); + _services.AddScoped(); + + var actor = CreateActor(); + var envelope = Envelope(new ListSharedScriptsCommand()); + + actor.Tell(envelope); + + var response = ExpectMsg(TimeSpan.FromSeconds(5)); + Assert.Equal(envelope.CorrelationId, response.CorrelationId); + } + + [Fact] + public void ListDatabaseConnections_WithNoRoles_ReturnsSuccess() + { + var extRepo = Substitute.For(); + extRepo.GetAllDatabaseConnectionsAsync(Arg.Any()) + .Returns(new List()); + _services.AddScoped(_ => extRepo); + + var actor = CreateActor(); + var envelope = Envelope(new ListDatabaseConnectionsCommand()); + + actor.Tell(envelope); + + var response = ExpectMsg(TimeSpan.FromSeconds(5)); + Assert.Equal(envelope.CorrelationId, response.CorrelationId); + } + + [Fact] + public void ListApiMethods_WithNoRoles_ReturnsSuccess() + { + var apiRepo = Substitute.For(); + apiRepo.GetAllApiMethodsAsync(Arg.Any()) + .Returns(new List()); + _services.AddScoped(_ => apiRepo); + + var actor = CreateActor(); + var envelope = Envelope(new ListApiMethodsCommand()); + + actor.Tell(envelope); + + var response = ExpectMsg(TimeSpan.FromSeconds(5)); + Assert.Equal(envelope.CorrelationId, response.CorrelationId); + } + + [Fact] + public void ListScopeRules_WithAdminRole_ReturnsSuccess() + { + var secRepo = Substitute.For(); + secRepo.GetScopeRulesForMappingAsync(1, Arg.Any()) + .Returns(new List()); + _services.AddScoped(_ => secRepo); + + var actor = CreateActor(); + var envelope = Envelope(new ListScopeRulesCommand(1), "Admin"); + + actor.Tell(envelope); + + var response = ExpectMsg(TimeSpan.FromSeconds(5)); + Assert.Equal(envelope.CorrelationId, response.CorrelationId); + } + + // ======================================================================== + // New command error handling tests + // ======================================================================== + + [Fact] + public void ListDatabaseConnections_WhenRepoThrows_ReturnsError() + { + var extRepo = Substitute.For(); + extRepo.GetAllDatabaseConnectionsAsync(Arg.Any()) + .ThrowsAsync(new InvalidOperationException("Connection refused")); + _services.AddScoped(_ => extRepo); + + var actor = CreateActor(); + var envelope = Envelope(new ListDatabaseConnectionsCommand()); + + actor.Tell(envelope); + + var response = ExpectMsg(TimeSpan.FromSeconds(5)); + Assert.Equal("COMMAND_FAILED", response.ErrorCode); + Assert.Contains("Connection refused", response.Error); + } }