Skip to content

Commit 84e65d5

Browse files
committed
Add identity and policy schemas to spec package
Introduced new Zod schemas for authentication providers (OIDC, SAML, LDAP, etc.) in identity.zod.ts and for security policies (password, network, session, audit) in policy.zod.ts. Updated index.ts to export these new modules for broader use.
1 parent dbf8202 commit 84e65d5

File tree

8 files changed

+398
-8
lines changed

8 files changed

+398
-8
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# ObjectStack Security Model: The "Defense in Depth" Architecture
2+
3+
ObjectStack adopts the **"Salesforce-style"** metadata-driven security model. This is chosen over the Microsoft (Dataverse) model for its superior granularity and decoupling, which is essential for complex enterprise scenarios.
4+
5+
## 1. Comparison of Major Models
6+
7+
| Layer | ObjectStack / Salesforce | Microsoft Dataverse (Dynamics) | Why we chose ObjectStack's way? |
8+
| :--- | :--- | :--- | :--- |
9+
| **Who is this?** | **User + Identity** | User + Azure AD | Standard OIDC/SAML integration. |
10+
| **What can they do?**<br>(Functionality) | **Permission Set / Profile**<br>*(Boolean Flags)* | **Security Role**<br>*(The "Matrix" of Dots)* | Decoupling functional rights (e.g., "Export Data") from data scope allows more flexible assignment. |
11+
| **Where do they sit?**<br>(Hierarchy) | **Role**<br>*(Reporting Line)* | **Business Unit**<br>*(Department Tree)* | "Role" implies logical reporting (VP can see Manager), whereas "BU" implies rigid department silos. |
12+
| **What data can they see?**<br>(Visibility) | **Sharing Rules**<br>*(Criteria & Ownership)* | **Access Teams**<br>*(Ad-hoc)* | **Sharing Rules** are the "Killer Feature". They allow logic like "Share Deal > 1M with Finance", which Microsoft's static hierarchy cannot easily express. |
13+
| **Super Access** | **View All / Modify All** | **Organization Level** (Green Dot) | Separating "Super Access" from standard "Read" prevents accidental data leaks. |
14+
15+
## 2. The Protocols
16+
17+
### A. Functional permissions (`src/data/permission.zod.ts`)
18+
Defines the **Baseline**.
19+
* If `allowRead = false`, the user cannot see *any* record, regardless of sharing.
20+
* If `allowRead = true`, the user can see *their own* records (Ownership).
21+
22+
### B. Structural Hierarchy (`src/system/role.zod.ts`)
23+
Defines the **Reporting Line**.
24+
* A user is assigned to one Role (e.g., "Sales Manager").
25+
* **Automatic Inheritance**: The "Sales Manager" implicitly sees data owned by "Sales Rep".
26+
* *Note: This is closest to Microsoft's "Business Unit" concept.*
27+
28+
### C. Data Access Scope (`src/data/sharing.zod.ts`)
29+
Defines the **Expansion**.
30+
* We start with "Private" (OWD).
31+
* We expand access via **Sharing Rules**.
32+
* **Criteria-based**: "If Status = 'Published', share with All Internal Users."
33+
* **Owner-based**: "Share 'Western Region' records with 'Western VP'."
34+
35+
## 3. Terminology Map
36+
37+
If you are coming from the Microsoft/Dynamics ecosystem:
38+
39+
* **Security Role (User Level)** -> `allowRead: true`
40+
* **Security Role (BU Level)** -> `allowRead: true` + `Sharing Rule (Share with Role)`
41+
* **Security Role (Org Level)** -> `viewAllRecords: true`
42+
* **Business Unit** -> `Role` (Functional) / `OrgUnit` (Physical)
43+
44+
## 4. The Chinese Enterprise Extension (中国企业适配)
45+
46+
In the context of Chinese enterprises, the hierarchy is often strictly defined by **legal entities and departments** (Organization), which is distinct from the **reporting line** (Role).
47+
48+
* **OrgUnit (组织机构)**: `src/system/org_unit.zod.ts`
49+
* **Group (集团)**
50+
* **Company (分公司)**
51+
* **Department (部门)**
52+
* **Role (角色)**: `src/system/role.zod.ts`
53+
* Defines titles regardless of department (e.g., "Department Manager", "Accountant").
54+
55+
**Example Assignment:**
56+
* User: "Zhang San"
57+
* OrgUnit: "Shanghai Branch / Sales Dept"
58+
* Role: "Sales Manager"
59+
60+
The **Sharing Engine** can then support rules like:
61+
> "Share records owned by [Shanghai Branch] with [Headquarters Audit Role]."
62+

