Skip to content

Commit 44a2b79

Browse files
committed
Add logic for specific device/port user access
1 parent ef13136 commit 44a2b79

5 files changed

Lines changed: 45 additions & 23 deletions

File tree

app/Restify/UserRepository.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,19 @@ class UserRepository extends Repository
2525
public static function related(): array
2626
{
2727
return [
28-
'devices-owned' => BelongsToMany::make('devicesOwned', DeviceRepository::class)->label('devices-owned'),
29-
'ports-owned' => BelongsToMany::make('portsOwned', PortRepository::class)->label('ports-owned'),
28+
'devices-owned' => tap(
29+
BelongsToMany::make('devices-owned', DeviceRepository::class),
30+
static fn ($f) => $f->relation = 'devicesOwned',
31+
),
32+
'ports-owned' => tap(
33+
BelongsToMany::make('ports-owned', PortRepository::class),
34+
static fn ($f) => $f->relation = 'portsOwned',
35+
),
3036
'bills' => BelongsToMany::make('bills', BillRepository::class),
31-
'device-groups' => BelongsToMany::make('deviceGroups', DeviceGroupRepository::class)->label('device-groups'),
37+
'device-groups' => tap(
38+
BelongsToMany::make('device-groups', DeviceGroupRepository::class),
39+
static fn ($f) => $f->relation = 'deviceGroups',
40+
),
3241
];
3342
}
3443

app/Services/Api/OpenApi/OpenApiGenerator.php

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -482,12 +482,21 @@ private function attachablePathItems(string $parentUriKey, string $relationName,
482482
}
483483

484484
/**
485-
* @param array{cardinality: 'one'|'many', repository: class-string<\Binaryk\LaravelRestify\Repositories\Repository>} $info
485+
* @param array{cardinality: 'one'|'many', repository: class-string<\Binaryk\LaravelRestify\Repositories\Repository>, target_uri_key?: string} $info
486486
*/
487487
private function relatedPathItem(string $parentUriKey, string $relationName, array $info, string $tagName, Parameter $idParameter): PathItem
488488
{
489489
$relatedResourceName = Str::studly(Str::singular($info['repository']::uriKey()));
490490

491+
// For HasMany / BelongsToMany, Restify's relatable.index matches the
492+
// URL segment against the target repo's uriKey (plural), so the
493+
// advertised path needs to use that. For BelongsTo (a single related
494+
// model), JSON:API convention prefers the singular relation name (the
495+
// array key), so keep that.
496+
$segment = $info['cardinality'] === 'many'
497+
? ($info['target_uri_key'] ?? $relationName)
498+
: $relationName;
499+
491500
$envelopeRef = $info['cardinality'] === 'many'
492501
? '#/components/schemas/JsonApiList'
493502
: '#/components/schemas/JsonApiSingle';
@@ -502,8 +511,8 @@ private function relatedPathItem(string $parentUriKey, string $relationName, arr
502511
);
503512

504513
$operation = Operation::get()
505-
->operationId("{$parentUriKey}.{$relationName}")
506-
->summary("List {$relationName} of a " . Str::singular($parentUriKey))
514+
->operationId("{$parentUriKey}.{$segment}")
515+
->summary("List {$segment} of a " . Str::singular($parentUriKey))
507516
->tags($tagName)
508517
->parameters($idParameter)
509518
->responses(
@@ -516,7 +525,7 @@ private function relatedPathItem(string $parentUriKey, string $relationName, arr
516525
$this->errorResponse(500, 'Server Error'),
517526
);
518527

519-
return PathItem::create()->route("/api/v1/{$parentUriKey}/{id}/{$relationName}")->operations($operation);
528+
return PathItem::create()->route("/api/v1/{$parentUriKey}/{id}/{$segment}")->operations($operation);
520529
}
521530

522531
private function writeRequestBody(string $resourceName, string $uriKey): RequestBody

app/Services/Api/OpenApi/RepositoryIntrospector.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public function isReadonly(Field $field): bool
8686

8787
/**
8888
* @param class-string<RestifyRepository> $repositoryClass
89-
* @return array<string, array{cardinality: 'one'|'many', repository: class-string<RestifyRepository>, is_attachable: bool, attribute: string}>
89+
* @return array<string, array{cardinality: 'one'|'many', repository: class-string<RestifyRepository>, is_attachable: bool, attribute: string, target_uri_key: string}>
9090
*/
9191
public function related(string $repositoryClass): array
9292
{
@@ -110,6 +110,7 @@ public function related(string $repositoryClass): array
110110
'repository' => $field->repositoryClass,
111111
'is_attachable' => $field instanceof BelongsToMany,
112112
'attribute' => (string) $field->attribute,
113+
'target_uri_key' => $field->repositoryClass::uriKey(),
113114
];
114115
}
115116

