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({| `{}` | 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.
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)
})
object({| `{}` | 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.
max_age_days = optional(number, 0)
min_age_minutes = optional(number, 0)
remind_before_days = optional(number, 0)
})
object({| `{}` | 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.
max_attempts = optional(number, 10)
duration = optional(number, 60)
show_failures = optional(bool, false)
notification_channels = optional(set(string), ["EMAIL"])
})
object({| `{}` | no | +| [rules](#input\_rules) | (Optional) A configuration for rules of the Okta Password Policy. Each item of `rules` block as defined below.
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)
}), {})
})
list(object({| `[]` | 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 = <
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)
}))