1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-22 23:09:41 +02:00

feat(uac): add multi user management and UAC (#647)

This commit is contained in:
Anthony Lapenna 2017-03-12 17:24:15 +01:00 committed by GitHub
parent f28f223624
commit 80d50378c5
91 changed files with 3973 additions and 866 deletions

View file

@ -1,6 +1,8 @@
package http
import (
"strconv"
"github.com/portainer/portainer"
"encoding/json"
@ -15,43 +17,44 @@ import (
// UserHandler represents an HTTP API handler for managing users.
type UserHandler struct {
*mux.Router
Logger *log.Logger
UserService portainer.UserService
CryptoService portainer.CryptoService
middleWareService *middleWareService
Logger *log.Logger
UserService portainer.UserService
ResourceControlService portainer.ResourceControlService
CryptoService portainer.CryptoService
}
// NewUserHandler returns a new instance of UserHandler.
func NewUserHandler(middleWareService *middleWareService) *UserHandler {
func NewUserHandler(mw *middleWareService) *UserHandler {
h := &UserHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
middleWareService: middleWareService,
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.Handle("/users", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.handlePostUsers(w, r)
})))
h.Handle("/users/{username}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.handleGetUser(w, r)
}))).Methods(http.MethodGet)
h.Handle("/users/{username}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.handlePutUser(w, r)
}))).Methods(http.MethodPut)
h.Handle("/users/{username}/passwd", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.handlePostUserPasswd(w, r)
})))
h.HandleFunc("/users/admin/check", h.handleGetAdminCheck)
h.HandleFunc("/users/admin/init", h.handlePostAdminInit)
h.Handle("/users",
mw.administrator(http.HandlerFunc(h.handlePostUsers))).Methods(http.MethodPost)
h.Handle("/users",
mw.administrator(http.HandlerFunc(h.handleGetUsers))).Methods(http.MethodGet)
h.Handle("/users/{id}",
mw.administrator(http.HandlerFunc(h.handleGetUser))).Methods(http.MethodGet)
h.Handle("/users/{id}",
mw.authenticated(http.HandlerFunc(h.handlePutUser))).Methods(http.MethodPut)
h.Handle("/users/{id}",
mw.administrator(http.HandlerFunc(h.handleDeleteUser))).Methods(http.MethodDelete)
h.Handle("/users/{id}/passwd",
mw.authenticated(http.HandlerFunc(h.handlePostUserPasswd)))
h.Handle("/users/{userId}/resources/{resourceType}",
mw.authenticated(http.HandlerFunc(h.handlePostUserResource))).Methods(http.MethodPost)
h.Handle("/users/{userId}/resources/{resourceType}/{resourceId}",
mw.authenticated(http.HandlerFunc(h.handleDeleteUserResource))).Methods(http.MethodDelete)
h.Handle("/users/admin/check",
mw.public(http.HandlerFunc(h.handleGetAdminCheck)))
h.Handle("/users/admin/init",
mw.public(http.HandlerFunc(h.handlePostAdminInit)))
return h
}
// handlePostUsers handles POST requests on /users
func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
handleNotAllowed(w, []string{http.MethodPost})
return
}
var req postUsersRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
@ -64,8 +67,26 @@ func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Reque
return
}
user := &portainer.User{
var role portainer.UserRole
if req.Role == 1 {
role = portainer.AdministratorRole
} else {
role = portainer.StandardUserRole
}
user, err := handler.UserService.UserByUsername(req.Username)
if err != nil && err != portainer.ErrUserNotFound {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if user != nil {
Error(w, portainer.ErrUserAlreadyExists, http.StatusConflict, handler.Logger)
return
}
user = &portainer.User{
Username: req.Username,
Role: role,
}
user.Password, err = handler.CryptoService.Hash(req.Password)
if err != nil {
@ -73,7 +94,7 @@ func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Reque
return
}
err = handler.UserService.UpdateUser(user)
err = handler.UserService.CreateUser(user)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
@ -83,9 +104,24 @@ func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Reque
type postUsersRequest struct {
Username string `valid:"alphanum,required"`
Password string `valid:"required"`
Role int `valid:"required"`
}
// handlePostUserPasswd handles POST requests on /users/:username/passwd
// handleGetUsers handles GET requests on /users
func (handler *UserHandler) handleGetUsers(w http.ResponseWriter, r *http.Request) {
users, err := handler.UserService.Users()
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
for i := range users {
users[i].Password = ""
}
encodeJSON(w, users, handler.Logger)
}
// handlePostUserPasswd handles POST requests on /users/:id/passwd
func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
handleNotAllowed(w, []string{http.MethodPost})
@ -93,15 +129,21 @@ func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.
}
vars := mux.Vars(r)
username := vars["username"]
id := vars["id"]
userID, err := strconv.Atoi(id)
if err != nil {
Error(w, err, http.StatusBadRequest, handler.Logger)
return
}
var req postUserPasswdRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err := govalidator.ValidateStruct(req)
_, err = govalidator.ValidateStruct(req)
if err != nil {
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
@ -109,7 +151,7 @@ func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.
var password = req.Password
u, err := handler.UserService.User(username)
u, err := handler.UserService.User(portainer.UserID(userID))
if err == portainer.ErrUserNotFound {
Error(w, err, http.StatusNotFound, handler.Logger)
return
@ -135,12 +177,18 @@ type postUserPasswdResponse struct {
Valid bool `json:"valid"`
}
// handleGetUser handles GET requests on /users/:username
// handleGetUser handles GET requests on /users/:id
func (handler *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
username := vars["username"]
id := vars["id"]
user, err := handler.UserService.User(username)
userID, err := strconv.Atoi(id)
if err != nil {
Error(w, err, http.StatusBadRequest, handler.Logger)
return
}
user, err := handler.UserService.User(portainer.UserID(userID))
if err == portainer.ErrUserNotFound {
Error(w, err, http.StatusNotFound, handler.Logger)
return
@ -153,30 +201,74 @@ func (handler *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request
encodeJSON(w, &user, handler.Logger)
}
// handlePutUser handles PUT requests on /users/:username
// handlePutUser handles PUT requests on /users/:id
func (handler *UserHandler) handlePutUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
userID, err := strconv.Atoi(id)
if err != nil {
Error(w, err, http.StatusBadRequest, handler.Logger)
return
}
tokenData, err := extractTokenDataFromRequestContext(r)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
}
if tokenData.Role != portainer.AdministratorRole && tokenData.ID != portainer.UserID(userID) {
Error(w, portainer.ErrUnauthorized, http.StatusForbidden, handler.Logger)
return
}
var req putUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err := govalidator.ValidateStruct(req)
_, err = govalidator.ValidateStruct(req)
if err != nil {
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
user := &portainer.User{
Username: req.Username,
}
user.Password, err = handler.CryptoService.Hash(req.Password)
if err != nil {
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
if req.Password == "" && req.Role == 0 {
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
err = handler.UserService.UpdateUser(user)
user, err := handler.UserService.User(portainer.UserID(userID))
if err == portainer.ErrUserNotFound {
Error(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if req.Password != "" {
user.Password, err = handler.CryptoService.Hash(req.Password)
if err != nil {
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
return
}
}
if req.Role != 0 {
if tokenData.Role != portainer.AdministratorRole {
Error(w, portainer.ErrUnauthorized, http.StatusForbidden, handler.Logger)
return
}
if req.Role == 1 {
user.Role = portainer.AdministratorRole
} else {
user.Role = portainer.StandardUserRole
}
}
err = handler.UserService.UpdateUser(user.ID, user)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
@ -184,8 +276,8 @@ func (handler *UserHandler) handlePutUser(w http.ResponseWriter, r *http.Request
}
type putUserRequest struct {
Username string `valid:"alphanum,required"`
Password string `valid:"required"`
Password string `valid:"-"`
Role int `valid:"-"`
}
// handlePostAdminInit handles GET requests on /users/admin/check
@ -195,17 +287,15 @@ func (handler *UserHandler) handleGetAdminCheck(w http.ResponseWriter, r *http.R
return
}
user, err := handler.UserService.User("admin")
if err == portainer.ErrUserNotFound {
Error(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
users, err := handler.UserService.UsersByRole(portainer.AdministratorRole)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
user.Password = ""
encodeJSON(w, &user, handler.Logger)
if len(users) == 0 {
Error(w, portainer.ErrUserNotFound, http.StatusNotFound, handler.Logger)
return
}
}
// handlePostAdminInit handles POST requests on /users/admin/init
@ -227,10 +317,11 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R
return
}
user, err := handler.UserService.User("admin")
user, err := handler.UserService.UserByUsername("admin")
if err == portainer.ErrUserNotFound {
user := &portainer.User{
Username: "admin",
Role: portainer.AdministratorRole,
}
user.Password, err = handler.CryptoService.Hash(req.Password)
if err != nil {
@ -238,7 +329,7 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R
return
}
err = handler.UserService.UpdateUser(user)
err = handler.UserService.CreateUser(user)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
@ -256,3 +347,134 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R
type postAdminInitRequest struct {
Password string `valid:"required"`
}
// handleDeleteUser handles DELETE requests on /users/:id
func (handler *UserHandler) handleDeleteUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
userID, err := strconv.Atoi(id)
if err != nil {
Error(w, err, http.StatusBadRequest, handler.Logger)
return
}
_, err = handler.UserService.User(portainer.UserID(userID))
if err == portainer.ErrUserNotFound {
Error(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
err = handler.UserService.DeleteUser(portainer.UserID(userID))
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
// handlePostUserResource handles POST requests on /users/:userId/resources/:resourceType
func (handler *UserHandler) handlePostUserResource(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
userID := vars["userId"]
resourceType := vars["resourceType"]
uid, err := strconv.Atoi(userID)
if err != nil {
Error(w, err, http.StatusBadRequest, handler.Logger)
return
}
var rcType portainer.ResourceControlType
if resourceType == "container" {
rcType = portainer.ContainerResourceControl
} else if resourceType == "service" {
rcType = portainer.ServiceResourceControl
} else if resourceType == "volume" {
rcType = portainer.VolumeResourceControl
} else {
Error(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
return
}
tokenData, err := extractTokenDataFromRequestContext(r)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
}
if tokenData.ID != portainer.UserID(uid) {
Error(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
return
}
var req postUserResourceRequest
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
return
}
_, err = govalidator.ValidateStruct(req)
if err != nil {
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
resource := portainer.ResourceControl{
OwnerID: portainer.UserID(uid),
ResourceID: req.ResourceID,
AccessLevel: portainer.RestrictedResourceAccessLevel,
}
err = handler.ResourceControlService.CreateResourceControl(req.ResourceID, &resource, rcType)
if err != nil {
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
}
type postUserResourceRequest struct {
ResourceID string `valid:"required"`
}
// handleDeleteUserResource handles DELETE requests on /users/:userId/resources/:resourceType/:resourceId
func (handler *UserHandler) handleDeleteUserResource(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
userID := vars["userId"]
resourceID := vars["resourceId"]
resourceType := vars["resourceType"]
uid, err := strconv.Atoi(userID)
if err != nil {
Error(w, err, http.StatusBadRequest, handler.Logger)
return
}
var rcType portainer.ResourceControlType
if resourceType == "container" {
rcType = portainer.ContainerResourceControl
} else if resourceType == "service" {
rcType = portainer.ServiceResourceControl
} else if resourceType == "volume" {
rcType = portainer.VolumeResourceControl
} else {
Error(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
return
}
tokenData, err := extractTokenDataFromRequestContext(r)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
}
if tokenData.Role != portainer.AdministratorRole && tokenData.ID != portainer.UserID(uid) {
Error(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
return
}
err = handler.ResourceControlService.DeleteResourceControl(resourceID, rcType)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}