Token导航 LogoToken导航

Agent Skill · 研究检索

multi-tenant(多租户)

multi-tenant 用于查找、检索和筛选相关信息,适合在 Codex、Claude、Cursor、Gemini CLI 中需要根据关键词、任务场景或来源线索快速定位候选结果时使用。可结合来源仓库、安装命令和原始 README 继续核验具体用法。安装前建议确认权限范围、维护状态,以及是否会触发联网、命令执行或文件读写。

multi-tenant 是研究检索类 Agent Skill,可用宿主为 Codex、Claude、Cursor、Gemini CLI,通过GitHub 仓库获取,权限为接触敏感数据;许可证信息还不明确,第三方扫描已通过,按安装量在研究检索类的 73,675 个 Skill 里排第 59,000。

总安装 396周安装 17GitHub Stars 11下载 139核验 2026-05-01

安装说明

本站只整理中文说明,不托管安装包,也不代用户安装。
GitHub
许可证
unknown

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:multi-tenant(多租户)
来源仓库:https://github.com/lobbi-docs/claude
仓库路径:skills/multi-tenant
安装命令:
npx skills add https://github.com/lobbi-docs/claude --skill multi-tenant
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。该命令会通过 npx skills 从第三方来源获取 Skill;本站只展示命令,不托管安装包,也不自动执行。

skills.shnpx skills
npx skills add https://github.com/lobbi-docs/claude --skill multi-tenant

这个 Skill 是做什么的

研究检索类 Skill 主要解决「找资料、核对事实、把来源整理清楚」的问题,通常会让助手先检索再总结。

用于查找、检索和筛选相关信息。适合在 Codex、Claude、Cursor、Gemini CLI 中根据关键词、任务场景或来源线索快速定位候选结果。可结合来源仓库、安装命令和原始 README 继续核验具体用法。安装前建议确认权限范围、维护状态,以及是否会触发联网、命令执行或文件读写。

使用时不能把工具输出直接当最终结论,需人工复核关键信息。

来源仓库内路径:skills/multi-tenant

上手步骤

4 步
  1. 1

    确认自己的宿主工具

    公开资料标注支持 Codex、Claude、Cursor、Gemini CLI;先确认自己用的是其中之一,再按对应工具的 Skills 目录放置。

  2. 2

    拿到安装命令

    可以直接复制的命令是 npx skills add https://github.com/lobbi-docs/claude --skill multi-tenant;命令里的占位符换成自己的路径或密钥再执行。

  3. 3

    安装后重启宿主

    命令执行完要完全退出宿主工具再打开,让新装的 Skill 被重新扫描到,否则列表里可能看不到。

  4. 4

    按最小权限试用

    它需要接触敏感数据,先在测试目录或测试账号里跑,确认没问题再放到正式项目。

常见问题

6 条

multi-tenant 怎么安装?

在终端执行 npx skills add https://github.com/lobbi-docs/claude --skill multi-tenant。装完完全退出宿主工具再打开,让它重新扫描本地 Skill。

multi-tenant 支持哪些 AI 工具?

公开资料标注支持 Codex、Claude、Cursor、Gemini CLI。其他工具只要能读取本地 Skill 目录,一般也能手动放进去,但要自己确认触发方式是否一致。

multi-tenant 需要什么权限?

权限级别是接触敏感数据。该 Skill 可能接触密钥、Token、环境变量或敏感配置,应进入高风险复核队列,默认不自动发布。

multi-tenant 是免费的吗?

公开资料没有标注明确许可证,收费和商用限制都无法确认。正式使用前请到来源仓库核对授权说明。

multi-tenant 用的人多吗?

公开数据是累计安装 396 次,近一周安装 17 次,下载 139 次,GitHub 11 星。这些数字来自第三方目录统计,只能当作热度参考,不代表质量或安全性。

multi-tenant 安全吗?

