1package swift
2
3import (
4	"bytes"
5	"encoding/json"
6	"net/http"
7	"net/url"
8	"strings"
9)
10
11// Auth defines the operations needed to authenticate with swift
12//
13// This encapsulates the different authentication schemes in use
14type Authenticator interface {
15	// Request creates an http.Request for the auth - return nil if not needed
16	Request(*Connection) (*http.Request, error)
17	// Response parses the http.Response
18	Response(resp *http.Response) error
19	// The public storage URL - set Internal to true to read
20	// internal/service net URL
21	StorageUrl(Internal bool) string
22	// The access token
23	Token() string
24	// The CDN url if available
25	CdnUrl() string
26}
27
28type CustomEndpointAuthenticator interface {
29	StorageUrlForEndpoint(endpointType EndpointType) string
30}
31
32type EndpointType string
33
34const (
35	// Use public URL as storage URL
36	EndpointTypePublic = EndpointType("public")
37
38	// Use internal URL as storage URL
39	EndpointTypeInternal = EndpointType("internal")
40
41	// Use admin URL as storage URL
42	EndpointTypeAdmin = EndpointType("admin")
43)
44
45// newAuth - create a new Authenticator from the AuthUrl
46//
47// A hint for AuthVersion can be provided
48func newAuth(c *Connection) (Authenticator, error) {
49	AuthVersion := c.AuthVersion
50	if AuthVersion == 0 {
51		if strings.Contains(c.AuthUrl, "v3") {
52			AuthVersion = 3
53		} else if strings.Contains(c.AuthUrl, "v2") {
54			AuthVersion = 2
55		} else if strings.Contains(c.AuthUrl, "v1") {
56			AuthVersion = 1
57		} else {
58			return nil, newErrorf(500, "Can't find AuthVersion in AuthUrl - set explicitly")
59		}
60	}
61	switch AuthVersion {
62	case 1:
63		return &v1Auth{}, nil
64	case 2:
65		return &v2Auth{
66			// Guess as to whether using API key or
67			// password it will try both eventually so
68			// this is just an optimization.
69			useApiKey: len(c.ApiKey) >= 32,
70		}, nil
71	case 3:
72		return &v3Auth{}, nil
73	}
74	return nil, newErrorf(500, "Auth Version %d not supported", AuthVersion)
75}
76
77// ------------------------------------------------------------
78
79// v1 auth
80type v1Auth struct {
81	Headers http.Header // V1 auth: the authentication headers so extensions can access them
82}
83
84// v1 Authentication - make request
85func (auth *v1Auth) Request(c *Connection) (*http.Request, error) {
86	req, err := http.NewRequest("GET", c.AuthUrl, nil)
87	if err != nil {
88		return nil, err
89	}
90	req.Header.Set("User-Agent", c.UserAgent)
91	req.Header.Set("X-Auth-Key", c.ApiKey)
92	req.Header.Set("X-Auth-User", c.UserName)
93	return req, nil
94}
95
96// v1 Authentication - read response
97func (auth *v1Auth) Response(resp *http.Response) error {
98	auth.Headers = resp.Header
99	return nil
100}
101
102// v1 Authentication - read storage url
103func (auth *v1Auth) StorageUrl(Internal bool) string {
104	storageUrl := auth.Headers.Get("X-Storage-Url")
105	if Internal {
106		newUrl, err := url.Parse(storageUrl)
107		if err != nil {
108			return storageUrl
109		}
110		newUrl.Host = "snet-" + newUrl.Host
111		storageUrl = newUrl.String()
112	}
113	return storageUrl
114}
115
116// v1 Authentication - read auth token
117func (auth *v1Auth) Token() string {
118	return auth.Headers.Get("X-Auth-Token")
119}
120
121// v1 Authentication - read cdn url
122func (auth *v1Auth) CdnUrl() string {
123	return auth.Headers.Get("X-CDN-Management-Url")
124}
125
126// ------------------------------------------------------------
127
128// v2 Authentication
129type v2Auth struct {
130	Auth        *v2AuthResponse
131	Region      string
132	useApiKey   bool // if set will use API key not Password
133	useApiKeyOk bool // if set won't change useApiKey any more
134	notFirst    bool // set after first run
135}
136
137// v2 Authentication - make request
138func (auth *v2Auth) Request(c *Connection) (*http.Request, error) {
139	auth.Region = c.Region
140	// Toggle useApiKey if not first run and not OK yet
141	if auth.notFirst && !auth.useApiKeyOk {
142		auth.useApiKey = !auth.useApiKey
143	}
144	auth.notFirst = true
145	// Create a V2 auth request for the body of the connection
146	var v2i interface{}
147	if !auth.useApiKey {
148		// Normal swift authentication
149		v2 := v2AuthRequest{}
150		v2.Auth.PasswordCredentials.UserName = c.UserName
151		v2.Auth.PasswordCredentials.Password = c.ApiKey
152		v2.Auth.Tenant = c.Tenant
153		v2.Auth.TenantId = c.TenantId
154		v2i = v2
155	} else {
156		// Rackspace special with API Key
157		v2 := v2AuthRequestRackspace{}
158		v2.Auth.ApiKeyCredentials.UserName = c.UserName
159		v2.Auth.ApiKeyCredentials.ApiKey = c.ApiKey
160		v2.Auth.Tenant = c.Tenant
161		v2.Auth.TenantId = c.TenantId
162		v2i = v2
163	}
164	body, err := json.Marshal(v2i)
165	if err != nil {
166		return nil, err
167	}
168	url := c.AuthUrl
169	if !strings.HasSuffix(url, "/") {
170		url += "/"
171	}
172	url += "tokens"
173	req, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
174	if err != nil {
175		return nil, err
176	}
177	req.Header.Set("Content-Type", "application/json")
178	req.Header.Set("User-Agent", c.UserAgent)
179	return req, nil
180}
181
182// v2 Authentication - read response
183func (auth *v2Auth) Response(resp *http.Response) error {
184	auth.Auth = new(v2AuthResponse)
185	err := readJson(resp, auth.Auth)
186	// If successfully read Auth then no need to toggle useApiKey any more
187	if err == nil {
188		auth.useApiKeyOk = true
189	}
190	return err
191}
192
193// Finds the Endpoint Url of "type" from the v2AuthResponse using the
194// Region if set or defaulting to the first one if not
195//
196// Returns "" if not found
197func (auth *v2Auth) endpointUrl(Type string, endpointType EndpointType) string {
198	for _, catalog := range auth.Auth.Access.ServiceCatalog {
199		if catalog.Type == Type {
200			for _, endpoint := range catalog.Endpoints {
201				if auth.Region == "" || (auth.Region == endpoint.Region) {
202					switch endpointType {
203					case EndpointTypeInternal:
204						return endpoint.InternalUrl
205					case EndpointTypePublic:
206						return endpoint.PublicUrl
207					case EndpointTypeAdmin:
208						return endpoint.AdminUrl
209					default:
210						return ""
211					}
212				}
213			}
214		}
215	}
216	return ""
217}
218
219// v2 Authentication - read storage url
220//
221// If Internal is true then it reads the private (internal / service
222// net) URL.
223func (auth *v2Auth) StorageUrl(Internal bool) string {
224	endpointType := EndpointTypePublic
225	if Internal {
226		endpointType = EndpointTypeInternal
227	}
228	return auth.StorageUrlForEndpoint(endpointType)
229}
230
231// v2 Authentication - read storage url
232//
233// Use the indicated endpointType to choose a URL.
234func (auth *v2Auth) StorageUrlForEndpoint(endpointType EndpointType) string {
235	return auth.endpointUrl("object-store", endpointType)
236}
237
238// v2 Authentication - read auth token
239func (auth *v2Auth) Token() string {
240	return auth.Auth.Access.Token.Id
241}
242
243// v2 Authentication - read cdn url
244func (auth *v2Auth) CdnUrl() string {
245	return auth.endpointUrl("rax:object-cdn", EndpointTypePublic)
246}
247
248// ------------------------------------------------------------
249
250// V2 Authentication request
251//
252// http://docs.openstack.org/developer/keystone/api_curl_examples.html
253// http://docs.rackspace.com/servers/api/v2/cs-gettingstarted/content/curl_auth.html
254// http://docs.openstack.org/api/openstack-identity-service/2.0/content/POST_authenticate_v2.0_tokens_.html
255type v2AuthRequest struct {
256	Auth struct {
257		PasswordCredentials struct {
258			UserName string `json:"username"`
259			Password string `json:"password"`
260		} `json:"passwordCredentials"`
261		Tenant   string `json:"tenantName,omitempty"`
262		TenantId string `json:"tenantId,omitempty"`
263	} `json:"auth"`
264}
265
266// V2 Authentication request - Rackspace variant
267//
268// http://docs.openstack.org/developer/keystone/api_curl_examples.html
269// http://docs.rackspace.com/servers/api/v2/cs-gettingstarted/content/curl_auth.html
270// http://docs.openstack.org/api/openstack-identity-service/2.0/content/POST_authenticate_v2.0_tokens_.html
271type v2AuthRequestRackspace struct {
272	Auth struct {
273		ApiKeyCredentials struct {
274			UserName string `json:"username"`
275			ApiKey   string `json:"apiKey"`
276		} `json:"RAX-KSKEY:apiKeyCredentials"`
277		Tenant   string `json:"tenantName,omitempty"`
278		TenantId string `json:"tenantId,omitempty"`
279	} `json:"auth"`
280}
281
282// V2 Authentication reply
283//
284// http://docs.openstack.org/developer/keystone/api_curl_examples.html
285// http://docs.rackspace.com/servers/api/v2/cs-gettingstarted/content/curl_auth.html
286// http://docs.openstack.org/api/openstack-identity-service/2.0/content/POST_authenticate_v2.0_tokens_.html
287type v2AuthResponse struct {
288	Access struct {
289		ServiceCatalog []struct {
290			Endpoints []struct {
291				InternalUrl string
292				PublicUrl   string
293				AdminUrl    string
294				Region      string
295				TenantId    string
296			}
297			Name string
298			Type string
299		}
300		Token struct {
301			Expires string
302			Id      string
303			Tenant  struct {
304				Id   string
305				Name string
306			}
307		}
308		User struct {
309			DefaultRegion string `json:"RAX-AUTH:defaultRegion"`
310			Id            string
311			Name          string
312			Roles         []struct {
313				Description string
314				Id          string
315				Name        string
316				TenantId    string
317			}
318		}
319	}
320}
321