|
| 1 | +# Event Mesh Identity Provider Plugin |
| 2 | + |
| 3 | +A generic, vendor-agnostic identity and employee service provider for [Solace Agent Mesh](https://solacelabs.github.io/solace-agent-mesh/) that communicates with any backend system via Solace Event Mesh request-response messaging. |
| 4 | + |
| 5 | +## About Solace Agent Mesh |
| 6 | + |
| 7 | +[Solace Agent Mesh](https://solacelabs.github.io/solace-agent-mesh/) is a framework for building AI agent systems that communicate over Solace event brokers. It enables multi-agent collaboration, tool integration, and gateway connectivity for AI-powered applications. |
| 8 | + |
| 9 | +## When to Use This Plugin |
| 10 | + |
| 11 | +Use this plugin when: |
| 12 | + |
| 13 | +- **You have an HR or identity system** (e.g., SAP SuccessFactors, Workday, BambooHR, custom LDAP) that is accessible via a service connected to a Solace event broker. |
| 14 | +- **You need identity enrichment** in your gateway — enrich user auth claims with profile data (title, department, manager) from your HR system. |
| 15 | +- **You need employee directory access** in your agents — let agents look up employees, search users, fetch org structure, or retrieve profile pictures. |
| 16 | +- **You want a single plugin** that serves both identity and employee service roles without writing custom code. |
| 17 | + |
| 18 | +The plugin sends requests to configurable topics on your Solace broker and expects a backend service (your integration layer) to respond. This makes it compatible with **any** backend system — you just need a message-driven integration that responds on the event mesh. |
| 19 | + |
| 20 | +## Key Features |
| 21 | + |
| 22 | +- **Vendor-agnostic**: Works with any backend connected to Solace Event Mesh |
| 23 | +- **Configurable field mapping**: Map source fields to canonical schema via YAML (no code changes) |
| 24 | +- **Computed fields**: Derive values from multiple source fields (e.g., displayName from first + last name) |
| 25 | +- **Field exclusion and renaming**: Fine-grained control over output fields |
| 26 | +- **Flexible topic configuration**: Single topic string for all operations, or per-operation dict |
| 27 | +- **Built-in caching**: Configurable TTL caching for all operations |
| 28 | +- **Dual-purpose**: Use as identity service (gateways) and/or employee service (agents) |
| 29 | + |
| 30 | +## Installation |
| 31 | + |
| 32 | +```bash |
| 33 | +sam plugin install sam-event-mesh-identity-provider |
| 34 | +``` |
| 35 | + |
| 36 | +Or install directly: |
| 37 | + |
| 38 | +```bash |
| 39 | +pip install sam-event-mesh-identity-provider |
| 40 | +``` |
| 41 | + |
| 42 | +## Configuration |
| 43 | + |
| 44 | +### As Identity Service (Gateway) |
| 45 | + |
| 46 | +Add to your gateway configuration to enrich user identity on each request: |
| 47 | + |
| 48 | +```yaml |
| 49 | +identity_service: |
| 50 | + type: "event-mesh-identity-provider" |
| 51 | + |
| 52 | + broker_url: "${IDENTITY_BROKER_URL}" |
| 53 | + broker_vpn: "${IDENTITY_BROKER_VPN}" |
| 54 | + broker_username: "${IDENTITY_BROKER_USERNAME}" |
| 55 | + broker_password: "${IDENTITY_BROKER_PASSWORD}" |
| 56 | + |
| 57 | + lookup_key: "email" |
| 58 | + cache_ttl_seconds: 3600 |
| 59 | + request_expiry_ms: 120000 |
| 60 | + response_topic_prefix: "mycompany/identity/response" |
| 61 | + |
| 62 | + # Single topic for all operations: |
| 63 | + # request_topic: "mycompany/identity/request/v1/{request_id}" |
| 64 | + |
| 65 | + # Or per-operation topics: |
| 66 | + request_topic: |
| 67 | + user_profile: "mycompany/identity/user-profile/request/v1/{request_id}" |
| 68 | + search_users: "mycompany/identity/search-users/request/v1/{request_id}" |
| 69 | + |
| 70 | + field_mapping_config: |
| 71 | + field_mapping: |
| 72 | + emp_email: "workEmail" |
| 73 | + title: "jobTitle" |
| 74 | + dept_name: "department" |
| 75 | + computed_fields: |
| 76 | + - target: "displayName" |
| 77 | + source_fields: ["first_name", "last_name"] |
| 78 | + separator: " " |
| 79 | + pass_through_unmapped: true |
| 80 | +``` |
| 81 | +
|
| 82 | +### As People/Employee Service (Agent) |
| 83 | +
|
| 84 | +Add to your agent configuration for employee directory access: |
| 85 | +
|
| 86 | +```yaml |
| 87 | +people_service: |
| 88 | + type: "event-mesh-identity-provider" |
| 89 | + |
| 90 | + broker_url: "${IDENTITY_BROKER_URL}" |
| 91 | + broker_vpn: "${IDENTITY_BROKER_VPN}" |
| 92 | + broker_username: "${IDENTITY_BROKER_USERNAME}" |
| 93 | + broker_password: "${IDENTITY_BROKER_PASSWORD}" |
| 94 | + |
| 95 | + lookup_key: "email" |
| 96 | + cache_ttl_seconds: 3600 |
| 97 | + response_topic_prefix: "mycompany/identity/response" |
| 98 | + |
| 99 | + request_topic: |
| 100 | + user_profile: "mycompany/identity/user-profile/request/v1/{request_id}" |
| 101 | + search_users: "mycompany/identity/search-users/request/v1/{request_id}" |
| 102 | + employee_data: "mycompany/identity/employee-data/request/v1/{request_id}" |
| 103 | + employee_profile: "mycompany/identity/employee-profile/request/v1/{request_id}" |
| 104 | + time_off: "mycompany/identity/time-off/request/v1/{request_id}" |
| 105 | + profile_picture: "mycompany/identity/profile-picture/request/v1/{request_id}" |
| 106 | + |
| 107 | + field_mapping_config: |
| 108 | + field_mapping: {} |
| 109 | + pass_through_unmapped: true |
| 110 | +``` |
| 111 | +
|
| 112 | +## Configuration Reference |
| 113 | +
|
| 114 | +| Option | Description | Default | |
| 115 | +|--------|-------------|---------| |
| 116 | +| `type` | Must be `"event-mesh-identity-provider"` | Required | |
| 117 | +| `broker_url` | Solace broker URL | Required | |
| 118 | +| `broker_vpn` | Solace message VPN name | Required | |
| 119 | +| `broker_username` | Broker authentication username | Required | |
| 120 | +| `broker_password` | Broker authentication password | Required | |
| 121 | +| `dev_mode` | Enable development mode (no TLS verification) | `false` | |
| 122 | +| `lookup_key` | Key in auth_claims used to extract the lookup value | `"email"` | |
| 123 | +| `payload_key` | Key name used in the request payload sent to the backend | Value of `lookup_key` | |
| 124 | +| `cache_ttl_seconds` | Cache TTL in seconds (0 to disable) | `3600` | |
| 125 | +| `request_expiry_ms` | Request timeout in milliseconds | `120000` | |
| 126 | +| `response_topic_prefix` | Prefix for the response correlation topic | `"sam/identity-provider/response"` | |
| 127 | +| `user_properties_reply_topic_key` | Key in message user properties where the reply topic is stored | `"replyTopic"` | |
| 128 | +| `response_topic_insertion_expression` | Expression used by the broker to insert the response topic into the request | `"replyTopic"` | |
| 129 | +| `request_topic` | Topic template (string) or per-operation map (dict) | Required | |
| 130 | +| `field_mapping_config` | Field transformation configuration | `{}` (passthrough) | |
| 131 | + |
| 132 | +### `request_topic` Formats |
| 133 | + |
| 134 | +**String** — one topic for every operation: |
| 135 | +```yaml |
| 136 | +request_topic: "mycompany/identity/request/v1/{request_id}" |
| 137 | +``` |
| 138 | + |
| 139 | +**Dict** — per-operation topics (operations not listed return `None` with a warning): |
| 140 | +```yaml |
| 141 | +request_topic: |
| 142 | + user_profile: "mycompany/identity/user-profile/request/v1/{request_id}" |
| 143 | + search_users: "mycompany/identity/search-users/request/v1/{request_id}" |
| 144 | + employee_data: "mycompany/identity/employee-data/request/v1/{request_id}" |
| 145 | +``` |
| 146 | + |
| 147 | +Available operation keys: `user_profile`, `search_users`, `employee_data`, `employee_profile`, `time_off`, `profile_picture`. |
| 148 | + |
| 149 | +## Field Mapping Guide |
| 150 | + |
| 151 | +The field mapping engine transforms source data through a three-phase pipeline: |
| 152 | + |
| 153 | +``` |
| 154 | +Source Data -> [1. Field Mapping] -> [2. Exclusion] -> [3. Renaming] -> Output |
| 155 | + ^ |
| 156 | + [Computed Fields] |
| 157 | +``` |
| 158 | + |
| 159 | +### Phase 1: field_mapping |
| 160 | + |
| 161 | +Rename source fields to canonical names: |
| 162 | + |
| 163 | +```yaml |
| 164 | +field_mapping: |
| 165 | + emp_email: "workEmail" # "emp_email" in source -> "workEmail" in output |
| 166 | + position: "jobTitle" # "position" in source -> "jobTitle" in output |
| 167 | +``` |
| 168 | + |
| 169 | +### Computed Fields |
| 170 | + |
| 171 | +Derive values from multiple source fields: |
| 172 | + |
| 173 | +```yaml |
| 174 | +computed_fields: |
| 175 | + # Concatenation (skips empty parts) |
| 176 | + - target: "displayName" |
| 177 | + source_fields: ["firstName", "middleName", "lastName"] |
| 178 | + separator: " " |
| 179 | +
|
| 180 | + # Template-based (with fallback to concatenation if template fails) |
| 181 | + - target: "fullAddress" |
| 182 | + source_fields: ["city", "country"] |
| 183 | + template: "{city}, {country}" |
| 184 | +``` |
| 185 | + |
| 186 | +### Phase 2: exclusion_list |
| 187 | + |
| 188 | +Remove fields from output: |
| 189 | + |
| 190 | +```yaml |
| 191 | +exclusion_list: |
| 192 | + - "mobilePhone" # Don't expose phone numbers |
| 193 | + - "salaryStructure" # Don't expose salary data |
| 194 | +``` |
| 195 | + |
| 196 | +### Phase 3: rename_mapping |
| 197 | + |
| 198 | +Rename fields in the final output: |
| 199 | + |
| 200 | +```yaml |
| 201 | +rename_mapping: |
| 202 | + supervisorId: "managerId" # Rename for downstream consumers |
| 203 | + workEmail: "email" # Simplify field name |
| 204 | +``` |
| 205 | + |
| 206 | +### Zero-Configuration |
| 207 | + |
| 208 | +If your backend already returns data using the canonical field names (`id`, `displayName`, `workEmail`, `jobTitle`, `department`, `location`, `supervisorId`, `hireDate`, `mobilePhone`), you don't need any field mapping configuration. Just leave `field_mapping_config` empty or omit it entirely. |
| 209 | + |
| 210 | +## Backend Integration Guide |
| 211 | + |
| 212 | +Your backend service needs to listen on the configured topics and respond. Here's what each operation expects: |
| 213 | + |
| 214 | +### user_profile |
| 215 | + |
| 216 | +**Request payload:** |
| 217 | +```json |
| 218 | +{"email": "jane@company.com"} |
| 219 | +``` |
| 220 | + |
| 221 | +**Expected response:** A single employee record (dict). |
| 222 | + |
| 223 | +### search_users |
| 224 | + |
| 225 | +**Request payload:** |
| 226 | +```json |
| 227 | +{"query": "jan", "limit": 10} |
| 228 | +``` |
| 229 | + |
| 230 | +**Expected response:** A list of user dicts, or `{"results": [...]}`. |
| 231 | + |
| 232 | +### employee_data |
| 233 | + |
| 234 | +**Request payload:** |
| 235 | +```json |
| 236 | +{} |
| 237 | +``` |
| 238 | + |
| 239 | +**Expected response:** A list of all employee dicts, or `{"employees": [...]}`. |
| 240 | + |
| 241 | +### employee_profile |
| 242 | + |
| 243 | +**Request payload:** |
| 244 | +```json |
| 245 | +{"employee_id": "jane@company.com"} |
| 246 | +``` |
| 247 | + |
| 248 | +**Expected response:** A single employee record (dict). |
| 249 | + |
| 250 | +### time_off |
| 251 | + |
| 252 | +**Request payload:** |
| 253 | +```json |
| 254 | +{"employee_id": "jane@company.com", "start_date": "2025-01-01", "end_date": "2025-12-31"} |
| 255 | +``` |
| 256 | + |
| 257 | +**Expected response:** A list of time-off entry dicts, or `{"entries": [...]}`. |
| 258 | +Each entry must contain: `start` (YYYY-MM-DD), `end` (YYYY-MM-DD), `type` (string), `amount` ("full_day" or "half_day"). |
| 259 | + |
| 260 | +### profile_picture |
| 261 | + |
| 262 | +**Request payload:** |
| 263 | +```json |
| 264 | +{"employee_id": "jane@company.com"} |
| 265 | +``` |
| 266 | + |
| 267 | +**Expected response:** A data URI string (e.g., `"data:image/jpeg;base64,..."`) or `{"data_uri": "..."}`. |
| 268 | + |
| 269 | +## Canonical Employee Schema |
| 270 | + |
| 271 | +| Field | Type | Description | |
| 272 | +|-------|------|-------------| |
| 273 | +| `id` | string | Unique, stable, lowercase identifier | |
| 274 | +| `displayName` | string | Full name for display | |
| 275 | +| `workEmail` | string | Primary work email | |
| 276 | +| `jobTitle` | string | Official job title | |
| 277 | +| `department` | string | Department name | |
| 278 | +| `location` | string | Physical/regional location | |
| 279 | +| `supervisorId` | string | Manager's unique id | |
| 280 | +| `hireDate` | string | ISO 8601 date (YYYY-MM-DD) | |
| 281 | +| `mobilePhone` | string | Mobile phone number | |
| 282 | + |
| 283 | +Additional fields from your backend are passed through by default (when `pass_through_unmapped: true`). |
| 284 | + |
| 285 | +## Development |
| 286 | + |
| 287 | +```bash |
| 288 | +# Clone the repository |
| 289 | +git clone https://github.com/SolaceLabs/solace-agent-mesh-core-plugins.git |
| 290 | +cd solace-agent-mesh-core-plugins/sam-event-mesh-identity-provider |
| 291 | +
|
| 292 | +# Install in development mode |
| 293 | +pip install -e . |
| 294 | +
|
| 295 | +# Install test dependencies |
| 296 | +pip install pytest pytest-asyncio pytest-mock pytest-cov |
| 297 | +
|
| 298 | +# Run tests |
| 299 | +pytest tests/ -v |
| 300 | +
|
| 301 | +# Run with coverage |
| 302 | +pytest tests/ --cov=sam_event_mesh_identity_provider --cov-report=term-missing |
| 303 | +``` |
| 304 | + |
| 305 | +## License |
| 306 | + |
| 307 | +Apache License 2.0 |
0 commit comments