@omnifyjp/ts 1.1.4 → 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -0
- package/dist/php/index.js +2 -0
- package/dist/php/policy-generator.d.ts +13 -0
- package/dist/php/policy-generator.js +406 -0
- package/dist/php/types.d.ts +7 -0
- package/dist/php/types.js +9 -0
- package/dist/types.d.ts +30 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
package/dist/php/index.js
CHANGED
|
@@ -21,6 +21,7 @@ import { generateResources } from './resource-generator.js';
|
|
|
21
21
|
import { generateFactories } from './factory-generator.js';
|
|
22
22
|
import { generateServiceProvider } from './service-provider-generator.js';
|
|
23
23
|
import { generateTranslationModels } from './translation-model-generator.js';
|
|
24
|
+
import { generatePolicies } from './policy-generator.js';
|
|
24
25
|
export { derivePhpConfig } from './types.js';
|
|
25
26
|
/** Generate all PHP files from schemas.json data. */
|
|
26
27
|
export function generatePhp(data, overrides) {
|
|
@@ -38,5 +39,6 @@ export function generatePhp(data, overrides) {
|
|
|
38
39
|
files.push(...generateRequests(reader, config));
|
|
39
40
|
files.push(...generateResources(reader, config));
|
|
40
41
|
files.push(...generateFactories(reader, config));
|
|
42
|
+
files.push(...generatePolicies(reader, config));
|
|
41
43
|
return files;
|
|
42
44
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Laravel Policy class generator.
|
|
3
|
+
*
|
|
4
|
+
* Generates Policy classes from Cedar-style ABAC policy definitions.
|
|
5
|
+
* Only generates for schemas that have `policies` defined.
|
|
6
|
+
*/
|
|
7
|
+
import { SchemaReader } from './schema-reader.js';
|
|
8
|
+
import type { GeneratedFile, PhpConfig } from './types.js';
|
|
9
|
+
import type { SchemaDefinition, SingleCondition } from '../types.js';
|
|
10
|
+
/** Generate Laravel Policy classes for all schemas that have policies. */
|
|
11
|
+
export declare function generatePolicies(reader: SchemaReader, config: PhpConfig): GeneratedFile[];
|
|
12
|
+
/** Convert a single condition to a PHP expression. */
|
|
13
|
+
export declare function conditionToPhp(cond: SingleCondition, schema: SchemaDefinition, reader: SchemaReader): string;
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Laravel Policy class generator.
|
|
3
|
+
*
|
|
4
|
+
* Generates Policy classes from Cedar-style ABAC policy definitions.
|
|
5
|
+
* Only generates for schemas that have `policies` defined.
|
|
6
|
+
*/
|
|
7
|
+
import { toPascalCase, toSnakeCase, toCamelCase } from './naming-helper.js';
|
|
8
|
+
import { baseFile, userFile } from './types.js';
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Action mapping: Omnify → Laravel
|
|
11
|
+
// ============================================================================
|
|
12
|
+
/** Maps omnify action names to Laravel policy method names. */
|
|
13
|
+
const ACTION_METHOD_MAP = {
|
|
14
|
+
view: 'view',
|
|
15
|
+
list: 'viewAny',
|
|
16
|
+
create: 'create',
|
|
17
|
+
edit: 'update',
|
|
18
|
+
delete: 'delete',
|
|
19
|
+
};
|
|
20
|
+
/** All standard actions (used for wildcard expansion). */
|
|
21
|
+
const ALL_ACTIONS = ['view', 'list', 'create', 'edit', 'delete'];
|
|
22
|
+
/** Actions that receive both $user and $record parameters. */
|
|
23
|
+
const RECORD_ACTIONS = new Set(['view', 'edit', 'delete']);
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Public API
|
|
26
|
+
// ============================================================================
|
|
27
|
+
/** Generate Laravel Policy classes for all schemas that have policies. */
|
|
28
|
+
export function generatePolicies(reader, config) {
|
|
29
|
+
const files = [];
|
|
30
|
+
for (const [name, schema] of Object.entries(reader.getProjectVisibleObjectSchemas())) {
|
|
31
|
+
if (!schema.policies || schema.policies.length === 0)
|
|
32
|
+
continue;
|
|
33
|
+
files.push(...generateForSchema(name, schema, reader, config));
|
|
34
|
+
}
|
|
35
|
+
return files;
|
|
36
|
+
}
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Per-schema generation
|
|
39
|
+
// ============================================================================
|
|
40
|
+
function generateForSchema(name, schema, reader, config) {
|
|
41
|
+
return [
|
|
42
|
+
generateBasePolicyClass(name, schema, reader, config),
|
|
43
|
+
generateUserPolicyClass(name, config),
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
function generateBasePolicyClass(name, schema, reader, config) {
|
|
47
|
+
const modelName = toPascalCase(name);
|
|
48
|
+
const baseNamespace = config.policies.baseNamespace;
|
|
49
|
+
const modelNamespace = config.models.namespace;
|
|
50
|
+
const policies = schema.policies;
|
|
51
|
+
const hasSoftDelete = schema.options?.softDelete ?? false;
|
|
52
|
+
const needsCidrHelper = policiesUseCidr(policies);
|
|
53
|
+
// Collect all actions that have policies
|
|
54
|
+
const expandedPolicies = expandWildcards(policies);
|
|
55
|
+
// Build methods
|
|
56
|
+
const methods = [];
|
|
57
|
+
for (const action of ALL_ACTIONS) {
|
|
58
|
+
const methodName = ACTION_METHOD_MAP[action];
|
|
59
|
+
const hasRecord = RECORD_ACTIONS.has(action);
|
|
60
|
+
const body = generateMethodBody(action, expandedPolicies, schema, reader, hasRecord);
|
|
61
|
+
methods.push(buildMethod(methodName, modelName, hasRecord, body));
|
|
62
|
+
}
|
|
63
|
+
// SoftDelete: restore + forceDelete
|
|
64
|
+
if (hasSoftDelete) {
|
|
65
|
+
const restoreBody = generateMethodBody('delete', expandedPolicies, schema, reader);
|
|
66
|
+
methods.push(buildMethod('restore', modelName, true, restoreBody));
|
|
67
|
+
const forceDeleteBody = generateMethodBody('delete', expandedPolicies, schema, reader);
|
|
68
|
+
methods.push(buildMethod('forceDelete', modelName, true, forceDeleteBody));
|
|
69
|
+
}
|
|
70
|
+
const cidrHelper = needsCidrHelper ? buildCidrHelper() : '';
|
|
71
|
+
const content = `<?php
|
|
72
|
+
|
|
73
|
+
namespace ${baseNamespace};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* DO NOT EDIT - This file is auto-generated by Omnify.
|
|
77
|
+
* Any changes will be overwritten on next generation.
|
|
78
|
+
*
|
|
79
|
+
* @generated by omnify
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
use ${modelNamespace}\\${modelName};
|
|
83
|
+
use App\\Models\\User;
|
|
84
|
+
use Illuminate\\Auth\\Access\\HandlesAuthorization;
|
|
85
|
+
|
|
86
|
+
class ${modelName}PolicyBase
|
|
87
|
+
{
|
|
88
|
+
use HandlesAuthorization;
|
|
89
|
+
${methods.join('\n')}${cidrHelper}
|
|
90
|
+
}
|
|
91
|
+
`;
|
|
92
|
+
return baseFile(`${config.policies.basePath}/${modelName}PolicyBase.php`, content);
|
|
93
|
+
}
|
|
94
|
+
function generateUserPolicyClass(name, config) {
|
|
95
|
+
const modelName = toPascalCase(name);
|
|
96
|
+
const policyNamespace = config.policies.namespace;
|
|
97
|
+
const baseNamespace = config.policies.baseNamespace;
|
|
98
|
+
const content = `<?php
|
|
99
|
+
|
|
100
|
+
namespace ${policyNamespace};
|
|
101
|
+
|
|
102
|
+
use ${baseNamespace}\\${modelName}PolicyBase;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* ${modelName} Policy
|
|
106
|
+
*
|
|
107
|
+
* This file is generated once and can be customized.
|
|
108
|
+
* Add your custom authorization logic here.
|
|
109
|
+
*/
|
|
110
|
+
class ${modelName}Policy extends ${modelName}PolicyBase
|
|
111
|
+
{
|
|
112
|
+
// Add your custom policy methods here
|
|
113
|
+
}
|
|
114
|
+
`;
|
|
115
|
+
return userFile(`${config.policies.path}/${modelName}Policy.php`, content);
|
|
116
|
+
}
|
|
117
|
+
// ============================================================================
|
|
118
|
+
// Method body generation
|
|
119
|
+
// ============================================================================
|
|
120
|
+
/**
|
|
121
|
+
* Generate the body of a policy method.
|
|
122
|
+
* Cedar evaluation order: forbid first → permit → default deny.
|
|
123
|
+
*
|
|
124
|
+
* When hasRecord is false (e.g. viewAny, create), conditions referencing
|
|
125
|
+
* $record properties are dropped. A conditional policy whose condition is
|
|
126
|
+
* entirely record-dependent becomes unconditional.
|
|
127
|
+
*/
|
|
128
|
+
function generateMethodBody(action, policies, schema, reader, hasRecord = true) {
|
|
129
|
+
const relevantPolicies = policies.filter(p => p.actions.includes(action));
|
|
130
|
+
if (relevantPolicies.length === 0) {
|
|
131
|
+
return ' return false;';
|
|
132
|
+
}
|
|
133
|
+
const lines = [];
|
|
134
|
+
// 1. Forbid rules first
|
|
135
|
+
const forbids = relevantPolicies.filter(p => p.effect === 'forbid');
|
|
136
|
+
for (const policy of forbids) {
|
|
137
|
+
if (policy.when) {
|
|
138
|
+
if (!hasRecord && whenUsesRecord(policy.when)) {
|
|
139
|
+
// Skip record-dependent conditions for collection methods
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const condition = whenToPhp(policy.when, schema, reader);
|
|
143
|
+
lines.push(` // ${policy.desc ?? 'Forbid rule'}`);
|
|
144
|
+
lines.push(` if (${condition}) {`);
|
|
145
|
+
lines.push(` return false;`);
|
|
146
|
+
lines.push(` }`);
|
|
147
|
+
lines.push('');
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
// Unconditional forbid
|
|
151
|
+
lines.push(` // ${policy.desc ?? 'Forbid rule'}`);
|
|
152
|
+
lines.push(` return false;`);
|
|
153
|
+
return lines.join('\n');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// 2. Permit rules
|
|
157
|
+
const permits = relevantPolicies.filter(p => p.effect === 'permit');
|
|
158
|
+
for (const policy of permits) {
|
|
159
|
+
if (policy.when) {
|
|
160
|
+
if (!hasRecord && whenUsesRecord(policy.when)) {
|
|
161
|
+
// Skip record-dependent conditions for collection methods
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const condition = whenToPhp(policy.when, schema, reader);
|
|
165
|
+
lines.push(` // ${policy.desc ?? 'Permit rule'}`);
|
|
166
|
+
lines.push(` if (${condition}) {`);
|
|
167
|
+
lines.push(` return true;`);
|
|
168
|
+
lines.push(` }`);
|
|
169
|
+
lines.push('');
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
// Unconditional permit
|
|
173
|
+
lines.push(` // ${policy.desc ?? 'Permit rule'}`);
|
|
174
|
+
lines.push(` return true;`);
|
|
175
|
+
return lines.join('\n');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// 3. Default deny
|
|
179
|
+
lines.push(` return false;`);
|
|
180
|
+
return lines.join('\n');
|
|
181
|
+
}
|
|
182
|
+
// ============================================================================
|
|
183
|
+
// Method builder
|
|
184
|
+
// ============================================================================
|
|
185
|
+
function buildMethod(methodName, modelName, hasRecord, body) {
|
|
186
|
+
const recordParam = hasRecord ? `, ${modelName} $record` : '';
|
|
187
|
+
const returnType = 'bool';
|
|
188
|
+
return `
|
|
189
|
+
/**
|
|
190
|
+
* Determine whether the user can ${methodName}.
|
|
191
|
+
*/
|
|
192
|
+
public function ${methodName}(User $user${recordParam}): ${returnType}
|
|
193
|
+
{
|
|
194
|
+
${body}
|
|
195
|
+
}`;
|
|
196
|
+
}
|
|
197
|
+
// ============================================================================
|
|
198
|
+
// Condition → PHP translation
|
|
199
|
+
// ============================================================================
|
|
200
|
+
/** Convert a PolicyWhen (single or AND group) to a PHP expression. */
|
|
201
|
+
function whenToPhp(when, schema, reader) {
|
|
202
|
+
if (isAndCondition(when)) {
|
|
203
|
+
const parts = when.conditions.map(c => conditionToPhp(c, schema, reader));
|
|
204
|
+
return parts.map(p => `(${p})`).join(' && ');
|
|
205
|
+
}
|
|
206
|
+
return conditionToPhp(when, schema, reader);
|
|
207
|
+
}
|
|
208
|
+
/** Type guard for AND condition groups. */
|
|
209
|
+
function isAndCondition(when) {
|
|
210
|
+
return when.operator === 'and' && Array.isArray(when.conditions);
|
|
211
|
+
}
|
|
212
|
+
/** Convert a single condition to a PHP expression. */
|
|
213
|
+
export function conditionToPhp(cond, schema, reader) {
|
|
214
|
+
const { operator, left, right } = cond;
|
|
215
|
+
// Handle "in" / "not in" operators
|
|
216
|
+
if (operator === 'in' || operator === 'not in') {
|
|
217
|
+
return handleInOperator(operator, left, right, schema, reader);
|
|
218
|
+
}
|
|
219
|
+
// Standard comparison operators
|
|
220
|
+
const phpOp = mapOperator(operator);
|
|
221
|
+
const leftExpr = operandToPhp(left, schema, reader, 'left');
|
|
222
|
+
const rightExpr = operandToPhp(right, schema, reader, 'right');
|
|
223
|
+
return `${leftExpr} ${phpOp} ${rightExpr}`;
|
|
224
|
+
}
|
|
225
|
+
/** Map Cedar operator to PHP operator. */
|
|
226
|
+
function mapOperator(op) {
|
|
227
|
+
switch (op) {
|
|
228
|
+
case '=': return '===';
|
|
229
|
+
case '!=': return '!==';
|
|
230
|
+
case '>': return '>';
|
|
231
|
+
case '<': return '<';
|
|
232
|
+
case '>=': return '>=';
|
|
233
|
+
case '<=': return '<=';
|
|
234
|
+
default: return op;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/** Convert a condition operand to a PHP expression. */
|
|
238
|
+
function operandToPhp(operand, schema, reader, side) {
|
|
239
|
+
switch (operand.type) {
|
|
240
|
+
case 'property': {
|
|
241
|
+
const propName = operand.name;
|
|
242
|
+
// Check if it's an association (ManyToOne) → use _id
|
|
243
|
+
const prop = schema.properties?.[propName];
|
|
244
|
+
if (prop?.relation === 'ManyToOne') {
|
|
245
|
+
return `$record->${toSnakeCase(propName)}_id`;
|
|
246
|
+
}
|
|
247
|
+
return `$record->${toSnakeCase(propName)}`;
|
|
248
|
+
}
|
|
249
|
+
case 'variable': {
|
|
250
|
+
const varName = operand.name;
|
|
251
|
+
if (varName === '$user') {
|
|
252
|
+
return '$user->id';
|
|
253
|
+
}
|
|
254
|
+
if (varName === '$now') {
|
|
255
|
+
return 'now()';
|
|
256
|
+
}
|
|
257
|
+
if (varName === '$ip') {
|
|
258
|
+
return "request()->ip()";
|
|
259
|
+
}
|
|
260
|
+
// $user.attribute
|
|
261
|
+
if (varName.startsWith('$user.')) {
|
|
262
|
+
const attr = varName.slice('$user.'.length);
|
|
263
|
+
return `$user->${toSnakeCase(attr)}`;
|
|
264
|
+
}
|
|
265
|
+
return varName;
|
|
266
|
+
}
|
|
267
|
+
case 'literal': {
|
|
268
|
+
const val = operand.value;
|
|
269
|
+
if (typeof val === 'string') {
|
|
270
|
+
return `'${val}'`;
|
|
271
|
+
}
|
|
272
|
+
if (typeof val === 'number') {
|
|
273
|
+
return String(val);
|
|
274
|
+
}
|
|
275
|
+
if (typeof val === 'boolean') {
|
|
276
|
+
return val ? 'true' : 'false';
|
|
277
|
+
}
|
|
278
|
+
return String(val);
|
|
279
|
+
}
|
|
280
|
+
case 'set': {
|
|
281
|
+
const items = operand.items ?? [];
|
|
282
|
+
const phpItems = items.map(i => `'${i}'`).join(', ');
|
|
283
|
+
return `[${phpItems}]`;
|
|
284
|
+
}
|
|
285
|
+
case 'cidr': {
|
|
286
|
+
return `'${operand.name}'`;
|
|
287
|
+
}
|
|
288
|
+
case 'path': {
|
|
289
|
+
// Through-association: e.g. "org.members"
|
|
290
|
+
const pathName = operand.name;
|
|
291
|
+
const parts = pathName.split('.');
|
|
292
|
+
// $record->org->members()
|
|
293
|
+
const chain = parts.slice(0, -1).map(p => toSnakeCase(p)).join('->');
|
|
294
|
+
const lastRel = toCamelCase(parts[parts.length - 1]);
|
|
295
|
+
return `$record->${chain}->${lastRel}()`;
|
|
296
|
+
}
|
|
297
|
+
default:
|
|
298
|
+
return `/* unknown operand type: ${operand.type} */`;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/** Handle "in" and "not in" operators with various operand types. */
|
|
302
|
+
function handleInOperator(operator, left, right, schema, reader) {
|
|
303
|
+
const isNotIn = operator === 'not in';
|
|
304
|
+
// $user in members (ManyToMany)
|
|
305
|
+
if (left.type === 'variable' && left.name === '$user' && right.type === 'property') {
|
|
306
|
+
const relName = toCamelCase(right.name);
|
|
307
|
+
const expr = `$record->${relName}()->where('users.id', $user->id)->exists()`;
|
|
308
|
+
return isNotIn ? `!${expr}` : expr;
|
|
309
|
+
}
|
|
310
|
+
// $user in org.members (through-association)
|
|
311
|
+
if (left.type === 'variable' && left.name === '$user' && right.type === 'path') {
|
|
312
|
+
const pathName = right.name;
|
|
313
|
+
const parts = pathName.split('.');
|
|
314
|
+
const chain = parts.slice(0, -1).map(p => toCamelCase(p)).join('->');
|
|
315
|
+
const lastRel = toCamelCase(parts[parts.length - 1]);
|
|
316
|
+
const expr = `$record->${chain}->${lastRel}()->where('users.id', $user->id)->exists()`;
|
|
317
|
+
return isNotIn ? `!${expr}` : expr;
|
|
318
|
+
}
|
|
319
|
+
// $ip in cidr(...)
|
|
320
|
+
if (left.type === 'variable' && left.name === '$ip' && right.type === 'cidr') {
|
|
321
|
+
const expr = `$this->ipInCidr(request()->ip(), '${right.name}')`;
|
|
322
|
+
return isNotIn ? `!${expr}` : expr;
|
|
323
|
+
}
|
|
324
|
+
// property in set / property not in set
|
|
325
|
+
if (left.type === 'property' && right.type === 'set') {
|
|
326
|
+
const propExpr = operandToPhp(left, schema, reader, 'left');
|
|
327
|
+
const items = right.items ?? [];
|
|
328
|
+
const phpItems = items.map(i => `'${i}'`).join(', ');
|
|
329
|
+
if (isNotIn) {
|
|
330
|
+
return `!in_array(${propExpr}, [${phpItems}])`;
|
|
331
|
+
}
|
|
332
|
+
return `in_array(${propExpr}, [${phpItems}])`;
|
|
333
|
+
}
|
|
334
|
+
// Fallback: generic in
|
|
335
|
+
const leftExpr = operandToPhp(left, schema, reader, 'left');
|
|
336
|
+
const rightExpr = operandToPhp(right, schema, reader, 'right');
|
|
337
|
+
if (isNotIn) {
|
|
338
|
+
return `!in_array(${leftExpr}, ${rightExpr})`;
|
|
339
|
+
}
|
|
340
|
+
return `in_array(${leftExpr}, ${rightExpr})`;
|
|
341
|
+
}
|
|
342
|
+
// ============================================================================
|
|
343
|
+
// Helpers
|
|
344
|
+
// ============================================================================
|
|
345
|
+
/** Check if a when clause uses $record (property or path operands). */
|
|
346
|
+
function whenUsesRecord(when) {
|
|
347
|
+
if (isAndCondition(when)) {
|
|
348
|
+
return when.conditions.some(conditionUsesRecord);
|
|
349
|
+
}
|
|
350
|
+
return conditionUsesRecord(when);
|
|
351
|
+
}
|
|
352
|
+
/** Check if a single condition references $record. */
|
|
353
|
+
function conditionUsesRecord(cond) {
|
|
354
|
+
return operandUsesRecord(cond.left) || operandUsesRecord(cond.right);
|
|
355
|
+
}
|
|
356
|
+
/** Check if an operand references $record (property or path type). */
|
|
357
|
+
function operandUsesRecord(operand) {
|
|
358
|
+
return operand.type === 'property' || operand.type === 'path';
|
|
359
|
+
}
|
|
360
|
+
/** Expand wildcard (*) actions in policies. */
|
|
361
|
+
function expandWildcards(policies) {
|
|
362
|
+
return policies.map(p => {
|
|
363
|
+
if (p.actions.includes('*')) {
|
|
364
|
+
return { ...p, actions: ALL_ACTIONS };
|
|
365
|
+
}
|
|
366
|
+
return p;
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
/** Check if any policy uses CIDR conditions. */
|
|
370
|
+
function policiesUseCidr(policies) {
|
|
371
|
+
for (const p of policies) {
|
|
372
|
+
if (!p.when)
|
|
373
|
+
continue;
|
|
374
|
+
if (isAndCondition(p.when)) {
|
|
375
|
+
if (p.when.conditions.some(c => c.left.type === 'cidr' || c.right.type === 'cidr'))
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
const cond = p.when;
|
|
380
|
+
if (cond.left.type === 'cidr' || cond.right.type === 'cidr')
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
/** Build the ipInCidr helper method. */
|
|
387
|
+
function buildCidrHelper() {
|
|
388
|
+
return `
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Check if an IP address is within a CIDR range.
|
|
392
|
+
*/
|
|
393
|
+
protected function ipInCidr(?string $ip, string $cidr): bool
|
|
394
|
+
{
|
|
395
|
+
if ($ip === null) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
[$subnet, $bits] = explode('/', $cidr);
|
|
400
|
+
$subnet = ip2long($subnet);
|
|
401
|
+
$ip = ip2long($ip);
|
|
402
|
+
$mask = -1 << (32 - (int) $bits);
|
|
403
|
+
|
|
404
|
+
return ($ip & $mask) === ($subnet & $mask);
|
|
405
|
+
}`;
|
|
406
|
+
}
|
package/dist/php/types.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface LaravelCodegenOverrides {
|
|
|
24
24
|
resource?: LaravelPathOverride;
|
|
25
25
|
factory?: LaravelPathOverride;
|
|
26
26
|
provider?: LaravelPathOverride;
|
|
27
|
+
policy?: LaravelPathOverride;
|
|
27
28
|
}
|
|
28
29
|
/** PHP codegen configuration (resolved with defaults). */
|
|
29
30
|
export interface PhpConfig {
|
|
@@ -53,6 +54,12 @@ export interface PhpConfig {
|
|
|
53
54
|
namespace: string;
|
|
54
55
|
path: string;
|
|
55
56
|
};
|
|
57
|
+
policies: {
|
|
58
|
+
namespace: string;
|
|
59
|
+
baseNamespace: string;
|
|
60
|
+
path: string;
|
|
61
|
+
basePath: string;
|
|
62
|
+
};
|
|
56
63
|
}
|
|
57
64
|
/**
|
|
58
65
|
* Derive full PHP config from optional overrides.
|
package/dist/php/types.js
CHANGED
|
@@ -22,6 +22,7 @@ const DEFAULT_REQUEST_PATH = 'app/Http/Requests';
|
|
|
22
22
|
const DEFAULT_RESOURCE_PATH = 'app/Http/Resources';
|
|
23
23
|
const DEFAULT_FACTORY_PATH = 'database/factories';
|
|
24
24
|
const DEFAULT_PROVIDER_PATH = 'app/Providers';
|
|
25
|
+
const DEFAULT_POLICY_PATH = 'app/Policies/Omnify';
|
|
25
26
|
/**
|
|
26
27
|
* Derive full PHP config from optional overrides.
|
|
27
28
|
* All paths and namespaces fall back to sensible defaults.
|
|
@@ -37,6 +38,8 @@ export function derivePhpConfig(overrides) {
|
|
|
37
38
|
const factoryNs = overrides?.factory?.namespace ?? pathToNamespace(factoryPath);
|
|
38
39
|
const providerPath = overrides?.provider?.path ?? DEFAULT_PROVIDER_PATH;
|
|
39
40
|
const providerNs = overrides?.provider?.namespace ?? pathToNamespace(providerPath);
|
|
41
|
+
const policyPath = overrides?.policy?.path ?? DEFAULT_POLICY_PATH;
|
|
42
|
+
const policyNs = overrides?.policy?.namespace ?? pathToNamespace(policyPath);
|
|
40
43
|
return {
|
|
41
44
|
models: {
|
|
42
45
|
namespace: modelNs,
|
|
@@ -64,5 +67,11 @@ export function derivePhpConfig(overrides) {
|
|
|
64
67
|
namespace: providerNs,
|
|
65
68
|
path: providerPath,
|
|
66
69
|
},
|
|
70
|
+
policies: {
|
|
71
|
+
namespace: policyNs,
|
|
72
|
+
baseNamespace: `${policyNs}\\Base`,
|
|
73
|
+
path: policyPath,
|
|
74
|
+
basePath: `${policyPath}/Base`,
|
|
75
|
+
},
|
|
67
76
|
};
|
|
68
77
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -106,6 +106,36 @@ export interface SchemaDefinition {
|
|
|
106
106
|
readonly expandedProperties?: Record<string, ExpandedProperty>;
|
|
107
107
|
readonly values?: readonly EnumValueDefinition[];
|
|
108
108
|
readonly pivotFor?: readonly string[];
|
|
109
|
+
readonly policies?: readonly PolicyDefinition[];
|
|
110
|
+
}
|
|
111
|
+
/** Operand type in a policy condition. */
|
|
112
|
+
export type OperandType = 'property' | 'variable' | 'literal' | 'cidr' | 'set' | 'path';
|
|
113
|
+
/** One side of a condition expression. */
|
|
114
|
+
export interface ConditionOperand {
|
|
115
|
+
readonly type: OperandType;
|
|
116
|
+
readonly name?: string;
|
|
117
|
+
readonly value?: string | number | boolean;
|
|
118
|
+
readonly items?: readonly string[];
|
|
119
|
+
}
|
|
120
|
+
/** A parsed binary condition expression. */
|
|
121
|
+
export interface SingleCondition {
|
|
122
|
+
readonly operator: string;
|
|
123
|
+
readonly left: ConditionOperand;
|
|
124
|
+
readonly right: ConditionOperand;
|
|
125
|
+
}
|
|
126
|
+
/** AND group of conditions. */
|
|
127
|
+
export interface PolicyConditionAnd {
|
|
128
|
+
readonly operator: 'and';
|
|
129
|
+
readonly conditions: readonly SingleCondition[];
|
|
130
|
+
}
|
|
131
|
+
/** Policy when clause — either a single condition or AND group. */
|
|
132
|
+
export type PolicyWhen = SingleCondition | PolicyConditionAnd;
|
|
133
|
+
/** A single policy rule (permit or forbid). */
|
|
134
|
+
export interface PolicyDefinition {
|
|
135
|
+
readonly effect: 'permit' | 'forbid';
|
|
136
|
+
readonly actions: readonly string[];
|
|
137
|
+
readonly when?: PolicyWhen;
|
|
138
|
+
readonly desc?: string;
|
|
109
139
|
}
|
|
110
140
|
/** Application-level validation rules. Single source of truth for both Laravel validation and Zod schemas. */
|
|
111
141
|
export interface ValidationRules {
|