Files
ArchiTools/docs/architecture/SECURITY-AND-ROLES.md
Marius Tarau 4c46e8bcdd Initial commit: ArchiTools modular dashboard platform
Complete Next.js 16 application with 13 fully implemented modules:
Email Signature, Word XML Generator, Registratura, Dashboard,
Tag Manager, IT Inventory, Address Book, Password Vault,
Mini Utilities, Prompt Generator, Digital Signatures,
Word Templates, and AI Chat.

Includes core platform systems (module registry, feature flags,
storage abstraction, i18n, theming, auth stub, tagging),
16 technical documentation files, Docker deployment config,
and legacy HTML tool reference.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 12:50:25 +02:00

16 KiB

Security & Roles

ArchiTools internal architecture reference -- security layers, role model, and auth integration plan.


Overview

ArchiTools is an internal-only platform deployed on-premise behind Nginx Proxy Manager. It is not exposed to the public internet. The current security posture reflects this: no authentication is required, and all features are available to anyone with network access.

This document defines the layered security model that is designed today and will be progressively enforced as the platform matures. The architecture is future-ready -- every entity, module, and field already carries the metadata needed for access control, even before authentication is turned on.


Security Layers

Security is enforced at five distinct layers, from outermost to innermost:

┌─────────────────────────────────────────────┐
│  1. Network Layer                           │
│     Nginx Proxy Manager, CrowdSec, VPN     │
├─────────────────────────────────────────────┤
│  2. Application Layer                       │
│     Authentik SSO, session management       │
├─────────────────────────────────────────────┤
│  3. Module Layer                            │
│     Feature flags, role-based module gating │
├─────────────────────────────────────────────┤
│  4. Data Layer                              │
│     Visibility field on all entities        │
├─────────────────────────────────────────────┤
│  5. Field Layer                             │
│     Admin-only fields on entities           │
└─────────────────────────────────────────────┘

Layer 1: Network

Component Purpose Status
Nginx Proxy Manager Reverse proxy, TLS termination, access control lists Active
CrowdSec Intrusion detection, IP reputation, automated banning Planned
Internal DNS ArchiTools resolves only on the office network Active
VPN Remote access for authorized employees only Active (WireGuard)

ArchiTools is never exposed on a public IP. All traffic enters through Nginx Proxy Manager on the Docker host. CrowdSec will be added as a sidecar container to provide real-time threat detection and community-sourced IP blocklists.

Layer 2: Application (Authentik SSO)

Not yet active. When enabled, Authentik will serve as the single sign-on provider for all ArchiTools users. See Auth Integration Plan below.

Layer 3: Module

Every module has a featureFlag and a visibility field in its ModuleConfig. Today, feature flags control whether a module is loaded at all. When auth is active, the visibility field will additionally gate access based on the user's role.

// Current enforcement (no auth):
// featureFlag=true  -> module is loaded
// featureFlag=false -> module is not loaded, chunk is not fetched

// Future enforcement (with auth):
// featureFlag=true AND role meets visibility -> module is loaded
// Otherwise -> module is hidden from navigation and routes return 403

Layer 4: Data

Every persistent entity in ArchiTools carries a visibility field:

type Visibility = 'all' | 'internal' | 'admin' | 'guest-safe';
Value Who can see it
all Any authenticated user (or everyone, in no-auth mode)
internal Users with role user or above (not guest)
admin Users with role admin only
guest-safe Explicitly marked as safe for external/guest viewers

This field is stored with the entity data and checked at query time. Services must filter results by visibility before returning them to the UI.

Layer 5: Field

Individual fields on entities can be marked as admin-only. This is enforced at the component level -- admin-only fields are not rendered (not merely hidden with CSS) for non-admin users.

interface FieldConfig {
  key: string;
  label: string;
  adminOnly?: boolean;      // field is excluded from render for non-admins
  visibility?: Visibility;  // field-level visibility override
}

When auth is inactive, all fields are rendered (the stub returns admin role).


Role Model

Roles are designed now and embedded into interfaces and type definitions so that the codebase is ready for auth without structural changes.

type Role = 'admin' | 'manager' | 'user' | 'viewer' | 'guest';

Role Definitions

Role Module Access Data Scope Write Access Configuration
admin All modules All data, all companies Full CRUD Full (flags, settings, users)
manager All internal modules Company-scoped: all data within assigned companies Full CRUD within scope Module-level settings
user Standard modules Own data + shared data CRUD on own data Personal preferences only
viewer Permitted modules (read-only) Shared data visible to their role None None
guest Public-facing views only guest-safe entities only None None