第三方扫描结果为通过,但这只覆盖已知的静态检查项,不能替代自己读一遍源码和安装命令。

值得选的地方和要先想清楚的地方

值得选的地方

  • 同时适配 4 种工具,换客户端不用重新找替代品。
  • 第三方扫描已通过,常见的高风险行为没有命中。

先想清楚的地方

  • 需要接触敏感数据,装到常用环境前要先评估影响面。
  • 许可证没有明确标注,能不能商用、能不能再分发都不确定。
  • 第三方目录整理,版本更新和行为细节以来源仓库为准。

适不适合你

更适合

  • 已经在用 Codex,想直接扩展它能力的人。
  • 需要处理研究检索类任务、又不想每次都手写提示词的人。

不太适合

  • 环境里有大量密钥、凭证,不方便给第三方 Skill 读取的人。
  • 需要明确授权才能用到公司项目里的场景。
  • 需要厂商级技术支持和 SLA 的生产系统。

研究检索类 Skill 对比

6 个同类
Skill总安装GitHub 星权限许可证扫描
find-skills

寻找技能 · Codex、Claude

31,199,81316,486操作浏览器MIT有提醒
self-improving-agent

自我改进 Agent · OpenClaw

16,464,0023,396接触敏感数据MIT-0通过
soultrace

灵魂追踪 · Codex、Claude

7,588,94412需要联网未明确有提醒
self-improving

自我完善 · OpenClaw

6,829,2841,046需要联网MIT-0通过
microsoft-foundry

微软代工厂 · Codex、Claude

6,614,144764调用外部服务未明确有提醒
multi-tenant

多租户 · 本页

39611接触敏感数据未明确通过

按安装量取同分类前几名;数字来自第三方目录统计,只作热度参考,不代表质量结论。

SKILL.md 原文

Multi-Tenant Architecture Skill

Comprehensive multi-tenant architecture patterns for the keycloak-alpha platform with organization-based isolation.

When to Use This Skill

Activate this skill when:

  • Implementing multi-tenant architecture with org_id claims
  • Setting up database isolation strategies
  • Configuring per-organization themes
  • Building tenant provisioning workflows
  • Ensuring data isolation and security
  • Implementing cross-tenant access controls
  • Managing organization-scoped resources

Multi-Tenant Architecture Overview

The keycloak-alpha platform uses shared database, isolated schema approach with org_id-based isolation:

┌─────────────────────────────────────────────┐
│          Keycloak (Identity Provider)        │
│  - Manages users across all organizations   │
│  - Issues JWT tokens with org_id claim      │
│  - Handles authentication & SSO             │
└─────────────────────────────────────────────┘
                    ↓ JWT with org_id
┌─────────────────────────────────────────────┐
│            API Gateway                       │
│  - Validates tokens                         │
│  - Extracts org_id claim                    │
│  - Routes to microservices                  │
└─────────────────────────────────────────────┘
                    ↓ org_id in headers
┌─────────────────────────────────────────────┐
│         Microservices (8 services)          │
│  - Enforce org_id filtering                 │
│  - Isolate data by organization             │
│  - Apply org-specific business logic        │
└─────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────┐
│    MongoDB / PostgreSQL                     │
│  - Shared database                          │
│  - org_id indexed on all collections/tables │
│  - Row-level security (PostgreSQL)          │
└─────────────────────────────────────────────┘

JWT Token Structure with Organization Context

Token Claims

{
  "sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "email": "john.doe@acme.com",
  "name": "John Doe",
  "given_name": "John",
  "family_name": "Doe",
  "org_id": "org_acme",
  "org_name": "ACME Corporation",
  "realm_access": {
    "roles": ["org_admin", "user"]
  },
  "resource_access": {
    "lobbi-web-app": {
      "roles": ["user"]
    }
  },
  "email_verified": true,
  "preferred_username": "john.doe@acme.com",
  "iss": "http://localhost:8080/realms/lobbi",
  "aud": "account",
  "exp": 1702000000,
  "iat": 1701999700,
  "jti": "unique-token-id"
}

