Skip to content

Commit 77cfacb

Browse files
felixlutkfcampbell
andauthored
feat: expose SAML external identity exposed for GitHub user (#1796)
* 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]>
1 parent 0fb2e1d commit 77cfacb

6 files changed

+223
-24
lines changed

github/data_source_github_organization_external_identities.go

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,31 @@ import (
55
"github.com/shurcooL/githubv4"
66
)
77

8+
type ExternalIdentities struct {
9+
Edges []struct {
10+
Node struct {
11+
User struct {
12+
Login githubv4.String
13+
}
14+
SamlIdentity struct {
15+
NameId githubv4.String
16+
Username githubv4.String
17+
GivenName githubv4.String
18+
FamilyName githubv4.String
19+
}
20+
ScimIdentity struct {
21+
Username githubv4.String
22+
GivenName githubv4.String
23+
FamilyName githubv4.String
24+
}
25+
}
26+
}
27+
PageInfo struct {
28+
EndCursor githubv4.String
29+
HasNextPage bool
30+
}
31+
}
32+
833
func dataSourceGithubOrganizationExternalIdentities() *schema.Resource {
934
return &schema.Resource{
1035
Read: dataSourceGithubOrganizationExternalIdentitiesRead,
@@ -49,30 +74,7 @@ func dataSourceGithubOrganizationExternalIdentitiesRead(d *schema.ResourceData,
4974
var query struct {
5075
Organization struct {
5176
SamlIdentityProvider struct {
52-
ExternalIdentities struct {
53-
Edges []struct {
54-
Node struct {
55-
User struct {
56-
Login githubv4.String
57-
}
58-
SamlIdentity struct {
59-
NameId githubv4.String
60-
Username githubv4.String
61-
GivenName githubv4.String
62-
FamilyName githubv4.String
63-
}
64-
ScimIdentity struct {
65-
Username githubv4.String
66-
GivenName githubv4.String
67-
FamilyName githubv4.String
68-
}
69-
}
70-
}
71-
PageInfo struct {
72-
EndCursor githubv4.String
73-
HasNextPage bool
74-
}
75-
} `graphql:"externalIdentities(first: 100, after: $after)"`
77+
ExternalIdentities `graphql:"externalIdentities(first: 100, after: $after)"`
7678
}
7779
} `graphql:"organization(login: $login)"`
7880
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package github
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
7+
"github.com/shurcooL/githubv4"
8+
)
9+
10+
func dataSourceGithubUserExternalIdentity() *schema.Resource {
11+
return &schema.Resource{
12+
Read: dataSourceGithubUserExternalIdentityRead,
13+
14+
Schema: map[string]*schema.Schema{
15+
"username": {
16+
Type: schema.TypeString,
17+
Required: true,
18+
},
19+
"saml_identity": {
20+
Type: schema.TypeMap,
21+
Computed: true,
22+
Elem: &schema.Schema{
23+
Type: schema.TypeString,
24+
},
25+
},
26+
"scim_identity": {
27+
Type: schema.TypeMap,
28+
Computed: true,
29+
Elem: &schema.Schema{
30+
Type: schema.TypeString,
31+
},
32+
},
33+
"login": {
34+
Type: schema.TypeString,
35+
Computed: true,
36+
},
37+
},
38+
}
39+
}
40+
41+
func dataSourceGithubUserExternalIdentityRead(d *schema.ResourceData, meta interface{}) error {
42+
username := d.Get("username").(string)
43+
44+
client := meta.(*Owner).v4client
45+
orgName := meta.(*Owner).name
46+
47+
var query struct {
48+
Organization struct {
49+
SamlIdentityProvider struct {
50+
ExternalIdentities `graphql:"externalIdentities(first: 1, login:$username)"` // There should only ever be one external identity configured
51+
}
52+
} `graphql:"organization(login: $orgName)"`
53+
}
54+
55+
variables := map[string]interface{}{
56+
"orgName": githubv4.String(orgName),
57+
"username": githubv4.String(username),
58+
}
59+
60+
err := client.Query(meta.(*Owner).StopContext, &query, variables)
61+
if err != nil {
62+
return err
63+
}
64+
if len(query.Organization.SamlIdentityProvider.ExternalIdentities.Edges) == 0 {
65+
return fmt.Errorf("there was no external identity found for username %q in Organization %q", username, orgName)
66+
}
67+
68+
externalIdentityNode := query.Organization.SamlIdentityProvider.ExternalIdentities.Edges[0].Node // There should only be one user in this list
69+
70+
samlIdentity := map[string]string{
71+
"family_name": string(externalIdentityNode.SamlIdentity.FamilyName),
72+
"given_name": string(externalIdentityNode.SamlIdentity.GivenName),
73+
"name_id": string(externalIdentityNode.SamlIdentity.NameId),
74+
"username": string(externalIdentityNode.SamlIdentity.Username),
75+
}
76+
77+
scimIdentity := map[string]string{
78+
"family_name": string(externalIdentityNode.ScimIdentity.FamilyName),
79+
"given_name": string(externalIdentityNode.ScimIdentity.GivenName),
80+
"username": string(externalIdentityNode.ScimIdentity.Username),
81+
}
82+
83+
login := string(externalIdentityNode.User.Login)
84+
85+
d.SetId(fmt.Sprintf("%s/%s", orgName, username))
86+
d.Set("saml_identity", samlIdentity)
87+
d.Set("scim_identity", scimIdentity)
88+
d.Set("login", login)
89+
d.Set("username", login)
90+
return nil
91+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package github
2+
3+
import (
4+
"testing"
5+
6+
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
7+
)
8+
9+
func TestAccGithubUserExternalIdentity(t *testing.T) {
10+
if isEnterprise != "true" {
11+
t.Skip("Skipping because `ENTERPRISE_ACCOUNT` is not set or set to false")
12+
}
13+
14+
t.Run("queries without error", func(t *testing.T) {
15+
config := `
16+
data "github_user_external_identity" "test" {
17+
18+
19+
}`
20+
21+
check := resource.ComposeAggregateTestCheckFunc(
22+
resource.TestCheckResourceAttrSet("data.github_user_external_identity.test", "login"),
23+
resource.TestCheckResourceAttrSet("data.github_user_external_identity.test", "saml_identity.name_id"),
24+
resource.TestCheckResourceAttrSet("data.github_user_external_identity.test", "scim_identity.username"),
25+
)
26+
27+
testCase := func(t *testing.T, mode string) {
28+
resource.Test(t, resource.TestCase{
29+
Providers: testAccProviders,
30+
Steps: []resource.TestStep{
31+
{
32+
Config: config,
33+
Check: check,
34+
},
35+
},
36+
})
37+
}
38+
39+
t.Run("with an anonymous account", func(t *testing.T) {
40+
t.Skip("anonymous account not supported for this operation")
41+
})
42+
43+
t.Run("with an individual account", func(t *testing.T) {
44+
t.Skip("individual account not supported for this operation")
45+
})
46+
47+
t.Run("with an user accoy", func(t *testing.T) {
48+
testCase(t, organization)
49+
})
50+
})
51+
}

github/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ func Provider() terraform.ResourceProvider {
218218
"github_team": dataSourceGithubTeam(),
219219
"github_tree": dataSourceGithubTree(),
220220
"github_user": dataSourceGithubUser(),
221+
"github_user_external_identity": dataSourceGithubUserExternalIdentity(),
221222
"github_users": dataSourceGithubUsers(),
222223
"github_enterprise": dataSourceGithubEnterprise(),
223224
},
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
layout: "github"
3+
page_title: "GitHub: github_user_external_identity"
4+
description: |-
5+
Get a specific organization member's SAML/SCIM linked external identity
6+
---
7+
8+
# github_user_external_identity
9+
10+
Use this data source to retrieve a specific organization member's SAML or SCIM user
11+
attributes.
12+
13+
## Example Usage
14+
15+
```hcl
16+
data "github_user_external_identity" "example_user" {
17+
username = "example-user"
18+
}
19+
```
20+
21+
## Argument Reference
22+
23+
The following arguments are supported:
24+
25+
- `username` - (Required) The username of the member to fetch external identity for.
26+
27+
## Attributes Reference
28+
29+
- `login` - The username of the GitHub user
30+
- `saml_identity` - An Object containing the user's SAML data. This object will
31+
be empty if the user is not managed by SAML.
32+
- `scim_identity` - An Object contining the user's SCIM data. This object will
33+
be empty if the user is not managed by SCIM.
34+
35+
---
36+
37+
If a user is managed by SAML, the `saml_identity` object will contain:
38+
39+
- `name_id` - The member's SAML NameID
40+
- `username` - The member's SAML Username
41+
- `family_name` - The member's SAML Family Name
42+
- `given_name` - The member's SAML Given Name
43+
44+
---
45+
46+
If a user is managed by SCIM, the `scim_identity` object will contain:
47+
48+
- `scim_username` - The member's SCIM Username. (will be empty string if user is
49+
not managed by SCIM)
50+
- `scim_family_name` - The member's SCIM Family Name
51+
- `scim_given_name` - The member's SCIM Given Name

website/github.erb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,9 @@
178178
<li>
179179
<a href="/docs/providers/github/d/user.html">github_user</a>
180180
</li>
181+
<li>
182+
<a href="/docs/providers/github/d/user_external_identity.html">github_user_external_identity</a>
183+
</li>
181184
<li>
182185
<a href="/docs/providers/github/d/users.html">github_users</a>
183186
</li>

0 commit comments

Comments
 (0)