1package github
2
3import (
4	"context"
5	"fmt"
6	"net/url"
7	"strings"
8
9	"github.com/google/go-github/github"
10	"github.com/hashicorp/vault/sdk/framework"
11	"github.com/hashicorp/vault/sdk/helper/cidrutil"
12	"github.com/hashicorp/vault/sdk/helper/policyutil"
13	"github.com/hashicorp/vault/sdk/logical"
14)
15
16func pathLogin(b *backend) *framework.Path {
17	return &framework.Path{
18		Pattern: "login",
19		Fields: map[string]*framework.FieldSchema{
20			"token": {
21				Type:        framework.TypeString,
22				Description: "GitHub personal API token",
23			},
24		},
25
26		Callbacks: map[logical.Operation]framework.OperationFunc{
27			logical.UpdateOperation:         b.pathLogin,
28			logical.AliasLookaheadOperation: b.pathLoginAliasLookahead,
29		},
30	}
31}
32
33func (b *backend) pathLoginAliasLookahead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
34	token := data.Get("token").(string)
35
36	var verifyResp *verifyCredentialsResp
37	if verifyResponse, resp, err := b.verifyCredentials(ctx, req, token); err != nil {
38		return nil, err
39	} else if resp != nil {
40		return resp, nil
41	} else {
42		verifyResp = verifyResponse
43	}
44
45	return &logical.Response{
46		Auth: &logical.Auth{
47			Alias: &logical.Alias{
48				Name: *verifyResp.User.Login,
49			},
50		},
51	}, nil
52}
53
54func (b *backend) pathLogin(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
55	token := data.Get("token").(string)
56
57	var verifyResp *verifyCredentialsResp
58	if verifyResponse, resp, err := b.verifyCredentials(ctx, req, token); err != nil {
59		return nil, err
60	} else if resp != nil {
61		return resp, nil
62	} else {
63		verifyResp = verifyResponse
64	}
65
66	auth := &logical.Auth{
67		InternalData: map[string]interface{}{
68			"token": token,
69		},
70		Metadata: map[string]string{
71			"username": *verifyResp.User.Login,
72			"org":      *verifyResp.Org.Login,
73		},
74		DisplayName: *verifyResp.User.Login,
75		Alias: &logical.Alias{
76			Name: *verifyResp.User.Login,
77		},
78	}
79	verifyResp.Config.PopulateTokenAuth(auth)
80
81	// Add in configured policies from user/group mapping
82	if len(verifyResp.Policies) > 0 {
83		auth.Policies = append(auth.Policies, verifyResp.Policies...)
84	}
85
86	resp := &logical.Response{
87		Auth: auth,
88	}
89
90	for _, teamName := range verifyResp.TeamNames {
91		if teamName == "" {
92			continue
93		}
94		resp.Auth.GroupAliases = append(resp.Auth.GroupAliases, &logical.Alias{
95			Name: teamName,
96		})
97	}
98
99	return resp, nil
100}
101
102func (b *backend) pathLoginRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
103	if req.Auth == nil {
104		return nil, fmt.Errorf("request auth was nil")
105	}
106
107	tokenRaw, ok := req.Auth.InternalData["token"]
108	if !ok {
109		return nil, fmt.Errorf("token created in previous version of Vault cannot be validated properly at renewal time")
110	}
111	token := tokenRaw.(string)
112
113	var verifyResp *verifyCredentialsResp
114	if verifyResponse, resp, err := b.verifyCredentials(ctx, req, token); err != nil {
115		return nil, err
116	} else if resp != nil {
117		return resp, nil
118	} else {
119		verifyResp = verifyResponse
120	}
121	if !policyutil.EquivalentPolicies(verifyResp.Policies, req.Auth.TokenPolicies) {
122		return nil, fmt.Errorf("policies do not match")
123	}
124
125	resp := &logical.Response{Auth: req.Auth}
126	resp.Auth.Period = verifyResp.Config.TokenPeriod
127	resp.Auth.TTL = verifyResp.Config.TokenTTL
128	resp.Auth.MaxTTL = verifyResp.Config.TokenMaxTTL
129
130	// Remove old aliases
131	resp.Auth.GroupAliases = nil
132
133	for _, teamName := range verifyResp.TeamNames {
134		resp.Auth.GroupAliases = append(resp.Auth.GroupAliases, &logical.Alias{
135			Name: teamName,
136		})
137	}
138
139	return resp, nil
140}
141
142func (b *backend) verifyCredentials(ctx context.Context, req *logical.Request, token string) (*verifyCredentialsResp, *logical.Response, error) {
143	config, err := b.Config(ctx, req.Storage)
144	if err != nil {
145		return nil, nil, err
146	}
147	if config == nil {
148		return nil, logical.ErrorResponse("configuration has not been set"), nil
149	}
150
151	// Check for a CIDR match.
152	if len(config.TokenBoundCIDRs) > 0 {
153		if req.Connection == nil {
154			b.Logger().Warn("token bound CIDRs found but no connection information available for validation")
155			return nil, nil, logical.ErrPermissionDenied
156		}
157		if !cidrutil.RemoteAddrIsOk(req.Connection.RemoteAddr, config.TokenBoundCIDRs) {
158			return nil, nil, logical.ErrPermissionDenied
159		}
160	}
161
162	if config.Organization == "" {
163		return nil, logical.ErrorResponse(
164			"organization not found in configuration"), nil
165	}
166
167	client, err := b.Client(token)
168	if err != nil {
169		return nil, nil, err
170	}
171
172	if config.BaseURL != "" {
173		parsedURL, err := url.Parse(config.BaseURL)
174		if err != nil {
175			return nil, nil, fmt.Errorf("successfully parsed base_url when set but failing to parse now: %w", err)
176		}
177		client.BaseURL = parsedURL
178	}
179
180	// Get the user
181	user, _, err := client.Users.Get(ctx, "")
182	if err != nil {
183		return nil, nil, err
184	}
185
186	// Verify that the user is part of the organization
187	var org *github.Organization
188
189	orgOpt := &github.ListOptions{
190		PerPage: 100,
191	}
192
193	var allOrgs []*github.Organization
194	for {
195		orgs, resp, err := client.Organizations.List(ctx, "", orgOpt)
196		if err != nil {
197			return nil, nil, err
198		}
199		allOrgs = append(allOrgs, orgs...)
200		if resp.NextPage == 0 {
201			break
202		}
203		orgOpt.Page = resp.NextPage
204	}
205
206	for _, o := range allOrgs {
207		if strings.EqualFold(*o.Login, config.Organization) {
208			org = o
209			break
210		}
211	}
212	if org == nil {
213		return nil, logical.ErrorResponse("user is not part of required org"), nil
214	}
215
216	// Get the teams that this user is part of to determine the policies
217	var teamNames []string
218
219	teamOpt := &github.ListOptions{
220		PerPage: 100,
221	}
222
223	var allTeams []*github.Team
224	for {
225		teams, resp, err := client.Teams.ListUserTeams(ctx, teamOpt)
226		if err != nil {
227			return nil, nil, err
228		}
229		allTeams = append(allTeams, teams...)
230		if resp.NextPage == 0 {
231			break
232		}
233		teamOpt.Page = resp.NextPage
234	}
235
236	for _, t := range allTeams {
237		// We only care about teams that are part of the organization we use
238		if *t.Organization.ID != *org.ID {
239			continue
240		}
241
242		// Append the names so we can get the policies
243		teamNames = append(teamNames, *t.Name)
244		if *t.Name != *t.Slug {
245			teamNames = append(teamNames, *t.Slug)
246		}
247	}
248
249	groupPoliciesList, err := b.TeamMap.Policies(ctx, req.Storage, teamNames...)
250	if err != nil {
251		return nil, nil, err
252	}
253
254	userPoliciesList, err := b.UserMap.Policies(ctx, req.Storage, []string{*user.Login}...)
255	if err != nil {
256		return nil, nil, err
257	}
258
259	return &verifyCredentialsResp{
260		User:      user,
261		Org:       org,
262		Policies:  append(groupPoliciesList, userPoliciesList...),
263		TeamNames: teamNames,
264		Config:    config,
265	}, nil, nil
266}
267
268type verifyCredentialsResp struct {
269	User      *github.User
270	Org       *github.Organization
271	Policies  []string
272	TeamNames []string
273
274	// This is just a cache to send back to the caller
275	Config *config
276}
277