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  }