OLD | NEW |
(Empty) | |
| 1 package identityservice |
| 2 |
| 3 import ( |
| 4 "encoding/json" |
| 5 "fmt" |
| 6 "io/ioutil" |
| 7 "net/http" |
| 8 ) |
| 9 |
| 10 // Implement the v2 User Pass form of identity (Keystone) |
| 11 |
| 12 type ErrorResponse struct { |
| 13 Message string `json:"message"` |
| 14 Code int `json:"code"` |
| 15 Title string `json:"title"` |
| 16 } |
| 17 |
| 18 type ErrorWrapper struct { |
| 19 Error ErrorResponse `json:"error"` |
| 20 } |
| 21 |
| 22 type UserPassRequest struct { |
| 23 Auth struct { |
| 24 PasswordCredentials struct { |
| 25 Username string `json:"username"` |
| 26 Password string `json:"password"` |
| 27 } `json:"passwordCredentials"` |
| 28 TenantName string `json:"tenantName"` |
| 29 } `json:"auth"` |
| 30 } |
| 31 |
| 32 type Endpoint struct { |
| 33 AdminURL string `json:"adminURL"` |
| 34 InternalURL string `json:"internalURL"` |
| 35 PublicURL string `json:"publicURL"` |
| 36 Region string `json:"region"` |
| 37 } |
| 38 |
| 39 type Service struct { |
| 40 Name string `json:"name"` |
| 41 Type string `json:"type"` |
| 42 Endpoints []Endpoint |
| 43 } |
| 44 |
| 45 type TokenResponse struct { |
| 46 Expires string `json:"expires"` // should this be a date object? |
| 47 Id string `json:"id"` // Actual token string |
| 48 Tenant struct { |
| 49 Id string `json:"id"` |
| 50 Name string `json:"name"` |
| 51 } `json:"tenant"` |
| 52 } |
| 53 |
| 54 type RoleResponse struct { |
| 55 Id string `json:"id"` |
| 56 Name string `json:"name"` |
| 57 TenantId string `json:"tenantId"` |
| 58 } |
| 59 |
| 60 type UserResponse struct { |
| 61 Id string `json:"id"` |
| 62 Name string `json:"name"` |
| 63 Roles []RoleResponse `json:"roles"` |
| 64 } |
| 65 |
| 66 type AccessResponse struct { |
| 67 Access struct { |
| 68 ServiceCatalog []Service `json:"serviceCatalog"` |
| 69 Token TokenResponse `json:"token"` |
| 70 User UserResponse `json:"user"` |
| 71 } `json:"access"` |
| 72 } |
| 73 |
| 74 // Taken from: http://docs.openstack.org/api/quick-start/content/index.html#Gett
ing-Credentials-a00665 |
| 75 var exampleResponse = `{ |
| 76 "access": { |
| 77 "serviceCatalog": [ |
| 78 { |
| 79 "endpoints": [ |
| 80 { |
| 81 "adminURL": "https://nova-api.trystack.org:9774/v1.1/1",······· |
| 82 "internalURL": "https://nova-api.trystack.org:9774/v1.1/
1",· |
| 83 "publicURL": "https://nova-api.trystack.org:9774/v1.1/1"
,· |
| 84 "region": "RegionOne" |
| 85 } |
| 86 ],· |
| 87 "name": "nova",· |
| 88 "type": "compute" |
| 89 },· |
| 90 { |
| 91 "endpoints": [ |
| 92 { |
| 93 "adminURL": "https://GLANCE_API_IS_NOT_DISCLOSED/v1.1/1"
,· |
| 94 "internalURL": "https://GLANCE_API_IS_NOT_DISCLOSED/v1.1
/1",· |
| 95 "publicURL": "https://GLANCE_API_IS_NOT_DISCLOSED/v1.1/1
",· |
| 96 "region": "RegionOne" |
| 97 } |
| 98 ],· |
| 99 "name": "glance",· |
| 100 "type": "image" |
| 101 },· |
| 102 { |
| 103 "endpoints": [ |
| 104 { |
| 105 "adminURL": "https://nova-api.trystack.org:5443/v2.0",· |
| 106 "internalURL": "https://keystone.trystack.org:5000/v2.0"
,· |
| 107 "publicURL": "https://keystone.trystack.org:5000/v2.0",· |
| 108 "region": "RegionOne" |
| 109 } |
| 110 ],· |
| 111 "name": "keystone",· |
| 112 "type": "identity" |
| 113 } |
| 114 ],· |
| 115 "token": { |
| 116 "expires": "2012-02-15T19:32:21",· |
| 117 "id": "5df9d45d-d198-4222-9b4c-7a280aa35666",· |
| 118 "tenant": { |
| 119 "id": "1",· |
| 120 "name": "admin" |
| 121 } |
| 122 },· |
| 123 "user": { |
| 124 "id": "14",· |
| 125 "name": "annegentle",· |
| 126 "roles": [ |
| 127 { |
| 128 "id": "2",· |
| 129 "name": "Member",· |
| 130 "tenantId": "1" |
| 131 } |
| 132 ] |
| 133 } |
| 134 } |
| 135 }` |
| 136 |
| 137 type UserPass struct { |
| 138 users map[string]UserInfo |
| 139 services []Service |
| 140 } |
| 141 |
| 142 func NewUserPass() *UserPass { |
| 143 userpass := &UserPass{ |
| 144 users: make(map[string]UserInfo), |
| 145 services: make([]Service, 0), |
| 146 } |
| 147 return userpass |
| 148 } |
| 149 |
| 150 func (u *UserPass) AddUser(user, secret string) string { |
| 151 token := randomHexToken() |
| 152 u.users[user] = UserInfo{secret: secret, token: token} |
| 153 return token |
| 154 } |
| 155 |
| 156 func (u *UserPass) AddService(service Service) { |
| 157 u.services = append(u.services, service) |
| 158 } |
| 159 |
| 160 var internalError = []byte(`{ |
| 161 "error": { |
| 162 "message": "Internal failure", |
| 163 "code": 500, |
| 164 "title": Internal Server Error" |
| 165 } |
| 166 }`) |
| 167 |
| 168 func (u *UserPass) ReturnFailure(w http.ResponseWriter, status int, message stri
ng) { |
| 169 e := ErrorWrapper{ |
| 170 Error: ErrorResponse{ |
| 171 Message: message, |
| 172 Code: status, |
| 173 Title: http.StatusText(status), |
| 174 }, |
| 175 } |
| 176 if content, err := json.Marshal(e); err != nil { |
| 177 w.Header().Set("Content-Length", fmt.Sprintf("%d", len(internalE
rror))) |
| 178 w.WriteHeader(http.StatusInternalServerError) |
| 179 w.Write(internalError) |
| 180 } else { |
| 181 w.Header().Set("Content-Length", fmt.Sprintf("%d", len(content))
) |
| 182 w.WriteHeader(status) |
| 183 w.Write(content) |
| 184 } |
| 185 } |
| 186 |
| 187 // Taken from an actual responses, however it may vary based on actual Openstack
implementation |
| 188 const ( |
| 189 notJSON = ("Expecting to find application/json in Content-Type header."
+ |
| 190 " The server could not comply with the request since it is eithe
r malformed" + |
| 191 " or otherwise incorrect. The client is assumed to be in error."
) |
| 192 notAuthorized = "The request you have made requires authentication." |
| 193 invalidUser = "Invalid user / password" |
| 194 ) |
| 195 |
| 196 func (u *UserPass) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| 197 var req UserPassRequest |
| 198 // Testing against Canonistack, all responses are application/json, even
failures |
| 199 w.Header().Set("Content-Type", "application/json") |
| 200 if r.Header.Get("Content-Type") != "application/json" { |
| 201 u.ReturnFailure(w, http.StatusBadRequest, notJSON) |
| 202 return |
| 203 } |
| 204 if content, err := ioutil.ReadAll(r.Body); err != nil { |
| 205 w.WriteHeader(http.StatusBadRequest) |
| 206 return |
| 207 } else { |
| 208 if err := json.Unmarshal(content, &req); err != nil { |
| 209 u.ReturnFailure(w, http.StatusBadRequest, notJSON) |
| 210 return |
| 211 } |
| 212 } |
| 213 userInfo, ok := u.users[req.Auth.PasswordCredentials.Username] |
| 214 if !ok { |
| 215 u.ReturnFailure(w, http.StatusUnauthorized, notAuthorized) |
| 216 return |
| 217 } |
| 218 if userInfo.secret != req.Auth.PasswordCredentials.Password { |
| 219 u.ReturnFailure(w, http.StatusUnauthorized, invalidUser) |
| 220 return |
| 221 } |
| 222 res := AccessResponse{} |
| 223 // We pre-populate the response with genuine entries so that it looks sa
ne. |
| 224 // XXX: We should really build up valid state for this instead, at the |
| 225 // very least, we should manage the URLs better. |
| 226 if err := json.Unmarshal([]byte(exampleResponse), &res); err != nil { |
| 227 u.ReturnFailure(w, http.StatusInternalServerError, err.Error()) |
| 228 return |
| 229 } |
| 230 res.Access.ServiceCatalog = u.services |
| 231 res.Access.Token.Id = userInfo.token |
| 232 if content, err := json.Marshal(res); err != nil { |
| 233 u.ReturnFailure(w, http.StatusInternalServerError, err.Error()) |
| 234 return |
| 235 } else { |
| 236 w.WriteHeader(http.StatusOK) |
| 237 w.Write(content) |
| 238 return |
| 239 } |
| 240 panic("All paths should have already returned") |
| 241 } |
OLD | NEW |