packages/spec/src/data/permission.zod.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,31 @@ import { z } from 'zod';
22

33
/**
44
* Entity (Object) Level Permissions
5+
* Defines CRUD + VAMA (View All / Modify All) access.
56
*/
67
export const ObjectPermissionSchema = z.object({
7-
/** Can create new records */
8+
/** C: Create */
89
allowCreate: z.boolean().default(false).describe('Create permission'),
9-
/** Can read records */
10+
/** R: Read (Owned records or Shared records) */
1011
allowRead: z.boolean().default(false).describe('Read permission'),
11-
/** Can edit records */
12+
/** U: Edit (Owned records or Shared records) */
1213
allowEdit: z.boolean().default(false).describe('Edit permission'),
13-
/** Can delete records */
14+
/** D: Delete (Owned records or Shared records) */
1415
allowDelete: z.boolean().default(false).describe('Delete permission'),
15-
/** Can view all records (ignores sharing rules) */
16-
viewAllNodes: z.boolean().default(false).describe('View All Data (admin)'),
17-
/** Can modify all records (ignores sharing rules) */
18-
modifyAllNodes: z.boolean().default(false).describe('Modify All Data (admin)'),
16+
17+
/**
18+
* View All Records: Super-user read access.
19+
* Bypasses Sharing Rules and Ownership checks.
20+
* Equivalent to Microsoft Dataverse "Organization" level read access.
21+
*/
22+
viewAllRecords: z.boolean().default(false).describe('View All Data (Bypass Sharing)'),
23+
24+
/**
25+
* Modify All Records: Super-user write access.
26+
* Bypasses Sharing Rules and Ownership checks.
27+
* Equivalent to Microsoft Dataverse "Organization" level write access.
28+
*/
29+
modifyAllRecords: z.boolean().default(false).describe('Modify All Data (Bypass Sharing)'),
1930
});
2031

