Skip to content

Commit

Permalink
feat: expose SAML external identity exposed for GitHub user (#1796)
Browse files Browse the repository at this point in the history
* create github_user_external_identity datasource

* add scim information and error handling for bad username org combo

* cleanup commentzs

* add docs for external identity

* move external identity to its own struct

* add variable to make referencing external identity easier

* add test

* add documentation

* remove old docs

* add docs reference in github.erb

---------

Co-authored-by: Keegan Campbell <[email protected]>
  • Loading branch information
felixlut and kfcampbell authored Aug 11, 2023
1 parent 0fb2e1d commit 77cfacb
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 24 deletions.
50 changes: 26 additions & 24 deletions github/data_source_github_organization_external_identities.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,31 @@ import (
"github.com/shurcooL/githubv4"
)

type 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
}
}

func dataSourceGithubOrganizationExternalIdentities() *schema.Resource {
return &schema.Resource{
Read: dataSourceGithubOrganizationExternalIdentitiesRead,
Expand Down Expand Up @@ -49,30 +74,7 @@ func dataSourceGithubOrganizationExternalIdentitiesRead(d *schema.ResourceData,
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)"`
ExternalIdentities `graphql:"externalIdentities(first: 100, after: $after)"`
}
} `graphql:"organization(login: $login)"`
}
Expand Down
91 changes: 91 additions & 0 deletions github/data_source_github_user_external_identity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package github

import (
"fmt"

"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/shurcooL/githubv4"
)

func dataSourceGithubUserExternalIdentity() *schema.Resource {
return &schema.Resource{
Read: dataSourceGithubUserExternalIdentityRead,

Schema: map[string]*schema.Schema{
"username": {
Type: schema.TypeString,
Required: 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,
},
},
"login": {
Type: schema.TypeString,
Computed: true,
},
},
}
}

func dataSourceGithubUserExternalIdentityRead(d *schema.ResourceData, meta interface{}) error {
username := d.Get("username").(string)

client := meta.(*Owner).v4client
orgName := meta.(*Owner).name

var query struct {
Organization struct {
SamlIdentityProvider struct {
ExternalIdentities `graphql:"externalIdentities(first: 1, login:$username)"` // There should only ever be one external identity configured
}
} `graphql:"organization(login: $orgName)"`
}

variables := map[string]interface{}{
"orgName": githubv4.String(orgName),
"username": githubv4.String(username),
}

err := client.Query(meta.(*Owner).StopContext, &query, variables)
if err != nil {
return err
}
if len(query.Organization.SamlIdentityProvider.ExternalIdentities.Edges) == 0 {
return fmt.Errorf("there was no external identity found for username %q in Organization %q", username, orgName)
}

externalIdentityNode := query.Organization.SamlIdentityProvider.ExternalIdentities.Edges[0].Node // There should only be one user in this list

samlIdentity := map[string]string{
"family_name": string(externalIdentityNode.SamlIdentity.FamilyName),
"given_name": string(externalIdentityNode.SamlIdentity.GivenName),
"name_id": string(externalIdentityNode.SamlIdentity.NameId),
"username": string(externalIdentityNode.SamlIdentity.Username),
}

scimIdentity := map[string]string{
"family_name": string(externalIdentityNode.ScimIdentity.FamilyName),
"given_name": string(externalIdentityNode.ScimIdentity.GivenName),
"username": string(externalIdentityNode.ScimIdentity.Username),
}

login := string(externalIdentityNode.User.Login)

d.SetId(fmt.Sprintf("%s/%s", orgName, username))
d.Set("saml_identity", samlIdentity)
d.Set("scim_identity", scimIdentity)
d.Set("login", login)
d.Set("username", login)
return nil
}
51 changes: 51 additions & 0 deletions github/data_source_github_user_external_identity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package github

import (
"testing"

"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
)

func TestAccGithubUserExternalIdentity(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_user_external_identity" "test" {
}`

check := resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrSet("data.github_user_external_identity.test", "login"),
resource.TestCheckResourceAttrSet("data.github_user_external_identity.test", "saml_identity.name_id"),
resource.TestCheckResourceAttrSet("data.github_user_external_identity.test", "scim_identity.username"),
)

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 user accoy", 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 @@ -218,6 +218,7 @@ func Provider() terraform.ResourceProvider {
"github_team": dataSourceGithubTeam(),
"github_tree": dataSourceGithubTree(),
"github_user": dataSourceGithubUser(),
"github_user_external_identity": dataSourceGithubUserExternalIdentity(),
"github_users": dataSourceGithubUsers(),
"github_enterprise": dataSourceGithubEnterprise(),
},
Expand Down
51 changes: 51 additions & 0 deletions website/docs/d/user_external_identity.html.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
layout: "github"
page_title: "GitHub: github_user_external_identity"
description: |-
Get a specific organization member's SAML/SCIM linked external identity
---

# github_user_external_identity

Use this data source to retrieve a specific organization member's SAML or SCIM user
attributes.

## Example Usage

```hcl
data "github_user_external_identity" "example_user" {
username = "example-user"
}
```

## Argument Reference

The following arguments are supported:

- `username` - (Required) The username of the member to fetch external identity for.

## Attributes Reference

- `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_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 @@ -178,6 +178,9 @@
<li>
<a href="/docs/providers/github/d/user.html">github_user</a>
</li>
<li>
<a href="/docs/providers/github/d/user_external_identity.html">github_user_external_identity</a>
</li>
<li>
<a href="/docs/providers/github/d/users.html">github_users</a>
</li>
Expand Down

0 comments on commit 77cfacb

Please sign in to comment.