Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
75.76% |
75 / 99 |
|
54.55% |
12 / 22 |
CRAP | |
0.00% |
0 / 1 |
| MSGraph | |
75.76% |
75 / 99 |
|
54.55% |
12 / 22 |
48.52 | |
0.00% |
0 / 1 |
| __construct | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
3.02 | |||
| getInstance | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| getCachedData | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
3 | |||
| clearCache | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| clearAllCache | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| getAllUsers | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
| getAllUsersByName | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
| getConditionalAccessNamedLocations | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
| getConditionalAccessNamedLocationsByDisplayName | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
| getConditionalAccessPolicies | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
| getConditionalAccessPoliciesByID | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
| loadConditionalAccessNamedLocationsFromApi | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| loadConditionalAccessNamedLocationsByDisplayNameFromApi | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
| getAllUsersByNameFromCache | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
| loadPoliciesFromApi | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| loadPoliciesByIDFromCache | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| loadUsersFromApi | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
4 | |||
| getUsersByDisplayNameFromApi | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| offsetExists | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| offsetGet | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| offsetSet | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| offsetUnset | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace AmgGroup; |
| 4 | |
| 5 | use AmgGroup\ApiClient; |
| 6 | use AmgGroup\Config; |
| 7 | use \ArrayAccess; |
| 8 | use \AmgGroup\Logger; |
| 9 | use GuzzleHttp\Client; |
| 10 | use GuzzleHttp\Exception\GuzzleException; |
| 11 | use GuzzleHttp\Exception\RequestException; |
| 12 | use Psr\Http\Message\ResponseInterface; |
| 13 | |
| 14 | class MSGraph implements \ArrayAccess |
| 15 | { |
| 16 | /** |
| 17 | * Holds the single instance of Smarty. |
| 18 | * |
| 19 | * @var \AmgGroup\MSGraph|null |
| 20 | */ |
| 21 | private static ?MSGraph $instance = null; |
| 22 | |
| 23 | private \AmgGroup\Config $config; |
| 24 | |
| 25 | private \AmgGroup\Logger $logger; |
| 26 | // Session cache configuration |
| 27 | private int $defaultCacheTTL = 300; // 5 minutes default |
| 28 | private array $cacheTTLs = [ |
| 29 | 'users' => 600, // 10 minutes - users don't change often |
| 30 | 'policies' => 120, // 2 minutes - policies might be updated more frequently |
| 31 | 'namedLocations' => 1800, // 30 minutes - named locations rarely change |
| 32 | ]; |
| 33 | private $apiClient; |
| 34 | |
| 35 | |
| 36 | private function __construct() |
| 37 | { |
| 38 | $this->config = Config::getInstance(); |
| 39 | $this->logger = Logger::getInstance(); |
| 40 | |
| 41 | // Ensure session is started for caching |
| 42 | if (session_status() === PHP_SESSION_NONE) { |
| 43 | session_start(); |
| 44 | } |
| 45 | |
| 46 | // Initialize session cache structure if not exists |
| 47 | if (!isset($_SESSION['msgraph_cache'])) { |
| 48 | $_SESSION['msgraph_cache'] = []; |
| 49 | } |
| 50 | |
| 51 | // Load cache TTL settings from config if available |
| 52 | $configTTLs = $this->config->get('cache.msgraph.ttl', []); |
| 53 | $this->cacheTTLs = array_merge($this->cacheTTLs, $configTTLs); |
| 54 | |
| 55 | } |
| 56 | |
| 57 | /** |
| 58 | * Provides access to the single instance of Smarty. |
| 59 | * |
| 60 | * @return \AmgGroup\MSGraph |
| 61 | */ |
| 62 | public static function getInstance(): MSGraph |
| 63 | { |
| 64 | if (self::$instance === null) { |
| 65 | self::$instance = new self(); |
| 66 | } |
| 67 | return self::$instance; |
| 68 | } |
| 69 | |
| 70 | /** |
| 71 | * Generic method to get data from session cache or load fresh |
| 72 | * |
| 73 | * @param string $cacheKey The cache key to use |
| 74 | * @param callable $loadCallback Function to call if cache miss |
| 75 | * @param int|null $ttl Time to live in seconds (null uses default) |
| 76 | * @return mixed Cached or freshly loaded data |
| 77 | */ |
| 78 | private function getCachedData(string $cacheKey, callable $loadCallback, ?int $ttl = null): mixed |
| 79 | { |
| 80 | $ttl = $ttl ?? $this->defaultCacheTTL; |
| 81 | $fullKey = "msgraph_cache.{$cacheKey}"; |
| 82 | |
| 83 | // Check if we have valid cached data |
| 84 | if (isset($_SESSION['msgraph_cache'][$cacheKey])) { |
| 85 | $cached = $_SESSION['msgraph_cache'][$cacheKey]; |
| 86 | $age = time() - $cached['timestamp']; |
| 87 | |
| 88 | if ($age < $ttl) { |
| 89 | $this->logger->debug("Cache hit for {$cacheKey}, age: {$age}s"); |
| 90 | return $cached['data']; |
| 91 | } else { |
| 92 | $this->logger->debug("Cache expired for {$cacheKey}, age: {$age}s, TTL: {$ttl}s"); |
| 93 | } |
| 94 | } else { |
| 95 | $this->logger->debug("Cache miss for {$cacheKey}"); |
| 96 | } |
| 97 | |
| 98 | // Cache miss or expired - load fresh data |
| 99 | $this->logger->info("Loading fresh data for {$cacheKey}"); |
| 100 | $data = $loadCallback(); |
| 101 | |
| 102 | // Store in cache |
| 103 | $_SESSION['msgraph_cache'][$cacheKey] = [ |
| 104 | 'data' => $data, |
| 105 | 'timestamp' => time() |
| 106 | ]; |
| 107 | |
| 108 | return $data; |
| 109 | } |
| 110 | |
| 111 | /** |
| 112 | * Clear specific cache entry |
| 113 | * |
| 114 | * @param string $cacheKey The cache key to clear |
| 115 | */ |
| 116 | private function clearCache(string $cacheKey): void |
| 117 | { |
| 118 | if (isset($_SESSION['msgraph_cache'][$cacheKey])) { |
| 119 | unset($_SESSION['msgraph_cache'][$cacheKey]); |
| 120 | $this->logger->debug("Cleared cache for {$cacheKey}"); |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | /** |
| 125 | * Clear all MSGraph cache entries |
| 126 | */ |
| 127 | public function clearAllCache(): void |
| 128 | { |
| 129 | $_SESSION['msgraph_cache'] = []; |
| 130 | $this->logger->info("Cleared all MSGraph cache"); |
| 131 | } |
| 132 | |
| 133 | /** |
| 134 | * Return a list of all users indexed by there UUID |
| 135 | * |
| 136 | * @param $active |
| 137 | * @return mixed |
| 138 | */ |
| 139 | public function getAllUsers($active = true) |
| 140 | { |
| 141 | $cacheKey = $active ? 'usersUUID_active' : 'usersUUID_all'; |
| 142 | |
| 143 | return $this->getCachedData( |
| 144 | $cacheKey, |
| 145 | fn() => $this->loadUsersFromApi($active), |
| 146 | $this->cacheTTLs['users'] |
| 147 | ); |
| 148 | } |
| 149 | |
| 150 | public function getAllUsersByName($active = true) |
| 151 | { |
| 152 | $cacheKey = $active ? 'usersByName_active' : 'usersByName_all'; |
| 153 | return $this->getCachedData( |
| 154 | $cacheKey, |
| 155 | fn() => $this->getAllUsersByNameFromCache($active), |
| 156 | $this->cacheTTLs['users'] |
| 157 | ); |
| 158 | |
| 159 | } |
| 160 | |
| 161 | public function getConditionalAccessNamedLocations() |
| 162 | { |
| 163 | return $this->getCachedData( |
| 164 | 'namedLocations', |
| 165 | fn() => $this->loadConditionalAccessNamedLocationsFromApi(), |
| 166 | $this->cacheTTLs['namedLocations'] |
| 167 | ); |
| 168 | } |
| 169 | |
| 170 | public function getConditionalAccessNamedLocationsByDisplayName() |
| 171 | { |
| 172 | return $this->getCachedData( |
| 173 | 'namedLocationsByName', |
| 174 | fn() => $this->loadConditionalAccessNamedLocationsByDisplayNameFromApi(), |
| 175 | $this->cacheTTLs['namedLocations'] |
| 176 | ); |
| 177 | } |
| 178 | |
| 179 | |
| 180 | public function getConditionalAccessPolicies() |
| 181 | { |
| 182 | $cacheKey = 'policies'; |
| 183 | |
| 184 | return $this->getCachedData( |
| 185 | $cacheKey, |
| 186 | fn() => $this->loadPoliciesFromApi(), |
| 187 | $this->cacheTTLs['policies'] |
| 188 | ); |
| 189 | } |
| 190 | public function getConditionalAccessPoliciesByID() |
| 191 | { |
| 192 | $cacheKey = 'policies'; |
| 193 | |
| 194 | return $this->getCachedData( |
| 195 | $cacheKey, |
| 196 | fn() => $this->loadPoliciesByIDFromCache(), |
| 197 | $this->cacheTTLs['policies'] |
| 198 | ); |
| 199 | } |
| 200 | |
| 201 | |
| 202 | |
| 203 | |
| 204 | /* |
| 205 | * |
| 206 | * |
| 207 | * |
| 208 | * |
| 209 | * |
| 210 | * API Method Calls below |
| 211 | * |
| 212 | * |
| 213 | * |
| 214 | * |
| 215 | * |
| 216 | * |
| 217 | */ |
| 218 | |
| 219 | private function loadConditionalAccessNamedLocationsFromApi() |
| 220 | { |
| 221 | $namedLocations = $this->apiClient->getAllPages('https://graph.microsoft.com/v1.0/identity/conditionalAccess/namedLocations'); |
| 222 | return $namedLocations; |
| 223 | } |
| 224 | |
| 225 | private function loadConditionalAccessNamedLocationsByDisplayNameFromApi(): array |
| 226 | { |
| 227 | $namedLocations = $this->getConditionalAccessNamedLocations(); |
| 228 | $namedLocationsByName=array_column($namedLocations, null, 'displayName'); |
| 229 | ksort($namedLocationsByName, SORT_STRING | SORT_FLAG_CASE); |
| 230 | return $namedLocationsByName; |
| 231 | } |
| 232 | |
| 233 | private function getAllUsersByNameFromCache($active = true): array |
| 234 | { |
| 235 | $usersByUUID=$this->getAllUsers($active); |
| 236 | $usersByName=array_column($usersByUUID, null, 'userPrincipalName'); |
| 237 | ksort($usersByName, SORT_STRING | SORT_FLAG_CASE); |
| 238 | return $usersByName; |
| 239 | } |
| 240 | |
| 241 | private function loadPoliciesFromApi(): array |
| 242 | { |
| 243 | $policies = $this->apiClient->getAllPages('https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies'); |
| 244 | $policiesByName=array_column($policies, null, 'displayName'); |
| 245 | |
| 246 | return $policies; |
| 247 | } |
| 248 | |
| 249 | private function loadPoliciesByIDFromCache(): array |
| 250 | { |
| 251 | $policies=$this->getConditionalAccessPolicies(); |
| 252 | $policiesByID=array_column($policies, null, 'id'); |
| 253 | return $policiesByID; |
| 254 | } |
| 255 | |
| 256 | |
| 257 | |
| 258 | |
| 259 | |
| 260 | /** |
| 261 | * Load users from Microsoft Graph API |
| 262 | * |
| 263 | * @param bool $active Whether to load only active users |
| 264 | * @return array Array of users indexed by UUID |
| 265 | */ |
| 266 | private function loadUsersFromApi(bool $active = true): array |
| 267 | { |
| 268 | if ($active) { |
| 269 | $params = ['$filter' => 'accountEnabled eq true']; |
| 270 | } else { |
| 271 | $params = []; |
| 272 | } |
| 273 | |
| 274 | $userList = $this->apiClient->getAllPages('https://graph.microsoft.com/v1.0/users', $params); |
| 275 | $userUUID = array_column($userList, null, 'id'); |
| 276 | |
| 277 | // Remove excluded users |
| 278 | foreach ($this->config->get('excludeUsers') as $uuid => $value) { |
| 279 | if (isset($userUUID[$uuid])) { |
| 280 | unset($userUUID[$uuid]); |
| 281 | } |
| 282 | } |
| 283 | |
| 284 | return $userUUID; |
| 285 | } |
| 286 | |
| 287 | public function getUsersByDisplayNameFromApi($active = true) |
| 288 | { |
| 289 | $userList = $this->getAllUsers(); |
| 290 | $userListByName=array_column($userList, null, 'displayName'); |
| 291 | return $userListByName; |
| 292 | } |
| 293 | |
| 294 | |
| 295 | |
| 296 | public function offsetExists(mixed $offset): bool |
| 297 | { |
| 298 | // TODO: Implement offsetExists() method. |
| 299 | } |
| 300 | |
| 301 | public function offsetGet(mixed $offset): mixed |
| 302 | { |
| 303 | // TODO: Implement offsetGet() method. |
| 304 | } |
| 305 | |
| 306 | public function offsetSet(mixed $offset, mixed $value): void |
| 307 | { |
| 308 | // TODO: Implement offsetSet() method. |
| 309 | } |
| 310 | |
| 311 | public function offsetUnset(mixed $offset): void |
| 312 | { |
| 313 | // TODO: Implement offsetUnset() method. |
| 314 | } |
| 315 | } |