diff --git a/gr_security.md b/gr_security.md
new file mode 100644
index 0000000..5595394
--- /dev/null
+++ b/gr_security.md
@@ -0,0 +1,190 @@
+# Galaxy Repository — Security, Users, Roles & Permissions
+
+## Overview
+
+The Galaxy Repository uses a role-based access control (RBAC) system with three layers:
+1. **Users** — individual accounts in `user_profile`
+2. **Roles** — assigned to users, control what operations they can perform
+3. **Security Groups** — assigned to objects/packages, control which users can see/modify them
+4. **Security Classification** — per-attribute write protection levels (0-6)
+
+## 1. Users (`user_profile` table)
+
+| Column | Type | Description |
+|---|---|---|
+| `user_profile_id` | int | Primary key |
+| `user_profile_name` | nvarchar | Username (e.g., SystemEngineer, Administrator) |
+| `user_guid` | uniqueidentifier | Unique identifier for check-out tracking |
+| `password_hash` | int | Legacy password hash |
+| `crypto_secure_hashed_pwd` | ntext | Encrypted password |
+| `default_security_group` | nvarchar | Default security group for new objects |
+| `roles` | ntext | Serialized role assignments (binary) |
+| `intouch_access_level` | int | InTouch HMI access level |
+| `user_full_name` | nvarchar | Display name |
+| `default_platform_tag_name` | nvarchar | Default platform for deployments |
+| `default_app_engine_tag_name` | nvarchar | Default engine for new objects |
+| `default_history_engine_tag_name` | nvarchar | Default historian engine |
+| `default_area_tag_name` | nvarchar | Default area for new objects |
+
+### Current Users
+
+| ID | Name | Security Group | Roles | Notes |
+|---|---|---|---|---|
+| 1 | SystemEngineer | Default | (none) | Built-in engineering account |
+| 2 | Administrator | Default | Administrator | Has password, admin role |
+| 3 | DefaultUser | Default | (assigned) | Default runtime user |
+
+### User Preferences (`user_preferences` table)
+
+Stores per-user UI preferences as binary blobs, keyed by `user_profile_id` and `preference_type` (GUID).
+
+## 2. Roles & Permissions
+
+Roles are managed through Galaxy runtime attributes, not SQL tables. The Galaxy platform object exposes:
+
+### Role Management Attributes
+
+| Attribute | Description |
+|---|---|
+| `RoleList` | Array of defined role names |
+| `AddRole` | Command: create a new role |
+| `DeleteRole` | Command: remove a role |
+| `CurrentRole` | The active role for the current session |
+| `RolePermission` | Array of permission flags per role |
+| `TogglePermission` | Command: toggle a permission flag |
+
+### User-Role Assignment Attributes
+
+| Attribute | Description |
+|---|---|
+| `SetUserRole` | Command: assign a role to a user |
+| `DelUserRole` | Command: remove a role from a user |
+| `ListUserRole` | Command: list roles for a user |
+
+### Operation Permissions
+
+| Attribute | Description |
+|---|---|
+| `OpPermission` | Operation permission list |
+| `OpPermissionSGList` | Operation permissions per security group |
+| `ToggleOpPermission` | Command: toggle operation permission |
+
+## 3. Security Groups
+
+Security groups control which users can access which objects. Each `package` (object version) belongs to a security group.
+
+### `package.security_group` column
+
+- Type: nvarchar
+- Default: `"Default"`
+- All packages in the current Galaxy use the `"Default"` security group
+
+### Key Stored Procedures
+
+| Procedure | Description |
+|---|---|
+| `list_object_by_security_group` | Lists all objects in a given security group |
+| `internal_change_objects_security_group` | Bulk-changes security groups for objects |
+| `is_package_visible_to_user` | Checks if a user can see a specific package version |
+
+### Visibility Rules
+
+- Objects checked out by a user are only visible to that user (exclusive edit)
+- `gobject.checked_out_by_user_guid` tracks who has the object checked out
+- `is_package_visible_to_user` determines which package version (checked-in vs checked-out) a user sees
+
+## 4. Security Classification (Attribute-Level)
+
+Each attribute has a `security_classification` value controlling write access:
+
+| Value | Level | OPC UA Access | Description |
+|---|---|---|---|
+| 0 | FreeAccess | ReadWrite | No restrictions |
+| 1 | Operate | ReadWrite | Normal operating level (default) |
+| 2 | SecuredWrite | ReadOnly | Elevated write access required |
+| 3 | VerifiedWrite | ReadOnly | Verified/confirmed write required |
+| 4 | Tune | ReadWrite | Tuning-level access |
+| 5 | Configure | ReadWrite | Configuration-level access |
+| 6 | ViewOnly | ReadOnly | Read-only, no writes |
+
+### Distribution in Database
+
+| Classification | Count | Example Attributes |
+|---|---|---|
+| -1 | 15,905 | Most system attributes (no restriction) |
+| 0 | 2,194 | Basic attributes |
+| 1 | 1,420 | Description fields, standard attributes |
+| 4 | 1,392 | Tune-level (e.g., `_ChangePassword`) |
+| 5 | 969 | Configure-level attributes |
+| 6 | 2,250 | ViewOnly (e.g., `_ExternalName`) |
+
+### Lock Types (`dynamic_attribute.lock_type`)
+
+| Value | Description |
+|---|---|
+| 0 | Standard (unlocked) |
+| Other | Locked by template or system |
+
+## 5. Authentication
+
+The Galaxy supports multiple authentication modes, configured via the `AuthenticationMode` attribute on the platform:
+
+| Mode | Description |
+|---|---|
+| Galaxy Only | Users authenticate against Galaxy's internal user_profile table |
+| OS Group Only | Users authenticate via Windows domain groups |
+| Windows Integrated Security | Users authenticate via Windows credentials (SSO) |
+
+### Related Attributes
+
+| Attribute | Description |
+|---|---|
+| `AuthenticationMode` | Active authentication method |
+| `SecurityEnabled` | Boolean: whether security enforcement is active |
+| `_SecuritySchema` | XML security schema definition (binary) |
+| `_MasterSecuritySchema` | Master security schema (binary) |
+
+## 6. Check-Out/Check-In Security
+
+Galaxy objects use an exclusive check-out model for editing:
+
+| `gobject` Column | Description |
+|---|---|
+| `checked_out_by_user_guid` | GUID of user who has the object checked out |
+| `checked_out_package_id` | Package version created for the check-out |
+| `checked_in_package_id` | Last checked-in package version |
+
+Only the user who checked out an object can modify it. Other users see the last checked-in version.
+
+## 7. Protected Objects
+
+The `gobject_protected` table lists objects that cannot be modified:
+- Contains only `gobject_id` column
+- Protects system-critical objects from accidental changes
+
+## 8. Operation/Workflow Groups
+
+| Table | Description |
+|---|---|
+| `ow_group_def` | Operation/workflow group definitions |
+| `ow_group_id` | Group ID tracking |
+| `ow_group_override` | Group overrides with `property_bitmask` |
+
+## Summary
+
+The Galaxy security model operates at four levels:
+
+```
+Authentication (Galaxy/OS/Windows)
+ └─ Users (user_profile)
+ └─ Roles (RoleList, RolePermission)
+ └─ Security Groups (package.security_group)
+ └─ Attribute Security Classification (0-6)
+```
+
+- **Authentication** determines who can connect
+- **Roles** determine what operations a user can perform
+- **Security Groups** determine which objects a user can access
+- **Security Classification** determines per-attribute write permissions
+
+For the OPC UA server, the most relevant layer is **Security Classification** (already implemented — maps to OPC UA AccessLevel). The server currently uses anonymous access and does not enforce user/role/security-group checks.
diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/HistorianConfiguration.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/HistorianConfiguration.cs
index 7a698b4..2121cf0 100644
--- a/src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/HistorianConfiguration.cs
+++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/HistorianConfiguration.cs
@@ -5,6 +5,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Configuration
///
public class HistorianConfiguration
{
+ ///
+ /// Gets or sets a value indicating whether OPC UA historical data access is enabled.
+ ///
+ public bool Enabled { get; set; } = false;
+
///
/// Gets or sets the connection string for the Wonderware Historian Runtime database.
///
diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/OpcUaConfiguration.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/OpcUaConfiguration.cs
index 40fe1da..64f7363 100644
--- a/src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/OpcUaConfiguration.cs
+++ b/src/ZB.MOM.WW.LmxOpcUa.Host/Configuration/OpcUaConfiguration.cs
@@ -34,5 +34,11 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Configuration
/// Gets or sets the session timeout, in minutes, before idle client sessions are closed.
///
public int SessionTimeoutMinutes { get; set; } = 30;
+
+ ///
+ /// Gets or sets a value indicating whether alarm tracking is enabled.
+ /// When enabled, AlarmConditionState nodes are created for alarm attributes and InAlarm transitions are monitored.
+ ///
+ public bool AlarmTrackingEnabled { get; set; } = false;
}
}
diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/LmxNodeManager.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/LmxNodeManager.cs
index 1d66feb..7b4a76c 100644
--- a/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/LmxNodeManager.cs
+++ b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/LmxNodeManager.cs
@@ -23,6 +23,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
private readonly IMxAccessClient _mxAccessClient;
private readonly PerformanceMetrics _metrics;
private readonly HistorianDataSource? _historianDataSource;
+ private readonly bool _alarmTrackingEnabled;
private readonly string _namespaceUri;
// NodeId → full_tag_reference for read/write resolution
@@ -123,13 +124,15 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
string namespaceUri,
IMxAccessClient mxAccessClient,
PerformanceMetrics metrics,
- HistorianDataSource? historianDataSource = null)
+ HistorianDataSource? historianDataSource = null,
+ bool alarmTrackingEnabled = false)
: base(server, configuration, namespaceUri)
{
_namespaceUri = namespaceUri;
_mxAccessClient = mxAccessClient;
_metrics = metrics;
_historianDataSource = historianDataSource;
+ _alarmTrackingEnabled = alarmTrackingEnabled;
// Wire up data change delivery
_mxAccessClient.OnTagValueChanged += OnMxAccessDataChange;
@@ -280,7 +283,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
}
// Build alarm tracking: create AlarmConditionState for each alarm attribute
- foreach (var obj in sorted)
+ if (_alarmTrackingEnabled) foreach (var obj in sorted)
{
if (obj.IsArea) continue;
if (!attrsByObject.TryGetValue(obj.GobjectId, out var objAttrs)) continue;
@@ -354,7 +357,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
}
// Auto-subscribe to InAlarm tags so we detect alarm transitions
- SubscribeAlarmTags();
+ if (_alarmTrackingEnabled)
+ SubscribeAlarmTags();
Log.Information("Address space built: {Objects} objects, {Variables} variables, {Mappings} tag references, {Alarms} alarm tags",
ObjectNodeCount, VariableNodeCount, _nodeIdToTagReference.Count, _alarmInAlarmTags.Count);
diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/LmxOpcUaServer.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/LmxOpcUaServer.cs
index 3092333..e763836 100644
--- a/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/LmxOpcUaServer.cs
+++ b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/LmxOpcUaServer.cs
@@ -16,6 +16,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
private readonly IMxAccessClient _mxAccessClient;
private readonly PerformanceMetrics _metrics;
private readonly HistorianDataSource? _historianDataSource;
+ private readonly bool _alarmTrackingEnabled;
private LmxNodeManager? _nodeManager;
///
@@ -42,19 +43,20 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
/// The runtime client used by the node manager for live data access.
/// The metrics collector shared with the node manager.
public LmxOpcUaServer(string galaxyName, IMxAccessClient mxAccessClient, PerformanceMetrics metrics,
- HistorianDataSource? historianDataSource = null)
+ HistorianDataSource? historianDataSource = null, bool alarmTrackingEnabled = false)
{
_galaxyName = galaxyName;
_mxAccessClient = mxAccessClient;
_metrics = metrics;
_historianDataSource = historianDataSource;
+ _alarmTrackingEnabled = alarmTrackingEnabled;
}
///
protected override MasterNodeManager CreateMasterNodeManager(IServerInternal server, ApplicationConfiguration configuration)
{
var namespaceUri = $"urn:{_galaxyName}:LmxOpcUa";
- _nodeManager = new LmxNodeManager(server, configuration, namespaceUri, _mxAccessClient, _metrics, _historianDataSource);
+ _nodeManager = new LmxNodeManager(server, configuration, namespaceUri, _mxAccessClient, _metrics, _historianDataSource, _alarmTrackingEnabled);
var nodeManagers = new List { _nodeManager };
return new MasterNodeManager(server, configuration, null, nodeManagers.ToArray());
diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/OpcUaServerHost.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/OpcUaServerHost.cs
index d46ecb8..3b1400b 100644
--- a/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/OpcUaServerHost.cs
+++ b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUa/OpcUaServerHost.cs
@@ -159,7 +159,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
certOk = await _application.CheckApplicationInstanceCertificate(false, 2048);
}
- _server = new LmxOpcUaServer(_config.GalaxyName, _mxAccessClient, _metrics, _historianDataSource);
+ _server = new LmxOpcUaServer(_config.GalaxyName, _mxAccessClient, _metrics, _historianDataSource, _config.AlarmTrackingEnabled);
await _application.Start(_server);
Log.Information("OPC UA server started on opc.tcp://localhost:{Port}{EndpointPath} (namespace={Namespace})",
diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUaService.cs b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUaService.cs
index 0260133..83cfb53 100644
--- a/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUaService.cs
+++ b/src/ZB.MOM.WW.LmxOpcUa.Host/OpcUaService.cs
@@ -153,7 +153,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host
// Step 8: Create OPC UA server host + node manager
var effectiveMxClient = (IMxAccessClient?)_mxAccessClient ?? _mxAccessClientForWiring ?? new NullMxAccessClient();
- var historianDataSource = new Historian.HistorianDataSource(_config.Historian);
+ var historianDataSource = _config.Historian.Enabled
+ ? new Historian.HistorianDataSource(_config.Historian)
+ : null;
_serverHost = new OpcUaServerHost(_config.OpcUa, effectiveMxClient, _metrics, historianDataSource);
// Step 9-10: Query hierarchy, start server, build address space
diff --git a/src/ZB.MOM.WW.LmxOpcUa.Host/appsettings.json b/src/ZB.MOM.WW.LmxOpcUa.Host/appsettings.json
index b10bb8d..d259b69 100644
--- a/src/ZB.MOM.WW.LmxOpcUa.Host/appsettings.json
+++ b/src/ZB.MOM.WW.LmxOpcUa.Host/appsettings.json
@@ -5,7 +5,8 @@
"ServerName": "LmxOpcUa",
"GalaxyName": "ZB",
"MaxSessions": 100,
- "SessionTimeoutMinutes": 30
+ "SessionTimeoutMinutes": 30,
+ "AlarmTrackingEnabled": false
},
"MxAccess": {
"ClientName": "LmxOpcUa",
@@ -31,6 +32,7 @@
"RefreshIntervalSeconds": 10
},
"Historian": {
+ "Enabled": false,
"ConnectionString": "Server=localhost;Database=Runtime;Integrated Security=true;",
"CommandTimeoutSeconds": 30,
"MaxValuesPerRead": 10000