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  }