Configure org_id Claim Mapper

# Add protocol mapper to include org_id in tokens
TOKEN=$(curl -X POST "http://localhost:8080/realms/master/protocol/openid-connect/token" \
  -d "username=admin&password=admin&grant_type=password&client_id=admin-cli" \
  | jq -r '.access_token')

CLIENT_UUID=$(curl -H "Authorization: Bearer $TOKEN" \
  "http://localhost:8080/admin/realms/lobbi/clients?clientId=lobbi-web-app" \
  | jq -r '.[0].id')

curl -X POST "http://localhost:8080/admin/realms/lobbi/clients/$CLIENT_UUID/protocol-mappers/models" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "org_id_mapper",
    "protocol": "openid-connect",
    "protocolMapper": "oidc-usermodel-attribute-mapper",
    "config": {
      "user.attribute": "org_id",
      "claim.name": "org_id",
      "jsonType.label": "String",
      "id.token.claim": "true",
      "access.token.claim": "true",
      "userinfo.token.claim": "true"
    }
  }'

curl -X POST "http://localhost:8080/admin/realms/lobbi/clients/$CLIENT_UUID/protocol-mappers/models" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "org_name_mapper",
    "protocol": "openid-connect",
    "protocolMapper": "oidc-usermodel-attribute-mapper",
    "config": {
      "user.attribute": "org_name",
      "claim.name": "org_name",
      "jsonType.label": "String",
      "id.token.claim": "true",
      "access.token.claim": "true",
      "userinfo.token.claim": "false"
    }
  }'

Token Verification Middleware

// services/api-gateway/src/middleware/auth.js
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
import { UnauthorizedError, ForbiddenError } from '../utils/AppError.js';

const client = jwksClient({
  jwksUri: `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/certs`,
  cache: true,
  rateLimit: true,
  jwksRequestsPerMinute: 10
});

function getKey(header, callback) {
  client.getSigningKey(header.kid, (err, key) => {
    const signingKey = key.publicKey || key.rsaPublicKey;
    callback(null, signingKey);
  });
}

export async function authMiddleware(req, res, next) {
  const token = req.headers.authorization?.replace('Bearer ', '');

  if (!token) {
    return next(new UnauthorizedError('No token provided'));
  }

  jwt.verify(token, getKey, {
    audience: 'account',
    issuer: `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}`,
    algorithms: ['RS256']
  }, (err, decoded) => {
    if (err) {
      return next(new UnauthorizedError('Invalid token'));
    }

    // CRITICAL: Verify org_id claim exists
    if (!decoded.org_id) {
      return next(new ForbiddenError('Missing org_id claim in token'));
    }

    // Attach user context to request
    req.user = {
      sub: decoded.sub,
      email: decoded.email,
      name: decoded.name,
      orgId: decoded.org_id,
      orgName: decoded.org_name,
      roles: decoded.realm_access?.roles || []
    };

    next();
  });
}

// Optional: Verify org_id matches resource being accessed
export function requireOrgAccess(req, res, next) {
  const resourceOrgId = req.params.orgId || req.query.org_id || req.body.org_id;

  if (resourceOrgId && resourceOrgId !== req.user.orgId) {
    // Allow super_admin to access any org
    if (!req.user.roles.includes('super_admin')) {
      return next(new ForbiddenError('Cannot access resources from different organization'));
    }
  }

  next();
}

Database Isolation Strategies

MongoDB Isolation with org_id

// services/user-service/src/models/User.js
import mongoose from 'mongoose';

const userSchema = new mongoose.Schema({
  keycloakId: {
    type: String,
    required: true,
    unique: true,
    index: true
  },
  email: {
    type: String,
    required: true,
    lowercase: true,
    trim: true
  },
  org_id: {
    type: String,
    required: true,
    index: true  // CRITICAL: Always index org_id
  },
  firstName: String,
  lastName: String,
  metadata: {
    type: Map,
    of: String
  }
}, {
  timestamps: true
});

