Skip to content

Commit

Permalink
edit wizard, skip aws as default for other plugins
Browse files Browse the repository at this point in the history
- move aws config init to aws plugin configuration
- create/update secrets locally without aws
- new method set_required_secure_text_values()
- upsert_secret method allow refactor operations
- exclude iambic_managed when it is undefined for okta, azure and google ws
- refactor:
    configuration_wizard_google_workspace_add,
    configuration_wizard_azure_ad_organization_add,
    configuration_wizard_okta_organization_add
    run()
- fix: not update okta user status when it is deleted (or it is going to be deleted)
- add coverage report to pyproject
  • Loading branch information
JonathanLoscalzo committed Apr 28, 2023
1 parent 9494c3e commit 2815335
Show file tree
Hide file tree
Showing 10 changed files with 461 additions and 304 deletions.
532 changes: 294 additions & 238 deletions iambic/config/wizard.py

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ Resources:
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- sqs:DeleteMessage
- sqs:ReceiveMessage
- sqs:GetQueueAttributes
Resource:
- 'arn:aws:sqs:us-east-1:*:IAMbicChangeDetectionQueue'
- Effect: Allow
Action:
- ec2:Describe*
Expand Down
14 changes: 9 additions & 5 deletions iambic/plugins/v0_1_0/azure_ad/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
from iambic.core.logger import log
from iambic.core.models import BaseTemplate, TemplateChangeDetails

if TYPE_CHECKING:
from iambic.plugins.v0_1_0.azure_ad.iambic_plugin import AzureADConfig
if TYPE_CHECKING: # pragma: no cover
from iambic.plugins.v0_1_0.azure_ad.iambic_plugin import (
AzureADConfig,
)

MappingIntStrAny = typing.Mapping[int | str, any]
MappingIntStrAny = typing.Mapping[int | str, Any]
AbstractSetIntStr = typing.AbstractSet[int | str]


Expand All @@ -39,7 +41,7 @@ class AzureADOrganization(BaseModel):
class Config:
arbitrary_types_allowed = True

async def set_azure_access_token(self):
async def set_azure_access_token(self): # pragma: no cover
if not self.access_token:
# initialize the client here
self.client = msal.ConfidentialClientApplication(
Expand All @@ -58,7 +60,7 @@ async def set_azure_access_token(self):

async def _make_request(
self, request_type: str, endpoint: str, **kwargs
) -> Union[dict, list, None]:
) -> Union[dict, list, None]: # pragma: no cover
await self.set_azure_access_token()

