Skip to content

Commit

Permalink
Merge pull request #82 from almenscorner/dev
Browse files Browse the repository at this point in the history
v1.3.1
  • Loading branch information
almenscorner authored Jan 16, 2023
2 parents 03aef3b + 0d63359 commit 69a66bb
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 35 deletions.
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ The package can also be run standalone outside a pipeline, or in one to only bac
# Exciting news 📣
The front end for IntuneCD has now been released. Check it out [here](https://github.com/almenscorner/intunecd-monitor)

## Whats new in 1.3.1
- Bug fix for Filters not being able to created if they do not exist
- Bug fix for Conditional Access policies not being able to be created if `authenticationStrength` is configured
- Added Graph throttling handling to `makeapirequestPost` to handle creation of large amounts of CA policies

## What's new in 1.3.0
- New summary of changes, instead of just a count, a summary of the changes with old and new values are sent to the front end
- Report mode, if you want to send a summary of changes to the front end without actually updating the configuration in Intune, you can activate report mode when running the update using `-r` -> `IntuneCD-startupdate -r`
Expand All @@ -36,12 +41,6 @@ The front end for IntuneCD has now been released. Check it out [here](https://gi
- **After**

![Screenshot 2022-12-08 at 10 26 18](https://user-images.githubusercontent.com/78877636/206411063-66838684-3511-44dc-8eaf-43aaa0b3cb94.png)

## What's new in 1.2.5
- Additional improvements to documentation
- **NOTE:** The split option `-s` has been changed so you only need to include the parameter without anything else. If you are using this option you will have to change it from `-s Y` to `-s`
- All items in lists are now included, before some items were left out
- Bug fix to Conditional Access where the backup failed if no policys exists

## I use Powershell, Do I need to learn Python?
No.
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = IntuneCD
version = 1.3.0
version = 1.3.1
author = Tobias Almén
author_email = [email protected]
description = Tool to backup and update configurations in Intune
Expand Down
87 changes: 69 additions & 18 deletions src/IntuneCD/graph_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,46 @@ def makeapirequest(endpoint, token, q_param=None):
:return: The response from the request.
"""

headers = {"Content-Type": "application/json", "Authorization": "Bearer {0}".format(token["accessToken"])}
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer {0}".format(token["accessToken"]),
}

if q_param is not None:
response = requests.get(endpoint, headers=headers, params=q_param)
if response.status_code == 504 or response.status_code == 502 or response.status_code == 503:
print("Ran into issues with Graph request, waiting 10 seconds and trying again...")
if (
response.status_code == 504
or response.status_code == 502
or response.status_code == 503
):
print(
"Ran into issues with Graph request, waiting 10 seconds and trying again..."
)
time.sleep(10)
response = requests.get(endpoint, headers=headers)
elif response.status_code == 429:
print(f"Hit Graph throttling, trying again after {response.headers['Retry-After']} seconds")
print(
f"Hit Graph throttling, trying again after {response.headers['Retry-After']} seconds"
)
while response.status_code == 429:
time.sleep(int(response.headers["Retry-After"]))
response = requests.get(endpoint, headers=headers)
else:
response = requests.get(endpoint, headers=headers)
if response.status_code == 504 or response.status_code == 502 or response.status_code == 503:
print("Ran into issues with Graph request, waiting 10 seconds and trying again...")
if (
response.status_code == 504
or response.status_code == 502
or response.status_code == 503
):
print(
"Ran into issues with Graph request, waiting 10 seconds and trying again..."
)
time.sleep(10)
response = requests.get(endpoint, headers=headers)
elif response.status_code == 429:
print(f"Hit Graph throttling, trying again after {response.headers['Retry-After']} seconds")
print(
f"Hit Graph throttling, trying again after {response.headers['Retry-After']} seconds"
)
while response.status_code == 429:
time.sleep(int(response.headers["Retry-After"]))
response = requests.get(endpoint, headers=headers)
Expand All @@ -62,10 +81,14 @@ def makeapirequest(endpoint, token, q_param=None):
elif ("assignmentFilters" in endpoint) and ("FeatureNotEnabled" in response.text):
print("Assignment filters not enabled in tenant, skipping")
else:
raise Exception("Request failed with ", response.status_code, " - ", response.text)
raise Exception(
"Request failed with ", response.status_code, " - ", response.text
)


def makeapirequestPatch(patchEndpoint, token, q_param=None, jdata=None, status_code=200):
def makeapirequestPatch(
patchEndpoint, token, q_param=None, jdata=None, status_code=200
):
"""
This function makes a PATCH request to the Microsoft Graph API.
Expand All @@ -76,16 +99,23 @@ def makeapirequestPatch(patchEndpoint, token, q_param=None, jdata=None, status_c
:param status_code: The status code to expect from the request.
"""

headers = {"Content-Type": "application/json", "Authorization": "Bearer {0}".format(token["accessToken"])}
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer {0}".format(token["accessToken"]),
}

if q_param is not None:
response = requests.patch(patchEndpoint, headers=headers, params=q_param, data=jdata)
response = requests.patch(
patchEndpoint, headers=headers, params=q_param, data=jdata
)
else:
response = requests.patch(patchEndpoint, headers=headers, data=jdata)
if response.status_code == status_code:
pass
else:
raise Exception("Request failed with ", response.status_code, " - ", response.text)
raise Exception(
"Request failed with ", response.status_code, " - ", response.text
)


def makeapirequestPost(patchEndpoint, token, q_param=None, jdata=None, status_code=200):
Expand All @@ -99,10 +129,15 @@ def makeapirequestPost(patchEndpoint, token, q_param=None, jdata=None, status_co
:param status_code: The status code to expect from the request.
"""

headers = {"Content-Type": "application/json", "Authorization": "Bearer {0}".format(token["accessToken"])}
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer {0}".format(token["accessToken"]),
}

if q_param is not None:
response = requests.post(patchEndpoint, headers=headers, params=q_param, data=jdata)
response = requests.post(
patchEndpoint, headers=headers, params=q_param, data=jdata
)
else:
response = requests.post(patchEndpoint, headers=headers, data=jdata)
if response.status_code == status_code:
Expand All @@ -111,8 +146,17 @@ def makeapirequestPost(patchEndpoint, token, q_param=None, jdata=None, status_co
return json_data
else:
pass
elif response.status_code == 429:
print(
f"Hit Graph throttling, trying again after {response.headers['Retry-After']} seconds"
)
while response.status_code == 429:
time.sleep(int(response.headers["Retry-After"]))
response = requests.post(patchEndpoint, headers=headers, data=jdata)
else:
raise Exception("Request failed with ", response.status_code, " - ", response.text)
raise Exception(
"Request failed with ", response.status_code, " - ", response.text
)


def makeapirequestPut(patchEndpoint, token, q_param=None, jdata=None, status_code=200):
Expand All @@ -126,13 +170,20 @@ def makeapirequestPut(patchEndpoint, token, q_param=None, jdata=None, status_cod
:param status_code: The status code to expect from the request.
"""

headers = {"Content-Type": "application/json", "Authorization": "Bearer {0}".format(token["accessToken"])}
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer {0}".format(token["accessToken"]),
}

if q_param is not None:
response = requests.put(patchEndpoint, headers=headers, params=q_param, data=jdata)
response = requests.put(
patchEndpoint, headers=headers, params=q_param, data=jdata
)
else:
response = requests.put(patchEndpoint, headers=headers, data=jdata)
if response.status_code == status_code:
pass
else:
raise Exception("Request failed with ", response.status_code, " - ", response.text)
raise Exception(
"Request failed with ", response.status_code, " - ", response.text
)
1 change: 1 addition & 0 deletions src/IntuneCD/update_assignmentFilter.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ def update(path, token, report):
+ repo_data["displayName"]
)
if report is False:
repo_data.pop("payloads", None)
request_json = json.dumps(repo_data)
post_request = makeapirequestPost(
ENDPOINT,
Expand Down
44 changes: 40 additions & 4 deletions src/IntuneCD/update_conditionalAccess.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,23 @@ def update(path, token, report):
)

if report is False:
# If authenticationStrength is set, set operator to AND and remove unnecessary keys
if repo_data.get("grantControls"):
if repo_data["grantControls"].get(
"authenticationStrength"
):
id = (
repo_data["grantControls"]
.get("authenticationStrength", {})
.get("id")
)
repo_data["grantControls"][
"authenticationStrength"
] = ({"id": id} if id else None)
repo_data["grantControls"]["operator"] = (
"AND" if id else None
)

request_data = json.dumps(repo_data)
q_param = None
makeapirequestPatch(
Expand All @@ -100,6 +117,23 @@ def update(path, token, report):
+ repo_data["displayName"]
)
if report is False:
# Users is a required key, set to None as updating assignment is currently not supported
repo_data["conditions"]["users"] = {"includeUsers": ["None"]}
# If authenticationStrength is set, set operator to AND and remove unnecessary keys
if repo_data.get("grantControls"):
if repo_data["grantControls"].get("authenticationStrength"):
id = (
repo_data["grantControls"]
.get("authenticationStrength", {})
.get("id")
)
repo_data["grantControls"]["authenticationStrength"] = (
{"id": id} if id else None
)
repo_data["grantControls"]["operator"] = (
"AND" if id else None
)

request_json = json.dumps(repo_data)
post_request = makeapirequestPost(
ENDPOINT,
Expand All @@ -108,9 +142,11 @@ def update(path, token, report):
jdata=request_json,
status_code=201,
)
print(
"Conditional Access policy created with id: "
+ post_request["id"]
)

if post_request:
print(
"Conditional Access policy created with id: "
+ post_request["id"]
)

return diff_count
20 changes: 17 additions & 3 deletions tests/Update/test_update_conditionalAccess.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,14 @@ def setUp(self):
"excludedApplications": ["d4ebce55-015a-49b5-a083-c84d1797ae8c"],
},
},
"grantControls": {},
"grantControls": {
"operator": "OR",
"authenticationStrength": {
"id": "test",
"displayName": "test",
"description": "test",
},
},
}
]
}
Expand All @@ -44,7 +51,14 @@ def setUp(self):
"excludedApplications": ["d4ebce55-015a-49b5-a083-c84d1797ae8c", "2"],
},
},
"grantControls": {},
"grantControls": {
"operator": "OR",
"authenticationStrength": {
"id": "test",
"displayName": "test",
"description": "test",
},
},
}

self.makeapirequest_patch = patch("src.IntuneCD.update_conditionalAccess.makeapirequest")
Expand Down Expand Up @@ -113,7 +127,7 @@ def test_update_config_grantControls(self):
"""The count should be 0 and makeapirequestPost should not be called."""

self.repo_data["conditions"]["applications"]["excludedApplications"] = ["d4ebce55-015a-49b5-a083-c84d1797ae8c"]
self.mem_data["value"][0]["grantControls"] = {"[email protected]": "test"}

self.makeapirequest.return_value = self.mem_data

self.count = update(self.directory.path, self.token, report=False)
Expand Down
26 changes: 23 additions & 3 deletions tests/test_graph_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,30 +198,50 @@ def test_makeapirequestPatch_not_matching_status_code(self, mock_patch, mock_mak

@patch("src.IntuneCD.graph_request.makeapirequestPost")
@patch("requests.post")
@patch("time.sleep", return_value=None)
class TestGraphRequestPost(unittest.TestCase):
def setUp(self):
self.token = {"accessToken": "token"}
self.jdata = {"id": "0"}
self.content = '{"id": "0"}'
self.expected_result = {"id": "0"}

def test_makeapirequestPost_no_q_param(self, mock_patch, mock_makeapirequestPost):

def test_makeapirequestPost_status_429_no_q_param(self, mock_sleep, mock_patch, mock_makeapirequestPost):
"""The request should be made twice."""
self.mock_resp = _mock_response(self, status=429, content="Too Many equests", headers={"Retry-After": "10"})
self.mock_resp2 = _mock_response(self, status=200, content="Success")
mock_patch.side_effect = self.mock_resp, self.mock_resp2
makeapirequestPost("https://endpoint", self.token)

self.assertEqual(2, mock_patch.call_count)

def test_makeapirequestPost_status_429_with_q_param(self, mock_sleep, mock_patch, mock_makeapirequestPost):
"""The request should be made twice."""
self.mock_resp = _mock_response(self, status=429, content="Too Many Requests", headers={"Retry-After": "10"})
self.mock_resp2 = _mock_response(self, status=200, content="Success")
mock_patch.side_effect = self.mock_resp, self.mock_resp2
makeapirequestPost("https://endpoint", self.token, q_param="$filter=id eq '0'")

self.assertEqual(2, mock_patch.call_count)

def test_makeapirequestPost_no_q_param(self, mock_sleep, mock_patch, mock_makeapirequestPost):
"""The request should be made and the response should be returned."""
self.mock_resp = _mock_response(self, status=200, content=self.content)
mock_patch.return_value = self.mock_resp
self.result = makeapirequestPost("https://endpoint", self.token, q_param=None, jdata=self.jdata)

self.assertEqual(self.result, self.expected_result)

def test_makeapirequestPost_with_q_param(self, mock_patch, mock_makeapirequestPost):
def test_makeapirequestPost_with_q_param(self, mock_sleep, mock_patch, mock_makeapirequestPost):
"""The request should be made and the response should be returned."""
self.mock_resp = _mock_response(self, status=200, content=self.content)
mock_patch.return_value = self.mock_resp
self.result = makeapirequestPost("https://endpoint", self.token, q_param="$filter=id eq '0'", jdata=self.jdata)

self.assertEqual(self.result, self.expected_result)

def test_makeapirequestPost_not_matching_status_code(self, mock_patch, mock_makeapirequestPost):
def test_makeapirequestPost_not_matching_status_code(self, mock_sleep, mock_patch, mock_makeapirequestPost):
"""The request should be made and the response should be returned."""
with self.assertRaises(Exception):
self.mock_resp = _mock_response(self, status=204, content="")
Expand Down

0 comments on commit 69a66bb

Please sign in to comment.