diff --git a/README.md b/README.md index 63bdb22ac..62961d26f 100644 --- a/README.md +++ b/README.md @@ -212,6 +212,14 @@ It is possible to use Docker `docker-compose-test.yaml` to run ConsoleMe and its in Docker with the default plugin set. Configure a new Docker Python interpreter to run __main__.py with your working directory set to `/apps/consoleme` (on the container). This flow was tested on Windows 10. +### Generating Models from Swagger Spec + +When changes are made to the Swagger spec, models may need to be regenerated using [datamodel-code-generator](https://github.com/koxudaxi/datamodel-code-generator). + +```bash +pip install datamodel-code-generator +datamodel-codegen --input swagger.yaml --output consoleme/models/models.py +``` ## Generate an AMI to deploy ConsoleMe to EC2 diff --git a/consoleme/models/__init__.py b/consoleme/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/consoleme/models/base_models.py b/consoleme/models/base_models.py new file mode 100644 index 000000000..0ea79bdba --- /dev/null +++ b/consoleme/models/base_models.py @@ -0,0 +1,127 @@ +# generated by datamodel-codegen: +# filename: swagger.yaml +# timestamp: 2020-05-14T17:32:50+00:00 + +from __future__ import annotations + +from datetime import datetime +from enum import Enum +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, Field, constr + + +class ResourceModel(BaseModel): + arn: str = Field(..., description='resource ARN') + account_id: str = Field(..., description='AWS account ID') + account_name: str = Field(..., description='human-friendly AWS account name') + policy_sha256: str = Field( + ..., description='hash of the most recent resource policy seen by ConsoleMe' + ) + policy: str + owner: str = Field( + ..., description='email address of team or individual who owns this resource' + ) + approvers: List[str] + resource_type: str + last_updated: datetime = Field( + ..., description='last time resource was updated from source-of-truth' + ) + + +class RequestModel(BaseModel): + id: str + arn: str = Field(..., description='ARN of principal being modified') + timestamp: datetime + justification: str + requester_email: str + approvers: List[str] = Field( + ..., + description='list of approvers, derived from approvers of `resource`s in `changes`', + ) + status: str + + +class ChangeType(Enum): + inline_policy = 'inline_policy' + managed_policy = 'managed_policy' + resource_policy = 'resource_policy' + + +class ChangeModel(BaseModel): + change_type: ChangeType + resource: ResourceModel + + +class Action(Enum): + attach = 'attach' + detach = 'detach' + + +class ManagedPolicyChangeModel(ChangeModel): + arn: str + policy_name: str + action: Action + + +class PolicyModel(BaseModel): + policy_document: str = Field(..., description='JSON policy document') + policy_sha256: str = Field(..., description='hash of policy_document') + + +class RoleModel(BaseModel): + name: str + account_id: Optional[constr(min_length=12, max_length=12)] = None + account_name: Optional[str] = None + arn: Optional[str] = None + + +class ExtendedRoleModel(RoleModel): + inline_policies: List[Dict[str, Any]] + assume_role_policies: List[Dict[str, Any]] + managed_policies: List[Dict[str, Any]] + tags: List[Dict[str, Any]] + templated: Optional[bool] = None + template_link: Optional[str] = None + + +class UserModel(BaseModel): + email: Optional[str] = None + extended_info: Optional[Dict[str, Any]] = None + + +class InlinePolicyChangeModel(ChangeModel): + arn: str + additional_arns: Optional[str] = Field( + None, + description='additional ARNs relevant to request, e.g. S3 bucket ARN with path prefix', + ) + policy_name: str + new: bool + policy: PolicyModel + old_policy: Optional[PolicyModel] = None + + +class ResourcePolicyChangeModel(ChangeModel): + arn: str + policy: PolicyModel + old_policy: Optional[PolicyModel] = None + + +class CommentModel(BaseModel): + id: str + timestamp: datetime + edited: Optional[bool] = None + last_modified: Optional[datetime] = None + user_email: str + user: Optional[UserModel] = None + text: str + + +class ExtendedRequestModel(RequestModel): + changes: List[ + InlinePolicyChangeModel, ManagedPolicyChangeModel, ResourcePolicyChangeModel + ] + requester_info: UserModel + reviewer: Optional[str] = None + comments: Optional[CommentModel] = None diff --git a/consoleme/models/change.py b/consoleme/models/change.py new file mode 100644 index 000000000..b9cae5952 --- /dev/null +++ b/consoleme/models/change.py @@ -0,0 +1,28 @@ +from consoleme.models.base_models import ChangeModel +from consoleme.models.base_models import InlinePolicyChangeModel +from consoleme.models.base_models import ManagedPolicyChangeModel +from consoleme.models.base_models import ResourcePolicyChangeModel + + +class Change(ChangeModel): + def __init__(self, *args, **kwargs): + super().__init__(self) + + def store(self): + """save to dynamodb""" + pass + + +class InlinePolicyChange(InlinePolicyChangeModel): + def __init__(self, *args, **kwargs): + super().__init__(self) + + +class ManagedPolicyChange(ManagedPolicyChangeModel): + def __init__(self, *args, **kwargs): + super().__init__(self) + + +class ResourcePolicyChange(ResourcePolicyChangeModel): + def __init__(self, *args, **kwargs): + super().__init__(self) diff --git a/consoleme/models/policy.py b/consoleme/models/policy.py new file mode 100644 index 000000000..ba8f90cc1 --- /dev/null +++ b/consoleme/models/policy.py @@ -0,0 +1,6 @@ +from consoleme.models.base_models import PolicyModel + + +class Policy(PolicyModel): + def __init__(self, *args, **kwargs): + super().__init__(self) diff --git a/consoleme/models/request.py b/consoleme/models/request.py new file mode 100644 index 000000000..e2bf9d9df --- /dev/null +++ b/consoleme/models/request.py @@ -0,0 +1,18 @@ +from consoleme.models.base_models import CommentModel +from consoleme.models.base_models import ExtendedRequestModel +from consoleme.models.base_models import RequestModel + + +# Putting Comment here since it is only used in the context of Requests +class Comment(CommentModel): + def __init__(self, *args, **kwargs): + super().__init__(self) + + +class Request(ExtendedRequestModel): + def __init__(self, *args, **kwargs): + super().__init__(self) + + def store(self, dynamodb_client): + """save to dynamodb""" + pass diff --git a/consoleme/models/resource.py b/consoleme/models/resource.py new file mode 100644 index 000000000..e4be6af00 --- /dev/null +++ b/consoleme/models/resource.py @@ -0,0 +1,6 @@ +from consoleme.models.models import ResourceModel + + +class Resource(ResourceModel): + def __init__(self, *args, **kwargs): + super().__init__(self) diff --git a/consoleme/models/role.py b/consoleme/models/role.py new file mode 100644 index 000000000..71954fbf5 --- /dev/null +++ b/consoleme/models/role.py @@ -0,0 +1,7 @@ +from consoleme.models.base_models import ExtendedRoleModel +from consoleme.models.base_models import RoleModel + + +class Role(ExtendedRoleModel): + def __init__(self, *args, **kwargs): + super().__init__(self) diff --git a/consoleme/models/user.py b/consoleme/models/user.py new file mode 100644 index 000000000..55dfffff3 --- /dev/null +++ b/consoleme/models/user.py @@ -0,0 +1,6 @@ +from consoleme.models.models import UserModel + + +class User(UserModel): + def __init__(self, *args, **kwargs): + super().__init__(self) diff --git a/swagger.yaml b/swagger.yaml new file mode 100644 index 000000000..98b76b76f --- /dev/null +++ b/swagger.yaml @@ -0,0 +1,503 @@ +openapi: 3.0.0 +info: + description: ConsoleMe API definition + version: '2.0.0' + title: ConsoleMe v2 API + contact: + email: consoleme-owners@netflix.com + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.txt +tags: + - name: roles + description: IAM Role endpoints + - name: requests + description: Policy Request endpoints + - name: privileged + description: Endpoints requiring elevated permissions +paths: + /requests: + get: + summary: get a list of requests + tags: + - requests + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RequestModel' + post: + summary: submit a new request + tags: + - requests + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ExtendedRequestModel' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/ExtendedRequestModel' + /requests/{request_id}: + get: + summary: get details about a request + tags: + - requests + parameters: + - $ref: '#/components/parameters/RequestId' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ExtendedRequestModel' + put: + summary: update a request + tags: + - requests + parameters: + - $ref: '#/components/parameters/RequestId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ExtendedRequestModel' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ExtendedRequestModel' + /resources: + get: + summary: get resources + parameters: + - $ref: '#/components/parameters/AccountIdQueryString' + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ResourceModel' + /roles: + get: + summary: get a list of roles + tags: + - roles + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RoleModel' + /roles/{account_id}: + get: + summary: get a list of roles in an account + tags: + - roles + parameters: + - $ref: '#/components/parameters/AccountId' + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RoleModel' + post: + summary: create a role + tags: + - roles + - privileged + parameters: + - $ref: '#/components/parameters/AccountId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ExtendedRoleModel' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/ExtendedRoleModel' + /roles/{account_id}/{role_name}: + get: + summary: get details about a role + tags: + - roles + parameters: + - $ref: '#/components/parameters/AccountId' + - $ref: '#/components/parameters/RoleName' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ExtendedRoleModel' + put: + summary: update a role + tags: + - roles + - privileged + parameters: + - $ref: '#/components/parameters/AccountId' + - $ref: '#/components/parameters/RoleName' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ExtendedRoleModel' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ExtendedRoleModel' + delete: + summary: delete a role + tags: + - roles + - privileged + parameters: + - $ref: '#/components/parameters/AccountId' + - $ref: '#/components/parameters/RoleName' + responses: + '204': + description: No Content + /user: + get: + summary: get details about current user + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/UserModel' +components: + parameters: + AccountId: + name: account_id + in: path + required: true + example: 012345678901 + schema: + type: string + pattern: '^\d{12}$' + RoleName: + name: role_name + in: path + required: true + example: fake_account_admin + schema: + type: string + RequestId: + name: request_id + in: path + required: true + example: 16fd2706-8baf-433b-82eb-8c7fada847da + schema: + type: string + AccountIdQueryString: + name: account_id + in: query + example: 012345678901 + schema: + type: string + pattern: '^\d{12}$' + schemas: + ResourceModel: + type: object + required: + - arn + - account_id + - account_name + - policy_sha256 + - policy + - owner + - approvers + - resource_type + - last_updated + properties: + arn: + description: resource ARN + type: string + readOnly: true + account_id: + description: AWS account ID + type: string + readOnly: true + account_name: + description: human-friendly AWS account name + type: string + readOnly: true + policy_sha256: + description: hash of the most recent resource policy seen by ConsoleMe + type: string + readOnly: true + policy: + type: string + owner: + description: email address of team or individual who owns this resource + type: string + approvers: + type: array + items: + type: string + resource_type: + type: string + last_updated: + description: last time resource was updated from source-of-truth + type: string + format: date-time + RequestModel: + type: object + required: + - id + - arn + - timestamp + - justification + - requester_email + - approvers + - status + properties: + id: + type: string + readOnly: true + arn: + type: string + description: ARN of principal being modified + example: arn:aws:iam::123456789012:role/super_awesome_admin + timestamp: + type: string + format: date-time + justification: + type: string + requester_email: + type: string + approvers: + description: list of approvers, derived from approvers of `resource`s in `changes` + type: array + items: + type: string + readOnly: true + status: + type: string + ExtendedRequestModel: + allOf: + - $ref: '#/components/schemas/RequestModel' + - type: object + required: + - changes + - requester_info + properties: + changes: + type: array + items: + anyOf: + - $ref: '#/components/schemas/InlinePolicyChangeModel' + - $ref: '#/components/schemas/ManagedPolicyChangeModel' + - $ref: '#/components/schemas/ResourcePolicyChangeModel' + discriminator: + propertyName: change_type + requester_info: + $ref: '#/components/schemas/UserModel' + reviewer: + type: string + comments: + $ref: '#/components/schemas/CommentModel' + ChangeModel: + type: object + required: + - change_type + - resource + properties: + change_type: + type: string + enum: + - inline_policy + - managed_policy + - resource_policy + resource: + $ref: '#/components/schemas/ResourceModel' + InlinePolicyChangeModel: + allOf: + - $ref: '#/components/schemas/ChangeModel' + - type: object + required: + - arn + - policy_name + - new + - policy + properties: + arn: + type: string + additional_arns: + description: additional ARNs relevant to request, e.g. S3 bucket ARN with path prefix + type: string + policy_name: + type: string + new: + type: boolean + policy: + $ref: '#/components/schemas/PolicyModel' + old_policy: + $ref: '#/components/schemas/PolicyModel' + ResourcePolicyChangeModel: + allOf: + - $ref: '#/components/schemas/ChangeModel' + - type: object + required: + - arn + - policy + properties: + arn: + type: string + policy: + $ref: '#/components/schemas/PolicyModel' + old_policy: + $ref: '#/components/schemas/PolicyModel' + ManagedPolicyChangeModel: + allOf: + - $ref: '#/components/schemas/ChangeModel' + - type: object + required: + - arn + - policy_name + - action + properties: + arn: + type: string + policy_name: + type: string + action: + type: string + enum: + - attach + - detach + PolicyModel: + type: object + required: + - policy_document + - policy_sha256 + properties: + policy_document: + type: string + description: JSON policy document + policy_sha256: + type: string + description: hash of policy_document + CommentModel: + type: object + required: + - id + - timestamp + - user_email + - text + properties: + id: + type: string + readOnly: true + timestamp: + type: string + format: date-time + edited: + type: boolean + last_modified: + type: string + format: date-time + user_email: + type: string + user: + $ref: '#/components/schemas/UserModel' + text: + type: string + RoleModel: + type: object + required: + - name + properties: + name: + type: string + example: super_awesome_admin + account_id: + type: string + minLength: 12 + maxLength: 12 + example: '123456789012' + readOnly: true + account_name: + type: string + example: super_awesome + readOnly: true + arn: + type: string + example: arn:aws:iam::123456789012:role/super_awesome_admin + readOnly: true + ExtendedRoleModel: + allOf: + - $ref: '#/components/schemas/RoleModel' + - type: object + required: + - inline_policies + - assume_role_policies + - managed_policies + - tags + properties: + inline_policies: + type: array + items: + type: object # TODO: make inline_policies schema + assume_role_policies: + type: array + items: + type: object # TODO: make assume_role_policies schema + managed_policies: + type: array + items: + type: object # TODO: make managed_policies schema + tags: + type: array + items: + type: object # TODO: make tags schema + templated: + type: boolean + readOnly: true + template_link: + type: string + readOnly: true + UserModel: + type: object + readOnly: true + properties: + email: + type: string + extended_info: + type: object