response = []
Expand Down Expand Up @@ -140,6 +142,8 @@ def dict(
exclude = required_exclude
elif isinstance(exclude, set):
exclude.update(required_exclude)
# elif isinstance(exclude, dict):
# exclude.update({i: ... for i in required_exclude})

return super().dict(
include=include,
Expand Down
7 changes: 6 additions & 1 deletion iambic/plugins/v0_1_0/google_workspace/iambic_plugin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from __future__ import annotations

import os
from typing import Optional
import typing
from typing import TYPE_CHECKING, Any, Optional, Union

import googleapiclient.discovery
from google.oauth2 import service_account
Expand All @@ -20,6 +21,10 @@
load,
)

if TYPE_CHECKING: # pragma: no cover
MappingIntStrAny = typing.Mapping[int | str, Any]
AbstractSetIntStr = typing.AbstractSet[int | str]


class GoogleSubject(BaseModel):
domain: str
Expand Down
4 changes: 2 additions & 2 deletions iambic/plugins/v0_1_0/okta/iambic_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from typing import Any, Optional

from okta.client import Client as OktaClient
from pydantic import BaseModel, Extra, Field, SecretStr, validator

from iambic.core.iambic_enum import IambicManaged
Expand All @@ -11,7 +12,6 @@
from iambic.plugins.v0_1_0.okta.group.models import OktaGroupTemplate
from iambic.plugins.v0_1_0.okta.handlers import import_okta_resources, load
from iambic.plugins.v0_1_0.okta.user.models import OktaUserTemplate
from okta.client import Client as OktaClient


class OktaOrganization(BaseModel):
Expand All @@ -21,7 +21,7 @@ class OktaOrganization(BaseModel):
request_timeout: int = 60
client: Any = None # OktaClient
iambic_managed: Optional[IambicManaged] = Field(
IambicManaged.IMPORT_ONLY,
IambicManaged.UNDEFINED,
description="Controls the directionality of iambic changes",
)

Expand Down
8 changes: 6 additions & 2 deletions iambic/plugins/v0_1_0/okta/user/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,14 +240,18 @@ async def _apply_to_account(
)
return change_details

tasks.extend(
[
if current_user and not self.deleted:
tasks.append(
update_user_status(
current_user,
self.properties.status.value,
okta_organization,
log_params,
),
)

tasks.extend(
[
update_user_profile(
self,
current_user,
Expand Down
3 changes: 3 additions & 0 deletions iambic/plugins/v0_1_0/okta/user/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,9 @@ async def update_user_status(
current_status: str = user.status.value
if current_status == new_status:
return response
if user.deleted:
return response

response.append(
ProposedChange(
change_type=ProposedChangeType.UPDATE,
Expand Down
125 changes: 69 additions & 56 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,78 +1,91 @@
[build-system]
requires = [ "poetry-core",]
build-backend = "poetry.core.masonry.api"
requires = ["poetry-core"]

[tool.poetry]
authors = ["Noq Software <hello@noq.dev>"]
description = "The python package used to generate, parse, and execute noqform yaml templates."
exclude = ["build_util/*"]
include = ["iambic/output/templates/*"]
license = "Apache-2.0"
name = "iambic-core"
packages = [
{ include="iambic", from="." },
{include = "iambic", from = "."},
]
version = "0.3.10"
license = "Apache-2.0"
description = "The python package used to generate, parse, and execute noqform yaml templates."
authors = ["Noq Software <hello@noq.dev>"]
readme = "README.md"
exclude = ["build_util/*"]
include = ["iambic/output/templates/*"]
version = "0.3.10"

[tool.isort]
profile = "black"
add_imports = "from __future__ import annotations"
profile = "black"

[tool.poetry.dependencies]
python = "^3.9"
boto3 = "^1.26.95"
click = "^8.1.3"
"ruamel.yaml" = "^0.17.21"
asgiref = "^3.6.0"
structlog = "^22.3.0"
pydantic = "^1.10.6"
deepdiff = "^6.3.0"
Jinja2 = "^3.1.2"
black = "^23.1.0"
isort = "^5.12.0"
flake8 = "^6.0.0"
pytest = "^7.2.2"
pytest-asyncio = "^0.21.0"
pytest-cov = "^4.0.0"
pytest-xdist = "^3.2.1"
pytest-rerunfailures = "^11.1.2"
pre-commit = "^3.2.0"
ujson = "^5.7.0"
aiofiles = "^23.1.0"
xxhash = "^3.2.0"
slack-bolt = "^1.16.4"
google-api-python-client = "^2.81.0"
google-auth = "^2.16.2"
jsonschema2md2 = "^0.6.0"
GitPython = "^3.1.31"
GitPython = "^3.1.31"
Jinja2 = "^3.1.2"
PyGithub = "==1.57" # Since 1.58, PyGithub does not work with Lambda. See https://github.com/PyGithub/PyGithub/issues/2430
pydantic-factories = "^1.17.2"
okta = "^2.9.2"
asyncache = "^0.3.1"
dateparser = "^1.1.7"
pytest-mock = "^3.10.0"
questionary = "^1.10.0"
types-dateparser = "^1.1.4.5"
cryptography = "^39.0.1"
aws-error-utils = "^2.7.0"
types-mock = "^5.0.0.5"
rich = "^13.3.2"
dictdiffer = "^0.9.0"
msal = "^1.21.0"
aiohttp = "^3.8.4"
pyopenssl = "^23.0.0"
aiofiles = "^23.1.0"
aiohttp = "^3.8.4"
asgiref = "^3.6.0"
asyncache = "^0.3.1"
aws-error-utils = "^2.7.0"
black = "^23.1.0"
boto3 = "^1.26.95"
click = "^8.1.3"
cryptography = "^39.0.1"
dateparser = "^1.1.7"
deepdiff = "^6.3.0"
dictdiffer = "^0.9.0"
flake8 = "^6.0.0"
google-api-python-client = "^2.81.0"
google-auth = "^2.16.2"
isort = "^5.12.0"
jsonschema2md2 = "^0.6.0"
msal = "^1.21.0"
okta = "^2.9.2"
pre-commit = "^3.2.0"
pydantic = "^1.10.6"
pydantic-factories = "^1.17.2"
pyopenssl = "^23.0.0"
pytest = "^7.2.2"
pytest-asyncio = "^0.21.0"
pytest-cov = "^4.0.0"
pytest-mock = "^3.10.0"
pytest-rerunfailures = "^11.1.2"
pytest-xdist = "^3.2.1"
python = "^3.9"
questionary = "^1.10.0"
rich = "^13.3.2"
"ruamel.yaml" = "^0.17.21"
slack-bolt = "^1.16.4"
structlog = "^22.3.0"
types-dateparser = "^1.1.4.5"
types-mock = "^5.0.0.5"
ujson = "^5.7.0"
xxhash = "^3.2.0"

[tool.poetry.scripts]
iambic = "iambic.main:cli"

[tool.poetry.group.dev.dependencies]
dateparser = "^1.1.7"
mock = "^5.0.1"
moto = {extras = ["all"], version = "^4.1.5"}
pycryptodome = "^3.17"
pytest-mock-generator = "^1.2.0"
types-aiofiles = "^23.1.0.0"
types-cachetools = "^5.3.0.4"
types-pyyaml = "^6.0.12.8"
types-aiofiles = "^23.1.0.0"
types-ujson = "^5.7.0.1"
mock = "^5.0.1"
pytest-mock-generator = "^1.2.0"
pycryptodome = "^3.17"
moto = {extras = ["all"], version = "^4.1.5"}
dateparser = "^1.1.7"

[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"if self.debug:",
"if settings.DEBUG",
"raise AssertionError",
"raise NotImplementedError",
"if 0:",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
]
25 changes: 25 additions & 0 deletions test/plugins/v0_1_0/azure_ad/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from pydantic import SecretStr
import pytest

from iambic.core.iambic_enum import IambicManaged
from iambic.plugins.v0_1_0.azure_ad.models import AzureADOrganization


@pytest.mark.parametrize("exclude", [None, {"other"}])
def test_organization_to_dict(exclude):
organization = AzureADOrganization(
idp_name="idp_name",
tenant_id="tenant_id",
client_id="client_id",
client_secret=SecretStr("client_secret"),
) # type: ignore

assert organization.dict(exclude=exclude) == dict(
idp_name="idp_name",
tenant_id="tenant_id",
client_id="client_id",
client_secret=SecretStr("client_secret"),
request_timeout=60,
iambic_managed=IambicManaged.UNDEFINED,
require_user_mfa_on_create=False,
) # type: ignore
40 changes: 40 additions & 0 deletions test/plugins/v0_1_0/okta/user/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ class TestUpdateUserStatus:
(UserStatus.deprovisioned, UserStatus.provisioned),
(UserStatus.provisioned, UserStatus.active),
(UserStatus.locked_out, UserStatus.active),
(UserStatus.provisioned, UserStatus.recovery),
(UserStatus.provisioned, UserStatus.password_expired),
],
)
async def test_update_user_status(
Expand Down Expand Up @@ -216,6 +218,44 @@ async def test_update_user_status(
"proposed_status": transition[1].value,
}

@pytest.mark.asyncio
async def test_update_user_status_when_deleted(
self,
mock_okta_organization: OktaOrganization, # noqa: F811 # intentional for mocks
mock_ctx,
):

username = "example_username"
idp_name = "example.org"
user_properties = UserProperties(
username=username,
profile={"login": username},
status=UserStatus.deprovisioned.value,
) # type: ignore

template = OktaUserTemplate(
file_path="example",
idp_name=idp_name,
properties=user_properties,
deleted=True,
) # type: ignore

okta_user = await create_user(
template,
mock_okta_organization,
)
okta_user.deleted = True

mock_ctx(eval_only=True)
proposed_changes = await update_user_status(
okta_user,
UserStatus.provisioned.value,
mock_okta_organization,
{},
)

assert proposed_changes == []


@pytest.mark.asyncio
async def test_maybe_deprovision_user(
Expand Down

0 comments on commit 2815335

Please sign in to comment.