Allow customers to read own attributes

This commit is contained in:
Jeppe Bundgaard
2026-07-07 13:05:24 +02:00
parent 08ac16e665
commit bf1d6a583e
3 changed files with 115 additions and 10 deletions
+2 -2
View File
@@ -10606,10 +10606,10 @@ paths:
tags: tags:
- Users - Users
summary: Get customer attributes summary: Get customer attributes
description: Get custom attributes for a customer description: Get custom attributes for a customer. Authenticated customer accounts may read their own attributes without list_customer_attributes.
operationId: getCustomerAttributes operationId: getCustomerAttributes
parameters: parameters:
- name: customer_id - name: customer_number
in: query in: query
schema: schema:
type: integer type: integer
@@ -16,11 +16,12 @@ class customerAttributes
$this->get('/customer/attributes', function () { $this->get('/customer/attributes', function () {
// Require the user to be logged in // Require the user to be logged in
global $response; global $response;
$this->requirePermission('list_customer_attributes');
// Get the user object // Get the user object
$user = (new authentication())->get_user(); $auth = new authentication();
$user = $auth->get_user();
$subuser = $auth->get_subuser();
// Check if the request was successful // Check if the request was successful
if ($user) { if ($user || $subuser) {
// Get the query parameters from the URL // Get the query parameters from the URL
$data = $_GET; $data = $_GET;
// Check if the required fields are set // Check if the required fields are set
@@ -32,14 +33,19 @@ class customerAttributes
$response->error('Customer Number must be a number', 400); $response->error('Customer Number must be a number', 400);
} }
// Check if the user exists // Check if the user exists
if (!(new users_o())->automaticGetTargetUserFromRequest()->exists()) { $target_user = (new users_o())->automaticGetTargetUserFromRequest();
if (!$target_user->exists()) {
$response->error('Customer not found', 400); $response->error('Customer not found', 400);
} }
if (!$this->canListTargetCustomerAttributes($target_user)) {
$this->requirePermission('list_customer_attributes');
}
// Log the incident // Log the incident
(new logs_o())->add('customer_attributes', 'global', 1, $user->id, 'LIST_CUSTOMER_ATTRIBUTES', 'Successfully listed customer attributes'); $actor_id = $user !== false ? (int)$user->id : (int)($subuser->id ?? 0);
(new logs_o())->add('customer_attributes', 'global', 1, $actor_id, 'LIST_CUSTOMER_ATTRIBUTES', 'Successfully listed customer attributes');
// Return the list of customer notes // Return the list of customer notes
$response->success( $response->success(
(new users_o())->automaticGetTargetUserFromRequest()->getUserAttributes() $target_user->getUserAttributes()
); );
} else { } else {
// Log the incident // Log the incident
@@ -49,7 +55,7 @@ class customerAttributes
} }
}, },
[ [
'list_customer_attributes' => 'List all customer attributes' 'list_customer_attributes' => 'List all customer attributes. Authenticated customer accounts may list their own customer attributes without this permission.'
] ]
); );
@@ -127,4 +133,29 @@ class customerAttributes
] ]
); );
} }
}
private function canListTargetCustomerAttributes(users_o $target_user): bool
{
if (!$target_user->exists()) {
return false;
}
$target_customer_number = (int)$target_user->customer_number->value();
if ($target_customer_number <= 0) {
return false;
}
$auth = new authentication();
$user = $auth->get_user();
if (
$user !== false
&& $user->exists()
&& $this->hasPermission('user')
&& (int)$user->customer_number->value() === $target_customer_number
) {
return true;
}
return $auth->get_subuser() !== false && $this->isOwnCustomerContext($target_customer_number);
}
}
@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
usesApiSuite();
it('lets customer booking sessions read their own customer attributes', function (): void {
api_test_covers('GET /customer/attributes', 'customer-access');
$session = api_fixtures()->createUserSession(['user']);
api_fixtures()->addCustomerAttribute((int)$session['user']['id'], 'onlyTankCleaning');
$response = api_client()->get(
'/customer/attributes?customer_number=' . (int)$session['user']['customer_number'],
$session['headers']
);
$response
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
$attributes = array_map(
static fn(array $attribute): string => (string)($attribute['attribute'] ?? ''),
is_array($response->data()) ? $response->data() : []
);
expect($attributes)->toContain('onlyTankCleaning');
expect($response->body)->not->toContain('list_customer_attributes');
});
it('keeps customer attribute reads scoped to the authenticated customer', function (): void {
api_test_covers('GET /customer/attributes', 'customer-access');
$session = api_fixtures()->createUserSession(['user']);
$otherCustomer = api_fixtures()->createUser(['display_name' => 'Other Attribute Customer']);
api_fixtures()->addCustomerAttribute((int)$otherCustomer['id'], 'onlyTankCleaning');
$response = api_client()->get(
'/customer/attributes?customer_number=' . (int)$otherCustomer['customer_number'],
$session['headers']
);
$response
->assertStatus(403)
->assertEnvelope()
->assertSuccess(false)
->assertMissingPermissions(['list_customer_attributes']);
});
it('still lets attribute managers read another customer attributes', function (): void {
api_test_covers('GET /customer/attributes', 'permissions');
$session = api_fixtures()->createUserSession(['list_customer_attributes']);
$customer = api_fixtures()->createUser(['display_name' => 'Managed Attribute Customer']);
api_fixtures()->addCustomerAttribute((int)$customer['id'], 'onlyTankCleaning');
$response = api_client()->get(
'/customer/attributes?customer_number=' . (int)$customer['customer_number'],
$session['headers']
);
$response
->assertStatus(200)
->assertEnvelope()
->assertSuccess();
$attributes = array_map(
static fn(array $attribute): string => (string)($attribute['attribute'] ?? ''),
is_array($response->data()) ? $response->data() : []
);
expect($attributes)->toContain('onlyTankCleaning');
});