github.com/jbking/gohan@v0.0.0-20151217002006-b41ccf1c2a96/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 v3tenants "github.com/cloudwan/gophercloud/openstack/identity/v3/projects" 25 "github.com/rackspace/gophercloud" 26 "github.com/rackspace/gophercloud/openstack" 27 v2tenants "github.com/rackspace/gophercloud/openstack/identity/v2/tenants" 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 permisions 55 func (identity *KeystoneIdentity) GetServiceAuthorization() (schema.Authorization, error) { 56 return identity.Client.GetServiceAuthorization() 57 } 58 59 //KeystoneClient represents keystone client 60 type KeystoneClient interface { 61 GetTenantID(string) (string, error) 62 GetTenantName(string) (string, error) 63 VerifyToken(string) (schema.Authorization, error) 64 GetServiceAuthorization() (schema.Authorization, error) 65 } 66 67 type keystoneV2Client struct { 68 client *gophercloud.ServiceClient 69 } 70 71 type keystoneV3Client struct { 72 client *gophercloud.ServiceClient 73 } 74 75 func matchVersionFromAuthURL(authURL string) (version string) { 76 re := regexp.MustCompile(`(?P<version>v[\d\.]+)/?$`) 77 match := re.FindStringSubmatch(authURL) 78 for i, name := range re.SubexpNames() { 79 if name == "version" && i < len(match) { 80 version = match[i] 81 break 82 } 83 } 84 return 85 } 86 87 //NewKeystoneIdentity is an constructor for KeystoneIdentity middleware 88 func NewKeystoneIdentity(authURL, userName, password, domainName, tenantName, version string) (*KeystoneIdentity, error) { 89 var client KeystoneClient 90 var err error 91 if version == "" { 92 version = matchVersionFromAuthURL(authURL) 93 } 94 if version == "v2.0" { 95 client, err = NewKeystoneV2Client(authURL, userName, password, tenantName) 96 } else if version == "v3" { 97 client, err = NewKeystoneV3Client(authURL, userName, password, domainName, tenantName) 98 } else { 99 return nil, fmt.Errorf("Unsupported keystone version: %s", version) 100 } 101 if err != nil { 102 return nil, err 103 } 104 return &KeystoneIdentity{ 105 Client: client, 106 }, nil 107 } 108 109 //RoundTripper limits number of Reauth attempts 110 type RoundTripper struct { 111 rt http.RoundTripper 112 numReauthAttempts int 113 maxReauthAttempts int 114 } 115 116 func newHTTPClient() http.Client { 117 return http.Client{ 118 Transport: &RoundTripper{ 119 rt: http.DefaultTransport, 120 maxReauthAttempts: maxReauthAttempts, 121 }, 122 } 123 } 124 125 //RoundTrip limits number of Reauth attempts 126 func (lrt *RoundTripper) RoundTrip(request *http.Request) (*http.Response, error) { 127 var err error 128 129 response, err := lrt.rt.RoundTrip(request) 130 if response == nil { 131 return nil, err 132 } 133 134 if response.StatusCode == http.StatusUnauthorized { 135 if lrt.numReauthAttempts == lrt.maxReauthAttempts { 136 return response, fmt.Errorf("Failed to reauthenticate to keystone with %d attempts", lrt.maxReauthAttempts) 137 } 138 lrt.numReauthAttempts++ 139 } 140 return response, err 141 } 142 143 //NewKeystoneV2Client is an constructor for KeystoneV2Client 144 func NewKeystoneV2Client(authURL, userName, password, tenantName string) (KeystoneClient, error) { 145 opts := gophercloud.AuthOptions{ 146 IdentityEndpoint: authURL, 147 Username: userName, 148 Password: password, 149 TenantName: tenantName, 150 AllowReauth: true, 151 } 152 153 client, err := openstack.AuthenticatedClient(opts) 154 if err != nil { 155 return nil, err 156 } 157 return &keystoneV2Client{client: openstack.NewIdentityV2(client)}, nil 158 } 159 160 //NewKeystoneV3Client is an constructor for KeystoneV3Client 161 func NewKeystoneV3Client(authURL, userName, password, domainName, tenantName string) (KeystoneClient, error) { 162 opts := gophercloud.AuthOptions{ 163 IdentityEndpoint: authURL, 164 Username: userName, 165 Password: password, 166 DomainName: domainName, 167 TenantName: tenantName, 168 AllowReauth: true, 169 } 170 171 client, err := openstack.AuthenticatedClient(opts) 172 if err != nil { 173 return nil, err 174 } 175 client.HTTPClient = newHTTPClient() 176 return &keystoneV3Client{client: openstack.NewIdentityV3(client)}, nil 177 } 178 179 //VerifyToken verifies keystone v3.0 token 180 func (client *keystoneV3Client) VerifyToken(token string) (schema.Authorization, error) { 181 tokenResult := v3tokens.Get(client.client, token) 182 if tokenResult.Err != nil { 183 return nil, fmt.Errorf("Error during verifying token: %s", tokenResult.Err.Error()) 184 } 185 _, err := tokenResult.Extract() 186 if err != nil { 187 return nil, fmt.Errorf("Invalid token") 188 } 189 tokenBody := tokenResult.Body.(map[string]interface{})["token"] 190 roles := tokenBody.(map[string]interface{})["roles"] 191 roleIDs := []string{} 192 for _, roleBody := range roles.([]interface{}) { 193 roleIDs = append(roleIDs, roleBody.(map[string]interface{})["name"].(string)) 194 } 195 tokenBodyMap := tokenBody.(map[string]interface{}) 196 project := tokenBodyMap["project"].(map[string]interface{}) 197 tenantID := project["id"].(string) 198 tenantName := project["name"].(string) 199 catalogList, ok := tokenBodyMap["catalog"].([]interface{}) 200 catalogObj := []*schema.Catalog{} 201 if ok { 202 for _, rawCatalog := range catalogList { 203 catalog := rawCatalog.(map[string]interface{}) 204 endPoints := []*schema.Endpoint{} 205 rawEndpoints, ok := catalog["endpoints"].([]interface{}) 206 if ok { 207 for _, rawEndpoint := range rawEndpoints { 208 endpoint := rawEndpoint.(map[string]interface{}) 209 endPoints = append(endPoints, 210 schema.NewEndpoint(endpoint["url"].(string), endpoint["region"].(string), endpoint["interface"].(string))) 211 } 212 } 213 catalogObj = append(catalogObj, schema.NewCatalog(catalog["name"].(string), catalog["type"].(string), endPoints)) 214 } 215 } 216 return schema.NewAuthorization(tenantID, tenantName, token, roleIDs, catalogObj), nil 217 } 218 219 // GetTenantID maps the given v3.0 project ID to the projects's name 220 func (client *keystoneV3Client) GetTenantID(tenantName string) (string, error) { 221 tenant, err := client.getTenant(func(tenant *v3tenants.Project) bool { return tenant.Name == tenantName }) 222 if err != nil { 223 return "", err 224 } 225 226 if tenant == nil { 227 return "", fmt.Errorf("Tenant with name '%s' not found", tenantName) 228 } 229 230 return tenant.ID, nil 231 } 232 233 // GetTenantName maps the given v3.0 project name to the projects's ID 234 func (client *keystoneV3Client) GetTenantName(tenantID string) (string, error) { 235 tenant, err := client.getTenant(func(tenant *v3tenants.Project) bool { return tenant.ID == tenantID }) 236 if err != nil { 237 return "", err 238 } 239 240 if tenant == nil { 241 return "", fmt.Errorf("Tenant with ID '%s' not found", tenantID) 242 } 243 244 return tenant.Name, nil 245 } 246 247 // GetServiceAuthorization returns the master authorization with full permisions 248 func (client *keystoneV3Client) GetServiceAuthorization() (schema.Authorization, error) { 249 return client.VerifyToken(client.client.TokenID) 250 } 251 252 //VerifyToken verifies keystone v2.0 token 253 func (client *keystoneV2Client) VerifyToken(token string) (schema.Authorization, error) { 254 tokenResult, err := verifyV2Token(client.client, token) 255 if err != nil { 256 return nil, fmt.Errorf("Invalid token") 257 } 258 tokenBody := tokenResult.(map[string]interface{})["access"] 259 userBody := tokenBody.(map[string]interface{})["user"] 260 roles := userBody.(map[string]interface{})["roles"] 261 roleIDs := []string{} 262 for _, roleBody := range roles.([]interface{}) { 263 roleIDs = append(roleIDs, roleBody.(map[string]interface{})["name"].(string)) 264 } 265 tokenBodyMap := tokenBody.(map[string]interface{}) 266 tenant := tokenBodyMap["token"].(map[string]interface{})["tenant"].(map[string]interface{}) 267 tenantID := tenant["id"].(string) 268 tenantName := tenant["name"].(string) 269 catalogList := tokenBodyMap["serviceCatalog"].([]interface{}) 270 catalogObj := []*schema.Catalog{} 271 for _, rawCatalog := range catalogList { 272 catalog := rawCatalog.(map[string]interface{}) 273 endPoints := []*schema.Endpoint{} 274 rawEndpoints := catalog["endpoints"].([]interface{}) 275 for _, rawEndpoint := range rawEndpoints { 276 endpoint := rawEndpoint.(map[string]interface{}) 277 region := endpoint["region"].(string) 278 adminURL, ok := endpoint["adminURL"].(string) 279 if ok { 280 endPoints = append(endPoints, 281 schema.NewEndpoint(adminURL, region, "admin")) 282 } 283 internalURL, ok := endpoint["internalURL"].(string) 284 if ok { 285 endPoints = append(endPoints, 286 schema.NewEndpoint(internalURL, region, "internal")) 287 } 288 publicURL, ok := endpoint["publicURL"].(string) 289 if ok { 290 endPoints = append(endPoints, 291 schema.NewEndpoint(publicURL, region, "public")) 292 } 293 } 294 catalogObj = append(catalogObj, schema.NewCatalog(catalog["name"].(string), catalog["type"].(string), endPoints)) 295 } 296 return schema.NewAuthorization(tenantID, tenantName, token, roleIDs, catalogObj), nil 297 } 298 299 // GetTenantID maps the given v2.0 project name to the tenant's id 300 func (client *keystoneV2Client) GetTenantID(tenantName string) (string, error) { 301 tenant, err := client.getTenant(func(tenant *v2tenants.Tenant) bool { return tenant.Name == tenantName }) 302 if err != nil { 303 return "", err 304 } 305 306 if tenant == nil { 307 return "", fmt.Errorf("Tenant with name '%s' not found", tenantName) 308 } 309 310 return tenant.ID, nil 311 } 312 313 // GetTenantName maps the given v2.0 project id to the tenant's name 314 func (client *keystoneV2Client) GetTenantName(tenantID string) (string, error) { 315 tenant, err := client.getTenant(func(tenant *v2tenants.Tenant) bool { return tenant.ID == tenantID }) 316 if err != nil { 317 return "", err 318 } 319 320 if tenant == nil { 321 return "", fmt.Errorf("Tenant with ID '%s' not found", tenantID) 322 } 323 324 return tenant.Name, nil 325 } 326 327 // GetServiceAuthorization returns the master authorization with full permisions 328 func (client *keystoneV2Client) GetServiceAuthorization() (schema.Authorization, error) { 329 return client.VerifyToken(client.client.TokenID) 330 } 331 332 func (client *keystoneV2Client) getTenant(filter func(*v2tenants.Tenant) bool) (*v2tenants.Tenant, error) { 333 opts := v2tenants.ListOpts{} 334 pager := v2tenants.List(client.client, &opts) 335 var result *v2tenants.Tenant 336 err := pager.EachPage(func(page pagination.Page) (bool, error) { 337 tenantsList, err := v2tenants.ExtractTenants(page) 338 if err != nil { 339 return false, err 340 } 341 342 for _, tenant := range tenantsList { 343 if filter(&tenant) { 344 result = &tenant 345 return false, nil 346 } 347 } 348 349 return true, nil 350 }) 351 if err != nil { 352 return nil, err 353 } 354 355 return result, nil 356 } 357 358 func (client *keystoneV3Client) getTenant(filter func(*v3tenants.Project) bool) (*v3tenants.Project, error) { 359 opts := v3tenants.ListOpts{} 360 pager := v3tenants.List(client.client, opts) 361 var result *v3tenants.Project 362 err := pager.EachPage(func(page pagination.Page) (bool, error) { 363 tenantsList, err := v3tenants.ExtractProjects(page) 364 if err != nil { 365 return false, err 366 } 367 368 for _, tenant := range tenantsList { 369 if filter(&tenant) { 370 result = &tenant 371 return false, nil 372 } 373 } 374 375 return true, nil 376 }) 377 if err != nil { 378 return nil, err 379 } 380 381 return result, nil 382 } 383 384 //TODO(nati) this should be implemented in openstack go client side package 385 func verifyV2Token(c *gophercloud.ServiceClient, token string) (interface{}, error) { 386 var result interface{} 387 _, err := c.Get(tokenURL(c, token), &result, &gophercloud.RequestOpts{ 388 OkCodes: []int{200, 203}, 389 }) 390 if err != nil { 391 return nil, err 392 } 393 return result, nil 394 } 395 396 func tokenURL(c *gophercloud.ServiceClient, token string) string { 397 return c.ServiceURL("tokens", token) 398 }