Skip to content

Commit

Permalink
Implement conditional access policies guests_or_internal_users
Browse files Browse the repository at this point in the history
WIP

Fixes #1136, #1196
  • Loading branch information
agileknight committed Oct 24, 2023
1 parent d6a6a27 commit 22851d5
Show file tree
Hide file tree
Showing 8 changed files with 375 additions and 17 deletions.
18 changes: 17 additions & 1 deletion docs/resources/conditional_access_policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,11 +203,27 @@ The following arguments are supported:
* `excluded_groups` - (Optional) A list of group IDs excluded from scope of policy.
* `excluded_roles` - (Optional) A list of role IDs excluded from scope of policy.
* `excluded_users` - (Optional) A list of user IDs excluded from scope of policy and/or `GuestsOrExternalUsers`.
* `excluded_guests_or_external_users` - (Optional) A `guests_or_external_users` block as documented below, which specifies internal guests and external users excluded from scope of policy.
* `included_groups` - (Optional) A list of group IDs in scope of policy unless explicitly excluded.
* `included_roles` - (Optional) A list of role IDs in scope of policy unless explicitly excluded.
* `included_users` - (Optional) A list of user IDs in scope of policy unless explicitly excluded, or `None` or `All` or `GuestsOrExternalUsers`.
* `included_guests_or_external_users` - (Optional) A `guests_or_external_users` block as documented below, which specifies internal guests and external users in scope of policy.

-> At least one of `included_groups`, `included_roles` or `included_users` must be specified.
-> At least one of `included_groups`, `included_roles`, `included_users` or `included_guests_or_external_users` must be specified.

---

`guests_or_external_users` block supports the following:

* `guest_or_external_user_types` - (Required) A list of guest or external user types. Possible values are: `none`, `internalGuest`, `b2bCollaborationGuest`, `b2bCollaborationMember`, `b2bDirectConnectUser`, `otherExternalUser`, `serviceProvider`, `unknownFutureValue`.
* `external_tenants` - (Optional) An `external_tenants` block as documented below, which specifies external tenants in a policy scope.

---

`external_tenants` block supports the following:

* `membership_kind` - (Required) The external tenant membership kind. Possible values are: `all`, `enumerated`, `unknownFutureValue`.
* `members` - (Optional) A list tenant IDs. Can only be specified if `membership_kind` is `enumerated`.

---

Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,5 @@ require (
)

go 1.21.3