// CRITICAL: Compound index for org-scoped queries
userSchema.index({ org_id: 1, email: 1 }, { unique: true });
userSchema.index({ org_id: 1, createdAt: -1 });

// Pre-query hook to enforce org_id filtering
userSchema.pre(/^find/, function(next) {
  // Only enforce if org_id is not already in query
  if (!this.getQuery().org_id && this.options.orgId) {
    this.where({ org_id: this.options.orgId });
  }
  next();
});

export const UserModel = mongoose.model('User', userSchema);

Repository Pattern with org_id Isolation

// services/user-service/src/repositories/user.repository.js
import { UserModel } from '../models/User.js';
import { ForbiddenError, NotFoundError } from '../utils/AppError.js';

export class UserRepository {

  constructor(orgId) {
    this.orgId = orgId;
  }

  async findAll(filter = {}, options = {}) {
    // ALWAYS enforce org_id filtering
    const query = {
      ...filter,
      org_id: this.orgId
    };

    const { page = 1, limit = 20, sort = { createdAt: -1 } } = options;

    const users = await UserModel.find(query)
      .select('-password')
      .limit(limit)
      .skip((page - 1) * limit)
      .sort(sort);

    const total = await UserModel.countDocuments(query);

    return {
      data: users,
      pagination: {
        page,
        limit,
        total,
        pages: Math.ceil(total / limit)
      }
    };
  }

  async findById(id) {
    const user = await UserModel.findOne({
      _id: id,
      org_id: this.orgId  // CRITICAL: Always filter by org_id
    }).select('-password');

    if (!user) {
      throw new NotFoundError('User');
    }

    return user;
  }

  async create(userData) {
    const user = new UserModel({
      ...userData,
      org_id: this.orgId  // CRITICAL: Always set org_id
    });

    await user.save();
    return user;
  }

  async update(id, updates) {
    // Prevent changing org_id
    delete updates.org_id;

    const user = await UserModel.findOneAndUpdate(
      { _id: id, org_id: this.orgId },  // CRITICAL: Filter by org_id
      updates,
      { new: true, runValidators: true }
    ).select('-password');

    if (!user) {
      throw new NotFoundError('User');
    }

    return user;
  }

  async delete(id) {
    const result = await UserModel.deleteOne({
      _id: id,
      org_id: this.orgId  // CRITICAL: Filter by org_id
    });

    if (result.deletedCount === 0) {
      throw new NotFoundError('User');
    }

    return true;
  }
}

// Usage in controller
export async function listUsers(req, res, next) {
  try {
    const repository = new UserRepository(req.user.orgId);
    const result = await repository.findAll(
      { status: 'active' },
      { page: req.query.page, limit: req.query.limit }
    );

    res.json(result);
  } catch (error) {
    next(error);
  }
}

PostgreSQL Row-Level Security

-- services/billing-service/migrations/001_create_subscriptions.sql

-- Enable row-level security
ALTER TABLE subscriptions ENABLE ROW LEVEL SECURITY;

-- Create policy for org isolation
CREATE POLICY org_isolation ON subscriptions
  USING (org_id = current_setting('app.current_org_id')::text);

-- Grant access to application role
GRANT SELECT, INSERT, UPDATE, DELETE ON subscriptions TO app_user;

-- Function to set org context
CREATE OR REPLACE FUNCTION set_org_context(p_org_id text)
RETURNS void AS $$
BEGIN
  PERFORM set_config('app.current_org_id', p_org_id, false);
END;
$$ LANGUAGE plpgsql;
// services/billing-service/src/config/postgres.js
import { Pool } from 'pg';

export class PostgresClient {

