package swift import ( "bytes" "encoding/json" "net/http" "net/url" "strings" "time" ) // Auth defines the operations needed to authenticate with swift // // This encapsulates the different authentication schemes in use type Authenticator interface { // Request creates an http.Request for the auth - return nil if not needed Request(*Connection) (*http.Request, error) // Response parses the http.Response Response(resp *http.Response) error // The public storage URL - set Internal to true to read // internal/service net URL StorageUrl(Internal bool) string // The access token Token() string // The CDN url if available CdnUrl() string } // Expireser is an optional interface to read the expiration time of the token type Expireser interface { Expires() time.Time } type CustomEndpointAuthenticator interface { StorageUrlForEndpoint(endpointType EndpointType) string } type EndpointType string const ( // Use public URL as storage URL EndpointTypePublic = EndpointType("public") // Use internal URL as storage URL EndpointTypeInternal = EndpointType("internal") // Use admin URL as storage URL EndpointTypeAdmin = EndpointType("admin") ) // newAuth - create a new Authenticator from the AuthUrl // // A hint for AuthVersion can be provided func newAuth(c *Connection) (Authenticator, error) { AuthVersion := c.AuthVersion if AuthVersion == 0 { if strings.Contains(c.AuthUrl, "v3") { AuthVersion = 3 } else if strings.Contains(c.AuthUrl, "v2") { AuthVersion = 2 } else if strings.Contains(c.AuthUrl, "v1") { AuthVersion = 1 } else { return nil, newErrorf(500, "Can't find AuthVersion in AuthUrl - set explicitly") } } switch AuthVersion { case 1: return &v1Auth{}, nil case 2: return &v2Auth{ // Guess as to whether using API key or // password it will try both eventually so // this is just an optimization. useApiKey: len(c.ApiKey) >= 32, }, nil case 3: return &v3Auth{}, nil } return nil, newErrorf(500, "Auth Version %d not supported", AuthVersion) } // ------------------------------------------------------------ // v1 auth type v1Auth struct { Headers http.Header // V1 auth: the authentication headers so extensions can access them } // v1 Authentication - make request func (auth *v1Auth) Request(c *Connection) (*http.Request, error) { req, err := http.NewRequest("GET", c.AuthUrl, nil) if err != nil { return nil, err } req.Header.Set("User-Agent", c.UserAgent) req.Header.Set("X-Auth-Key", c.ApiKey) req.Header.Set("X-Auth-User", c.UserName) return req, nil } // v1 Authentication - read response func (auth *v1Auth) Response(resp *http.Response) error { auth.Headers = resp.Header return nil } // v1 Authentication - read storage url func (auth *v1Auth) StorageUrl(Internal bool) string { storageUrl := auth.Headers.Get("X-Storage-Url") if Internal { newUrl, err := url.Parse(storageUrl) if err != nil { return storageUrl } newUrl.Host = "snet-" + newUrl.Host storageUrl = newUrl.String() } return storageUrl } // v1 Authentication - read auth token func (auth *v1Auth) Token() string { return auth.Headers.Get("X-Auth-Token") } // v1 Authentication - read cdn url func (auth *v1Auth) CdnUrl() string { return auth.Headers.Get("X-CDN-Management-Url") } // ------------------------------------------------------------ // v2 Authentication type v2Auth struct { Auth *v2AuthResponse Region string useApiKey bool // if set will use API key not Password useApiKeyOk bool // if set won't change useApiKey any more notFirst bool // set after first run } // v2 Authentication - make request func (auth *v2Auth) Request(c *Connection) (*http.Request, error) { auth.Region = c.Region // Toggle useApiKey if not first run and not OK yet if auth.notFirst && !auth.useApiKeyOk { auth.useApiKey = !auth.useApiKey } auth.notFirst = true // Create a V2 auth request for the body of the connection var v2i interface{} if !auth.useApiKey { // Normal swift authentication v2 := v2AuthRequest{} v2.Auth.PasswordCredentials.UserName = c.UserName v2.Auth.PasswordCredentials.Password = c.ApiKey v2.Auth.Tenant = c.Tenant v2.Auth.TenantId = c.TenantId v2i = v2 } else { // Rackspace special with API Key v2 := v2AuthRequestRackspace{} v2.Auth.ApiKeyCredentials.UserName = c.UserName v2.Auth.ApiKeyCredentials.ApiKey = c.ApiKey v2.Auth.Tenant = c.Tenant v2.Auth.TenantId = c.TenantId v2i = v2 } body, err := json.Marshal(v2i) if err != nil { return nil, err } url := c.AuthUrl if !strings.HasSuffix(url, "/") { url += "/" } url += "tokens" req, err := http.NewRequest("POST", url, bytes.NewBuffer(body)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", c.UserAgent) return req, nil } // v2 Authentication - read response func (auth *v2Auth) Response(resp *http.Response) error { auth.Auth = new(v2AuthResponse) err := readJson(resp, auth.Auth) // If successfully read Auth then no need to toggle useApiKey any more if err == nil { auth.useApiKeyOk = true } return err } // Finds the Endpoint Url of "type" from the v2AuthResponse using the // Region if set or defaulting to the first one if not // // Returns "" if not found func (auth *v2Auth) endpointUrl(Type string, endpointType EndpointType) string { for _, catalog := range auth.Auth.Access.ServiceCatalog { if catalog.Type == Type { for _, endpoint := range catalog.Endpoints { if auth.Region == "" || (auth.Region == endpoint.Region) { switch endpointType { case EndpointTypeInternal: return endpoint.InternalUrl case EndpointTypePublic: return endpoint.PublicUrl case EndpointTypeAdmin: return endpoint.AdminUrl default: return "" } } } } } return "" } // v2 Authentication - read storage url // // If Internal is true then it reads the private (internal / service // net) URL. func (auth *v2Auth) StorageUrl(Internal bool) string { endpointType := EndpointTypePublic if Internal { endpointType = EndpointTypeInternal } return auth.StorageUrlForEndpoint(endpointType) } // v2 Authentication - read storage url // // Use the indicated endpointType to choose a URL. func (auth *v2Auth) StorageUrlForEndpoint(endpointType EndpointType) string { return auth.endpointUrl("object-store", endpointType) } // v2 Authentication - read auth token func (auth *v2Auth) Token() string { return auth.Auth.Access.Token.Id } // v2 Authentication - read expires func (auth *v2Auth) Expires() time.Time { t, err := time.Parse(time.RFC3339, auth.Auth.Access.Token.Expires) if err != nil { return time.Time{} // return Zero if not parsed } return t } // v2 Authentication - read cdn url func (auth *v2Auth) CdnUrl() string { return auth.endpointUrl("rax:object-cdn", EndpointTypePublic) } // ------------------------------------------------------------ // V2 Authentication request // // http://docs.openstack.org/developer/keystone/api_curl_examples.html // http://docs.rackspace.com/servers/api/v2/cs-gettingstarted/content/curl_auth.html // http://docs.openstack.org/api/openstack-identity-service/2.0/content/POST_authenticate_v2.0_tokens_.html type v2AuthRequest struct { Auth struct { PasswordCredentials struct { UserName string `json:"username"` Password string `json:"password"` } `json:"passwordCredentials"` Tenant string `json:"tenantName,omitempty"` TenantId string `json:"tenantId,omitempty"` } `json:"auth"` } // V2 Authentication request - Rackspace variant // // http://docs.openstack.org/developer/keystone/api_curl_examples.html // http://docs.rackspace.com/servers/api/v2/cs-gettingstarted/content/curl_auth.html // http://docs.openstack.org/api/openstack-identity-service/2.0/content/POST_authenticate_v2.0_tokens_.html type v2AuthRequestRackspace struct { Auth struct { ApiKeyCredentials struct { UserName string `json:"username"` ApiKey string `json:"apiKey"` } `json:"RAX-KSKEY:apiKeyCredentials"` Tenant string `json:"tenantName,omitempty"` TenantId string `json:"tenantId,omitempty"` } `json:"auth"` } // V2 Authentication reply // // http://docs.openstack.org/developer/keystone/api_curl_examples.html // http://docs.rackspace.com/servers/api/v2/cs-gettingstarted/content/curl_auth.html // http://docs.openstack.org/api/openstack-identity-service/2.0/content/POST_authenticate_v2.0_tokens_.html type v2AuthResponse struct { Access struct { ServiceCatalog []struct { Endpoints []struct { InternalUrl string PublicUrl string AdminUrl string Region string TenantId string } Name string Type string } Token struct { Expires string Id string Tenant struct { Id string Name string } } User struct { DefaultRegion string `json:"RAX-AUTH:defaultRegion"` Id string Name string Roles []struct { Description string Id string Name string TenantId string } } } }