Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions src/Http/Middleware/AccessCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,18 @@ public function handle($request, Closure $next)

return $next($request);
} catch (RestException $e) {

$permException = $e;
// Try schema access fallback for GET requests to _schema
if ($method === Verbs::GET && $component === '_schema') {
\Log::info('Attempting schema access fallback for _schema request');
$fallbackResponse = $this->handleSchemaAccessFallback($service, $method, $component, $requestor);
if ($fallbackResponse !== null) {
return $next($request);
} else {
$permException = $e;
}
} else {
$permException = $e;
}
}

// No access allowed, figure out the best error response
Expand Down Expand Up @@ -115,4 +125,29 @@ private function isOAuthService($service)
{
return str_ends_with($service, '_oauth');
}

/**
* Handle schema access fallback for GET requests to _schema endpoints
*
* @param string $service Service name
* @param string $method HTTP method
* @param string $component Component name
* @param int $requestor Requestor type
* @return mixed Response or null if fallback not possible
*/
private function handleSchemaAccessFallback($service, $method, $component, $requestor)
{
// Only handle GET requests to _schema endpoints
if ($method !== Verbs::GET || $component !== '_schema') {
return null;
}

$allowedComponents = Session::getAllowedComponentsForGet($service, $requestor);

if (empty($allowedComponents)) {
return null;
}

return $allowedComponents;
}
}
139 changes: 139 additions & 0 deletions src/Services/ServiceManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,10 @@ public function handleRequest(
if ($check_permission === true) {
if (false === Session::checkServicePermission($verb, $service, $resource, Session::getRequestor(),
$throw_exception)) {
// For schema requests, try to get filtered results
if ($verb === Verbs::GET && $resource === '_schema') {
return $this->getFilteredSchemaResponse($service, $query);
}
return new ServiceResponse([]);
}
}
Expand All @@ -547,6 +551,141 @@ public function handleRequest(
return $this->getService($service)->handleRequest($request, $resource);
}

/**
* Get filtered schema response based on user permissions
*
* @param string $service Service name
* @param array $query Query parameters
* @return ServiceResponse Filtered schema response
*/
private function getFilteredSchemaResponse($service, $query)
{
try {
$serviceInstance = $this->getService($service);

// Create a request with as_list parameter
$request = new ServiceRequest();
$request->setMethod(Verbs::GET);
$request->setParameters(array_merge($query, ['as_list' => true]));

// Handle the request without permission checks
$response = $serviceInstance->handleRequest($request, '_schema');

// Filter the response based on user permissions
$content = $response->getContent();
$allowedComponents = Session::getAllowedComponentsForGet($service);

if (!empty($allowedComponents) && !in_array('*', $allowedComponents)) {
$content = $this->filterSchemaContent($content, $allowedComponents);
$response->setContent($content);
}

return $response;

} catch (\Exception $e) {
return new ServiceResponse([], 'application/json', 500);
}
}

/**
* Filter schema content based on allowed components
*
* @param mixed $content Schema content to filter
* @param array $allowedComponents Array of allowed component patterns
* @return mixed Filtered content
*/
private function filterSchemaContent($content, $allowedComponents)
{
// Handle different content structures
if (is_array($content)) {
// Check if content is wrapped in resources wrapper
$wrapper = config('df.resources_wrapper', 'resource');
if (isset($content[$wrapper]) && is_array($content[$wrapper])) {
$resources = $content[$wrapper];
$filteredResources = $this->filterResourcesByAccess($resources, $allowedComponents);
$content[$wrapper] = $filteredResources;
return $content;
} else {
return $this->filterResourcesByAccess($content, $allowedComponents);
}
}

return $content;
}

/**
* Filter resources based on access patterns
*
* @param array $resources Array of resources to filter
* @param array $allowedComponents Array of allowed component patterns
* @return array Filtered resources
*/
private function filterResourcesByAccess($resources, $allowedComponents)
{
if (in_array('*', $allowedComponents)) {
return $resources; // Full access
}

$filtered = [];

foreach ($resources as $resource) {
if (is_array($resource) && isset($resource['name'])) {
$resourceName = $resource['name'];
} elseif (is_string($resource)) {
$resourceName = $resource;
} else {
continue;
}

// Check if this resource is allowed
if ($this->isResourceAllowed($resourceName, $allowedComponents)) {
$filtered[] = $resource;
}
}

return $filtered;
}

/**
* Check if a resource is allowed based on component patterns
*
* @param string $resourceName Name of the resource to check
* @param array $allowedComponents Array of allowed component patterns
* @return bool True if resource is allowed
*/
private function isResourceAllowed($resourceName, $allowedComponents)
{
foreach ($allowedComponents as $pattern) {
if ($pattern === '*') {
return true;
}

if ($pattern === $resourceName) {
return true;
}

// Handle wildcard patterns like "table/*"
if (strpos($pattern, '*') !== false) {
$pattern = str_replace('*', '.*', $pattern);
$pattern = '/^' . $pattern . '$/';
if (preg_match($pattern, $resourceName)) {
return true;
}
}

// Handle dynamic parameters like "table/{id}"
if (strpos($pattern, '{') !== false) {
$regexPattern = preg_replace('/\{[^}]+\}/', '[^/]+', $pattern);
$regexPattern = '/^' . $regexPattern . '$/';
if (preg_match($regexPattern, $resourceName)) {
return true;
}
}
}

return false;
}

/**
* Make the service instance.
*
Expand Down
52 changes: 52 additions & 0 deletions src/Utility/Session.php
Original file line number Diff line number Diff line change
Expand Up @@ -1102,4 +1102,56 @@ public static function hasRole()

return \Session::get('has_role');
}

/**
* Get allowed components for GET operations on a specific service
*
* @param string $service Service name
* @param int $requestor Requestor type
* @return array Array of allowed component names/patterns
*/
public static function getAllowedComponentsForGet($service, $requestor = ServiceRequestorTypes::API)
{
if (static::isSysAdmin()) {
return ['*']; // Admin has access to everything
}

$roleId = Session::getRoleId();
if ($roleId && !Role::getCachedInfo($roleId, 'is_active')) {
return [];
}

$services = (array)static::get('role.services');
$service = strval($service);
$allowedComponents = [];

foreach ($services as $svcInfo) {
$tempRequestors = array_get($svcInfo, 'requestor_mask', ServiceRequestorTypes::API);
if (!($requestor & $tempRequestors)) {
continue;
}

$tempService = strval(array_get($svcInfo, 'service'));
$tempComponent = strval(array_get($svcInfo, 'component'));
$tempVerbs = array_get($svcInfo, 'verb_mask');

// Check if this service entry allows GET operations
if (0 == strcasecmp($service, $tempService) && ($tempVerbs & VerbsMask::GET_MASK)) {
if (empty($tempComponent)) {
$allowedComponents[] = '*';
} elseif ($tempComponent === '*') {
$allowedComponents[] = '*';
} elseif (strpos($tempComponent, '*') !== false) {
$allowedComponents[] = $tempComponent;
} elseif (strpos($tempComponent, '{') !== false) {
$allowedComponents[] = $tempComponent;
} else {
$allowedComponents[] = $tempComponent;
}
}
}

$result = array_unique($allowedComponents);
return $result;
}
}