  constructor() {
    this.pool = new Pool({
      host: process.env.POSTGRES_HOST,
      port: process.env.POSTGRES_PORT,
      database: process.env.POSTGRES_DB,
      user: process.env.POSTGRES_USER,
      password: process.env.POSTGRES_PASSWORD,
      max: 20,
      idleTimeoutMillis: 30000
    });
  }

  async query(orgId, text, params) {
    const client = await this.pool.connect();

    try {
      // Set org context for row-level security
      await client.query('SELECT set_org_context($1)', [orgId]);

      // Execute query (RLS automatically filters by org_id)
      const result = await client.query(text, params);

      return result;
    } finally {
      client.release();
    }
  }
}

// Usage
const db = new PostgresClient();

export async function getSubscription(req, res, next) {
  try {
    const result = await db.query(
      req.user.orgId,
      'SELECT * FROM subscriptions WHERE id = $1',
      [req.params.id]
    );

    if (result.rows.length === 0) {
      throw new NotFoundError('Subscription');
    }

    res.json(result.rows[0]);
  } catch (error) {
    next(error);
  }
}

Theme Switching Per Organization

Theme Mapping Configuration

// services/keycloak-service/src/config/theme-mapping.js
export const themeMapping = {
  // Organization ID -> Theme name mapping
  org_acme: 'acme-custom',
  org_beta: 'beta-theme',
  org_gamma: 'gamma-dark',

  // Default theme for organizations without custom theme
  default: 'lobbi-base'
};

export function getThemeForOrg(orgId) {
  return themeMapping[orgId] || themeMapping.default;
}

export function getAllThemes() {
  const themes = new Set(Object.values(themeMapping));
  return Array.from(themes);
}

Dynamic Theme Application

// services/api-gateway/src/middleware/theme-redirect.js
import { getThemeForOrg } from '../config/theme-mapping.js';

export function themeRedirectMiddleware(req, res, next) {
  // Extract org_id from token or session
  const orgId = req.user?.orgId;

  if (!orgId) {
    return next();
  }

  // Get theme for organization
  const theme = getThemeForOrg(orgId);

  // If redirecting to Keycloak login, add theme parameter
  if (req.path.includes('/auth') || req.path.includes('/login')) {
    const keycloakUrl = new URL(process.env.KEYCLOAK_URL);
    keycloakUrl.pathname = `/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/auth`;

    keycloakUrl.searchParams.set('client_id', 'lobbi-web-app');
    keycloakUrl.searchParams.set('redirect_uri', req.query.redirect_uri);
    keycloakUrl.searchParams.set('response_type', 'code');
    keycloakUrl.searchParams.set('scope', 'openid profile email');
    keycloakUrl.searchParams.set('kc_theme', theme);  // Apply theme

    return res.redirect(keycloakUrl.toString());
  }

  // Store theme in session for frontend
  req.session.theme = theme;
  next();
}

Frontend Theme Consumption

// apps/web-app/src/contexts/ThemeContext.jsx
import { createContext, useContext, useState, useEffect } from 'react';
import { useAuth } from '@hooks/useAuth';
import { getThemeForOrg } from '@/api/theme';

const ThemeContext = createContext(null);

export function ThemeProvider({ children }) {
  const { user } = useAuth();
  const [theme, setTheme] = useState('lobbi-base');
  const [themeConfig, setThemeConfig] = useState(null);

  useEffect(() => {
    if (user?.orgId) {
      loadTheme(user.orgId);
    }
  }, [user?.orgId]);

  async function loadTheme(orgId) {
    try {
      const config = await getThemeForOrg(orgId);
      setTheme(config.name);
      setThemeConfig(config);

      // Apply CSS variables
      if (config.branding) {
        document.documentElement.style.setProperty('--primary-color', config.branding.primaryColor);
        document.documentElement.style.setProperty('--secondary-color', config.branding.secondaryColor);
      }
    } catch (error) {
      console.error('Failed to load theme:', error);
    }
  }

  return (
    <ThemeContext.Provider value={{ theme, themeConfig }}>
      {children}
    </ThemeContext.Provider>
  );
}

