feat: Update Authentik integration and enhance Docker Compose templates
This commit is contained in:
@@ -691,36 +691,63 @@ public class AuthentikService : IAuthentikService
|
||||
{
|
||||
_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>
|
||||
{
|
||||
["name"] = "saml-usertypeid",
|
||||
["saml_name"] = "usertypeid",
|
||||
["expression"] = expression,
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
// Try multiple endpoints (API versions vary)
|
||||
var endpoints = new[]
|
||||
{
|
||||
var errorBody = await resp.Content.ReadAsStringAsync(ct);
|
||||
_logger.LogWarning("[Authentik] Failed to create usertypeid mapping (HTTP {Status}): {Error}",
|
||||
(int)resp.StatusCode, errorBody);
|
||||
return null;
|
||||
"/api/v3/propertymappings/provider/saml/",
|
||||
"/api/v3/propertymappings/saml/",
|
||||
"/api/v3/propertymappings/",
|
||||
};
|
||||
|
||||
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);
|
||||
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");
|
||||
_logger.LogWarning("[Authentik] Could not create usertypeid mapping via any endpoint");
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -761,26 +788,46 @@ public class AuthentikService : IAuthentikService
|
||||
{
|
||||
foreach (var mapEl in mapsProp.EnumerateArray())
|
||||
{
|
||||
var mapId = mapEl.ValueKind == JsonValueKind.String
|
||||
? mapEl.GetString()
|
||||
: (mapEl.TryGetProperty("pk", out var pkProp)
|
||||
? pkProp.GetString()
|
||||
: null);
|
||||
string? mapId = null;
|
||||
|
||||
// Handle both string IDs and object IDs
|
||||
if (mapEl.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
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))
|
||||
{
|
||||
mappings.Add(mapId);
|
||||
_logger.LogDebug("[Authentik] Found existing mapping: {Id}", mapId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 3. Add new mapping if not already present ──────────────────
|
||||
if (!mappings.Contains(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 ───────────────────
|
||||
var patchPayload = JsonSerializer.Serialize(new Dictionary<string, object>
|
||||
{
|
||||
["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 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
|
||||
/// all {{PLACEHOLDER}} tokens with values from RenderContext.
|
||||
/// 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>
|
||||
public class ComposeRenderService
|
||||
{
|
||||
@@ -148,156 +147,6 @@ public class ComposeRenderService
|
||||
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>
|
||||
|
||||
@@ -49,7 +49,7 @@ public class GitTemplateService
|
||||
var yamlPath = FindFile(cacheDir, "template.yml");
|
||||
|
||||
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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user