2132
/**
@@ -31,6 +42,11 @@ export const FieldPermissionSchema = z.object({
3142
/**
3243
* Permission Set Schema
3344
* Defines a collection of permissions that can be assigned to users.
45+
*
46+
* DIFFERENTIATION:
47+
* - Profile: The ONE primary functional definition of a user (e.g. Standard User).
48+
* - Permission Set: Add-on capabilities assigned to users (e.g. Export Reports).
49+
* - Role: (Defined in src/system/role.zod.ts) Defines data visibility hierarchy.
3450
*/
3551
export const PermissionSetSchema = z.object({
3652
/** Unique permission set name */
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { z } from 'zod';
2+
3+
/**
4+
* Sharing Rule Type
5+
* How is the data shared?
6+
*/
7+
export const SharingRuleType = z.enum([
8+
'owner', // Based on record ownership (Role Hierarchy)
9+
'criteria', // Based on field values (e.g. Status = 'Open')
10+
'manual', // Ad-hoc sharing (User specific)
11+
'guest' // Public access
12+
]);
13+
14+
/**
15+
* Sharing Level
16+
* What access is granted?
17+
*/
18+
export const SharingLevel = z.enum([
19+
'read', // Read Only
20+
'edit' // Read / Write
21+
]);
22+
23+
/**
24+
* Sharing Rule Schema
25+
* Defines AUTOMATIC access grants based on logic.
26+
* The core engine of the governance layer.
27+
*/
28+
export const SharingRuleSchema = z.object({
29+
name: z.string().regex(/^[a-z_][a-z0-9_]*$/).describe('Unique rule name'),
30+
label: z.string().optional(),
31+
active: z.boolean().default(true),
32+
33+
/** Target Object */
34+
object: z.string().describe('Object to share'),
35+
36+
/** Grant Logic */
37+
type: SharingRuleType.default('criteria'),
38+
39+
/**
40+
* Criteria (for type='criteria')
41+
* SQL-like condition: "department = 'Sales' AND amount > 10000"
42+
*/
43+
criteria: z.string().optional(),
44+
45+
/** Access Level */
46+
accessLevel: SharingLevel.default('read'),
47+
48+
/**
49+
* Target Audience (Whom to share with)
50+
* ID of a Group, Role, or User.
51+
*/
52+
sharedWith: z.string().describe('Group/Role ID to share records with'),
53+
});
54+
55+
/**
56+
* Organization-Wide Defaults (OWD)
57+
* The baseline security posture for an object.
58+
*/
59+
export const OWDModel = z.enum([
60+
'private', // Only owner can see
61+
'public_read', // Everyone can see, owner can edit
62+
'public_read_write' // Everyone can see and edit
63+
]);
64+
65+
export type SharingRule = z.infer<typeof SharingRuleSchema>;
66+
export type SharingRuleType = z.infer<typeof SharingRuleType>;

packages/spec/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export * from './data/field.zod';
1212
export * from './data/object.zod';
1313
export * from './data/validation.zod';
1414
export * from './data/permission.zod';
15+
export * from './data/sharing.zod';
1516
export * from './data/workflow.zod';
1617
export * from './data/flow.zod';
1718
export * from './data/dataset.zod';
@@ -30,6 +31,11 @@ export * from './ui/action.zod';
3031
export * from './system/manifest.zod';
3132
export * from './system/datasource.zod';
3233
export * from './system/api.zod';
34+
export * from './system/identity.zod';
35+
export * from './system/policy.zod';
36+
export * from './system/role.zod';
37+
export * from './system/org_unit.zod';
38+
export * from './system/license.zod';
3339
export * from './system/translation.zod';
3440
export * from './system/constants';
3541
export * from './system/types';
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { z } from 'zod';
2+
3+
/**
4+
* Authentication Protocol
5+
* Defines supported authentication standards (OIDC, SAML, LDAP).
6+
*/
7+
export const AuthProtocol = z.enum([
8+
'oidc', // OpenID Connect (Modern standard)
9+
'saml', // SAML 2.0 (Legacy Enterprise)
10+
'ldap', // LDAP/Active Directory (On-premise)
11+
'oauth2', // Generic OAuth2
12+
'local', // Database username/password
13+
'mock' // Testing
14+
]);
15+
16+
/**
17+
* OIDC / OAuth2 Config (Standard)
18+
*/
19+
export const OIDCConfigSchema = z.object({
20+
issuer: z.string().url().describe('OIDC Issuer URL (.well-known/openid-configuration)'),
21+
clientId: z.string(),
22+
clientSecret: z.string(), // Usually value is ENV reference
23+
scopes: z.array(z.string()).default(['openid', 'profile', 'email']),
24+
attributeMapping: z.record(z.string()).optional().describe('Map IdP claims to User fields'),
25+
});
26+
27+
/**
28+
* SAML 2.0 Config (Enterprise)
29+
*/
30+
export const SAMLConfigSchema = z.object({
31+
entryPoint: z.string().url().describe('IdP SSO URL'),
32+
cert: z.string().describe('IdP Public Certificate'), // PEM format
33+
issuer: z.string().describe('Entity ID of the IdP'),
34+
signatureAlgorithm: z.enum(['sha256', 'sha512']).default('sha256'),
35+
attributeMapping: z.record(z.string()).optional(),
36+
});
37+
38+
/**
39+
* LDAP / AD Config (On-premise)
40+
*/
41+
export const LDAPConfigSchema = z.object({
42+
url: z.string().url().describe('LDAP Server URL (ldap:// or ldaps://)'),
43+
bindDn: z.string(),
44+
bindCredentials: z.string(),
45+
searchBase: z.string(),
46+
searchFilter: z.string(),
47+
groupSearchBase: z.string().optional(),
48+
});
49+
50+
/**
51+
* Identity Provider (IdP) Schema
52+
* Connects the OS to an external source of truth for identities.
53+
*/
54+
export const AuthProviderSchema = z.object({
55+
name: z.string().regex(/^[a-z_][a-z0-9_]*$/).describe('Provider ID'),
56+
label: z.string().describe('Button Label (e.g. "Login with Okta")'),
57+
type: AuthProtocol,
58+
59+
/** Configuration (Polymorphic based on type) */
60+
config: z.union([
61+
OIDCConfigSchema,
62+
SAMLConfigSchema,
63+
LDAPConfigSchema,
64+
z.record(z.any()) // Fallback
65+
]).describe('Provider specific configuration'),
66+
67+
/** Visuals */
68+
icon: z.string().optional().describe('Icon URL or helper class'),
69+
70+
/** Policies */
71+
active: z.boolean().default(true),
72+
registrationEnabled: z.boolean().default(false).describe('Allow new users to sign up via this provider'),
73+
});
74+
75+
export type AuthProvider = z.infer<typeof AuthProviderSchema>;
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { z } from 'zod';
2+
3+
/**
4+
* Metric Type Classification
5+
*/
6+
export const MetricType = z.enum([
7+
'boolean', // Feature Flag (Enabled/Disabled)
8+
'counter', // Usage Count (e.g. API Calls, Records Created) - Accumulates
9+
'gauge', // Current Level (e.g. Storage Used, Users Active) - Point in time
10+
]);
11+
12+
/**
13+
* Feature/Limit Definition Schema
14+
* Defines a controllable capability of the system.
15+
*/
16+
export const FeatureSchema = z.object({
17+
code: z.string().regex(/^[a-z_][a-z0-9_.]*$/).describe('Feature code (e.g. core.api_access)'),
18+
label: z.string(),
19+
description: z.string().optional(),
20+
21+
type: MetricType.default('boolean'),
22+
23+
/** For counters/gauges */
24+
unit: z.enum(['count', 'bytes', 'seconds', 'percent']).optional(),
25+
26+
/** Dependencies (e.g. 'audit_log' requires 'enterprise_tier') */
27+
requires: z.array(z.string()).optional(),
28+
});
29+
30+
/**
31+
* Subscription Plan Schema
32+
* Defines a tier of service (e.g. "Free", "Pro", "Enterprise").
33+
*/
34+
export const PlanSchema = z.object({
35+
code: z.string().describe('Plan code (e.g. pro_v1)'),
36+
label: z.string(),
37+
active: z.boolean().default(true),
38+
39+
/** Feature Entitlements */
40+
features: z.array(z.string()).describe('List of enabled boolean features'),
41+
42+
/** Limit Quotas */
43+
limits: z.record(z.number()).describe('Map of metric codes to limit values (e.g. { storage_gb: 10 })'),
44+
45+
/** Pricing (Optional Metadata) */
46+
currency: z.string().default('USD').optional(),
47+
priceMonthly: z.number().optional(),
48+
priceYearly: z.number().optional(),
49+
});
50+
51+
/**
52+
* License Schema
53+
* The actual entitlement object assigned to a Tenant.
54+
* Often signed as a JWT.
55+
*/
56+
export const LicenseSchema = z.object({
57+
/** Identity */
58+
tenantId: z.string(),
59+
planCode: z.string(),
60+
61+
/** Validity */
62+
issuedAt: z.string().datetime(),
63+
expiresAt: z.string().datetime().optional(), // Null = Perpetual
64+
65+
/** Status */
66+
status: z.enum(['active', 'expired', 'suspended', 'trial']),
67+
68+
/** Overrides (Specific to this tenant, exceeding the plan) */
69+
customFeatures: z.array(z.string()).optional(),
70+
customLimits: z.record(z.number()).optional(),
71+
72+
/** Signature */
73+
signature: z.string().optional().describe('Cryptographic signature of the license'),
74+
});
75+
76+
export type Feature = z.infer<typeof FeatureSchema>;
77+
export type Plan = z.infer<typeof PlanSchema>;
78+
export type License = z.infer<typeof LicenseSchema>;

0 commit comments

Comments
 (0)