export const useTheme = () => useContext(ThemeContext);

Tenant Provisioning Workflow

Organization Creation Service

// services/org-service/src/services/provisioning.service.js
import { OrganizationModel } from '../models/Organization.js';
import { KeycloakService } from './keycloak.service.js';
import { DatabaseService } from './database.service.js';
import { ThemeService } from './theme.service.js';
import { BillingService } from './billing.service.js';

export class ProvisioningService {

  async provisionOrganization(data) {
    const {
      name,
      domain,
      adminEmail,
      adminFirstName,
      adminLastName,
      plan = 'free'
    } = data;

    // Generate org_id
    const orgId = `org_${domain.replace(/[^a-z0-9]/gi, '_').toLowerCase()}`;

    try {
      // 1. Create organization in database
      const org = await this.createOrganization({
        orgId,
        name,
        domain,
        plan
      });

      // 2. Create Keycloak group for organization
      const keycloakService = new KeycloakService();
      const groupId = await keycloakService.createOrganizationGroup(orgId, name);

      // 3. Create admin user in Keycloak
      const adminUserId = await keycloakService.createUser({
        email: adminEmail,
        firstName: adminFirstName,
        lastName: adminLastName,
        orgId,
        roles: ['org_admin']
      });

      // 4. Add user to organization group
      await keycloakService.addUserToGroup(adminUserId, groupId);

      // 5. Initialize database schemas/collections
      const databaseService = new DatabaseService();
      await databaseService.initializeOrgCollections(orgId);

      // 6. Set up default theme
      const themeService = new ThemeService();
      await themeService.createOrgTheme(orgId, {
        parent: 'lobbi-base',
        branding: {
          logoUrl: null,
          primaryColor: '#3182ce',
          secondaryColor: '#805ad5'
        }
      });

      // 7. Create billing customer (if not free plan)
      if (plan !== 'free') {
        const billingService = new BillingService();
        await billingService.createCustomer({
          orgId,
          email: adminEmail,
          name,
          plan
        });
      }

      // 8. Send welcome email
      await this.sendWelcomeEmail(adminEmail, {
        orgName: name,
        loginUrl: process.env.APP_URL
      });

      return {
        orgId,
        organizationId: org._id,
        adminUserId,
        message: 'Organization provisioned successfully'
      };

    } catch (error) {
      // Rollback on failure
      await this.rollbackProvisioning(orgId);
      throw error;
    }
  }

  async createOrganization(data) {
    const org = new OrganizationModel({
      org_id: data.orgId,
      name: data.name,
      domain: data.domain,
      settings: {
        theme: 'lobbi-base',
        features: new Map([
          ['sso', data.plan !== 'free'],
          ['advanced_analytics', data.plan === 'enterprise'],
          ['custom_branding', data.plan !== 'free']
        ])
      },
      subscription: {
        plan: data.plan,
        status: 'active',
        billingCycle: 'monthly'
      },
      status: 'active'
    });

    await org.save();
    return org;
  }

  async rollbackProvisioning(orgId) {
    console.error(`Rolling back provisioning for ${orgId}`);

    try {
      // Delete organization from database
      await OrganizationModel.deleteOne({ org_id: orgId });

      // Delete Keycloak group and users
      const keycloakService = new KeycloakService();
      await keycloakService.deleteOrganizationGroup(orgId);

      // Clean up database collections
      const databaseService = new DatabaseService();
      await databaseService.cleanupOrgCollections(orgId);

    } catch (rollbackError) {
      console.error('Rollback failed:', rollbackError);
    }
  }
}

Tenant Provisioning API Endpoint

// services/org-service/src/controllers/provisioning.controller.js
import { ProvisioningService } from '../services/provisioning.service.js';
import { asyncHandler } from '../middleware/errorHandler.js';

