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