// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package github import ( "context" "errors" "fmt" "net/url" "github.com/google/go-github/github" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/helper/cidrutil" "github.com/hashicorp/vault/sdk/helper/policyutil" "github.com/hashicorp/vault/sdk/logical" ) func pathLogin(b *backend) *framework.Path { return &framework.Path{ Pattern: "login", DisplayAttrs: &framework.DisplayAttributes{ OperationPrefix: operationPrefixGithub, OperationVerb: "login", }, Fields: map[string]*framework.FieldSchema{ "token": { Type: framework.TypeString, Description: "GitHub personal API token", }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ logical.UpdateOperation: b.pathLogin, logical.AliasLookaheadOperation: b.pathLoginAliasLookahead, }, } } func (b *backend) pathLoginAliasLookahead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { token := data.Get("token").(string) verifyResp, err := b.verifyCredentials(ctx, req, token) if err != nil { return nil, err } return &logical.Response{ Warnings: verifyResp.Warnings, Auth: &logical.Auth{ Alias: &logical.Alias{ Name: *verifyResp.User.Login, }, }, }, nil } func (b *backend) pathLogin(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { token := data.Get("token").(string) verifyResp, err := b.verifyCredentials(ctx, req, token) if err != nil { return nil, err } auth := &logical.Auth{ InternalData: map[string]interface{}{ "token": token, }, Metadata: map[string]string{ "username": *verifyResp.User.Login, "org": *verifyResp.Org.Login, }, DisplayName: *verifyResp.User.Login, Alias: &logical.Alias{ Name: *verifyResp.User.Login, }, } verifyResp.Config.PopulateTokenAuth(auth) // Add in configured policies from user/group mapping if len(verifyResp.Policies) > 0 { auth.Policies = append(auth.Policies, verifyResp.Policies...) } resp := &logical.Response{ Warnings: verifyResp.Warnings, Auth: auth, } for _, teamName := range verifyResp.TeamNames { if teamName == "" { continue } resp.Auth.GroupAliases = append(resp.Auth.GroupAliases, &logical.Alias{ Name: teamName, }) } return resp, nil } func (b *backend) pathLoginRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { if req.Auth == nil { return nil, fmt.Errorf("request auth was nil") } tokenRaw, ok := req.Auth.InternalData["token"] if !ok { return nil, fmt.Errorf("token created in previous version of Vault cannot be validated properly at renewal time") } token := tokenRaw.(string) verifyResp, err := b.verifyCredentials(ctx, req, token) if err != nil { return nil, err } if !policyutil.EquivalentPolicies(verifyResp.Policies, req.Auth.TokenPolicies) { return nil, fmt.Errorf("policies do not match") } resp := &logical.Response{Auth: req.Auth} resp.Auth.Period = verifyResp.Config.TokenPeriod resp.Auth.TTL = verifyResp.Config.TokenTTL resp.Auth.MaxTTL = verifyResp.Config.TokenMaxTTL resp.Warnings = verifyResp.Warnings // Remove old aliases resp.Auth.GroupAliases = nil for _, teamName := range verifyResp.TeamNames { resp.Auth.GroupAliases = append(resp.Auth.GroupAliases, &logical.Alias{ Name: teamName, }) } return resp, nil } func (b *backend) verifyCredentials(ctx context.Context, req *logical.Request, token string) (*verifyCredentialsResp, error) { var warnings []string config, err := b.Config(ctx, req.Storage) if err != nil { return nil, err } if config == nil { return nil, errors.New("configuration has not been set") } // Check for a CIDR match. if len(config.TokenBoundCIDRs) > 0 { if req.Connection == nil { b.Logger().Error("token bound CIDRs found but no connection information available for validation") return nil, logical.ErrPermissionDenied } if !cidrutil.RemoteAddrIsOk(req.Connection.RemoteAddr, config.TokenBoundCIDRs) { return nil, logical.ErrPermissionDenied } } client, err := b.Client(token) if err != nil { return nil, err } if config.BaseURL != "" { parsedURL, err := url.Parse(config.BaseURL) if err != nil { return nil, fmt.Errorf("successfully parsed base_url when set but failing to parse now: %w", err) } client.BaseURL = parsedURL } if config.OrganizationID == 0 { // Previously we did not verify using the Org ID. So if the Org ID is // not set, we will trust-on-first-use and set it now. err = config.setOrganizationID(ctx, client) if err != nil { b.Logger().Error("failed to set the organization_id on login", "error", err) return nil, err } entry, err := logical.StorageEntryJSON("config", config) if err != nil { return nil, err } if err := req.Storage.Put(ctx, entry); err != nil { return nil, err } b.Logger().Info("set ID on a trust-on-first-use basis", "organization_id", config.OrganizationID) } // Get the user user, _, err := client.Users.Get(ctx, "") if err != nil { return nil, err } // Verify that the user is part of the organization var org *github.Organization orgOpt := &github.ListOptions{ PerPage: 100, } var allOrgs []*github.Organization for { orgs, resp, err := client.Organizations.List(ctx, "", orgOpt) if err != nil { return nil, err } allOrgs = append(allOrgs, orgs...) if resp.NextPage == 0 { break } orgOpt.Page = resp.NextPage } orgLoginName := "" for _, o := range allOrgs { if o.GetID() == config.OrganizationID { org = o orgLoginName = *o.Login break } } if org == nil { return nil, errors.New("user is not part of required org") } if orgLoginName != config.Organization { warningMsg := fmt.Sprintf( "the organization name has changed to %q. It is recommended to verify and update the organization name in the config: %s=%d", orgLoginName, "organization_id", config.OrganizationID, ) b.Logger().Warn(warningMsg) warnings = append(warnings, warningMsg) } // Get the teams that this user is part of to determine the policies var teamNames []string teamOpt := &github.ListOptions{ PerPage: 100, } var allTeams []*github.Team for { teams, resp, err := client.Teams.ListUserTeams(ctx, teamOpt) if err != nil { return nil, err } allTeams = append(allTeams, teams...) if resp.NextPage == 0 { break } teamOpt.Page = resp.NextPage } for _, t := range allTeams { // We only care about teams that are part of the organization we use if *t.Organization.ID != *org.ID { continue } // Append the names so we can get the policies teamNames = append(teamNames, *t.Name) if *t.Name != *t.Slug { teamNames = append(teamNames, *t.Slug) } } groupPoliciesList, err := b.TeamMap.Policies(ctx, req.Storage, teamNames...) if err != nil { return nil, err } userPoliciesList, err := b.UserMap.Policies(ctx, req.Storage, []string{*user.Login}...) if err != nil { return nil, err } verifyResp := &verifyCredentialsResp{ User: user, Org: org, Policies: append(groupPoliciesList, userPoliciesList...), TeamNames: teamNames, Config: config, Warnings: warnings, } return verifyResp, nil } type verifyCredentialsResp struct { User *github.User Org *github.Organization Policies []string TeamNames []string // Warnings to send back to the caller Warnings []string // This is just a cache to send back to the caller Config *config }