export const provisionOrganization = asyncHandler(async (req, res) => {
  const {
    name,
    domain,
    adminEmail,
    adminFirstName,
    adminLastName,
    plan
  } = req.body;

  const provisioningService = new ProvisioningService();

  const result = await provisioningService.provisionOrganization({
    name,
    domain,
    adminEmail,
    adminFirstName,
    adminLastName,
    plan
  });

  res.status(201).json(result);
});

export const deprovisionOrganization = asyncHandler(async (req, res) => {
  const { orgId } = req.params;

  // Only super_admin can deprovision
  if (!req.user.roles.includes('super_admin')) {
    throw new ForbiddenError('Insufficient permissions');
  }

  const provisioningService = new ProvisioningService();
  await provisioningService.deprovisionOrganization(orgId);

  res.json({ message: 'Organization deprovisioned successfully' });
});

Data Isolation Patterns

Query Middleware for Automatic org_id Filtering

// shared/middleware/org-scope.middleware.js
export function orgScopeMiddleware(Model) {
  // Pre-find hooks
  Model.schema.pre(/^find/, function(next) {
    if (this.options.skipOrgFilter) {
      return next();
    }

    // Automatically add org_id filter if not present
    if (!this.getQuery().org_id && this.options.orgId) {
      this.where({ org_id: this.options.orgId });
    }

    next();
  });

  // Pre-update hooks
  Model.schema.pre('updateOne', function(next) {
    if (this.options.skipOrgFilter) {
      return next();
    }

    if (!this.getQuery().org_id && this.options.orgId) {
      this.where({ org_id: this.options.orgId });
    }

    next();
  });

  // Pre-delete hooks
  Model.schema.pre('deleteOne', function(next) {
    if (this.options.skipOrgFilter) {
      return next();
    }

    if (!this.getQuery().org_id && this.options.orgId) {
      this.where({ org_id: this.options.orgId });
    }

    next();
  });
}

Service-Level Isolation

// services/user-service/src/services/user.service.js
export class UserService {

  constructor(orgId) {
    if (!orgId) {
      throw new Error('orgId is required for UserService');
    }
    this.orgId = orgId;
  }

  async findAll(filter = {}, options = {}) {
    // ALWAYS enforce org_id
    return await UserModel.find({
      ...filter,
      org_id: this.orgId
    }, null, {
      orgId: this.orgId,
      ...options
    });
  }

  async findById(id) {
    const user = await UserModel.findOne({
      _id: id,
      org_id: this.orgId
    });

    if (!user) {
      throw new NotFoundError('User');
    }

    return user;
  }

  // Prevent cross-org data leaks
  async bulkUpdate(userIds, updates) {
    // First verify all users belong to this org
    const count = await UserModel.countDocuments({
      _id: { $in: userIds },
      org_id: this.orgId
    });

    if (count !== userIds.length) {
      throw new ForbiddenError('Some users do not belong to this organization');
    }

    // Proceed with update
    return await UserModel.updateMany(
      {
        _id: { $in: userIds },
        org_id: this.orgId
      },
      updates
    );
  }
}

Cross-Tenant Security Considerations

Preventing Cross-Org Data Access

// services/api-gateway/src/middleware/org-validation.middleware.js
export function validateOrgAccess(extractOrgId) {
  return (req, res, next) => {
    // Extract org_id from request (params, query, or body)
    const resourceOrgId = extractOrgId(req);

    if (!resourceOrgId) {
      return next();
    }

    // Verify user has access to this org
    if (resourceOrgId !== req.user.orgId) {
      // Super admins can access any org
      if (req.user.roles.includes('super_admin')) {
        return next();
      }

      // Log potential security violation
      console.warn('Cross-org access attempt:', {
        userId: req.user.sub,
        userOrgId: req.user.orgId,
        attemptedOrgId: resourceOrgId,
        path: req.path,
        method: req.method,
        ip: req.ip
      });

      return next(new ForbiddenError('Access denied to organization resources'));
    }

    next();
  };
}

