Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
75.76% covered (warning)
75.76%
75 / 99
54.55% covered (warning)
54.55%
12 / 22
CRAP
0.00% covered (danger)
0.00%
0 / 1
MSGraph
75.76% covered (warning)
75.76%
75 / 99
54.55% covered (warning)
54.55%
12 / 22
48.52
0.00% covered (danger)
0.00%
0 / 1
 __construct
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 getInstance
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getCachedData
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
3
 clearCache
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 clearAllCache
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getAllUsers
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getAllUsersByName
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getConditionalAccessNamedLocations
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getConditionalAccessNamedLocationsByDisplayName
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getConditionalAccessPolicies
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getConditionalAccessPoliciesByID
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 loadConditionalAccessNamedLocationsFromApi
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 loadConditionalAccessNamedLocationsByDisplayNameFromApi
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getAllUsersByNameFromCache
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 loadPoliciesFromApi
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 loadPoliciesByIDFromCache
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 loadUsersFromApi
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 getUsersByDisplayNameFromApi
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 offsetExists
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 offsetGet
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 offsetSet
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 offsetUnset
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace AmgGroup;
4
5use AmgGroup\ApiClient;
6use AmgGroup\Config;
7use \ArrayAccess;
8use \AmgGroup\Logger;
9use GuzzleHttp\Client;
10use GuzzleHttp\Exception\GuzzleException;
11use GuzzleHttp\Exception\RequestException;
12use Psr\Http\Message\ResponseInterface;
13
14class 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}