replace github.com/manicminer/hamilton => /Users/philippmeier/github/agileknight/hamilton
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ func conditionalAccessPolicyResource() *pluginsdk.Resource {
"included_users": {
Type: pluginsdk.TypeList,
Optional: true,
AtLeastOneOf: []string{"conditions.0.users.0.included_groups", "conditions.0.users.0.included_roles", "conditions.0.users.0.included_users"},
AtLeastOneOf: []string{"conditions.0.users.0.included_groups", "conditions.0.users.0.included_roles", "conditions.0.users.0.included_users", "conditions.0.users.0.included_guests_or_external_users"},
Elem: &pluginsdk.Schema{
Type: pluginsdk.TypeString,
ValidateDiagFunc: validation.ValidateDiag(validation.StringIsNotEmpty),
Expand All @@ -161,7 +161,7 @@ func conditionalAccessPolicyResource() *pluginsdk.Resource {
"included_groups": {
Type: pluginsdk.TypeList,
Optional: true,
AtLeastOneOf: []string{"conditions.0.users.0.included_groups", "conditions.0.users.0.included_roles", "conditions.0.users.0.included_users"},
AtLeastOneOf: []string{"conditions.0.users.0.included_groups", "conditions.0.users.0.included_roles", "conditions.0.users.0.included_users", "conditions.0.users.0.included_guests_or_external_users"},
Elem: &pluginsdk.Schema{
Type: pluginsdk.TypeString,
ValidateDiagFunc: validation.ValidateDiag(validation.StringIsNotEmpty),
Expand All @@ -180,7 +180,7 @@ func conditionalAccessPolicyResource() *pluginsdk.Resource {
"included_roles": {
Type: pluginsdk.TypeList,
Optional: true,
AtLeastOneOf: []string{"conditions.0.users.0.included_groups", "conditions.0.users.0.included_roles", "conditions.0.users.0.included_users"},
AtLeastOneOf: []string{"conditions.0.users.0.included_groups", "conditions.0.users.0.included_roles", "conditions.0.users.0.included_users", "conditions.0.users.0.included_guests_or_external_users"},
Elem: &pluginsdk.Schema{
Type: pluginsdk.TypeString,
ValidateDiagFunc: validation.ValidateDiag(validation.StringIsNotEmpty),
Expand All @@ -195,6 +195,109 @@ func conditionalAccessPolicyResource() *pluginsdk.Resource {
ValidateDiagFunc: validation.ValidateDiag(validation.StringIsNotEmpty),
},
},

"included_guests_or_external_users": {
Type: pluginsdk.TypeList,
Optional: true,
AtLeastOneOf: []string{"conditions.0.users.0.included_groups", "conditions.0.users.0.included_roles", "conditions.0.users.0.included_users", "conditions.0.users.0.included_guests_or_external_users"},
Elem: &pluginsdk.Resource{
Schema: map[string]*pluginsdk.Schema{
"guest_or_external_user_types": {
Type: pluginsdk.TypeList,
Required: true,
Elem: &pluginsdk.Schema{
Type: pluginsdk.TypeString,
ValidateFunc: validation.StringInSlice([]string{
msgraph.ConditionalAccessGuestOrExternalUserTypeNone,
msgraph.ConditionalAccessGuestOrExternalUserTypeInternalGuest,
msgraph.ConditionalAccessGuestOrExternalUserTypeB2bCollaborationGuest,
msgraph.ConditionalAccessGuestOrExternalUserTypeB2bCollaborationMember,
msgraph.ConditionalAccessGuestOrExternalUserTypeB2bDirectConnectUser,
msgraph.ConditionalAccessGuestOrExternalUserTypeOtherExternalUser,
msgraph.ConditionalAccessGuestOrExternalUserTypeServiceProvider,
msgraph.ConditionalAccessGuestOrExternalUserTypeUnknownFutureValue,
}, false),
},
},
"external_tenants": {
Type: pluginsdk.TypeList,
Optional: true,
Elem: &pluginsdk.Resource{
Schema: map[string]*pluginsdk.Schema{
"membership_kind": {
Type: pluginsdk.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{
msgraph.ConditionalAccessExternalTenantsMembershipKindAll,
msgraph.ConditionalAccessExternalTenantsMembershipKindEnumerated,
msgraph.ConditionalAccessExternalTenantsMembershipKindUnknownFutureValue,
}, false),
},
"members": {
Type: pluginsdk.TypeList,
Optional: true,
Elem: &pluginsdk.Schema{
Type: pluginsdk.TypeString,
ValidateDiagFunc: validation.ValidateDiag(validation.StringIsNotEmpty),
},
},
},
},
},
},
},
},

"excluded_guests_or_external_users": {
Type: pluginsdk.TypeList,
Optional: true,
Elem: &pluginsdk.Resource{
Schema: map[string]*pluginsdk.Schema{
"guest_or_external_user_types": {
Type: pluginsdk.TypeList,
Required: true,
Elem: &pluginsdk.Schema{
Type: pluginsdk.TypeString,
ValidateFunc: validation.StringInSlice([]string{
msgraph.ConditionalAccessGuestOrExternalUserTypeNone,
msgraph.ConditionalAccessGuestOrExternalUserTypeInternalGuest,
msgraph.ConditionalAccessGuestOrExternalUserTypeB2bCollaborationGuest,
msgraph.ConditionalAccessGuestOrExternalUserTypeB2bCollaborationMember,
msgraph.ConditionalAccessGuestOrExternalUserTypeB2bDirectConnectUser,
msgraph.ConditionalAccessGuestOrExternalUserTypeOtherExternalUser,
msgraph.ConditionalAccessGuestOrExternalUserTypeServiceProvider,
msgraph.ConditionalAccessGuestOrExternalUserTypeUnknownFutureValue,
}, false),
},
},
"external_tenants": {
Type: pluginsdk.TypeList,
Optional: true,
Elem: &pluginsdk.Resource{
Schema: map[string]*pluginsdk.Schema{
"membership_kind": {
Type: pluginsdk.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{
msgraph.ConditionalAccessExternalTenantsMembershipKindAll,
msgraph.ConditionalAccessExternalTenantsMembershipKindEnumerated,
msgraph.ConditionalAccessExternalTenantsMembershipKindUnknownFutureValue,
}, false),
},
"members": {
Type: pluginsdk.TypeList,
Optional: true,
Elem: &pluginsdk.Schema{
Type: pluginsdk.TypeString,
ValidateDiagFunc: validation.ValidateDiag(validation.StringIsNotEmpty),
},
},
},
},
},
},
},
},
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,34 @@ func TestAccConditionalAccessPolicy_authenticationStrength(t *testing.T) {
})
}

func TestAccConditionalAccessPolicy_guestsOrExternalUsers(t *testing.T) {
data := acceptance.BuildTestData(t, "azuread_conditional_access_policy", "test")
r := ConditionalAccessPolicyResource{}

data.ResourceTest(t, r, []acceptance.TestStep{
{
Config: r.guestsOrExternalUsersAllServiceProvidersIncluded(data),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
check.That(data.ResourceName).Key("id").Exists(),
check.That(data.ResourceName).Key("display_name").HasValue(fmt.Sprintf("acctest-CONPOLICY-%d", data.RandomInteger)),
check.That(data.ResourceName).Key("conditions.0.users.0.included_guests_or_external_users.0.external_tenants.0.membership_kind").HasValue("all"),
),
},
data.ImportStep(),
{
Config: r.guestsOrExternalUsersAllServiceProvidersExcluded(data),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
check.That(data.ResourceName).Key("id").Exists(),
check.That(data.ResourceName).Key("display_name").HasValue(fmt.Sprintf("acctest-CONPOLICY-%d", data.RandomInteger)),
check.That(data.ResourceName).Key("conditions.0.users.0.excluded_guests_or_external_users.0.external_tenants.0.membership_kind").HasValue("all"),
),
},
data.ImportStep(),
})
}

func (r ConditionalAccessPolicyResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) {
var id *string

Expand Down Expand Up @@ -704,3 +732,66 @@ resource "azuread_conditional_access_policy" "test" {
}
`, AuthenticationStrengthPolicyResource{}.basic(data), data.RandomInteger)
}

func (ConditionalAccessPolicyResource) guestsOrExternalUsersAllServiceProvidersIncluded(data acceptance.TestData) string {
return fmt.Sprintf(`
resource "azuread_conditional_access_policy" "test" {
display_name = "acctest-CONPOLICY-%[1]d"
state = "disabled"
conditions {
client_app_types = ["browser"]
applications {
included_applications = ["None"]
}
users {
included_guests_or_external_users {
guest_or_external_user_types = ["internalGuest", "serviceProvider"]
external_tenants {
membership_kind = "all"
}
}
}
}
grant_controls {
operator = "OR"
built_in_controls = ["block"]
}
}
`, data.RandomInteger)
}

func (ConditionalAccessPolicyResource) guestsOrExternalUsersAllServiceProvidersExcluded(data acceptance.TestData) string {
return fmt.Sprintf(`
resource "azuread_conditional_access_policy" "test" {
display_name = "acctest-CONPOLICY-%[1]d"
state = "disabled"
conditions {
client_app_types = ["browser"]
applications {
included_applications = ["None"]
}
users {
included_users = ["None"]
excluded_guests_or_external_users {
guest_or_external_user_types = ["internalGuest", "serviceProvider"]
external_tenants {
membership_kind = "all"
}
}
}
}
grant_controls {
operator = "OR"
built_in_controls = ["block"]
}
}
`, data.RandomInteger)
}
82 changes: 76 additions & 6 deletions internal/services/conditionalaccess/conditionalaccess.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,14 @@ func flattenConditionalAccessUsers(in *msgraph.ConditionalAccessUsers) []interfa

return []interface{}{
map[string]interface{}{
"included_users": tf.FlattenStringSlicePtr(in.IncludeUsers),
"excluded_users": tf.FlattenStringSlicePtr(in.ExcludeUsers),
"included_groups": tf.FlattenStringSlicePtr(in.IncludeGroups),
"excluded_groups": tf.FlattenStringSlicePtr(in.ExcludeGroups),
"included_roles": tf.FlattenStringSlicePtr(in.IncludeRoles),
"excluded_roles": tf.FlattenStringSlicePtr(in.ExcludeRoles),
"included_users": tf.FlattenStringSlicePtr(in.IncludeUsers),
"excluded_users": tf.FlattenStringSlicePtr(in.ExcludeUsers),
"included_groups": tf.FlattenStringSlicePtr(in.IncludeGroups),
"excluded_groups": tf.FlattenStringSlicePtr(in.ExcludeGroups),
"included_roles": tf.FlattenStringSlicePtr(in.IncludeRoles),
"excluded_roles": tf.FlattenStringSlicePtr(in.ExcludeRoles),
"included_guests_or_external_users": flattenGuestsOrExternalUsers(in.IncludeGuestsOrExternalUsers),
"excluded_guests_or_external_users": flattenGuestsOrExternalUsers(in.ExcludeGuestsOrExternalUsers),
},
}
}
Expand Down Expand Up @@ -190,6 +192,32 @@ func flattenConditionalAccessDeviceFilter(in *msgraph.ConditionalAccessFilter) [
}
}

func flattenGuestsOrExternalUsers(in *msgraph.ConditionalAccessGuestsOrExternalUsers) []interface{} {
if in == nil {
return []interface{}{}
}

return []interface{}{
map[string]interface{}{
"guest_or_external_user_types": tf.FlattenStringSlicePtr(in.GuestOrExternalUserTypes),
"external_tenants": flattenExternalTenants(in.ExternalTenants),
},
}
}

func flattenExternalTenants(in *msgraph.ConditionalAccessExternalTenants) []interface{} {
if in == nil {
return []interface{}{}
}

return []interface{}{
map[string]interface{}{
"membership_kind": in.MembershipKind,
"members": tf.FlattenStringSlicePtr(in.Members),
},
}
}

func flattenCountryNamedLocation(in *msgraph.CountryNamedLocation) []interface{} {
if in == nil {
return []interface{}{}
Expand Down Expand Up @@ -332,6 +360,9 @@ func expandConditionalAccessUsers(in []interface{}) *msgraph.ConditionalAccessUs
result.IncludeRoles = tf.ExpandStringSlicePtr(includeRoles)
result.ExcludeRoles = tf.ExpandStringSlicePtr(excludeRoles)

result.IncludeGuestsOrExternalUsers = expandGuestsOrExternalUsers(config["included_guests_or_external_users"].([]interface{}))
result.ExcludeGuestsOrExternalUsers = expandGuestsOrExternalUsers(config["excluded_guests_or_external_users"].([]interface{}))

return &result
}

Expand Down Expand Up @@ -474,6 +505,45 @@ func expandConditionalAccessFilter(in []interface{}) *msgraph.ConditionalAccessF
return &result
}

func expandGuestsOrExternalUsers(in []interface{}) *msgraph.ConditionalAccessGuestsOrExternalUsers {
if len(in) == 0 || in[0] == nil {
return nil
}

result := msgraph.ConditionalAccessGuestsOrExternalUsers{}

config := in[0].(map[string]interface{})

types := config["guest_or_external_user_types"].([]interface{})

result.GuestOrExternalUserTypes = tf.ExpandStringSlicePtr(types)
result.ExternalTenants = expandExternalTenants(config["external_tenants"].([]interface{}))

return &result
}

func expandExternalTenants(in []interface{}) *msgraph.ConditionalAccessExternalTenants {
if len(in) == 0 || in[0] == nil {
return nil
}

result := msgraph.ConditionalAccessExternalTenants{}

config := in[0].(map[string]interface{})

members := config["members"].([]interface{})

result.MembershipKind = pointer.To(config["membership_kind"].(string))

// only membership_kind enumerated is allowed to have members field set
// so we omit setting an empty array when no members configured
if len(members) > 0 {
result.Members = tf.ExpandStringSlicePtr(members)
}

return &result
}

func expandCountryNamedLocation(in []interface{}) *msgraph.CountryNamedLocation {
if len(in) == 0 || in[0] == nil {
return nil
Expand Down
Loading

0 comments on commit 22851d5

Please sign in to comment.