ConsoleMe - AWS IAM policy linting
This contribution is a new feature.
Introduction
Project
You can find the ConsoleMe project presentation here.
Context
In order to understand the problem that the project addresses, you need to be familiar with what a Policy is.
ConsoleMe has a Policies view interface that allows you to list all resources across your synchronized environment from the AWS configuration such as organization's IAM roles and S3 buckets.
Policy
You can manage access by creating policies and attaching them to IAM identities (users - group of users - roles) or resources.
A policy is an object that defines permissions when associated with an identity or a resource.
Here is an example of a policy that allows the user to perform all actions (dynamodb:*
) on all the tables of the DynamoDB database in the account 123456789012
.
{
"Version": "2012-10-17",
"Statement": {
"Effect": "Allow",
"Action": "dynamodb:*",
"Resource": "arn:aws:dynamodb:us-east-2:123456789012:table/*"
}
}
What is IAM?
Identity and Access Management (IAM) is a service that helps you control access to your AWS resources.
You can use IAM to control WHO can do WHAT on which RESOURCES.
Project architecture
Here is the project Architecture Diagram taken from the ConsoleMe documentation.
Current behavior
ConsoleMe has a native policy editors which allows users to create or edit IAM roles, SQS queues, SNS topics, and S3 buckets.
It would be interesting to show to the user the potential linting errors he may have in his document.
Implement the solution
Here are the user stories we will use as a reference:
As a TYPE_OF_USER, I want SOME_GOAL so that SOME_REASON.
- As a User I want to see the different linting errors on the editor so that I can directly see which lines need updates.
- As a User I want to see the severity of the different linting errors so that I can directly focus on top priority errors.
- As a User I want to see the details of the different linting errors so that I can understand what I need to change.
To check the linting of our document we will use the parliament library which provides a way to reviews policies.
Add the new handler
The CheckPoliciesHandler
handler will be used to retrieve policy errors based on the provided policy string.
We will call the method analyze_policy_string
from the parliament
library which will return a Finding
list.
We will then need to enhance each of the elements to get the complete findings because the non-enhanced Finding representation is a simple string: ISSUE - DETAIL - LOCATION
class CheckPoliciesHandler(BaseAPIV2Handler):
async def post(self):
"""
POST /api/v2/policies/check
"""
policy = tornado.escape.json_decode(self.request.body)
analyzed_policy = analyze_policy_string(policy)
findings = analyzed_policy.findings
enhanced_findings = []
for finding in findings:
enhanced_finding = enhance_finding(finding)
enhanced_findings.append(
{
"issue": enhanced_finding.issue,
"detail": enhanced_finding.detail,
"location": enhanced_finding.location,
"severity": enhanced_finding.severity,
"title": enhanced_finding.title,
"description": enhanced_finding.description,
}
)
self.write(json.dumps(enhanced_findings))
return
The CheckPoliciesHandler
will be mapped to a new route /api/v2/policies/check
.
def make_app(jwt_validator=None):
"""make_app."""
path = pkg_resources.resource_filename("consoleme", "templates")
# ... other routes
(r"/api/v2/policies/check", CheckPoliciesHandler),
Add the parliament mocks
In order to test our handler, we will need to mock the parliament.analyze_policy_string
and parliament.enhance_finding
method calls.
class MockParliament:
def __init__(self, return_value=None):
self.return_value = return_value
@property
def findings(self):
return self.return_value
@pytest.fixture(scope="session")
def parliament(session_mocker):
session_mocker.patch(
"parliament.analyze_policy_string",
return_value=MockParliament(
return_value=[
"""
RESOURCE_MISMATCH
- {"action": "s3:GetObject", "required_format": "arn:*:s3:::*/*"}
- {"line": 3, "column": 18, "filepath": "test.json"}
"""
]
),
)
session_mocker.patch(
"parliament.enhance_finding",
return_value=Finding(
issue="RESOURCE_MISMATCH",
title="No resources match for the given action",
severity="MEDIUM",
description="",
detail="",
location={},
),
)
Add the handler tests
We can now test our handler by simulating a fetch with a given request body.
We can then test that the response of the fetch is the expected one (status code 200
and correct response body).
def test_policies_check_api(self):
from consoleme.config import config
headers = {
config.get("auth.user_header_name"): "user@example.com",
config.get("auth.groups_header_name"): "groupa,groupb,groupc",
}
body = """{
"Version": "2012-10-17",
"Statement": {
"Effect": "Allow",
"Action":["s3:GetObject"],
"Resource": ["arn:aws:s3:::bucket1"]
}
}"""
response = self.fetch(
"/api/v2/policies/check", headers=headers, method="POST", body=body
)
self.assertEqual(response.code, 200)
response_j = json.loads(response.body)
self.assertEqual(len(response_j), 1)
first_error = response_j[0]
self.assertEqual(first_error["issue"], "RESOURCE_MISMATCH")
self.assertEqual(
first_error["title"], "No resources match for the given action"
)
self.assertEqual(first_error["severity"], "MEDIUM")
Add the Swagger entry
Swagger is a a tool that helps you design, build and document REST APIs.We will add a new entry for our route /policies/check
with a documentation about the request body and the response schema.
/policies/check:
post:
summary: check a policy document
tags:
- policies
requestBody:
required: true
content:
application/json:
schema:
type: object
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/PolicyCheckModel"
Integrate the errors to the editor - UI
The idea is pretty straightforward, every time the User will edit the policy, we will trigger a timeout of CHECK_POLICY_TIMEOUT
to check the linting errors of the document.
The lint check is done with a delay to avoid too many calls when the user edits the document.
useEffect(() => {
// Avoid linting errors if disabled
if (!enableLinting) {
return false;
}
timeout.current = setTimeout(() => {
if (policyDocument.length) {
policyCheck(policyDocument);
}
}, CHECK_POLICY_TIMEOUT);
return () => {
clearInterval(timeout.current);
};
}, [policyCheck, policyDocument, enableLinting]);
Once we have retrieved the linting errors from the API, we can display them in the monaco editor.
We will use the deltaDecorations
method on the editor instance.
deltaDecorations(
oldDecorations: string[],
newDecorations: IModelDeltaDecoration[]
): string[]
A decoration accepts some options
and a range
with the values startLine
, endLine
, startColumn
and endColumn
.
interface IModelDeltaDecoration {
options: IModelDecorationOptions;
range: IRange;
}
The parliament library can return 6 different severity type which we will group into 3 severity levels:
INFO_ERRORS
:MUTE | INFO | LOW
WARNING_ERRORS
:MEDIUM
CRITICAL_ERRORS
:HIGH | CRITICAL
These groups allow us to display errors with a different color in the editor.
This lets the user identify the different level of severity at a glance.
The code bellow will allow us to display a highlighted area of the editor where the error is located.
const addEditorDecorations = ({ editor, errors }) => {
editor.deltaDecorations(
[],
errors.map((error) => ({
range: new monaco.Range(
(error.location && error.location.line) || 1,
1,
(error.location && error.location.line) || 1,
// Hardcoded has we don't have the endline number
Number.MAX_VALUE
),
options: {
isWholeLine: false,
className: lintingErrorMapping[error.severity],
marginClassName: "warningIcon",
hoverMessage: {
value: `[${error.severity}] ${error.title} - ${error.detail} - ${error.description}`,
},
},
}))
);
};
The hoverMessage
attribute is used to display the linting error detail to the user on hover.
Final result
Here is the final result that identifies bad IAM policy patterns in the editor.
Every time we update the policy document, a timeout is triggered and when it ends, the linting error fetch is made and the errors are highlighted in the editor.
Takeaway
Problems encountered
Setting up the project was a bit tedious and asked me to set up some things on my AWS account to be able to test behaviors locally.
I also had to modify the versions I had a few times, so it took me a little longer than expected.
What did I learn ?
This contribution allowed me to learn more about IAM Policies and its usage in a concrete use case.
In addition, the architecture of the project was interesting and challenged me to set up my development environment and test the platform.