client_auth.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. // Copyright 2015 CoreOS, Inc.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package etcdhttp
  15. import (
  16. "encoding/json"
  17. "net/http"
  18. "path"
  19. "strings"
  20. "github.com/coreos/etcd/etcdserver"
  21. "github.com/coreos/etcd/etcdserver/auth"
  22. "github.com/coreos/etcd/etcdserver/etcdhttp/httptypes"
  23. )
  24. type authHandler struct {
  25. sec auth.Store
  26. cluster etcdserver.Cluster
  27. }
  28. func hasWriteRootAccess(sec auth.Store, r *http.Request) bool {
  29. if r.Method == "GET" || r.Method == "HEAD" {
  30. return true
  31. }
  32. return hasRootAccess(sec, r)
  33. }
  34. func hasRootAccess(sec auth.Store, r *http.Request) bool {
  35. if sec == nil {
  36. // No store means no auth available, eg, tests.
  37. return true
  38. }
  39. if !sec.AuthEnabled() {
  40. return true
  41. }
  42. username, password, ok := r.BasicAuth()
  43. if !ok {
  44. return false
  45. }
  46. rootUser, err := sec.GetUser(username)
  47. if err != nil {
  48. return false
  49. }
  50. ok = rootUser.CheckPassword(password)
  51. if !ok {
  52. plog.Warningf("auth: wrong password for user %s", username)
  53. return false
  54. }
  55. for _, role := range rootUser.Roles {
  56. if role == auth.RootRoleName {
  57. return true
  58. }
  59. }
  60. plog.Warningf("auth: user %s does not have the %s role for resource %s.", username, auth.RootRoleName, r.URL.Path)
  61. return false
  62. }
  63. func hasKeyPrefixAccess(sec auth.Store, r *http.Request, key string, recursive bool) bool {
  64. if sec == nil {
  65. // No store means no auth available, eg, tests.
  66. return true
  67. }
  68. if !sec.AuthEnabled() {
  69. return true
  70. }
  71. if r.Header.Get("Authorization") == "" {
  72. plog.Warningf("auth: no authorization provided, checking guest access")
  73. return hasGuestAccess(sec, r, key)
  74. }
  75. username, password, ok := r.BasicAuth()
  76. if !ok {
  77. plog.Warningf("auth: malformed basic auth encoding")
  78. return false
  79. }
  80. user, err := sec.GetUser(username)
  81. if err != nil {
  82. plog.Warningf("auth: no such user: %s.", username)
  83. return false
  84. }
  85. authAsUser := user.CheckPassword(password)
  86. if !authAsUser {
  87. plog.Warningf("auth: incorrect password for user: %s.", username)
  88. return false
  89. }
  90. writeAccess := r.Method != "GET" && r.Method != "HEAD"
  91. for _, roleName := range user.Roles {
  92. role, err := sec.GetRole(roleName)
  93. if err != nil {
  94. continue
  95. }
  96. if recursive {
  97. if role.HasRecursiveAccess(key, writeAccess) {
  98. return true
  99. }
  100. } else if role.HasKeyAccess(key, writeAccess) {
  101. return true
  102. }
  103. }
  104. plog.Warningf("auth: invalid access for user %s on key %s.", username, key)
  105. return false
  106. }
  107. func hasGuestAccess(sec auth.Store, r *http.Request, key string) bool {
  108. writeAccess := r.Method != "GET" && r.Method != "HEAD"
  109. role, err := sec.GetRole(auth.GuestRoleName)
  110. if err != nil {
  111. return false
  112. }
  113. if role.HasKeyAccess(key, writeAccess) {
  114. return true
  115. }
  116. plog.Warningf("auth: invalid access for unauthenticated user on resource %s.", key)
  117. return false
  118. }
  119. func writeNoAuth(w http.ResponseWriter) {
  120. herr := httptypes.NewHTTPError(http.StatusUnauthorized, "Insufficient credentials")
  121. herr.WriteTo(w)
  122. }
  123. func handleAuth(mux *http.ServeMux, sh *authHandler) {
  124. mux.HandleFunc(authPrefix+"/roles", capabilityHandler(authCapability, sh.baseRoles))
  125. mux.HandleFunc(authPrefix+"/roles/", capabilityHandler(authCapability, sh.handleRoles))
  126. mux.HandleFunc(authPrefix+"/users", capabilityHandler(authCapability, sh.baseUsers))
  127. mux.HandleFunc(authPrefix+"/users/", capabilityHandler(authCapability, sh.handleUsers))
  128. mux.HandleFunc(authPrefix+"/enable", capabilityHandler(authCapability, sh.enableDisable))
  129. }
  130. func (sh *authHandler) baseRoles(w http.ResponseWriter, r *http.Request) {
  131. if !allowMethod(w, r.Method, "GET") {
  132. return
  133. }
  134. if !hasRootAccess(sh.sec, r) {
  135. writeNoAuth(w)
  136. return
  137. }
  138. w.Header().Set("X-Etcd-Cluster-ID", sh.cluster.ID().String())
  139. w.Header().Set("Content-Type", "application/json")
  140. roles, err := sh.sec.AllRoles()
  141. if err != nil {
  142. writeError(w, err)
  143. return
  144. }
  145. if roles == nil {
  146. roles = make([]string, 0)
  147. }
  148. err = r.ParseForm()
  149. if err != nil {
  150. writeError(w, err)
  151. return
  152. }
  153. var rolesCollections struct {
  154. Roles []auth.Role `json:"roles"`
  155. }
  156. for _, roleName := range roles {
  157. var role auth.Role
  158. role, err = sh.sec.GetRole(roleName)
  159. if err != nil {
  160. writeError(w, err)
  161. return
  162. }
  163. rolesCollections.Roles = append(rolesCollections.Roles, role)
  164. }
  165. err = json.NewEncoder(w).Encode(rolesCollections)
  166. if err != nil {
  167. plog.Warningf("baseRoles error encoding on %s", r.URL)
  168. writeError(w, err)
  169. return
  170. }
  171. }
  172. func (sh *authHandler) handleRoles(w http.ResponseWriter, r *http.Request) {
  173. subpath := path.Clean(r.URL.Path[len(authPrefix):])
  174. // Split "/roles/rolename/command".
  175. // First item is an empty string, second is "roles"
  176. pieces := strings.Split(subpath, "/")
  177. if len(pieces) == 2 {
  178. sh.baseRoles(w, r)
  179. return
  180. }
  181. if len(pieces) != 3 {
  182. writeError(w, httptypes.NewHTTPError(http.StatusBadRequest, "Invalid path"))
  183. return
  184. }
  185. sh.forRole(w, r, pieces[2])
  186. }
  187. func (sh *authHandler) forRole(w http.ResponseWriter, r *http.Request, role string) {
  188. if !allowMethod(w, r.Method, "GET", "PUT", "DELETE") {
  189. return
  190. }
  191. if !hasRootAccess(sh.sec, r) {
  192. writeNoAuth(w)
  193. return
  194. }
  195. w.Header().Set("X-Etcd-Cluster-ID", sh.cluster.ID().String())
  196. w.Header().Set("Content-Type", "application/json")
  197. switch r.Method {
  198. case "GET":
  199. data, err := sh.sec.GetRole(role)
  200. if err != nil {
  201. writeError(w, err)
  202. return
  203. }
  204. err = json.NewEncoder(w).Encode(data)
  205. if err != nil {
  206. plog.Warningf("forRole error encoding on %s", r.URL)
  207. return
  208. }
  209. return
  210. case "PUT":
  211. var in auth.Role
  212. err := json.NewDecoder(r.Body).Decode(&in)
  213. if err != nil {
  214. writeError(w, httptypes.NewHTTPError(http.StatusBadRequest, "Invalid JSON in request body."))
  215. return
  216. }
  217. if in.Role != role {
  218. writeError(w, httptypes.NewHTTPError(http.StatusBadRequest, "Role JSON name does not match the name in the URL"))
  219. return
  220. }
  221. var out auth.Role
  222. // create
  223. if in.Grant.IsEmpty() && in.Revoke.IsEmpty() {
  224. err = sh.sec.CreateRole(in)
  225. if err != nil {
  226. writeError(w, err)
  227. return
  228. }
  229. w.WriteHeader(http.StatusCreated)
  230. out = in
  231. } else {
  232. if !in.Permissions.IsEmpty() {
  233. writeError(w, httptypes.NewHTTPError(http.StatusBadRequest, "Role JSON contains both permissions and grant/revoke"))
  234. return
  235. }
  236. out, err = sh.sec.UpdateRole(in)
  237. if err != nil {
  238. writeError(w, err)
  239. return
  240. }
  241. w.WriteHeader(http.StatusOK)
  242. }
  243. err = json.NewEncoder(w).Encode(out)
  244. if err != nil {
  245. plog.Warningf("forRole error encoding on %s", r.URL)
  246. return
  247. }
  248. return
  249. case "DELETE":
  250. err := sh.sec.DeleteRole(role)
  251. if err != nil {
  252. writeError(w, err)
  253. return
  254. }
  255. }
  256. }
  257. type userWithRoles struct {
  258. User string `json:"user"`
  259. Roles []auth.Role `json:"roles,omitempty"`
  260. }
  261. func (sh *authHandler) baseUsers(w http.ResponseWriter, r *http.Request) {
  262. if !allowMethod(w, r.Method, "GET") {
  263. return
  264. }
  265. if !hasRootAccess(sh.sec, r) {
  266. writeNoAuth(w)
  267. return
  268. }
  269. w.Header().Set("X-Etcd-Cluster-ID", sh.cluster.ID().String())
  270. w.Header().Set("Content-Type", "application/json")
  271. users, err := sh.sec.AllUsers()
  272. if err != nil {
  273. writeError(w, err)
  274. return
  275. }
  276. if users == nil {
  277. users = make([]string, 0)
  278. }
  279. err = r.ParseForm()
  280. if err != nil {
  281. writeError(w, err)
  282. return
  283. }
  284. var usersCollections struct {
  285. Users []userWithRoles `json:"users"`
  286. }
  287. for _, userName := range users {
  288. var user auth.User
  289. user, err = sh.sec.GetUser(userName)
  290. if err != nil {
  291. writeError(w, err)
  292. return
  293. }
  294. uwr := userWithRoles{User: user.User}
  295. for _, roleName := range user.Roles {
  296. var role auth.Role
  297. role, err = sh.sec.GetRole(roleName)
  298. if err != nil {
  299. writeError(w, err)
  300. return
  301. }
  302. uwr.Roles = append(uwr.Roles, role)
  303. }
  304. usersCollections.Users = append(usersCollections.Users, uwr)
  305. }
  306. err = json.NewEncoder(w).Encode(usersCollections)
  307. if err != nil {
  308. plog.Warningf("baseUsers error encoding on %s", r.URL)
  309. writeError(w, err)
  310. return
  311. }
  312. }
  313. func (sh *authHandler) handleUsers(w http.ResponseWriter, r *http.Request) {
  314. subpath := path.Clean(r.URL.Path[len(authPrefix):])
  315. // Split "/users/username".
  316. // First item is an empty string, second is "users"
  317. pieces := strings.Split(subpath, "/")
  318. if len(pieces) == 2 {
  319. sh.baseUsers(w, r)
  320. return
  321. }
  322. if len(pieces) != 3 {
  323. writeError(w, httptypes.NewHTTPError(http.StatusBadRequest, "Invalid path"))
  324. return
  325. }
  326. sh.forUser(w, r, pieces[2])
  327. }
  328. func (sh *authHandler) forUser(w http.ResponseWriter, r *http.Request, user string) {
  329. if !allowMethod(w, r.Method, "GET", "PUT", "DELETE") {
  330. return
  331. }
  332. if !hasRootAccess(sh.sec, r) {
  333. writeNoAuth(w)
  334. return
  335. }
  336. w.Header().Set("X-Etcd-Cluster-ID", sh.cluster.ID().String())
  337. w.Header().Set("Content-Type", "application/json")
  338. switch r.Method {
  339. case "GET":
  340. u, err := sh.sec.GetUser(user)
  341. if err != nil {
  342. writeError(w, err)
  343. return
  344. }
  345. err = r.ParseForm()
  346. if err != nil {
  347. writeError(w, err)
  348. return
  349. }
  350. uwr := userWithRoles{User: u.User}
  351. for _, roleName := range u.Roles {
  352. var role auth.Role
  353. role, err = sh.sec.GetRole(roleName)
  354. if err != nil {
  355. writeError(w, err)
  356. return
  357. }
  358. uwr.Roles = append(uwr.Roles, role)
  359. }
  360. err = json.NewEncoder(w).Encode(uwr)
  361. if err != nil {
  362. plog.Warningf("forUser error encoding on %s", r.URL)
  363. return
  364. }
  365. return
  366. case "PUT":
  367. var u auth.User
  368. err := json.NewDecoder(r.Body).Decode(&u)
  369. if err != nil {
  370. writeError(w, httptypes.NewHTTPError(http.StatusBadRequest, "Invalid JSON in request body."))
  371. return
  372. }
  373. if u.User != user {
  374. writeError(w, httptypes.NewHTTPError(http.StatusBadRequest, "User JSON name does not match the name in the URL"))
  375. return
  376. }
  377. var (
  378. out auth.User
  379. created bool
  380. )
  381. if len(u.Grant) == 0 && len(u.Revoke) == 0 {
  382. // create or update
  383. if len(u.Roles) != 0 {
  384. out, err = sh.sec.CreateUser(u)
  385. } else {
  386. // if user passes in both password and roles, we are unsure about his/her
  387. // intention.
  388. out, created, err = sh.sec.CreateOrUpdateUser(u)
  389. }
  390. if err != nil {
  391. writeError(w, err)
  392. return
  393. }
  394. } else {
  395. // update case
  396. if len(u.Roles) != 0 {
  397. writeError(w, httptypes.NewHTTPError(http.StatusBadRequest, "User JSON contains both roles and grant/revoke"))
  398. return
  399. }
  400. out, err = sh.sec.UpdateUser(u)
  401. if err != nil {
  402. writeError(w, err)
  403. return
  404. }
  405. }
  406. if created {
  407. w.WriteHeader(http.StatusCreated)
  408. } else {
  409. w.WriteHeader(http.StatusOK)
  410. }
  411. out.Password = ""
  412. err = json.NewEncoder(w).Encode(out)
  413. if err != nil {
  414. plog.Warningf("forUser error encoding on %s", r.URL)
  415. return
  416. }
  417. return
  418. case "DELETE":
  419. err := sh.sec.DeleteUser(user)
  420. if err != nil {
  421. writeError(w, err)
  422. return
  423. }
  424. }
  425. }
  426. type enabled struct {
  427. Enabled bool `json:"enabled"`
  428. }
  429. func (sh *authHandler) enableDisable(w http.ResponseWriter, r *http.Request) {
  430. if !allowMethod(w, r.Method, "GET", "PUT", "DELETE") {
  431. return
  432. }
  433. if !hasWriteRootAccess(sh.sec, r) {
  434. writeNoAuth(w)
  435. return
  436. }
  437. w.Header().Set("X-Etcd-Cluster-ID", sh.cluster.ID().String())
  438. w.Header().Set("Content-Type", "application/json")
  439. isEnabled := sh.sec.AuthEnabled()
  440. switch r.Method {
  441. case "GET":
  442. jsonDict := enabled{isEnabled}
  443. err := json.NewEncoder(w).Encode(jsonDict)
  444. if err != nil {
  445. plog.Warningf("error encoding auth state on %s", r.URL)
  446. }
  447. case "PUT":
  448. err := sh.sec.EnableAuth()
  449. if err != nil {
  450. writeError(w, err)
  451. return
  452. }
  453. case "DELETE":
  454. err := sh.sec.DisableAuth()
  455. if err != nil {
  456. writeError(w, err)
  457. return
  458. }
  459. }
  460. }