database/factories/AlertRuleFactory.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@ public function definition(): array
1212
return [
1313
'name' => $this->faker->unique()->words(3, true),
1414
'severity' => $this->faker->randomElement(['ok', 'warning', 'critical']),
15-
'extra' => '{}',
15+
// 'extra' and 'builder' are array-cast on the model; passing a string
16+
// here would get JSON-encoded into a quoted string ("{}") in the DB,
17+
// which then decodes back to the string "{}" rather than [].
18+
'extra' => [],
1619
'disabled' => 0,
1720
'query' => 'SELECT * FROM devices WHERE status = 1',
18-
'builder' => '{}',
21+
'builder' => [],
1922
'proc' => null,
2023
'notes' => null,
2124
'invert_map' => 0,

tests/Feature/Api/RestifyRelationshipsTest.php

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public function testDeviceShowIncludesLocation(): void
7373
$response = $this->getJson("/api/v1/devices/{$device->device_id}?related=location");
7474

7575
$response->assertStatus(200)
76-
->assertJsonPath('data.relationships.location.attributes.location', $location->location);
76+
->assertJsonPath('data.relationships.location.attributes.name', $location->location);
7777
}
7878

7979
public function testDeviceShowIncludesGroups(): void
@@ -84,10 +84,10 @@ public function testDeviceShowIncludesGroups(): void
8484
$device->groups()->attach($groups->pluck('id'));
8585
Sanctum::actingAs($user);
8686

87-
$response = $this->getJson("/api/v1/devices/{$device->device_id}?related=groups");
87+
$response = $this->getJson("/api/v1/devices/{$device->device_id}?related=device-groups");
8888

8989
$response->assertStatus(200)
90-
->assertJsonCount(2, 'data.relationships.groups');
90+
->assertJsonCount(2, 'data.relationships.device-groups');
9191
}
9292

9393
public function testDeviceIndexIncludesRelationships(): void
@@ -102,7 +102,7 @@ public function testDeviceIndexIncludesRelationships(): void
102102

103103
$response->assertStatus(200)
104104
->assertJsonCount(2, 'data.0.relationships.ports')
105-
->assertJsonPath('data.0.relationships.location.attributes.location', $location->location);
105+
->assertJsonPath('data.0.relationships.location.attributes.name', $location->location);
106106
}
107107

108108
public function testDeviceWithoutRelatedParamExcludesRelationships(): void
@@ -234,10 +234,10 @@ public function testAlertRuleIncludesGroups(): void
234234
$rule->groups()->attach($groups->pluck('id'));
235235
Sanctum::actingAs($user);
236236

237-
$response = $this->getJson("/api/v1/alert-rules/{$rule->id}?related=groups");
237+
$response = $this->getJson("/api/v1/alert-rules/{$rule->id}?related=device-groups");
238238

239239
$response->assertStatus(200)
240-
->assertJsonCount(2, 'data.relationships.groups');
240+
->assertJsonCount(2, 'data.relationships.device-groups');
241241
}
242242

243243
public function testAlertRuleIncludesLocations(): void
@@ -639,12 +639,12 @@ public function testMultipleRelationshipsCanBeRequested(): void
639639
Port::factory()->count(2)->for($device)->create();
640640
Sanctum::actingAs($user);
641641

642-
$response = $this->getJson("/api/v1/devices/{$device->device_id}?related=ports,location,groups");
642+
$response = $this->getJson("/api/v1/devices/{$device->device_id}?related=ports,location,device-groups");
643643

644644
$response->assertStatus(200)
645645
->assertJsonCount(2, 'data.relationships.ports')
646-
->assertJsonPath('data.relationships.location.attributes.location', $location->location)
647-
->assertJsonCount(1, 'data.relationships.groups');
646+
->assertJsonPath('data.relationships.location.attributes.name', $location->location)
647+
->assertJsonCount(1, 'data.relationships.device-groups');
648648
}
649649

650650
// ── Empty relationships ─────────────────────────────────
@@ -667,10 +667,10 @@ public function testEmptyBelongsToManyReturnsEmptyArray(): void
667667
$device = Device::factory()->create();
668668
Sanctum::actingAs($user);
669669

670-
$response = $this->getJson("/api/v1/devices/{$device->device_id}?related=groups");
670+
$response = $this->getJson("/api/v1/devices/{$device->device_id}?related=device-groups");
671671

672672
$response->assertStatus(200)
673-
->assertJsonPath('data.relationships.groups', []);
673+
->assertJsonPath('data.relationships.device-groups', []);
674674
}
675675

676676
// ── Relationship permission tests ───────────────────────
@@ -829,12 +829,12 @@ public function testCustomRoleCannotSeeUnauthorizedDeviceGroupsViaAlertRule(): v
829829
$rule->groups()->attach($groups->pluck('id'));
830830
Sanctum::actingAs($user);
831831

832-
$response = $this->getJson("/api/v1/alert-rules/{$rule->id}?related=groups");
832+
$response = $this->getJson("/api/v1/alert-rules/{$rule->id}?related=device-groups");
833833

834834
$response->assertStatus(200);
835835

836836
// Device groups should be filtered out since user lacks device-group.view
837-
$relationships = $response->json('data.relationships.groups');
837+
$relationships = $response->json('data.relationships.device-groups');
838838
$nonNullGroups = collect($relationships)->filter()->values();
839839
$this->assertEmpty($nonNullGroups, 'User without device-group.view should not see related groups');
840840
}

0 commit comments

Comments
 (0)