diff --git a/github/data_source_github_organization_external_identities.go b/github/data_source_github_organization_external_identities.go new file mode 100644 index 0000000000..d3e95fdd63 --- /dev/null +++ b/github/data_source_github_organization_external_identities.go @@ -0,0 +1,127 @@ +package github + +import ( + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/shurcooL/githubv4" +) + +func dataSourceGithubOrganizationExternalIdentities() *schema.Resource { + return &schema.Resource{ + Read: dataSourceGithubOrganizationExternalIdentitiesRead, + + Schema: map[string]*schema.Schema{ + "identities": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "login": { + Type: schema.TypeString, + Computed: true, + }, + "saml_identity": { + Type: schema.TypeMap, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "scim_identity": { + Type: schema.TypeMap, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, + }, + } +} + +func dataSourceGithubOrganizationExternalIdentitiesRead(d *schema.ResourceData, meta interface{}) error { + name := meta.(*Owner).name + + client4 := meta.(*Owner).v4client + ctx := meta.(*Owner).StopContext + + var query struct { + Organization struct { + SamlIdentityProvider struct { + ExternalIdentities struct { + Edges []struct { + Node struct { + User struct { + Login githubv4.String + } + SamlIdentity struct { + NameId githubv4.String + Username githubv4.String + GivenName githubv4.String + FamilyName githubv4.String + } + ScimIdentity struct { + Username githubv4.String + GivenName githubv4.String + FamilyName githubv4.String + } + } + } + PageInfo struct { + EndCursor githubv4.String + HasNextPage bool + } + } `graphql:"externalIdentities(first: 100, after: $after)"` + } + } `graphql:"organization(login: $login)"` + } + variables := map[string]interface{}{ + "login": githubv4.String(name), + "after": (*githubv4.String)(nil), + } + + var identities []map[string]interface{} + + for { + err := client4.Query(ctx, &query, variables) + if err != nil { + return err + } + for _, edge := range query.Organization.SamlIdentityProvider.ExternalIdentities.Edges { + identity := map[string]interface{}{ + "login": string(edge.Node.User.Login), + "saml_identity": nil, + "scim_identity": nil, + } + + if edge.Node.SamlIdentity.NameId != "" { + identity["saml_identity"] = map[string]string{ + "name_id": string(edge.Node.SamlIdentity.NameId), + "username": string(edge.Node.SamlIdentity.Username), + "given_name": string(edge.Node.SamlIdentity.GivenName), + "family_name": string(edge.Node.SamlIdentity.FamilyName), + } + } + + if edge.Node.ScimIdentity.Username != "" { + identity["scim_identity"] = map[string]string{ + "username": string(edge.Node.ScimIdentity.Username), + "given_name": string(edge.Node.ScimIdentity.GivenName), + "family_name": string(edge.Node.ScimIdentity.FamilyName), + } + } + + identities = append(identities, identity) + } + if !query.Organization.SamlIdentityProvider.ExternalIdentities.PageInfo.HasNextPage { + break + } + variables["after"] = githubv4.NewString(query.Organization.SamlIdentityProvider.ExternalIdentities.PageInfo.EndCursor) + } + + d.SetId(name) + d.Set("identities", identities) + + return nil +} diff --git a/github/data_source_github_organization_external_identities_test.go b/github/data_source_github_organization_external_identities_test.go new file mode 100644 index 0000000000..a6f3dee5f9 --- /dev/null +++ b/github/data_source_github_organization_external_identities_test.go @@ -0,0 +1,47 @@ +package github + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" +) + +func TestAccGithubOrganizationExternalIdentities(t *testing.T) { + if isEnterprise != "true" { + t.Skip("Skipping because `ENTERPRISE_ACCOUNT` is not set or set to false") + } + + t.Run("queries without error", func(t *testing.T) { + config := `data "github_organization_external_identities" "test" {}` + + check := resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("data.github_organization_external_identities.test", "identities.#"), + resource.TestCheckResourceAttrSet("data.github_organization_external_identities.test", "identities.0.login"), + resource.TestCheckResourceAttrSet("data.github_organization_external_identities.test", "identities.0.saml_identity.name_id"), + ) + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + }, + }) + } + + t.Run("with an anonymous account", func(t *testing.T) { + t.Skip("anonymous account not supported for this operation") + }) + + t.Run("with an individual account", func(t *testing.T) { + t.Skip("individual account not supported for this operation") + }) + + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) + }) +} diff --git a/github/provider.go b/github/provider.go index daae203b1e..c684a53123 100644 --- a/github/provider.go +++ b/github/provider.go @@ -194,6 +194,7 @@ func Provider() terraform.ResourceProvider { "github_membership": dataSourceGithubMembership(), "github_organization": dataSourceGithubOrganization(), "github_organization_custom_role": dataSourceGithubOrganizationCustomRole(), + "github_organization_external_identities": dataSourceGithubOrganizationExternalIdentities(), "github_organization_ip_allow_list": dataSourceGithubOrganizationIpAllowList(), "github_organization_team_sync_groups": dataSourceGithubOrganizationTeamSyncGroups(), "github_organization_teams": dataSourceGithubOrganizationTeams(), diff --git a/website/docs/d/organization_external_identities.markdown b/website/docs/d/organization_external_identities.markdown new file mode 100644 index 0000000000..a900558d6f --- /dev/null +++ b/website/docs/d/organization_external_identities.markdown @@ -0,0 +1,50 @@ +--- +layout: "github" +page_title: "GitHub: github_organization_external_identities" +description: |- + Get a list of organization members and their SAML linked external identity NameID +--- + +# github_organization_external_identities + +Use this data source to retrieve each organization member's SAML or SCIM user +attributes. + +## Example Usage + +```hcl +data "github_organization_external_identities" "all" {} +``` + +## Attributes Reference + +- `identities` - An Array of identities returned from GitHub + +--- + +Each element in the `identities` block consists of: + +- `login` - The username of the GitHub user +- `saml_identity` - An Object containing the user's SAML data. This object will + be empty if the user is not managed by SAML. +- `scim_identity` - An Object contining the user's SCIM data. This object will + be empty if the user is not managed by SCIM. + +--- + +If a user is managed by SAML, the `saml_identity` object will contain: + +- `name_id` - The member's SAML NameID +- `username` - The member's SAML Username +- `family_name` - The member's SAML Family Name +- `given_name` - The member's SAML Given Name + +--- + +If a user is managed by SCIM, the `scim_identity` object will contain: + +- `scim_username` - The member's SCIM Username. (will be empty string if user is + not managed by SCIM) +- `scim_groups` - The member's SCIM Groups +- `scim_family_name` - The member's SCIM Family Name +- `scim_given_name` - The member's SCIM Given Name diff --git a/website/github.erb b/website/github.erb index 1bbf446061..20015cef64 100644 --- a/website/github.erb +++ b/website/github.erb @@ -112,6 +112,9 @@
  • github_organization_custom_role
  • +
  • + github_organization_external_identities +
  • github_organization_ip_allow_list