github.com/mmatczuk/gohan@v0.0.0-20170206152520-30e45d9bdb69/cloud/keystone.go (about) 1 // Copyright (C) 2015 NTT Innovation Institute, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 // implied. 13 // See the License for the specific language governing permissions and 14 // limitations under the License. 15 16 package cloud 17 18 import ( 19 "fmt" 20 "net/http" 21 "regexp" 22 23 "github.com/cloudwan/gohan/schema" 24 "github.com/rackspace/gophercloud" 25 "github.com/rackspace/gophercloud/openstack" 26 v2tenants "github.com/rackspace/gophercloud/openstack/identity/v2/tenants" 27 v3tenants "github.com/rackspace/gophercloud/openstack/identity/v3/projects" 28 v3tokens "github.com/rackspace/gophercloud/openstack/identity/v3/tokens" 29 "github.com/rackspace/gophercloud/pagination" 30 ) 31 32 const maxReauthAttempts = 3 33 34 //KeystoneIdentity middleware 35 type KeystoneIdentity struct { 36 Client KeystoneClient 37 } 38 39 // VerifyToken verifies identity 40 func (identity *KeystoneIdentity) VerifyToken(token string) (schema.Authorization, error) { 41 return identity.Client.VerifyToken(token) 42 } 43 44 // GetTenantID maps the given tenant/project name to the tenant's/project's ID 45 func (identity *KeystoneIdentity) GetTenantID(tenantName string) (string, error) { 46 return identity.Client.GetTenantID(tenantName) 47 } 48 49 // GetTenantName maps the given tenant/project name to the tenant's/project's ID 50 func (identity *KeystoneIdentity) GetTenantName(tenantID string) (string, error) { 51 return identity.Client.GetTenantName(tenantID) 52 } 53 54 // GetServiceAuthorization returns the master authorization with full permissions 55 func (identity *KeystoneIdentity) GetServiceAuthorization() (schema.Authorization, error) { 56 return identity.Client.GetServiceAuthorization() 57 } 58 59 // GetClient returns openstack client 60 func (identity *KeystoneIdentity) GetClient() *gophercloud.ServiceClient { 61 return identity.Client.GetClient() 62 } 63 64 //KeystoneClient represents keystone client 65 type KeystoneClient interface { 66 GetTenantID(string) (string, error) 67 GetTenantName(string) (string, error) 68 VerifyToken(string) (schema.Authorization, error) 69 GetServiceAuthorization() (schema.Authorization, error) 70 GetClient() *gophercloud.ServiceClient 71 } 72 73 type keystoneV2Client struct { 74 client *gophercloud.ServiceClient 75 } 76 77 type keystoneV3Client struct { 78 client *gophercloud.ServiceClient 79 } 80 81 func matchVersionFromAuthURL(authURL string) (version string) { 82 re := regexp.MustCompile(`(?P<version>v[\d\.]+)/?$`) 83 match := re.FindStringSubmatch(authURL) 84 for i, name := range re.SubexpNames() { 85 if name == "version" && i < len(match) { 86 version = match[i] 87 break 88 } 89 } 90 return 91 } 92 93 //NewKeystoneIdentity is an constructor for KeystoneIdentity middleware 94 func NewKeystoneIdentity(authURL, userName, password, domainName, tenantName, version string) (*KeystoneIdentity, error) { 95 var client KeystoneClient 96 var err error 97 if version == "" { 98 version = matchVersionFromAuthURL(authURL) 99 } 100 if version == "v2.0" { 101 client, err = NewKeystoneV2Client(authURL, userName, password, tenantName) 102 } else if version == "v3" { 103 client, err = NewKeystoneV3Client(authURL, userName, password, domainName, tenantName) 104 } else { 105 return nil, fmt.Errorf("Unsupported keystone version: %s", version) 106 } 107 if err != nil { 108 return nil, err 109 } 110 return &KeystoneIdentity{ 111 Client: client, 112 }, nil 113 } 114 115 //RoundTripper limits number of Reauth attempts 116 type RoundTripper struct { 117 rt http.RoundTripper 118 numReauthAttempts int 119 maxReauthAttempts int 120 } 121 122 //NewHTTPClient returns http client with max reauth retry support 123 func NewHTTPClient() http.Client { 124 return http.Client{ 125 Transport: &RoundTripper{ 126 rt: http.DefaultTransport, 127 maxReauthAttempts: maxReauthAttempts, 128 }, 129 } 130 } 131 132 //RoundTrip limits number of Reauth attempts 133 func (lrt *RoundTripper) RoundTrip(request *http.Request) (*http.Response, error) { 134 var err error 135 136 response, err := lrt.rt.RoundTrip(request) 137 if response == nil { 138 return nil, err 139 } 140 141 if response.StatusCode == http.StatusUnauthorized { 142 if lrt.numReauthAttempts == lrt.maxReauthAttempts { 143 return response, fmt.Errorf("Failed to reauthenticate to keystone with %d attempts", lrt.maxReauthAttempts) 144 } 145 lrt.numReauthAttempts++ 146 } 147 return response, err 148 } 149 150 //NewKeystoneV2Client is an constructor for KeystoneV2Client 151 func NewKeystoneV2Client(authURL, userName, password, tenantName string) (KeystoneClient, error) { 152 opts := gophercloud.AuthOptions{ 153 IdentityEndpoint: authURL, 154 Username: userName, 155 Password: password, 156 TenantName: tenantName, 157 AllowReauth: true, 158 } 159 160 client, err := openstack.AuthenticatedClient(opts) 161 if err != nil { 162 return nil, err 163 } 164 return &keystoneV2Client{client: openstack.NewIdentityV2(client)}, nil 165 } 166 167 //NewKeystoneV3Client is an constructor for KeystoneV3Client 168 func NewKeystoneV3Client(authURL, userName, password, domainName, tenantName string) (KeystoneClient, error) { 169 opts := gophercloud.AuthOptions{ 170 IdentityEndpoint: authURL, 171 Username: userName, 172 Password: password, 173 DomainName: domainName, 174 TenantName: tenantName, 175 AllowReauth: true, 176 } 177 178 client, err := openstack.AuthenticatedClient(opts) 179 if err != nil { 180 return nil, err 181 } 182 client.HTTPClient = NewHTTPClient() 183 return &keystoneV3Client{client: openstack.NewIdentityV3(client)}, nil 184 } 185 186 //VerifyToken verifies keystone v3.0 token 187 func (client *keystoneV3Client) VerifyToken(token string) (schema.Authorization, error) { 188 tokenResult := v3tokens.Get(client.client, token) 189 if tokenResult.Err != nil { 190 return nil, fmt.Errorf("Error during verifying token: %s", tokenResult.Err.Error()) 191 } 192 _, err := tokenResult.Extract() 193 if err != nil { 194 return nil, fmt.Errorf("Invalid token") 195 } 196 tokenBody := tokenResult.Body.(map[string]interface{})["token"] 197 roles := tokenBody.(map[string]interface{})["roles"] 198 roleIDs := []string{} 199 for _, roleBody := range roles.([]interface{}) { 200 roleIDs = append(roleIDs, roleBody.(map[string]interface{})["name"].(string)) 201 } 202 tokenBodyMap := tokenBody.(map[string]interface{}) 203 projectObj, ok := tokenBodyMap["project"] 204 if !ok { 205 return nil, fmt.Errorf("Token is unscoped") 206 } 207 project := projectObj.(map[string]interface{}) 208 tenantID := project["id"].(string) 209 tenantName := project["name"].(string) 210 catalogList, ok := tokenBodyMap["catalog"].([]interface{}) 211 catalogObj := []*schema.Catalog{} 212 if ok { 213 for _, rawCatalog := range catalogList { 214 catalog := rawCatalog.(map[string]interface{}) 215 endPoints := []*schema.Endpoint{} 216 rawEndpoints, ok := catalog["endpoints"].([]interface{}) 217 if ok { 218 for _, rawEndpoint := range rawEndpoints { 219 endpoint := rawEndpoint.(map[string]interface{}) 220 endPoints = append(endPoints, 221 schema.NewEndpoint(endpoint["url"].(string), endpoint["region"].(string), endpoint["interface"].(string))) 222 } 223 } 224 catalogObj = append(catalogObj, schema.NewCatalog(catalog["name"].(string), catalog["type"].(string), endPoints)) 225 } 226 } 227 return schema.NewAuthorization(tenantID, tenantName, token, roleIDs, catalogObj), nil 228 } 229 230 // GetTenantID maps the given v3.0 project ID to the projects's name 231 func (client *keystoneV3Client) GetTenantID(tenantName string) (string, error) { 232 tenant, err := client.getTenant(func(tenant *v3tenants.Project) bool { return tenant.Name == tenantName }) 233 if err != nil { 234 return "", err 235 } 236 237 if tenant == nil { 238 return "", fmt.Errorf("Tenant with name '%s' not found", tenantName) 239 } 240 241 return tenant.ID, nil 242 } 243 244 // GetTenantName maps the given v3.0 project name to the projects's ID 245 func (client *keystoneV3Client) GetTenantName(tenantID string) (string, error) { 246 tenant, err := client.getTenant(func(tenant *v3tenants.Project) bool { return tenant.ID == tenantID }) 247 if err != nil { 248 return "", err 249 } 250 251 if tenant == nil { 252 return "", fmt.Errorf("Tenant with ID '%s' not found", tenantID) 253 } 254 255 return tenant.Name, nil 256 } 257 258 // GetServiceAuthorization returns the master authorization with full permissions 259 func (client *keystoneV3Client) GetServiceAuthorization() (schema.Authorization, error) { 260 return client.VerifyToken(client.client.TokenID) 261 } 262 263 // GetClient returns openstack client 264 func (client *keystoneV3Client) GetClient() *gophercloud.ServiceClient { 265 return client.client 266 } 267 268 //VerifyToken verifies keystone v2.0 token 269 func (client *keystoneV2Client) VerifyToken(token string) (schema.Authorization, error) { 270 tokenResult, err := verifyV2Token(client.client, token) 271 if err != nil { 272 return nil, fmt.Errorf("Invalid token") 273 } 274 fmt.Printf("%v", tokenResult) 275 tokenBody := tokenResult.(map[string]interface{})["access"] 276 userBody := tokenBody.(map[string]interface{})["user"] 277 roles := userBody.(map[string]interface{})["roles"] 278 roleIDs := []string{} 279 for _, roleBody := range roles.([]interface{}) { 280 roleIDs = append(roleIDs, roleBody.(map[string]interface{})["name"].(string)) 281 } 282 tokenBodyMap := tokenBody.(map[string]interface{}) 283 tenantObj, ok := tokenBodyMap["token"].(map[string]interface{})["tenant"] 284 if !ok { 285 return nil, fmt.Errorf("Token is unscoped") 286 } 287 tenant := tenantObj.(map[string]interface{}) 288 tenantID := tenant["id"].(string) 289 tenantName := tenant["name"].(string) 290 catalogList := tokenBodyMap["serviceCatalog"].([]interface{}) 291 catalogObj := []*schema.Catalog{} 292 for _, rawCatalog := range catalogList { 293 catalog := rawCatalog.(map[string]interface{}) 294 endPoints := []*schema.Endpoint{} 295 rawEndpoints := catalog["endpoints"].([]interface{}) 296 for _, rawEndpoint := range rawEndpoints { 297 endpoint := rawEndpoint.(map[string]interface{}) 298 region := endpoint["region"].(string) 299 adminURL, ok := endpoint["adminURL"].(string) 300 if ok { 301 endPoints = append(endPoints, 302 schema.NewEndpoint(adminURL, region, "admin")) 303 } 304 internalURL, ok := endpoint["internalURL"].(string) 305 if ok { 306 endPoints = append(endPoints, 307 schema.NewEndpoint(internalURL, region, "internal")) 308 } 309 publicURL, ok := endpoint["publicURL"].(string) 310 if ok { 311 endPoints = append(endPoints, 312 schema.NewEndpoint(publicURL, region, "public")) 313 } 314 } 315 catalogObj = append(catalogObj, schema.NewCatalog(catalog["name"].(string), catalog["type"].(string), endPoints)) 316 } 317 return schema.NewAuthorization(tenantID, tenantName, token, roleIDs, catalogObj), nil 318 } 319 320 // GetTenantID maps the given v2.0 project name to the tenant's id 321 func (client *keystoneV2Client) GetTenantID(tenantName string) (string, error) { 322 tenant, err := client.getTenant(func(tenant *v2tenants.Tenant) bool { return tenant.Name == tenantName }) 323 if err != nil { 324 return "", err 325 } 326 327 if tenant == nil { 328 return "", fmt.Errorf("Tenant with name '%s' not found", tenantName) 329 } 330 331 return tenant.ID, nil 332 } 333 334 // GetTenantName maps the given v2.0 project id to the tenant's name 335 func (client *keystoneV2Client) GetTenantName(tenantID string) (string, error) { 336 tenant, err := client.getTenant(func(tenant *v2tenants.Tenant) bool { return tenant.ID == tenantID }) 337 if err != nil { 338 return "", err 339 } 340 341 if tenant == nil { 342 return "", fmt.Errorf("Tenant with ID '%s' not found", tenantID) 343 } 344 345 return tenant.Name, nil 346 } 347 348 // GetServiceAuthorization returns the master authorization with full permissions 349 func (client *keystoneV2Client) GetServiceAuthorization() (schema.Authorization, error) { 350 return client.VerifyToken(client.client.TokenID) 351 } 352 353 func (client *keystoneV2Client) getTenant(filter func(*v2tenants.Tenant) bool) (*v2tenants.Tenant, error) { 354 opts := v2tenants.ListOpts{} 355 pager := v2tenants.List(client.client, &opts) 356 var result *v2tenants.Tenant 357 err := pager.EachPage(func(page pagination.Page) (bool, error) { 358 tenantsList, err := v2tenants.ExtractTenants(page) 359 if err != nil { 360 return false, err 361 } 362 363 for _, tenant := range tenantsList { 364 if filter(&tenant) { 365 result = &tenant 366 return false, nil 367 } 368 } 369 370 return true, nil 371 }) 372 if err != nil { 373 return nil, err 374 } 375 376 return result, nil 377 } 378 379 func (client *keystoneV3Client) getTenant(filter func(*v3tenants.Project) bool) (*v3tenants.Project, error) { 380 opts := v3tenants.ListOpts{} 381 pager := v3tenants.List(client.client, opts) 382 var result *v3tenants.Project 383 err := pager.EachPage(func(page pagination.Page) (bool, error) { 384 tenantsList, err := v3tenants.ExtractProjects(page) 385 if err != nil { 386 return false, err 387 } 388 389 for _, tenant := range tenantsList { 390 if filter(&tenant) { 391 result = &tenant 392 return false, nil 393 } 394 } 395 396 return true, nil 397 }) 398 if err != nil { 399 return nil, err 400 } 401 402 return result, nil 403 } 404 405 //TODO(nati) this should be implemented in openstack go client side package 406 func verifyV2Token(c *gophercloud.ServiceClient, token string) (interface{}, error) { 407 var result interface{} 408 _, err := c.Get(tokenURL(c, token), &result, &gophercloud.RequestOpts{ 409 OkCodes: []int{200, 203}, 410 }) 411 if err != nil { 412 return nil, err 413 } 414 return result, nil 415 } 416 417 func tokenURL(c *gophercloud.ServiceClient, token string) string { 418 return c.ServiceURL("tokens", token) 419 } 420 421 // GetClient returns openstack client 422 func (client *keystoneV2Client) GetClient() *gophercloud.ServiceClient { 423 return client.client 424 }