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