Role Hierarchy

Roles form a strict hierarchy. A higher role inherits all permissions of lower roles:

admin > manager > user > viewer > guest

The hasRole check uses this hierarchy:

const ROLE_HIERARCHY: Record<Role, number> = {
  guest: 0,
  viewer: 1,
  user: 2,
  manager: 3,
  admin: 4,
};

function hasRole(requiredRole: Role, actualRole: Role): boolean {
  return ROLE_HIERARCHY[actualRole] >= ROLE_HIERARCHY[requiredRole];
}

Company Scoping

The three companies (Beletage SRL, Urban Switch SRL, Studii de Teren SRL) are first-class entities. Managers are assigned to one or more companies and can only manage data within their scope.

type CompanyId = 'beletage' | 'urban-switch' | 'studii-de-teren';

interface UserProfile {
  id: string;
  name: string;
  email: string;
  role: Role;
  companies: CompanyId[];  // which companies this user belongs to
  primaryCompany: CompanyId;
}

Admins are implicitly scoped to all companies. Users and viewers see data for the companies they belong to.


Visibility Model

type Visibility = 'all' | 'internal' | 'admin' | 'guest-safe';

Visibility is applied at three levels:

Level Where it lives What it controls
Module ModuleConfig.visibility Whether the module appears in navigation and is routable
Entity Entity data (e.g., registryEntry.visibility) Whether the entity appears in query results
Field FieldConfig.visibility or FieldConfig.adminOnly Whether the field is rendered in the UI

Resolution Logic

function canView(userRole: Role, visibility: Visibility): boolean {
  switch (visibility) {
    case 'all':
      return true;
    case 'internal':
      return hasRole('user', userRole);
    case 'admin':
      return hasRole('admin', userRole);
    case 'guest-safe':
      return true; // visible to everyone including guests
  }
}

Auth Integration Plan

Phase 1: No Auth (Current)

  • All features are available to any user on the network.
  • The AuthContext stub returns a synthetic admin user.
  • Feature flags are the only access control mechanism.
  • Visibility fields are stored on entities but not enforced at query time.

Phase 2: Authentik SSO + Module Gating

  • Authentik is deployed as a Docker container alongside ArchiTools.
  • OIDC integration: ArchiTools redirects unauthenticated requests to Authentik.
  • Authentik manages user accounts, passwords, and MFA.
  • Role is assigned in Authentik as a custom claim and mapped to the ArchiTools Role type.
  • Module-level gating is enforced: users only see modules permitted by their role + module visibility.
  • The AuthContext reads from the Authentik session instead of the stub.

Phase 3: Data-Level Permissions

  • Entity visibility is enforced at query time in services.
  • Company scoping is active: managers see only their companies' data.
  • Field-level visibility is enforced: admin-only fields are excluded from non-admin renders.
  • Audit logging: who accessed what, when.

Phase 4: External / Guest Access

  • Guest role is activated for external collaborators.
  • Scoped views: guests see only guest-safe entities, through dedicated guest routes.
  • Link-based sharing: generate time-limited URLs for specific records.
  • No direct database access -- guest views are read-only projections.

AuthContext Design

The AuthContext is the single source of truth for the current user's identity and permissions throughout the React component tree.

// src/lib/auth/types.ts

interface AuthContext {
  user: UserProfile | null;
  role: Role;
  isAuthenticated: boolean;
  hasRole(role: Role): boolean;
  canAccess(moduleId: string): boolean;
  canView(visibility: Visibility): boolean;
}

Stub Implementation (Phase 1)

// src/lib/auth/auth-context.ts

import { createContext, useContext } from 'react';
import type { AuthContext as AuthContextType } from './types';

const STUB_USER: UserProfile = {
  id: 'dev-admin',
  name: 'Developer',
  email: 'dev@architools.local',
  role: 'admin',
  companies: ['beletage', 'urban-switch', 'studii-de-teren'],
  primaryCompany: 'beletage',
};

const stubContext: AuthContextType = {
  user: STUB_USER,
  role: 'admin',
  isAuthenticated: true,
  hasRole: () => true,
  canAccess: () => true,
  canView: () => true,
};

const AuthContext = createContext<AuthContextType>(stubContext);

export function useAuth(): AuthContextType {
  return useContext(AuthContext);
}

export { AuthContext };

In development mode, the stub grants full admin access. When Authentik is integrated (Phase 2), the provider will be swapped to read from the OIDC session, and the stub will only be used in test environments.

Usage in Components

