Samuel A. Falvo II | 1d3fa66 | 2013-06-25 15:29:32 -0700 | [diff] [blame] | 1 | package gophercloud |
| 2 | |
Samuel A. Falvo II | 5d0d74c | 2013-06-25 17:23:18 -0700 | [diff] [blame] | 3 | import ( |
John Hopper | e2bc702 | 2014-06-14 11:30:20 -0500 | [diff] [blame] | 4 | "fmt" |
Samuel A. Falvo II | 5d0d74c | 2013-06-25 17:23:18 -0700 | [diff] [blame] | 5 | "github.com/racker/perigee" |
| 6 | ) |
| 7 | |
Samuel A. Falvo II | 4e89518 | 2013-06-26 15:44:18 -0700 | [diff] [blame] | 8 | // AuthOptions lets anyone calling Authenticate() supply the required access credentials. |
| 9 | // At present, only Identity V2 API support exists; therefore, only Username, Password, |
| 10 | // and optionally, TenantId are provided. If future Identity API versions become available, |
| 11 | // alternative fields unique to those versions may appear here. |
Samuel A. Falvo II | 1d3fa66 | 2013-06-25 15:29:32 -0700 | [diff] [blame] | 12 | type AuthOptions struct { |
Samuel A. Falvo II | 4e89518 | 2013-06-26 15:44:18 -0700 | [diff] [blame] | 13 | // Username and Password are required if using Identity V2 API. |
| 14 | // Consult with your provider's control panel to discover your |
| 15 | // account's username and password. |
| 16 | Username, Password string |
| 17 | |
Rafael Garcia | e4a550e | 2013-12-06 17:00:32 -0300 | [diff] [blame] | 18 | // ApiKey used for providers that support Api Key authentication |
| 19 | ApiKey string |
| 20 | |
Samuel A. Falvo II | 4e89518 | 2013-06-26 15:44:18 -0700 | [diff] [blame] | 21 | // The TenantId field is optional for the Identity V2 API. |
| 22 | TenantId string |
Samuel A. Falvo II | 9e64f6b | 2013-07-16 14:26:50 -0700 | [diff] [blame] | 23 | |
Justin Santa Barbara | 21682a4 | 2013-08-31 17:56:13 -0700 | [diff] [blame] | 24 | // The TenantName can be specified instead of the TenantId |
| 25 | TenantName string |
| 26 | |
Samuel A. Falvo II | 9e64f6b | 2013-07-16 14:26:50 -0700 | [diff] [blame] | 27 | // AllowReauth should be set to true if you grant permission for Gophercloud to cache |
| 28 | // your credentials in memory, and to allow Gophercloud to attempt to re-authenticate |
| 29 | // automatically if/when your token expires. If you set it to false, it will not cache |
| 30 | // these settings, but re-authentication will not be possible. This setting defaults |
| 31 | // to false. |
| 32 | AllowReauth bool |
Samuel A. Falvo II | 1d3fa66 | 2013-06-25 15:29:32 -0700 | [diff] [blame] | 33 | } |
| 34 | |
Samuel A. Falvo II | 4e89518 | 2013-06-26 15:44:18 -0700 | [diff] [blame] | 35 | // AuthContainer provides a JSON encoding wrapper for passing credentials to the Identity |
| 36 | // service. You will not work with this structure directly. |
Samuel A. Falvo II | 5d0d74c | 2013-06-25 17:23:18 -0700 | [diff] [blame] | 37 | type AuthContainer struct { |
| 38 | Auth Auth `json:"auth"` |
| 39 | } |
| 40 | |
Samuel A. Falvo II | 4e89518 | 2013-06-26 15:44:18 -0700 | [diff] [blame] | 41 | // Auth provides a JSON encoding wrapper for passing credentials to the Identity |
| 42 | // service. You will not work with this structure directly. |
Samuel A. Falvo II | 5d0d74c | 2013-06-25 17:23:18 -0700 | [diff] [blame] | 43 | type Auth struct { |
Rafael Garcia | e4a550e | 2013-12-06 17:00:32 -0300 | [diff] [blame] | 44 | PasswordCredentials *PasswordCredentials `json:"passwordCredentials,omitempty"` |
| 45 | ApiKeyCredentials *ApiKeyCredentials `json:"RAX-KSKEY:apiKeyCredentials,omitempty"` |
Rafael Garcia | 752cb33 | 2013-12-12 22:16:58 -0300 | [diff] [blame] | 46 | TenantId string `json:"tenantId,omitempty"` |
| 47 | TenantName string `json:"tenantName,omitempty"` |
Samuel A. Falvo II | 5d0d74c | 2013-06-25 17:23:18 -0700 | [diff] [blame] | 48 | } |
| 49 | |
Samuel A. Falvo II | 4e89518 | 2013-06-26 15:44:18 -0700 | [diff] [blame] | 50 | // PasswordCredentials provides a JSON encoding wrapper for passing credentials to the Identity |
| 51 | // service. You will not work with this structure directly. |
Samuel A. Falvo II | 5d0d74c | 2013-06-25 17:23:18 -0700 | [diff] [blame] | 52 | type PasswordCredentials struct { |
| 53 | Username string `json:"username"` |
| 54 | Password string `json:"password"` |
| 55 | } |
| 56 | |
Rafael Garcia | e4a550e | 2013-12-06 17:00:32 -0300 | [diff] [blame] | 57 | type ApiKeyCredentials struct { |
| 58 | Username string `json:"username"` |
Rafael Garcia | 752cb33 | 2013-12-12 22:16:58 -0300 | [diff] [blame] | 59 | ApiKey string `json:"apiKey"` |
Rafael Garcia | e4a550e | 2013-12-06 17:00:32 -0300 | [diff] [blame] | 60 | } |
| 61 | |
Samuel A. Falvo II | d1ee798 | 2013-06-26 14:32:45 -0700 | [diff] [blame] | 62 | // Access encapsulates the API token and its relevant fields, as well as the |
Samuel A. Falvo II | 2e2b877 | 2013-07-04 15:40:15 -0700 | [diff] [blame] | 63 | // services catalog that Identity API returns once authenticated. |
Samuel A. Falvo II | d1ee798 | 2013-06-26 14:32:45 -0700 | [diff] [blame] | 64 | type Access struct { |
| 65 | Token Token |
| 66 | ServiceCatalog []CatalogEntry |
| 67 | User User |
Samuel A. Falvo II | 20f1aa4 | 2013-07-31 14:32:03 -0700 | [diff] [blame] | 68 | provider Provider `json:"-"` |
Samuel A. Falvo II | 9e64f6b | 2013-07-16 14:26:50 -0700 | [diff] [blame] | 69 | options AuthOptions `json:"-"` |
Samuel A. Falvo II | 20f1aa4 | 2013-07-31 14:32:03 -0700 | [diff] [blame] | 70 | context *Context `json:"-"` |
Samuel A. Falvo II | 5d0d74c | 2013-06-25 17:23:18 -0700 | [diff] [blame] | 71 | } |
| 72 | |
Samuel A. Falvo II | d1ee798 | 2013-06-26 14:32:45 -0700 | [diff] [blame] | 73 | // Token encapsulates an authentication token and when it expires. It also includes |
| 74 | // tenant information if available. |
| 75 | type Token struct { |
| 76 | Id, Expires string |
| 77 | Tenant Tenant |
| 78 | } |
| 79 | |
| 80 | // Tenant encapsulates tenant authentication information. If, after authentication, |
| 81 | // no tenant information is supplied, both Id and Name will be "". |
| 82 | type Tenant struct { |
| 83 | Id, Name string |
| 84 | } |
| 85 | |
| 86 | // User encapsulates the user credentials, and provides visibility in what |
| 87 | // the user can do through its role assignments. |
| 88 | type User struct { |
| 89 | Id, Name string |
| 90 | XRaxDefaultRegion string `json:"RAX-AUTH:defaultRegion"` |
| 91 | Roles []Role |
| 92 | } |
| 93 | |
| 94 | // Role encapsulates a permission that a user can rely on. |
| 95 | type Role struct { |
| 96 | Description, Id, Name string |
| 97 | } |
| 98 | |
| 99 | // CatalogEntry encapsulates a service catalog record. |
| 100 | type CatalogEntry struct { |
| 101 | Name, Type string |
| 102 | Endpoints []EntryEndpoint |
| 103 | } |
| 104 | |
| 105 | // EntryEndpoint encapsulates how to get to the API of some service. |
| 106 | type EntryEndpoint struct { |
| 107 | Region, TenantId string |
| 108 | PublicURL, InternalURL string |
| 109 | VersionId, VersionInfo, VersionList string |
| 110 | } |
| 111 | |
John Hopper | e2bc702 | 2014-06-14 11:30:20 -0500 | [diff] [blame] | 112 | type AuthError struct { |
| 113 | StatusCode int |
| 114 | } |
| 115 | |
| 116 | func (ae *AuthError) Error() string { |
| 117 | switch ae.StatusCode { |
| 118 | case 401: |
| 119 | return "Auth failed. Bad credentials." |
| 120 | |
| 121 | default: |
| 122 | return fmt.Sprintf("Auth failed. Status code is: %s.", ae.StatusCode) |
| 123 | } |
| 124 | } |
| 125 | |
Rafael Garcia | e4a550e | 2013-12-06 17:00:32 -0300 | [diff] [blame] | 126 | // |
| 127 | func getAuthCredentials(options AuthOptions) Auth { |
Rafael Garcia | 752cb33 | 2013-12-12 22:16:58 -0300 | [diff] [blame] | 128 | if options.ApiKey == "" { |
Rafael Garcia | e4a550e | 2013-12-06 17:00:32 -0300 | [diff] [blame] | 129 | return Auth{ |
| 130 | PasswordCredentials: &PasswordCredentials{ |
| 131 | Username: options.Username, |
| 132 | Password: options.Password, |
| 133 | }, |
| 134 | TenantId: options.TenantId, |
| 135 | TenantName: options.TenantName, |
Rafael Garcia | 752cb33 | 2013-12-12 22:16:58 -0300 | [diff] [blame] | 136 | } |
Rafael Garcia | e4a550e | 2013-12-06 17:00:32 -0300 | [diff] [blame] | 137 | } else { |
| 138 | return Auth{ |
| 139 | ApiKeyCredentials: &ApiKeyCredentials{ |
| 140 | Username: options.Username, |
Rafael Garcia | 752cb33 | 2013-12-12 22:16:58 -0300 | [diff] [blame] | 141 | ApiKey: options.ApiKey, |
Rafael Garcia | e4a550e | 2013-12-06 17:00:32 -0300 | [diff] [blame] | 142 | }, |
| 143 | TenantId: options.TenantId, |
| 144 | TenantName: options.TenantName, |
Rafael Garcia | 752cb33 | 2013-12-12 22:16:58 -0300 | [diff] [blame] | 145 | } |
Rafael Garcia | e4a550e | 2013-12-06 17:00:32 -0300 | [diff] [blame] | 146 | } |
| 147 | } |
| 148 | |
Samuel A. Falvo II | 9e64f6b | 2013-07-16 14:26:50 -0700 | [diff] [blame] | 149 | // papersPlease contains the common logic between authentication and re-authentication. |
| 150 | // The name, obviously a joke on the process of authentication, was chosen because |
| 151 | // of how many other entities exist in the program containing the word Auth or Authorization. |
| 152 | // I didn't need another one. |
| 153 | func (c *Context) papersPlease(p Provider, options AuthOptions) (*Access, error) { |
Samuel A. Falvo II | d1ee798 | 2013-06-26 14:32:45 -0700 | [diff] [blame] | 154 | var access *Access |
Matt Martz | 3927d84 | 2014-06-04 10:30:35 -0500 | [diff] [blame] | 155 | access = new(Access) |
Samuel A. Falvo II | d1ee798 | 2013-06-26 14:32:45 -0700 | [diff] [blame] | 156 | |
Rafael Garcia | e4a550e | 2013-12-06 17:00:32 -0300 | [diff] [blame] | 157 | if (options.Username == "") || (options.Password == "" && options.ApiKey == "") { |
Samuel A. Falvo II | fd78c30 | 2013-06-25 16:35:32 -0700 | [diff] [blame] | 158 | return nil, ErrCredentials |
| 159 | } |
Samuel A. Falvo II | 5d0d74c | 2013-06-25 17:23:18 -0700 | [diff] [blame] | 160 | |
John Hopper | e2bc702 | 2014-06-14 11:30:20 -0500 | [diff] [blame] | 161 | resp, err := perigee.Request("POST", p.AuthEndpoint, perigee.Options{ |
Samuel A. Falvo II | 5d0d74c | 2013-06-25 17:23:18 -0700 | [diff] [blame] | 162 | CustomClient: c.httpClient, |
Samuel A. Falvo II | 839428e | 2013-06-25 18:02:24 -0700 | [diff] [blame] | 163 | ReqBody: &AuthContainer{ |
Rafael Garcia | e4a550e | 2013-12-06 17:00:32 -0300 | [diff] [blame] | 164 | Auth: getAuthCredentials(options), |
Samuel A. Falvo II | 5d0d74c | 2013-06-25 17:23:18 -0700 | [diff] [blame] | 165 | }, |
Samuel A. Falvo II | 4e89518 | 2013-06-26 15:44:18 -0700 | [diff] [blame] | 166 | Results: &struct { |
Samuel A. Falvo II | d1ee798 | 2013-06-26 14:32:45 -0700 | [diff] [blame] | 167 | Access **Access `json:"access"` |
| 168 | }{ |
| 169 | &access, |
| 170 | }, |
Samuel A. Falvo II | 5d0d74c | 2013-06-25 17:23:18 -0700 | [diff] [blame] | 171 | }) |
John Hopper | e2bc702 | 2014-06-14 11:30:20 -0500 | [diff] [blame] | 172 | |
Samuel A. Falvo II | 0167aaa | 2013-07-16 12:36:25 -0700 | [diff] [blame] | 173 | if err == nil { |
John Hopper | e2bc702 | 2014-06-14 11:30:20 -0500 | [diff] [blame] | 174 | switch resp.StatusCode { |
| 175 | case 200: |
| 176 | access.options = options |
| 177 | access.provider = p |
| 178 | access.context = c |
| 179 | |
| 180 | default: |
| 181 | err = &AuthError { |
| 182 | StatusCode: resp.StatusCode, |
| 183 | } |
| 184 | } |
Samuel A. Falvo II | 0167aaa | 2013-07-16 12:36:25 -0700 | [diff] [blame] | 185 | } |
John Hopper | e2bc702 | 2014-06-14 11:30:20 -0500 | [diff] [blame] | 186 | |
Samuel A. Falvo II | d1ee798 | 2013-06-26 14:32:45 -0700 | [diff] [blame] | 187 | return access, err |
Samuel A. Falvo II | 1d3fa66 | 2013-06-25 15:29:32 -0700 | [diff] [blame] | 188 | } |
Samuel A. Falvo II | 2e2b877 | 2013-07-04 15:40:15 -0700 | [diff] [blame] | 189 | |
Samuel A. Falvo II | 9e64f6b | 2013-07-16 14:26:50 -0700 | [diff] [blame] | 190 | // Authenticate() grants access to the OpenStack-compatible provider API. |
| 191 | // |
| 192 | // Providers are identified through a unique key string. |
| 193 | // See the RegisterProvider() method for more details. |
| 194 | // |
| 195 | // The supplied AuthOptions instance allows the client to specify only those credentials |
| 196 | // relevant for the authentication request. At present, support exists for OpenStack |
| 197 | // Identity V2 API only; support for V3 will become available as soon as documentation for it |
| 198 | // becomes readily available. |
| 199 | // |
| 200 | // For Identity V2 API requirements, you must provide at least the Username and Password |
| 201 | // options. The TenantId field is optional, and defaults to "". |
| 202 | func (c *Context) Authenticate(provider string, options AuthOptions) (*Access, error) { |
| 203 | p, err := c.ProviderByName(provider) |
| 204 | if err != nil { |
| 205 | return nil, err |
| 206 | } |
| 207 | return c.papersPlease(p, options) |
| 208 | } |
| 209 | |
| 210 | // Reauthenticate attempts to reauthenticate using the configured access credentials, if |
| 211 | // allowed. This method takes no action unless your AuthOptions has the AllowReauth flag |
| 212 | // set to true. |
| 213 | func (a *Access) Reauthenticate() error { |
| 214 | var other *Access |
| 215 | var err error |
| 216 | |
Samuel A. Falvo II | 9e64f6b | 2013-07-16 14:26:50 -0700 | [diff] [blame] | 217 | if a.options.AllowReauth { |
| 218 | other, err = a.context.papersPlease(a.provider, a.options) |
Samuel A. Falvo II | fb58669 | 2013-07-16 17:00:14 -0700 | [diff] [blame] | 219 | if err == nil { |
Samuel A. Falvo II | 9e64f6b | 2013-07-16 14:26:50 -0700 | [diff] [blame] | 220 | *a = *other |
| 221 | } |
| 222 | } |
| 223 | return err |
| 224 | } |
| 225 | |
Samuel A. Falvo II | 2e2b877 | 2013-07-04 15:40:15 -0700 | [diff] [blame] | 226 | // See AccessProvider interface definition for details. |
| 227 | func (a *Access) FirstEndpointUrlByCriteria(ac ApiCriteria) string { |
| 228 | ep := FindFirstEndpointByCriteria(a.ServiceCatalog, ac) |
| 229 | urls := []string{ep.PublicURL, ep.InternalURL} |
| 230 | return urls[ac.UrlChoice] |
| 231 | } |
Samuel A. Falvo II | bc0d54a | 2013-07-08 14:45:21 -0700 | [diff] [blame] | 232 | |
| 233 | // See AccessProvider interface definition for details. |
| 234 | func (a *Access) AuthToken() string { |
| 235 | return a.Token.Id |
| 236 | } |
Samuel A. Falvo II | 659e14b | 2013-07-16 12:04:54 -0700 | [diff] [blame] | 237 | |
| 238 | // See AccessProvider interface definition for details. |
| 239 | func (a *Access) Revoke(tok string) error { |
Samuel A. Falvo II | 0167aaa | 2013-07-16 12:36:25 -0700 | [diff] [blame] | 240 | url := a.provider.AuthEndpoint + "/" + tok |
| 241 | err := perigee.Delete(url, perigee.Options{ |
| 242 | MoreHeaders: map[string]string{ |
| 243 | "X-Auth-Token": a.AuthToken(), |
| 244 | }, |
| 245 | OkCodes: []int{204}, |
| 246 | }) |
| 247 | return err |
Samuel A. Falvo II | 659e14b | 2013-07-16 12:04:54 -0700 | [diff] [blame] | 248 | } |
Samuel A. Falvo II | cfb352a | 2013-11-19 14:39:37 -0800 | [diff] [blame] | 249 | |
| 250 | // See ServiceCatalogerForIdentityV2 interface definition for details. |
| 251 | // Note that the raw slice is returend; be careful not to alter the fields of any members, |
| 252 | // for other components of Gophercloud may depend upon them. |
| 253 | // If this becomes a problem in the future, |
| 254 | // a future revision may return a deep-copy of the service catalog instead. |
| 255 | func (a *Access) V2ServiceCatalog() []CatalogEntry { |
| 256 | return a.ServiceCatalog |
| 257 | } |