diff --git a/github/data_source_github_user_external_identity_by_saml.go b/github/data_source_github_user_external_identity_by_saml.go new file mode 100644 index 0000000000..879cae2323 --- /dev/null +++ b/github/data_source_github_user_external_identity_by_saml.go @@ -0,0 +1,102 @@ +package github + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/shurcooL/githubv4" +) + +func dataSourceGithubUserExternalIdentityBySaml() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceGithubUserExternalIdentityBySamlRead, + + Schema: map[string]*schema.Schema{ + "saml_name_id": { + Type: schema.TypeString, + Required: true, + Description: "The SAML NameID (typically an email address) to look up.", + }, + "login": { + Type: schema.TypeString, + Computed: true, + Description: "The GitHub username linked to this SAML identity.", + }, + "username": { + Type: schema.TypeString, + Computed: true, + Description: "The GitHub username linked to this SAML identity (same as login).", + }, + "saml_identity": { + Type: schema.TypeMap, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "The SAML identity attributes.", + }, + "scim_identity": { + Type: schema.TypeMap, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "The SCIM identity attributes.", + }, + }, + } +} + +func dataSourceGithubUserExternalIdentityBySamlRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + samlNameId := d.Get("saml_name_id").(string) + + client := meta.(*Owner).v4client + orgName := meta.(*Owner).name + + var query struct { + Organization struct { + SamlIdentityProvider struct { + ExternalIdentities `graphql:"externalIdentities(first: 1, userName:$userName)"` + } + } `graphql:"organization(login: $orgName)"` + } + + variables := map[string]any{ + "orgName": githubv4.String(orgName), + "userName": githubv4.String(samlNameId), + } + + err := client.Query(meta.(*Owner).StopContext, &query, variables) + if err != nil { + return diag.FromErr(err) + } + if len(query.Organization.SamlIdentityProvider.Edges) == 0 { + return diag.Errorf("no external identity found for SAML NameID %q in organization %q", samlNameId, orgName) + } + + node := query.Organization.SamlIdentityProvider.ExternalIdentities.Edges[0].Node + + samlIdentity := map[string]string{ + "family_name": string(node.SamlIdentity.FamilyName), + "given_name": string(node.SamlIdentity.GivenName), + "name_id": string(node.SamlIdentity.NameId), + "username": string(node.SamlIdentity.Username), + } + + scimIdentity := map[string]string{ + "family_name": string(node.ScimIdentity.FamilyName), + "given_name": string(node.ScimIdentity.GivenName), + "username": string(node.ScimIdentity.Username), + } + + login := string(node.User.Login) + + d.SetId(fmt.Sprintf("%s/%s", orgName, samlNameId)) + _ = d.Set("saml_identity", samlIdentity) + _ = d.Set("scim_identity", scimIdentity) + _ = d.Set("login", login) + _ = d.Set("username", login) + return nil +} diff --git a/github/data_source_github_user_external_identity_by_saml_test.go b/github/data_source_github_user_external_identity_by_saml_test.go new file mode 100644 index 0000000000..2c5a0246d8 --- /dev/null +++ b/github/data_source_github_user_external_identity_by_saml_test.go @@ -0,0 +1,31 @@ +package github + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccGithubUserExternalIdentityBySaml(t *testing.T) { + t.Run("queries without error", func(t *testing.T) { + config := `data "github_user_external_identity_by_saml" "test" { saml_name_id = "%s" }` + + check := resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("data.github_user_external_identity_by_saml.test", "login"), + resource.TestCheckResourceAttrSet("data.github_user_external_identity_by_saml.test", "username"), + resource.TestCheckResourceAttrSet("data.github_user_external_identity_by_saml.test", "saml_identity.name_id"), + ) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, enterprise) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(config, testAccConf.testExternalUser), + Check: check, + }, + }, + }) + }) +} diff --git a/github/provider.go b/github/provider.go index 2d5d6dc33a..ac115781c6 100644 --- a/github/provider.go +++ b/github/provider.go @@ -292,6 +292,7 @@ func Provider() *schema.Provider { "github_tree": dataSourceGithubTree(), "github_user": dataSourceGithubUser(), "github_user_external_identity": dataSourceGithubUserExternalIdentity(), + "github_user_external_identity_by_saml": dataSourceGithubUserExternalIdentityBySaml(), "github_users": dataSourceGithubUsers(), "github_enterprise": dataSourceGithubEnterprise(), "github_repository_environment_deployment_policies": dataSourceGithubRepositoryEnvironmentDeploymentPolicies(), diff --git a/website/docs/d/user_external_identity_by_saml.html.markdown b/website/docs/d/user_external_identity_by_saml.html.markdown new file mode 100644 index 0000000000..336133779d --- /dev/null +++ b/website/docs/d/user_external_identity_by_saml.html.markdown @@ -0,0 +1,47 @@ +--- +layout: "github" +page_title: "GitHub: github_user_external_identity_by_saml" +description: |- + Look up a GitHub user by their SAML NameID. +--- + +# github\_user\_external\_identity\_by\_saml + +Use this data source to retrieve a GitHub user's login by their SAML NameID +(typically an email address). This is a reverse lookup — given a SAML identity, +it returns the linked GitHub username. + +This complements `github_user_external_identity`, which performs the opposite +lookup (GitHub username to SAML/SCIM identity). + +## Example Usage + +```hcl +data "github_user_external_identity_by_saml" "example" { + saml_name_id = "user@example.com" +} + +resource "github_team_membership" "example" { + team_id = github_team.some_team.id + username = data.github_user_external_identity_by_saml.example.login +} +``` + +## Argument Reference + +* `saml_name_id` - (Required) The SAML NameID to look up. This is typically + the user's email address as configured in your identity provider. + +## Attribute Reference + +* `login` - The GitHub username linked to the SAML identity. +* `username` - Same as `login`. +* `saml_identity` - A map of SAML identity attributes: + * `name_id` - The SAML NameID value. + * `username` - The SAML username. + * `given_name` - The user's given name. + * `family_name` - The user's family name. +* `scim_identity` - A map of SCIM identity attributes: + * `username` - The SCIM username. + * `given_name` - The user's given name. + * `family_name` - The user's family name.