feat: Update Authentik integration and enhance Docker Compose templates
This commit is contained in:
73
.github/copilot-instructions.md
vendored
Normal file
73
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# Project Guidelines — OTS Signs Orchestrator
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Layered MVVM desktop app for provisioning and managing Xibo CMS instances on Docker Swarm.
|
||||||
|
|
||||||
|
- **OTSSignsOrchestrator.Core** — class library: services, EF Core data access, models, configuration. Reusable across UIs.
|
||||||
|
- **OTSSignsOrchestrator.Desktop** — Avalonia 11 UI: views, view models, DI setup. References Core.
|
||||||
|
- **templates/** — Docker Compose + PHP templates with `{{PLACEHOLDER}}` substitution.
|
||||||
|
|
||||||
|
### Key patterns
|
||||||
|
- Services injected via `IServiceProvider` (registered in `App.axaml.cs` → `ConfigureServices()`)
|
||||||
|
- Singletons: stateful services (SSH connections, Docker CLI). Transient: stateless logic.
|
||||||
|
- Configuration via `IOptions<T>` bound from `appsettings.json` (see `AppOptions.cs` for all sections).
|
||||||
|
- Bitwarden Secrets Manager is the source of truth for all sensitive config. `SettingsService` caches in-memory.
|
||||||
|
- Local SQLite DB (`otssigns-desktop.db`) stores SSH hosts + operation logs. Credentials encrypted via Data Protection API.
|
||||||
|
|
||||||
|
### External integrations
|
||||||
|
Xibo CMS REST API (OAuth2), Authentik SAML IdP, Bitwarden Secrets SDK, Docker Swarm (via SSH), Git (LibGit2Sharp), MySQL 8.4, NFS volumes, Pangolin/Newt VPN.
|
||||||
|
|
||||||
|
## Build and Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build
|
||||||
|
dotnet build OTSSignsOrchestrator.Desktop/OTSSignsOrchestrator.Desktop.csproj
|
||||||
|
|
||||||
|
# Run
|
||||||
|
dotnet run --project OTSSignsOrchestrator.Desktop/OTSSignsOrchestrator.Desktop.csproj
|
||||||
|
|
||||||
|
# No test suite currently — no xUnit/NUnit projects
|
||||||
|
```
|
||||||
|
|
||||||
|
- .NET 9.0, Avalonia 11.2.3, CommunityToolkit.Mvvm 8.4
|
||||||
|
- Runtime identifiers: `linux-x64`, `win-x64`, `osx-x64`, `osx-arm64`
|
||||||
|
- EF Core migrations in `OTSSignsOrchestrator.Core/Migrations/`
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
### ViewModels
|
||||||
|
- Inherit `ObservableObject` (CommunityToolkit.Mvvm). Use `[ObservableProperty]` for bindable fields and `[RelayCommand]` for commands.
|
||||||
|
- React to changes via `partial void OnXxxChanged(T value)` methods generated by the toolkit.
|
||||||
|
- Resolve services from `IServiceProvider` in constructors. Navigation via `MainWindowViewModel.CurrentView`.
|
||||||
|
- Confirmation dialogs use `Func<string, string, Task<bool>> ConfirmAsync` property — wired by the View.
|
||||||
|
|
||||||
|
### Views (Avalonia XAML)
|
||||||
|
- Compiled bindings enabled (`x:CompileBindings="True"`). DataTemplates in `MainWindow.axaml` map ViewModel types to View UserControls.
|
||||||
|
- Layout: DockPanel with status bar (bottom), sidebar nav (left), dynamic ContentControl (center).
|
||||||
|
- Style: Fluent theme, dark palette (`#0C0C14` accents).
|
||||||
|
|
||||||
|
### Services
|
||||||
|
- Interface + implementation pattern for testable services (`IXiboApiService`, `IDockerCliService`, etc.).
|
||||||
|
- `SshDockerCliService` is a singleton — **must call `SetHost(host)` before each operation** in loops.
|
||||||
|
- All long operations are `async Task`. Use `IsBusy` + `StatusMessage` properties for UI feedback.
|
||||||
|
|
||||||
|
### Naming
|
||||||
|
- Customer abbreviation: exactly 3 lowercase letters (`^[a-z]{3}$`).
|
||||||
|
- Stack name: `{abbrev}-cms-stack`. Service: `{abbrev}-web`. DB: `{abbrev}_cms_db`.
|
||||||
|
- Secret names built via `AppConstants` helpers (e.g., `CustomerMysqlPasswordSecretName(abbrev)`).
|
||||||
|
- `AppConstants.SanitizeName()` filters to `[a-z0-9_-]`.
|
||||||
|
|
||||||
|
### Data layer
|
||||||
|
- Entities in `Core/Models/Entities/`, DTOs in `Core/Models/DTOs/`.
|
||||||
|
- `XiboContext` applies unique index on `SshHost.Label` and encrypts credential fields.
|
||||||
|
- Add new migrations via: `dotnet ef migrations add <Name> --project OTSSignsOrchestrator.Core --startup-project OTSSignsOrchestrator.Desktop`
|
||||||
|
|
||||||
|
## Pitfalls
|
||||||
|
|
||||||
|
- **SSH singleton state**: `SshDockerCliService.SetHost()` must be called before each host operation — stale host causes silent failures.
|
||||||
|
- **Bitwarden cache**: After creating secrets during deployment, call `FlushCacheAsync()` before reading them back.
|
||||||
|
- **Data Protection keys**: Stored in `%APPDATA%/OTSSignsOrchestrator/keys`. If lost, encrypted SSH passwords are unrecoverable.
|
||||||
|
- **Docker volumes are sticky**: Failed deploys leave volumes with old NFS driver options. Use `PurgeStaleVolumes: true` to force fresh volumes (causes data loss).
|
||||||
|
- **No saga/rollback**: Instance creation spans Git → MySQL → Docker → Xibo. Partial failures leave orphaned resources; cleanup is manual via `OperationLog`.
|
||||||
|
- **Template CIFS→NFS compat**: Old `{{CIFS_*}}` tokens still render correctly as NFS equivalents.
|
||||||
Submodule .template-cache/2dc03e2b2b45fef3 updated: 0cc0da3c6e...9663c2ade8
@@ -691,36 +691,63 @@ public class AuthentikService : IAuthentikService
|
|||||||
{
|
{
|
||||||
_logger.LogInformation("[Authentik] Creating usertypeid property mapping");
|
_logger.LogInformation("[Authentik] Creating usertypeid property mapping");
|
||||||
|
|
||||||
var expression = @"return ""1"" if user.groups.all() | selectattr(""name"", ""equalto"", ""OTS IT"") else """"";
|
// Use proper Authentik Python syntax for group membership check
|
||||||
|
var expression = @"
|
||||||
|
# Check if user is in OTS IT admin group
|
||||||
|
admin_groups = [g.name for g in user.groups.all()]
|
||||||
|
return '1' if 'OTS IT' in admin_groups else ''
|
||||||
|
";
|
||||||
|
|
||||||
var payload = new Dictionary<string, object>
|
var payload = new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
["name"] = "saml-usertypeid",
|
["name"] = "saml-usertypeid",
|
||||||
["saml_name"] = "usertypeid",
|
|
||||||
["expression"] = expression,
|
["expression"] = expression,
|
||||||
};
|
};
|
||||||
|
|
||||||
var jsonBody = JsonSerializer.Serialize(payload);
|
var jsonBody = JsonSerializer.Serialize(payload);
|
||||||
var content = new StringContent(jsonBody, Encoding.UTF8, "application/json");
|
_logger.LogDebug("[Authentik] Creating property mapping with payload: {Payload}", jsonBody);
|
||||||
|
|
||||||
var resp = await client.PostAsync("/api/v3/propertymappings/provider/saml/", content, ct);
|
// Try multiple endpoints (API versions vary)
|
||||||
|
var endpoints = new[]
|
||||||
if (!resp.IsSuccessStatusCode)
|
|
||||||
{
|
{
|
||||||
var errorBody = await resp.Content.ReadAsStringAsync(ct);
|
"/api/v3/propertymappings/provider/saml/",
|
||||||
_logger.LogWarning("[Authentik] Failed to create usertypeid mapping (HTTP {Status}): {Error}",
|
"/api/v3/propertymappings/saml/",
|
||||||
(int)resp.StatusCode, errorBody);
|
"/api/v3/propertymappings/",
|
||||||
return null;
|
};
|
||||||
|
|
||||||
|
foreach (var endpoint in endpoints)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var content = new StringContent(jsonBody, Encoding.UTF8, "application/json");
|
||||||
|
var resp = await client.PostAsync(endpoint, content, ct);
|
||||||
|
|
||||||
|
_logger.LogDebug("[Authentik] POST {Endpoint} returned HTTP {Status}", endpoint, (int)resp.StatusCode);
|
||||||
|
|
||||||
|
if (!resp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var errorBody = await resp.Content.ReadAsStringAsync(ct);
|
||||||
|
_logger.LogDebug("[Authentik] Error response: {Error}", errorBody);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await resp.Content.ReadFromJsonAsync<AuthentikPropertyMapping>(cancellationToken: ct);
|
||||||
|
if (result?.Pk != null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("[Authentik] Created usertypeid mapping: {Id} ({Name}) via {Endpoint}",
|
||||||
|
result.Pk, result.Name, endpoint);
|
||||||
|
return result.Pk;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("[Authentik] Created usertypeid mapping but response had no Pk");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "[Authentik] Error creating usertypeid mapping via {Endpoint}", endpoint);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await resp.Content.ReadFromJsonAsync<AuthentikPropertyMapping>(cancellationToken: ct);
|
_logger.LogWarning("[Authentik] Could not create usertypeid mapping via any endpoint");
|
||||||
if (result?.Pk != null)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("[Authentik] Created usertypeid mapping: {Id}", result.Pk);
|
|
||||||
return result.Pk;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogWarning("[Authentik] Created usertypeid mapping but response had no Pk");
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -761,26 +788,46 @@ public class AuthentikService : IAuthentikService
|
|||||||
{
|
{
|
||||||
foreach (var mapEl in mapsProp.EnumerateArray())
|
foreach (var mapEl in mapsProp.EnumerateArray())
|
||||||
{
|
{
|
||||||
var mapId = mapEl.ValueKind == JsonValueKind.String
|
string? mapId = null;
|
||||||
? mapEl.GetString()
|
|
||||||
: (mapEl.TryGetProperty("pk", out var pkProp)
|
// Handle both string IDs and object IDs
|
||||||
? pkProp.GetString()
|
if (mapEl.ValueKind == JsonValueKind.String)
|
||||||
: null);
|
{
|
||||||
|
mapId = mapEl.GetString();
|
||||||
|
}
|
||||||
|
else if (mapEl.ValueKind == JsonValueKind.Object && mapEl.TryGetProperty("pk", out var pkProp))
|
||||||
|
{
|
||||||
|
mapId = pkProp.GetString();
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(mapId) && !mappings.Contains(mapId))
|
if (!string.IsNullOrEmpty(mapId) && !mappings.Contains(mapId))
|
||||||
|
{
|
||||||
mappings.Add(mapId);
|
mappings.Add(mapId);
|
||||||
|
_logger.LogDebug("[Authentik] Found existing mapping: {Id}", mapId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 3. Add new mapping if not already present ──────────────────
|
// ── 3. Add new mapping if not already present ──────────────────
|
||||||
if (!mappings.Contains(mappingId))
|
if (!mappings.Contains(mappingId))
|
||||||
|
{
|
||||||
mappings.Add(mappingId);
|
mappings.Add(mappingId);
|
||||||
|
_logger.LogInformation("[Authentik] Adding usertypeid mapping {Id} to provider", mappingId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogInformation("[Authentik] Usertypeid mapping {Id} already attached to provider", mappingId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// ── 4. Patch provider with updated mappings ───────────────────
|
// ── 4. Patch provider with updated mappings ───────────────────
|
||||||
var patchPayload = JsonSerializer.Serialize(new Dictionary<string, object>
|
var patchPayload = JsonSerializer.Serialize(new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
["property_mappings"] = mappings,
|
["property_mappings"] = mappings,
|
||||||
});
|
});
|
||||||
|
_logger.LogDebug("[Authentik] Patching provider {Id} with mappings: {Mappings}",
|
||||||
|
providerId, string.Join(", ", mappings));
|
||||||
|
|
||||||
var patchContent = new StringContent(patchPayload, Encoding.UTF8, "application/json");
|
var patchContent = new StringContent(patchPayload, Encoding.UTF8, "application/json");
|
||||||
var patchReq = new HttpRequestMessage(HttpMethod.Patch, $"/api/v3/providers/saml/{providerId}/")
|
var patchReq = new HttpRequestMessage(HttpMethod.Patch, $"/api/v3/providers/saml/{providerId}/")
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ namespace OTSSignsOrchestrator.Core.Services;
|
|||||||
/// Renders a Docker Compose file by loading a template from the git repo and substituting
|
/// Renders a Docker Compose file by loading a template from the git repo and substituting
|
||||||
/// all {{PLACEHOLDER}} tokens with values from RenderContext.
|
/// all {{PLACEHOLDER}} tokens with values from RenderContext.
|
||||||
/// The template file expected in the repo is <c>template.yml</c>.
|
/// The template file expected in the repo is <c>template.yml</c>.
|
||||||
/// Call <see cref="GetTemplateYaml"/> to obtain the canonical template to commit to your repo.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ComposeRenderService
|
public class ComposeRenderService
|
||||||
{
|
{
|
||||||
@@ -148,156 +147,6 @@ public class ComposeRenderService
|
|||||||
return folders;
|
return folders;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the canonical <c>template.yml</c> content with all placeholders.
|
|
||||||
/// Commit this file to the root of your template git repository.
|
|
||||||
/// </summary>
|
|
||||||
public static string GetTemplateYaml() => TemplateYaml;
|
|
||||||
|
|
||||||
// ── Canonical template ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
public const string TemplateYaml =
|
|
||||||
"""
|
|
||||||
# Customer: {{CUSTOMER_NAME}}
|
|
||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
|
||||||
|
|
||||||
{{ABBREV}}-web:
|
|
||||||
image: {{CMS_IMAGE}}
|
|
||||||
environment:
|
|
||||||
CMS_USE_MEMCACHED: "true"
|
|
||||||
MEMCACHED_HOST: memcached
|
|
||||||
MYSQL_HOST: {{MYSQL_HOST}}
|
|
||||||
MYSQL_PORT: "{{MYSQL_PORT}}"
|
|
||||||
MYSQL_DATABASE: {{MYSQL_DATABASE}}
|
|
||||||
MYSQL_USER: {{MYSQL_USER}}
|
|
||||||
MYSQL_PASSWORD: {{MYSQL_PASSWORD}}
|
|
||||||
CMS_SERVER_NAME: {{CMS_SERVER_NAME}}
|
|
||||||
CMS_SMTP_SERVER: {{SMTP_SERVER}}
|
|
||||||
CMS_SMTP_USERNAME: {{SMTP_USERNAME}}
|
|
||||||
CMS_SMTP_PASSWORD: {{SMTP_PASSWORD}}
|
|
||||||
CMS_SMTP_USE_TLS: {{SMTP_USE_TLS}}
|
|
||||||
CMS_SMTP_USE_STARTTLS: {{SMTP_USE_STARTTLS}}
|
|
||||||
CMS_SMTP_REWRITE_DOMAIN: {{SMTP_REWRITE_DOMAIN}}
|
|
||||||
CMS_SMTP_HOSTNAME: {{SMTP_HOSTNAME}}
|
|
||||||
CMS_SMTP_FROM_LINE_OVERRIDE: {{SMTP_FROM_LINE_OVERRIDE}}
|
|
||||||
CMS_PHP_POST_MAX_SIZE: {{PHP_POST_MAX_SIZE}}
|
|
||||||
CMS_PHP_UPLOAD_MAX_FILESIZE: {{PHP_UPLOAD_MAX_FILESIZE}}
|
|
||||||
CMS_PHP_MAX_EXECUTION_TIME: "{{PHP_MAX_EXECUTION_TIME}}"
|
|
||||||
secrets:
|
|
||||||
- {{ABBREV}}-cms-db-user
|
|
||||||
- global_mysql_host
|
|
||||||
- global_mysql_port
|
|
||||||
volumes:
|
|
||||||
- {{ABBREV}}-cms-custom:/var/www/cms/custom
|
|
||||||
- {{ABBREV}}-cms-backup:/var/www/backup
|
|
||||||
- {{THEME_HOST_PATH}}:/var/www/cms/web/theme/custom
|
|
||||||
- {{ABBREV}}-cms-library:/var/www/cms/library
|
|
||||||
- {{ABBREV}}-cms-userscripts:/var/www/cms/web/userscripts
|
|
||||||
- {{ABBREV}}-cms-ca-certs:/var/www/cms/ca-certs
|
|
||||||
ports:
|
|
||||||
- "{{HOST_HTTP_PORT}}:80"
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "curl -fsS --max-time 5 http://web:80/about | grep -Eo 'v?[0-9]+(\\.[0-9]+)+' >/dev/null || exit 1"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
start_period: 30s
|
|
||||||
networks:
|
|
||||||
{{ABBREV}}-net:
|
|
||||||
aliases:
|
|
||||||
- web
|
|
||||||
deploy:
|
|
||||||
restart_policy:
|
|
||||||
condition: any
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 1G
|
|
||||||
|
|
||||||
{{ABBREV}}-memcached:
|
|
||||||
image: {{MEMCACHED_IMAGE}}
|
|
||||||
command: [memcached, -m, "15"]
|
|
||||||
networks:
|
|
||||||
{{ABBREV}}-net:
|
|
||||||
aliases:
|
|
||||||
- memcached
|
|
||||||
deploy:
|
|
||||||
restart_policy:
|
|
||||||
condition: any
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 100M
|
|
||||||
|
|
||||||
{{ABBREV}}-quickchart:
|
|
||||||
image: {{QUICKCHART_IMAGE}}
|
|
||||||
networks:
|
|
||||||
{{ABBREV}}-net:
|
|
||||||
aliases:
|
|
||||||
- quickchart
|
|
||||||
deploy:
|
|
||||||
restart_policy:
|
|
||||||
condition: any
|
|
||||||
|
|
||||||
{{ABBREV}}-newt:
|
|
||||||
image: {{NEWT_IMAGE}}
|
|
||||||
environment:
|
|
||||||
PANGOLIN_ENDPOINT: {{PANGOLIN_ENDPOINT}}
|
|
||||||
NEWT_ID: {{NEWT_ID}}
|
|
||||||
NEWT_SECRET: {{NEWT_SECRET}}
|
|
||||||
depends_on:
|
|
||||||
- {{ABBREV}}-web
|
|
||||||
networks:
|
|
||||||
{{ABBREV}}-net: {}
|
|
||||||
deploy:
|
|
||||||
restart_policy:
|
|
||||||
condition: any
|
|
||||||
|
|
||||||
networks:
|
|
||||||
{{ABBREV}}-net:
|
|
||||||
driver: overlay
|
|
||||||
attachable: "false"
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
{{ABBREV}}-cms-custom:
|
|
||||||
driver: local
|
|
||||||
driver_opts:
|
|
||||||
type: nfs
|
|
||||||
device: "{{NFS_DEVICE_PREFIX}}/{{ABBREV}}/cms-custom"
|
|
||||||
o: "{{NFS_OPTS}}"
|
|
||||||
{{ABBREV}}-cms-backup:
|
|
||||||
driver: local
|
|
||||||
driver_opts:
|
|
||||||
type: nfs
|
|
||||||
device: "{{NFS_DEVICE_PREFIX}}/{{ABBREV}}/cms-backup"
|
|
||||||
o: "{{NFS_OPTS}}"
|
|
||||||
{{ABBREV}}-cms-library:
|
|
||||||
driver: local
|
|
||||||
driver_opts:
|
|
||||||
type: nfs
|
|
||||||
device: "{{NFS_DEVICE_PREFIX}}/{{ABBREV}}/cms-library"
|
|
||||||
o: "{{NFS_OPTS}}"
|
|
||||||
{{ABBREV}}-cms-userscripts:
|
|
||||||
driver: local
|
|
||||||
driver_opts:
|
|
||||||
type: nfs
|
|
||||||
device: "{{NFS_DEVICE_PREFIX}}/{{ABBREV}}/cms-userscripts"
|
|
||||||
o: "{{NFS_OPTS}}"
|
|
||||||
{{ABBREV}}-cms-ca-certs:
|
|
||||||
driver: local
|
|
||||||
driver_opts:
|
|
||||||
type: nfs
|
|
||||||
device: "{{NFS_DEVICE_PREFIX}}/{{ABBREV}}/cms-ca-certs"
|
|
||||||
o: "{{NFS_OPTS}}"
|
|
||||||
|
|
||||||
secrets:
|
|
||||||
{{ABBREV}}-cms-db-user:
|
|
||||||
external: true
|
|
||||||
global_mysql_host:
|
|
||||||
external: true
|
|
||||||
global_mysql_port:
|
|
||||||
external: true
|
|
||||||
""";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Context object with all inputs needed to render a Compose file.</summary>
|
/// <summary>Context object with all inputs needed to render a Compose file.</summary>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public class GitTemplateService
|
|||||||
var yamlPath = FindFile(cacheDir, "template.yml");
|
var yamlPath = FindFile(cacheDir, "template.yml");
|
||||||
|
|
||||||
if (yamlPath == null)
|
if (yamlPath == null)
|
||||||
throw new FileNotFoundException("template.yml not found in repository root. Commit the template file produced by ComposeRenderService.GetTemplateYaml() to the repo root.");
|
throw new FileNotFoundException("template.yml not found in repository root. Ensure template.yml is committed to the root of your template git repository.");
|
||||||
|
|
||||||
var yaml = await File.ReadAllTextAsync(yamlPath);
|
var yaml = await File.ReadAllTextAsync(yamlPath);
|
||||||
|
|
||||||
|
|||||||
@@ -2,20 +2,28 @@
|
|||||||
/**
|
/**
|
||||||
* SAML Authentication Configuration with Group-Based Admin Assignment
|
* SAML Authentication Configuration with Group-Based Admin Assignment
|
||||||
*
|
*
|
||||||
* Group-Based Admin Assignment:
|
* Group-Based Admin Assignment (AUTOMATIC):
|
||||||
* ────────────────────────────────────────────────────────────────────────
|
* ────────────────────────────────────────────────────────────────────────
|
||||||
* To make members of specific Authentik groups admins in Xibo:
|
* The OTSSignsOrchestrator AUTOMATICALLY creates and configures:
|
||||||
*
|
*
|
||||||
* 1. In Authentik, create a custom property mapping for your SAML provider:
|
* 1. A custom property mapping "saml-usertypeid" in Authentik that:
|
||||||
* - Name: saml-usertypeid
|
* - Returns "1" for users in the "OTS IT" group
|
||||||
* - Expression: Return "1" if user in admin group, else return empty string
|
* - Returns empty string for other users
|
||||||
* - Example: return "1" if user.groups.all() | selectattr("name", "equalto", "OTS IT") else ""
|
|
||||||
*
|
*
|
||||||
* 2. Attach this mapping to the SAML provider via the API or UI
|
* 2. Attaches this mapping to the SAML provider so the attribute is sent in SAML responses
|
||||||
*
|
*
|
||||||
* 3. The usertypeid mapping below will read this attribute from the SAML response
|
* 3. Configures Xibo to read the usertypeid SAML attribute (see 'usertypeid' mapping below)
|
||||||
*
|
*
|
||||||
* 4. On JIT provisioning, Xibo will assign users with usertypeid=1 as super-admins
|
* Result: OTS IT members are automatically assigned as super-admins on SSO login.
|
||||||
|
* Other users are assigned normal permissions.
|
||||||
|
*
|
||||||
|
* If OTS IT members are NOT getting admin access:
|
||||||
|
* ────────────────────────────────────────────────────────────────────────
|
||||||
|
* 1. Check Authentik logs for "saml-usertypeid" mapping creation errors
|
||||||
|
* 2. Verify the mapping exists: Authentik → Customization → Property Mappings
|
||||||
|
* 3. Verify it's attached to the SAML provider: Authentik → Providers → SAML → Properties
|
||||||
|
* 4. Enable Xibo SAML debug logging below ('debug' => true) and check xibo.log after login
|
||||||
|
* 5. Check SAML assertion in browser dev tools (Network tab, catch SAML response)
|
||||||
*
|
*
|
||||||
* Excluded Groups:
|
* Excluded Groups:
|
||||||
* ────────────────────────────────────────────────────────────────────────
|
* ────────────────────────────────────────────────────────────────────────
|
||||||
@@ -35,9 +43,10 @@ $samlSettings = [
|
|||||||
'slo' => true,
|
'slo' => true,
|
||||||
'mapping' => [
|
'mapping' => [
|
||||||
'UserID' => '',
|
'UserID' => '',
|
||||||
// usertypeid: Set to 1 (super-admin) for members of admin groups.
|
// usertypeid: Set to 1 (super-admin) for members of OTS IT group.
|
||||||
// Requires a custom SAML property mapping in Authentik (see notes above).
|
// The saml-usertypeid custom property mapping (auto-created during provisioning)
|
||||||
'usertypeid' => 'http://schemas.goauthentik.io/2021/02/saml/usertypeid',
|
// outputs this attribute. Xibo's JIT provisioning reads it and auto-assigns admins.
|
||||||
|
'usertypeid' => 'saml-usertypeid',
|
||||||
'UserName' => 'http://schemas.goauthentik.io/2021/02/saml/username',
|
'UserName' => 'http://schemas.goauthentik.io/2021/02/saml/username',
|
||||||
'email' => 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress',
|
'email' => 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ services:
|
|||||||
aliases:
|
aliases:
|
||||||
- web
|
- web
|
||||||
deploy:
|
deploy:
|
||||||
|
placement:
|
||||||
|
constraints: [node.role == worker]
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: any
|
condition: any
|
||||||
resources:
|
resources:
|
||||||
@@ -57,6 +59,8 @@ services:
|
|||||||
aliases:
|
aliases:
|
||||||
- memcached
|
- memcached
|
||||||
deploy:
|
deploy:
|
||||||
|
placement:
|
||||||
|
constraints: [node.role == worker]
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: any
|
condition: any
|
||||||
resources:
|
resources:
|
||||||
@@ -70,6 +74,8 @@ services:
|
|||||||
aliases:
|
aliases:
|
||||||
- quickchart
|
- quickchart
|
||||||
deploy:
|
deploy:
|
||||||
|
placement:
|
||||||
|
constraints: [node.role == worker]
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: any
|
condition: any
|
||||||
|
|
||||||
@@ -84,6 +90,8 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
{{ABBREV}}-net: {}
|
{{ABBREV}}-net: {}
|
||||||
deploy:
|
deploy:
|
||||||
|
placement:
|
||||||
|
constraints: [node.role == worker]
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: any
|
condition: any
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user