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