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