diff --git a/.github/labeler.yaml b/.github/labeler.yaml index e2791b3..94eabfe 100644 --- a/.github/labeler.yaml +++ b/.github/labeler.yaml @@ -39,6 +39,11 @@ - any-glob-to-any-file: - modules/organization/**/* +":floppy_disk: password-policy": +- changed-files: + - any-glob-to-any-file: + - modules/password-policy/**/* + ":floppy_disk: user": - changed-files: - any-glob-to-any-file: diff --git a/.github/labels.yaml b/.github/labels.yaml index efea2a7..1676de3 100644 --- a/.github/labels.yaml +++ b/.github/labels.yaml @@ -64,6 +64,9 @@ - color: "fbca04" description: "This issue or pull request is related to organization module." name: ":floppy_disk: organization" +- color: "fbca04" + description: "This issue or pull request is related to password-policy module." + name: ":floppy_disk: password-policy" - color: "fbca04" description: "This issue or pull request is related to user module." name: ":floppy_disk: user" diff --git a/README.md b/README.md index e95451e..aa1ee35 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Terraform module to manage all of things on Okta organization. - [group](./modules/group/) - [group-rule](./modules/group-rule/) - [organization](./modules/organization/) +- [password-policy](./modules/password-policy/) - [user](./modules/user/) diff --git a/modules/password-policy/README.md b/modules/password-policy/README.md new file mode 100644 index 0000000..5494bc3 --- /dev/null +++ b/modules/password-policy/README.md @@ -0,0 +1,68 @@ +# password-policy + +This module creates following resources. + +- `okta_policy_password` (optional) +- `okta_policy_password_default` (optional) +- `okta_policy_rule_password` (optional) + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.8 | +| [okta](#requirement\_okta) | >= 4.8 | + +## Providers + +| Name | Version | +|------|---------| +| [okta](#provider\_okta) | 4.8.1 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [okta_policy_password.this](https://registry.terraform.io/providers/okta/okta/latest/docs/resources/policy_password) | resource | +| [okta_policy_password_default.this](https://registry.terraform.io/providers/okta/okta/latest/docs/resources/policy_password_default) | resource | +| [okta_policy_rule_password.this](https://registry.terraform.io/providers/okta/okta/latest/docs/resources/policy_rule_password) | resource | +| [okta_group.this](https://registry.terraform.io/providers/okta/okta/latest/docs/data-sources/group) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [name](#input\_name) | (Required) A name of the Okta Password Policy. Use `default` to manage the default password policy. | `string` | n/a | yes | +| [authentication\_provider](#input\_authentication\_provider) | (Optional) The authentication provider which the Okta Password Policy applies to. Valid values are `OKTA`, `LDAP`, `ACTIVE_DIRECTORY`. Defaults to `OKTA`. | `string` | `"OKTA"` | no | +| [complexity](#input\_complexity) | (Optional) A configuration for password complexity requirements of the Okta Password Policy. `complexity` block as defined below.
(Optional) `min_length` - Minimum password length. Defaults to `8`.
(Optional) `lowercase_required` - If a password must contain at least one lower case letter. Defaults to `true`.
(Optional) `uppercase_required` - If a password must contain at least one upper case letter. Defaults to `true`.
(Optional) `number_required` - If a password must contain at least one number. Defaults to `true`.
(Optional) `symbol_required` - If a password must contain at least one symbol (!@#$%^&*). Defaults to `false`.
(Optional) `first_name_restricted` - If a password must not contain the user's first name. Defaults to `false`.
(Optional) `last_name_restricted` - If a password must not contain the user's last name. Defaults to `false`.
(Optional) `username_restricted` - If a password must not contain the user's username. Defaults to `true`.
(Optional) `common_password_restricted` - Whether to restrict passwords against common password dictionary. Defaults to `true`.
(Optional) `reuse_restriction_count` - The number of distinct passwords that must be created before they can be reused. The value of `0` means no restriction. Defaults to `0`. |
object({
min_length = optional(number, 8)

lowercase_required = optional(bool, true)
uppercase_required = optional(bool, true)
number_required = optional(bool, true)
symbol_required = optional(bool, false)

first_name_restricted = optional(bool, false)
last_name_restricted = optional(bool, false)
username_restricted = optional(bool, true)
common_password_restricted = optional(bool, true)

reuse_restriction_count = optional(number, 0)
})
| `{}` | no | +| [description](#input\_description) | (Optional) A description of the Okta Password Policy. Only used when `name` is not `default`. | `string` | `"Managed by Terraform."` | no | +| [enabled](#input\_enabled) | (Optional) Whether to enable the Okta Password Policy. Defaults to `true`. Only used when `name` is not `default`. | `bool` | `true` | no | +| [expiration](#input\_expiration) | (Optional) A configuration for password expiration of the Okta Password Policy. `expiration` block as defined below.
(Optional) `max_age_days` - The number of days before a password expires. The value of `0` means no expiration. Defaults to `0`.
(Optional) `min_age_minutes` - The minimum number of minutes that must pass before a password can be changed. The value of `0` means no limit. Defaults to `0
(Optional) `remind\_before\_days` - The number of days before a password expires to remind the user. The value of `0` means no reminder. Defaults to `0`.
` |
object({
max_age_days = optional(number, 0)
min_age_minutes = optional(number, 0)
remind_before_days = optional(number, 0)
})
| `{}` | no | +| [groups](#input\_groups) | (Optional) A set of group IDs to assign the Okta Password Policy to. | `set(string)` | `[]` | no | +| [lockout](#input\_lockout) | (Optional) A configuration for password lock-out of the Okta Password Policy. `lockout` block as defined below.
(Optional) `max_attempts` - Maximum number of unsuccessful login attempts before a user is locked out. The value of `0` means no limit. Defaults to `10`.
(Optional) `duration` - Number of minutes before a locked account is unlocked. The value of `0` means no limit. Defaults to `60`.
(Optional) `show_failures` - Whether to inform a user when their account is locked. Defaults to `false`.
(Optional) `notification_channels` - A set of notification channels to use to notify a user when their account has been locked. Valid values are `EMAIL`, `SMS`, `PUSH`. Defaults to `EMAIL`. |
object({
max_attempts = optional(number, 10)
duration = optional(number, 60)
show_failures = optional(bool, false)
notification_channels = optional(set(string), ["EMAIL"])
})
| `{}` | no | +| [priority](#input\_priority) | (Optional) A priority of the Okta Password Policy. Only used when `name` is not `default`. | `number` | `null` | no | +| [recovery](#input\_recovery) | (Optional) A configuration for password recovery of the Okta Password Policy. `recovery` block as defined below.
(Optional) `call` - A configuration for password recovery call. `call` block as defined below.
(Optional) `enabled` - Whether to enable password recovery call. Defaults to `false`.
(Optional) `email` - A configuration for password recovery email. `email` block as defined below.
(Optional) `enabled` - Whether to enable password recovery email. Defaults to `true`.
(Optional) `token_ttl` - Lifetime in minutes of the recovery email token. Defaults to `60`.
(Optional) `question` - A configuration for password recovery question. `question` block as defined below.
(Optional) `enabled` - Whether to enable password recovery question. Defaults to `false`.
(Optional) `min_answer_length` - Minimum length of the password recovery question answer. Defaults to `4`.
(Optional) `sms` - A configuration for password recovery sms. `sms` block as defined below.
(Optional) `enabled` - Whether to enable password recovery sms. Defaults to `false`. |
object({
call = optional(object({
enabled = optional(bool, false)
}), {})
email = optional(object({
enabled = optional(bool, true)
token_ttl = optional(number, 60)
}), {})
question = optional(object({
enabled = optional(bool, false)
min_answer_length = optional(number, 4)
}), {})
sms = optional(object({
enabled = optional(bool, false)
}), {})
})
| `{}` | no | +| [rules](#input\_rules) | (Optional) A configuration for rules of the Okta Password Policy. Each item of `rules` block as defined below.
(Required) `name` - A name of the password policy rule.
(Optional) `priority` - A priority of the password policy rule. To avoid an endless diff situation an error is thrown if an invalid property is provided. The Okta API defaults to the last (lowest) if not provided.
(Optional) `enabled` - Whether to enable password policy rule. Defaults to `true`.
(Optional) `condition` - A condition of the password policy rule. `condition` block as defined below.
(Optional) `excluded_users` - A set of user IDs to exclude.
(Optional) `network` - A configuration for network condition. `network` block as defined below.
(Optional) `excluded_zones` - A set of zone IDs to exclude.
(Optional) `included_zones` - A set of zone IDs to include.
(Optional) `allow_password_change` - Whether to allow users to change their password. Defaults to `true`.
(Optional) `allow_password_reset` - Whether to allow users to reset their password. Defaults to `true`.
(Optional) `allow_password_unlock` - Whether to allow users to unlock. Defaults to `false`. |
list(object({
name = string
priority = optional(number)
enabled = optional(bool, true)

condition = optional(object({
excluded_users = optional(set(string), [])
network = optional(object({
excluded_zones = optional(set(string), [])
included_zones = optional(set(string), [])
}), {})
}), {})

allow_password_change = optional(bool, true)
allow_password_reset = optional(bool, true)
allow_password_unlock = optional(bool, false)
}))
| `[]` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [authentication\_provider](#output\_authentication\_provider) | The authentication provider which the Okta Password Policy applies to. | +| [complexity](#output\_complexity) | The complexity requirements of the Okta Password Policy. | +| [description](#output\_description) | The description of the Okta Password Policy. | +| [enabled](#output\_enabled) | Whether the Okta Password Policy is enabled. | +| [expiration](#output\_expiration) | The configuration for password expiration of the Okta Password Policy. | +| [groups](#output\_groups) | The information for the assigned groups of the Okta Password Policy. | +| [id](#output\_id) | The ID of the Okta Password Policy. | +| [lockout](#output\_lockout) | The configuration for password lock-out of the Okta Password Policy. | +| [name](#output\_name) | The name of the Okta Password Policy. | +| [priority](#output\_priority) | The priority of the Okta Password Policy. | +| [recovery](#output\_recovery) | The configuration for password recovery of the Okta Password Policy. | +| [rules](#output\_rules) | The configuration for rules of the Okta Password Policy. | + diff --git a/modules/password-policy/main.tf b/modules/password-policy/main.tf new file mode 100644 index 0000000..e12ec73 --- /dev/null +++ b/modules/password-policy/main.tf @@ -0,0 +1,126 @@ +locals { + is_default = var.name == "default" + + policy = (local.is_default + ? merge(okta_policy_password_default.this[0], { + auth_provider = okta_policy_password_default.this[0].default_auth_provider + }) + : okta_policy_password.this[0] + ) +} + + +################################################### +# Okta Password Policy +################################################### + +# TODO: +# `skip_unlock` (Boolean) When an Active Directory user is locked out of Okta, the Okta unlock operation should also attempt to unlock the user's Windows account. Default: false +resource "okta_policy_password" "this" { + count = local.is_default ? 0 : 1 + + name = var.name + description = var.description + status = var.enabled ? "ACTIVE" : "INACTIVE" + + auth_provider = var.authentication_provider + priority = var.priority + groups_included = var.groups + + + ## Complexity Requirements + password_min_length = var.complexity.min_length + + password_min_lowercase = var.complexity.lowercase_required ? 1 : 0 + password_min_uppercase = var.complexity.uppercase_required ? 1 : 0 + password_min_number = var.complexity.number_required ? 1 : 0 + password_min_symbol = var.complexity.symbol_required ? 1 : 0 + + password_exclude_first_name = var.complexity.first_name_restricted + password_exclude_last_name = var.complexity.last_name_restricted + password_exclude_username = var.complexity.username_restricted + password_dictionary_lookup = var.complexity.common_password_restricted + + password_history_count = var.complexity.reuse_restriction_count + + + ## Expiration + password_max_age_days = var.expiration.max_age_days + password_min_age_minutes = var.expiration.min_age_minutes + password_expire_warn_days = var.expiration.remind_before_days + + + ## Lock-out + password_max_lockout_attempts = var.lockout.max_attempts + password_auto_unlock_minutes = var.lockout.duration + password_show_lockout_failures = var.lockout.show_failures + password_lockout_notification_channels = var.lockout.notification_channels + + + ## Recovery + call_recovery = var.recovery.call.enabled ? "ACTIVE" : "INACTIVE" + + email_recovery = var.recovery.email.enabled ? "ACTIVE" : "INACTIVE" + recovery_email_token = var.recovery.email.token_ttl + + question_recovery = var.recovery.question.enabled ? "ACTIVE" : "INACTIVE" + question_min_length = var.recovery.question.min_answer_length + + sms_recovery = var.recovery.sms.enabled ? "ACTIVE" : "INACTIVE" +} + +resource "okta_policy_password_default" "this" { + count = local.is_default ? 1 : 0 + + + ## Complexity Requirements + password_min_length = var.complexity.min_length + + password_min_lowercase = var.complexity.lowercase_required ? 1 : 0 + password_min_uppercase = var.complexity.uppercase_required ? 1 : 0 + password_min_number = var.complexity.number_required ? 1 : 0 + password_min_symbol = var.complexity.symbol_required ? 1 : 0 + + password_exclude_first_name = var.complexity.first_name_restricted + password_exclude_last_name = var.complexity.last_name_restricted + password_exclude_username = var.complexity.username_restricted + password_dictionary_lookup = var.complexity.common_password_restricted + + password_history_count = var.complexity.reuse_restriction_count + + + ## Expiration + password_max_age_days = var.expiration.max_age_days + password_min_age_minutes = var.expiration.min_age_minutes + password_expire_warn_days = var.expiration.remind_before_days + + + ## Lock-out + password_max_lockout_attempts = var.lockout.max_attempts + password_auto_unlock_minutes = var.lockout.duration + password_show_lockout_failures = var.lockout.show_failures + password_lockout_notification_channels = var.lockout.notification_channels + + + ## Recovery + call_recovery = var.recovery.call.enabled ? "ACTIVE" : "INACTIVE" + + email_recovery = var.recovery.email.enabled ? "ACTIVE" : "INACTIVE" + recovery_email_token = var.recovery.email.token_ttl + + question_recovery = var.recovery.question.enabled ? "ACTIVE" : "INACTIVE" + question_min_length = var.recovery.question.min_answer_length + + sms_recovery = var.recovery.sms.enabled ? "ACTIVE" : "INACTIVE" +} + + +################################################### +# Okta Groups for Password Policy +################################################### + +data "okta_group" "this" { + for_each = toset(var.groups) + + id = each.value +} diff --git a/modules/password-policy/outputs.tf b/modules/password-policy/outputs.tf new file mode 100644 index 0000000..2a3b577 --- /dev/null +++ b/modules/password-policy/outputs.tf @@ -0,0 +1,130 @@ +output "id" { + description = "The ID of the Okta Password Policy." + value = local.policy.id +} + +output "name" { + description = "The name of the Okta Password Policy." + value = local.policy.name +} + +output "description" { + description = "The description of the Okta Password Policy." + value = local.policy.description +} + +output "enabled" { + description = "Whether the Okta Password Policy is enabled." + value = local.policy.status == "ACTIVE" +} + +output "authentication_provider" { + description = "The authentication provider which the Okta Password Policy applies to." + value = local.policy.auth_provider +} + +output "priority" { + description = "The priority of the Okta Password Policy." + value = local.policy.priority +} + +output "groups" { + description = "The information for the assigned groups of the Okta Password Policy." + value = [ + for group in data.okta_group.this : + group.name + ] +} + +output "complexity" { + description = "The complexity requirements of the Okta Password Policy." + value = { + min_length = local.policy.password_min_length + + lowercase_required = local.policy.password_min_lowercase == 1 + uppercase_required = local.policy.password_min_uppercase == 1 + number_required = local.policy.password_min_number == 1 + symbol_required = local.policy.password_min_symbol == 1 + + first_name_restricted = local.policy.password_exclude_first_name + last_name_restricted = local.policy.password_exclude_last_name + username_restricted = local.policy.password_exclude_username + common_password_restricted = local.policy.password_dictionary_lookup + + reuse_restriction_count = local.policy.password_history_count + } +} + +output "expiration" { + description = "The configuration for password expiration of the Okta Password Policy." + value = { + max_age_days = local.policy.password_max_age_days + min_age_minutes = local.policy.password_min_age_minutes + remind_before_days = local.policy.password_expire_warn_days + } +} + +output "lockout" { + description = "The configuration for password lock-out of the Okta Password Policy." + value = { + max_attempts = local.policy.password_max_lockout_attempts + duration = local.policy.password_auto_unlock_minutes + show_failures = local.policy.password_show_lockout_failures + notification_channels = local.policy.password_lockout_notification_channels + } +} + +output "recovery" { + description = "The configuration for password recovery of the Okta Password Policy." + value = { + call = { + enabled = local.policy.call_recovery == "ACTIVE" + } + email = { + enabled = local.policy.email_recovery == "ACTIVE" + token_ttl = local.policy.recovery_email_token + } + question = { + enabled = local.policy.question_recovery == "ACTIVE" + min_answer_length = local.policy.question_min_length + } + sms = { + enabled = local.policy.sms_recovery == "ACTIVE" + } + } +} + +output "rules" { + description = "The configuration for rules of the Okta Password Policy." + value = { + for name, rule in okta_policy_rule_password.this : + name => { + id = rule.id + name = rule.name + priority = rule.priority + enabled = rule.status == "ACTIVE" + + condition = { + excluded_users = rule.users_excluded + network = { + connection = rule.network_connection + + excluded_zones = rule.network_excludes + included_zones = rule.network_includes + } + } + + allow_password_change = rule.password_change == "ALLOW" + allow_password_reset = rule.password_reset == "ALLOW" + allow_password_unlock = rule.password_unlock == "ALLOW" + } + } +} + +# output "debug" { +# value = { +# for k, v in okta_app_signon_policy.this : +# k => v +# if !contains(["id", "name", "description"], k) +# } +# } diff --git a/modules/password-policy/rules.tf b/modules/password-policy/rules.tf new file mode 100644 index 0000000..9139f91 --- /dev/null +++ b/modules/password-policy/rules.tf @@ -0,0 +1,40 @@ +################################################### +# Rules of Okta Password Policy +################################################### + +resource "okta_policy_rule_password" "this" { + + for_each = { + for rule in var.rules : + rule.name => rule + } + + policy_id = local.policy.id + + name = each.key + priority = each.value.priority + status = each.value.enabled ? "ACTIVE" : "INACTIVE" + + + ## Conditions + users_excluded = each.value.condition.excluded_users + + network_connection = anytrue([ + length(each.value.condition.network.excluded_zones) > 0, + length(each.value.condition.network.included_zones) > 0, + ]) ? "ZONE" : "ANYWHERE" + network_excludes = (length(each.value.condition.network.excluded_zones) > 0 + ? each.value.condition.network.excluded_zones + : null + ) + network_includes = (length(each.value.condition.network.included_zones) > 0 + ? each.value.condition.network.included_zones + : null + ) + + + ## Effects + password_change = each.value.allow_password_change ? "ALLOW" : "DENY" + password_reset = each.value.allow_password_reset ? "ALLOW" : "DENY" + password_unlock = each.value.allow_password_unlock ? "ALLOW" : "DENY" +} diff --git a/modules/password-policy/variables.tf b/modules/password-policy/variables.tf new file mode 100644 index 0000000..30f89f0 --- /dev/null +++ b/modules/password-policy/variables.tf @@ -0,0 +1,190 @@ +variable "name" { + description = "(Required) A name of the Okta Password Policy. Use `default` to manage the default password policy." + type = string + nullable = false +} + +variable "description" { + description = "(Optional) A description of the Okta Password Policy. Only used when `name` is not `default`." + type = string + default = "Managed by Terraform." + nullable = false +} + +variable "enabled" { + description = "(Optional) Whether to enable the Okta Password Policy. Defaults to `true`. Only used when `name` is not `default`." + type = bool + default = true + nullable = false +} + +variable "authentication_provider" { + description = "(Optional) The authentication provider which the Okta Password Policy applies to. Valid values are `OKTA`, `LDAP`, `ACTIVE_DIRECTORY`. Defaults to `OKTA`." + type = string + default = "OKTA" + nullable = false + + validation { + condition = contains(["OKTA", "LDAP", "ACTIVE_DIRECTORY"], var.authentication_provider) + error_message = "Valid values are `OKTA`, `LDAP`, `ACTIVE_DIRECTORY`." + } +} + +variable "priority" { + description = "(Optional) A priority of the Okta Password Policy. Only used when `name` is not `default`." + type = number + default = null + nullable = true +} + +variable "groups" { + description = "(Optional) A set of group IDs to assign the Okta Password Policy to." + type = set(string) + default = [] + nullable = false + + validation { + condition = anytrue([ + var.name == "default", + var.name != "default" && length(var.groups) > 0, + ]) + error_message = "At least one group ID must be assigned to the Okta Password Policy when the policy is not default." + } +} + +variable "complexity" { + description = <