Skip to content

Commit

Permalink
feat: add data source to get organization members' SAML/SCIM linked i…
Browse files Browse the repository at this point in the history
…dentities (#1778)

* add `github_organization_external_identities` which returns a list of github organization members and their SAML linked identity

* add docs

* add more fields to external_identities

* docs

---------

Co-authored-by: Keegan Campbell <me@kfcampbell.com>
  • Loading branch information
jsifuentes and kfcampbell committed Jul 24, 2023
1 parent f677f36 commit dc087bd
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 0 deletions.
127 changes: 127 additions & 0 deletions github/data_source_github_organization_external_identities.go
Original file line number Diff line number Diff line change
@@ -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
}
47 changes: 47 additions & 0 deletions github/data_source_github_organization_external_identities_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
})
}
1 change: 1 addition & 0 deletions github/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,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(),
Expand Down
50 changes: 50 additions & 0 deletions website/docs/d/organization_external_identities.markdown
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions website/github.erb
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@
<li>
<a href="/docs/providers/github/d/organization_custom_role.html">github_organization_custom_role</a>
</li>
<li>
<a href="/docs/providers/github/d/organization_external_identities.html">github_organization_external_identities</a>
</li>
<li>
<a href="/docs/providers/github/d/organization_ip_allow_list.html">github_organization_ip_allow_list</a>
</li>
Expand Down

0 comments on commit dc087bd

Please sign in to comment.