// Usage in routes
router.get('/organizations/:orgId/users',
  validateOrgAccess(req => req.params.orgId),
  listUsers
);

Audit Logging for Cross-Org Access

// services/analytics-service/src/services/audit.service.js
export class AuditService {

  async logAccess(event) {
    const log = {
      timestamp: new Date(),
      userId: event.userId,
      userOrgId: event.userOrgId,
      resourceOrgId: event.resourceOrgId,
      action: event.action,
      resource: event.resource,
      resourceId: event.resourceId,
      success: event.success,
      ipAddress: event.ipAddress,
      userAgent: event.userAgent
    };

    // Flag suspicious cross-org access
    if (event.userOrgId !== event.resourceOrgId && !event.isSuperAdmin) {
      log.suspicious = true;
      log.severity = 'high';

      // Alert security team
      await this.sendSecurityAlert(log);
    }

    await AuditLogModel.create(log);
  }
}

Best Practices

  1. ALWAYS include org_id in JWT tokens via Keycloak protocol mapper
  2. NEVER trust client-provided org_id - always use token claim
  3. INDEX org_id on ALL collections/tables for query performance
  4. Use repository pattern to enforce org_id filtering
  5. Implement row-level security in PostgreSQL for additional safety
  6. Validate org_id in middleware before reaching controllers
  7. Audit cross-org access attempts for security monitoring
  8. Test isolation thoroughly with automated tests
  9. Use compound indexes for org_id + frequently queried fields
  10. Prevent org_id modification in update operations
  11. Implement graceful tenant deprovisioning with cleanup
  12. Version control theme mappings for traceability
  13. Monitor query performance by org_id to detect issues
  14. Implement rate limiting per org to prevent abuse
  15. Use separate database connections per org for critical isolation (optional)

File Locations in keycloak-alpha

PathPurpose
services/org-service/Organization provisioning and management
services/api-gateway/src/middleware/auth.jsToken validation and org_id extraction
services/keycloak-service/src/config/theme-mapping.jsTheme per organization mapping
shared/middleware/org-scope.middleware.jsAutomatic org_id filtering
services/analytics-service/src/services/audit.service.jsCross-org access auditing

Testing Multi-Tenancy

Test Organization Isolation

// services/user-service/tests/isolation.test.js
describe('Multi-tenant isolation', () => {
  it('should prevent cross-org data access', async () => {
    // Create users in two different orgs
    const org1User = await createUser({ org_id: 'org_1', email: 'user1@org1.com' });
    const org2User = await createUser({ org_id: 'org_2', email: 'user2@org2.com' });

    // Try to access org_2 user with org_1 token
    const org1Token = generateToken({ org_id: 'org_1' });

    const response = await request(app)
      .get(`/api/users/${org2User._id}`)
      .set('Authorization', `Bearer ${org1Token}`)
      .expect(403);

    expect(response.body.error.message).toContain('Access denied');
  });

  it('should allow super_admin cross-org access', async () => {
    const org2User = await createUser({ org_id: 'org_2' });
    const superAdminToken = generateToken({
      org_id: 'org_1',
      roles: ['super_admin']
    });

    await request(app)
      .get(`/api/users/${org2User._id}`)
      .set('Authorization', `Bearer ${superAdminToken}`)
      .expect(200);
  });
});

平台分布

Codex

35.48%
按下载量换算49

Claude

32.34%
按下载量换算45

Cursor

19.65%
按下载量换算27

Gemini CLI

10.1%
按下载量换算14

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

通过

权限和风险

接触敏感数据

该 Skill 可能接触密钥、Token、环境变量或敏感配置,应进入高风险复核队列,默认不自动发布。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。目前只有一个公开来源,建议同时核对源仓库。

继续浏览同类 Skills