// Gating a module route
function ModulePage({ config }: { config: ModuleConfig }) {
  const auth = useAuth();

  if (!auth.canAccess(config.id)) {
    return <AccessDenied />;
  }

  return <ModuleLoader config={config} />;
}

// Gating a field
function EntityDetail({ entity }: { entity: RegistryEntry }) {
  const auth = useAuth();

  return (
    <div>
      <p>{entity.title}</p>
      {auth.hasRole('admin') && (
        <p className="text-sm text-muted-foreground">
          Internal notes: {entity.adminNotes}
        </p>
      )}
    </div>
  );
}

Module-Specific Security Notes

Password Vault

The Password Vault module is a convenience tool for non-critical credentials only. It is explicitly not a production secrets manager.

Constraints:

  • Passwords are stored in the browser's localStorage, encrypted with a user-provided passphrase using AES-256-GCM via the Web Crypto API.
  • The encryption key is derived from the passphrase using PBKDF2 with a per-vault salt.
  • There is no server-side key management, no HSM, and no key escrow.
  • If the user forgets the passphrase, the vault data is unrecoverable.
  • This module must display a persistent disclaimer banner:

Atentie: Acest modul este destinat exclusiv pentru credentiale non-critice (conturi de servicii, parole Wi-Fi, etc.). NU stocati parole bancare, chei SSH private, sau secrete de productie. Folositi un manager de parole dedicat (Bitwarden, 1Password) pentru credentiale critice.

What must NOT be stored here:

  • Banking credentials
  • SSH private keys
  • API keys for production services
  • Personal identity documents

Digital Signatures

  • The module stores file hashes (SHA-256), not raw private keys.
  • Signature verification is hash-based: the document is re-hashed and compared to the stored hash + signer metadata.
  • No cryptographic signing keys are generated or stored in ArchiTools.
  • The signature record structure:
interface SignatureRecord {
  id: string;
  documentHash: string;    // SHA-256 hex digest
  fileName: string;
  signerName: string;
  signerCompany: CompanyId;
  signedAt: string;        // ISO 8601 timestamp
  metadata?: Record<string, string>;
}

Data Export / Import

  • All import operations must sanitize incoming data before writing to storage.
  • JSON imports: validate against the expected schema using Zod. Reject payloads that do not conform.
  • CSV imports: escape all string fields, reject fields exceeding length limits.
  • No eval(), no Function() constructors, no dynamic code execution on imported data.
  • Export operations must strip admin-only fields when the exporter's role is below admin.

Web Security Controls

XSS Prevention

React escapes all interpolated values by default. This is the primary XSS defense.

Rules:

  • dangerouslySetInnerHTML is prohibited except in the Email Signature preview and Word XML preview modules. These modules render user-provided HTML in a sandboxed <iframe> with sandbox="allow-same-origin" (no allow-scripts).
  • User input that flows into HTML attributes (e.g., href, src) must be validated against an allowlist of protocols (https:, mailto:, tel:).
  • No inline <script> tags are generated anywhere in the application.

CSRF Protection

Next.js App Router provides built-in CSRF protection for Server Actions via origin checking. Since ArchiTools currently uses client-side data storage (localStorage/IndexedDB) rather than a backend API, CSRF is not an active vector. When server-side endpoints are added in future phases, all mutations must use Server Actions or include a CSRF token.

Content Security Policy

The following CSP headers are configured in the Nginx Proxy Manager for the ArchiTools domain:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'unsafe-inline' 'unsafe-eval';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: blob:;
  font-src 'self';
  connect-src 'self';
  frame-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';

Notes:

  • unsafe-inline and unsafe-eval in script-src are required by Next.js in development. In production, these should be tightened to use nonce-based CSP once the build pipeline supports it.
  • object-src 'none' blocks Flash and other plugin-based content.
  • frame-src 'self' allows the sandboxed iframes used by the Email Signature and Word XML preview modules.

Security Headers (Nginx)

In addition to CSP, the following headers are set at the Nginx level:

add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "0" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

X-XSS-Protection is set to 0 (disabled) because the modern approach is to rely on CSP rather than the browser's legacy XSS auditor, which can introduce vulnerabilities of its own.


Summary

Layer Current State Phase 2 Phase 3 Phase 4
Network Nginx + internal DNS + CrowdSec No change + guest ingress rules
Application No auth (stub) Authentik SSO No change + guest OIDC flow
Module Feature flags only + role-based gating No change + guest module set
Data Visibility stored, not enforced No change Enforced at query time + guest-safe filter
Field All fields rendered No change Admin-only fields gated No change