Add secured subrouter for REST API

Rename IP filter option
Add array helper in util
This commit is contained in:
Jan Eitzinger 2023-08-14 14:33:05 +02:00
parent 42e05fc999
commit 90bdfcfbb6
3 changed files with 135 additions and 84 deletions

View File

@ -26,6 +26,7 @@ import (
"github.com/ClusterCockpit/cc-backend/internal/importer" "github.com/ClusterCockpit/cc-backend/internal/importer"
"github.com/ClusterCockpit/cc-backend/internal/metricdata" "github.com/ClusterCockpit/cc-backend/internal/metricdata"
"github.com/ClusterCockpit/cc-backend/internal/repository" "github.com/ClusterCockpit/cc-backend/internal/repository"
"github.com/ClusterCockpit/cc-backend/internal/util"
"github.com/ClusterCockpit/cc-backend/pkg/archive" "github.com/ClusterCockpit/cc-backend/pkg/archive"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/schema" "github.com/ClusterCockpit/cc-backend/pkg/schema"
@ -76,23 +77,65 @@ func (api *RestApi) MountRoutes(r *mux.Router) {
r.HandleFunc("/jobs/delete_job/", api.deleteJobByRequest).Methods(http.MethodDelete) r.HandleFunc("/jobs/delete_job/", api.deleteJobByRequest).Methods(http.MethodDelete)
r.HandleFunc("/jobs/delete_job/{id}", api.deleteJobById).Methods(http.MethodDelete) r.HandleFunc("/jobs/delete_job/{id}", api.deleteJobById).Methods(http.MethodDelete)
r.HandleFunc("/jobs/delete_job_before/{ts}", api.deleteJobBefore).Methods(http.MethodDelete) r.HandleFunc("/jobs/delete_job_before/{ts}", api.deleteJobBefore).Methods(http.MethodDelete)
r.HandleFunc("/secured/addProject/{id}/{project}", api.secureUpdateUser).Methods(http.MethodPost) // r.HandleFunc("/secured/addProject/{id}/{project}", api.secureUpdateUser).Methods(http.MethodPost)
r.HandleFunc("/secured/addRole/{id}/{role}", api.secureUpdateUser).Methods(http.MethodPost) // r.HandleFunc("/secured/addRole/{id}/{role}", api.secureUpdateUser).Methods(http.MethodPost)
if api.Authentication != nil {
r.HandleFunc("/jwt/", api.getJWT).Methods(http.MethodGet)
r.HandleFunc("/roles/", api.getRoles).Methods(http.MethodGet)
r.HandleFunc("/users/", api.createUser).Methods(http.MethodPost, http.MethodPut)
r.HandleFunc("/users/", api.getUsers).Methods(http.MethodGet)
r.HandleFunc("/users/", api.deleteUser).Methods(http.MethodDelete)
r.HandleFunc("/user/{id}", api.updateUser).Methods(http.MethodPost)
r.HandleFunc("/configuration/", api.updateConfiguration).Methods(http.MethodPost)
}
if api.MachineStateDir != "" { if api.MachineStateDir != "" {
r.HandleFunc("/machine_state/{cluster}/{host}", api.getMachineState).Methods(http.MethodGet) r.HandleFunc("/machine_state/{cluster}/{host}", api.getMachineState).Methods(http.MethodGet)
r.HandleFunc("/machine_state/{cluster}/{host}", api.putMachineState).Methods(http.MethodPut, http.MethodPost) r.HandleFunc("/machine_state/{cluster}/{host}", api.putMachineState).Methods(http.MethodPut, http.MethodPost)
} }
if api.Authentication != nil {
rw := r.MatcherFunc(
func(rq *http.Request, rm *mux.RouteMatch) bool {
user := auth.GetUser(rq.Context())
return user.AuthType == auth.AuthSession
}).Subrouter()
rw.HandleFunc("/jwt/", api.getJWT).Methods(http.MethodGet)
rw.HandleFunc("/roles/", api.getRoles).Methods(http.MethodGet)
rw.HandleFunc("/users/", api.createUser).Methods(http.MethodPost, http.MethodPut)
rw.HandleFunc("/users/", api.getUsers).Methods(http.MethodGet)
rw.HandleFunc("/users/", api.deleteUser).Methods(http.MethodDelete)
rw.HandleFunc("/user/{id}", api.updateUser).Methods(http.MethodPost)
rw.HandleFunc("/configuration/", api.updateConfiguration).Methods(http.MethodPost)
rs := r.PathPrefix("/secured").MatcherFunc(
func(rq *http.Request, rm *mux.RouteMatch) bool {
user := auth.GetUser(rq.Context())
// this only applies for token based authorization
if user.AuthType != auth.AuthToken {
return false
}
// If nothing declared in config: deny all request to this endpoint
if config.Keys.ApiAllowedIPs == nil || len(config.Keys.ApiAllowedIPs) == 0 {
return false
}
// extract IP address
IPAddress := rq.Header.Get("X-Real-Ip")
if IPAddress == "" {
IPAddress = rq.Header.Get("X-Forwarded-For")
}
if IPAddress == "" {
IPAddress = rq.RemoteAddr
}
// check if IP is allowed
if !util.Contains(config.Keys.ApiAllowedIPs, IPAddress) {
return false
}
return true
}).Subrouter()
rs.HandleFunc("/jwt/", api.getJWT).Methods(http.MethodGet)
rs.HandleFunc("/roles/", api.getRoles).Methods(http.MethodGet)
rs.HandleFunc("/users/", api.createUser).Methods(http.MethodPost, http.MethodPut)
rs.HandleFunc("/users/", api.getUsers).Methods(http.MethodGet)
rs.HandleFunc("/users/", api.deleteUser).Methods(http.MethodDelete)
rs.HandleFunc("/user/{id}", api.updateUser).Methods(http.MethodPost)
rs.HandleFunc("/configuration/", api.updateConfiguration).Methods(http.MethodPost)
}
} }
// StartJobApiResponse model // StartJobApiResponse model
@ -1051,76 +1094,70 @@ func (api *RestApi) updateUser(rw http.ResponseWriter, r *http.Request) {
} }
} }
func (api *RestApi) secureUpdateUser(rw http.ResponseWriter, r *http.Request) { // func (api *RestApi) secureUpdateUser(rw http.ResponseWriter, r *http.Request) {
if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) { // if user := auth.GetUser(r.Context()); user != nil && !user.HasRole(auth.RoleApi) {
handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw) // handleError(fmt.Errorf("missing role: %v", auth.GetRoleString(auth.RoleApi)), http.StatusForbidden, rw)
return // return
} // }
//
// If nothing declared in config: deny all request to this endpint // // IP CHECK HERE (WIP)
if config.Keys.ApiAllowedAddrs == nil || len(config.Keys.ApiAllowedAddrs) == 0 { // // Probably better as private routine
handleError(fmt.Errorf("denied by default policy!"), http.StatusForbidden, rw) // IPAddress := r.Header.Get("X-Real-Ip")
return // if IPAddress == "" {
} // IPAddress = r.Header.Get("X-Forwarded-For")
// }
// IP CHECK HERE (WIP) // if IPAddress == "" {
// Probably better as private routine // IPAddress = r.RemoteAddr
IPAddress := r.Header.Get("X-Real-Ip") // }
if IPAddress == "" { //
IPAddress = r.Header.Get("X-Forwarded-For") // // Also This
} // ipOk := false
if IPAddress == "" { // for _, a := range config.Keys.ApiAllowedAddrs {
IPAddress = r.RemoteAddr // if a == IPAddress {
} // ipOk = true
// }
// Also This // }
ipOk := false //
for _, a := range config.Keys.ApiAllowedAddrs { // if IPAddress == "" || ipOk == false {
if a == IPAddress { // handleError(fmt.Errorf("unknown ip: %v", IPAddress), http.StatusForbidden, rw)
ipOk = true // return
} // }
} // // IP CHECK END
//
if IPAddress == "" || ipOk == false { // // Get Values
handleError(fmt.Errorf("unknown ip: %v", IPAddress), http.StatusForbidden, rw) // id := mux.Vars(r)["id"]
return // newproj := mux.Vars(r)["project"]
} // newrole := mux.Vars(r)["role"]
// IP CHECK END //
// // TODO: Handle anything but roles...
// Get Values // if newrole != "" {
id := mux.Vars(r)["id"] // if err := api.Authentication.AddRole(r.Context(), id, newrole); err != nil {
newproj := mux.Vars(r)["project"] // handleError(errors.New(err.Error()), http.StatusUnprocessableEntity, rw)
newrole := mux.Vars(r)["role"] // return
// }
// TODO: Handle anything but roles... //
if newrole != "" { // rw.Header().Add("Content-Type", "application/json")
if err := api.Authentication.AddRole(r.Context(), id, newrole); err != nil { // rw.WriteHeader(http.StatusOK)
handleError(errors.New(err.Error()), http.StatusUnprocessableEntity, rw) // json.NewEncoder(rw).Encode(UpdateUserApiResponse{
return // Message: fmt.Sprintf("Successfully added role %s to %s", newrole, id),
} // })
//
rw.Header().Add("Content-Type", "application/json") // } else if newproj != "" {
rw.WriteHeader(http.StatusOK) // if err := api.Authentication.AddProject(r.Context(), id, newproj); err != nil {
json.NewEncoder(rw).Encode(UpdateUserApiResponse{ // handleError(errors.New(err.Error()), http.StatusUnprocessableEntity, rw)
Message: fmt.Sprintf("Successfully added role %s to %s", newrole, id), // return
}) // }
//
} else if newproj != "" { // rw.Header().Add("Content-Type", "application/json")
if err := api.Authentication.AddProject(r.Context(), id, newproj); err != nil { // rw.WriteHeader(http.StatusOK)
handleError(errors.New(err.Error()), http.StatusUnprocessableEntity, rw) // json.NewEncoder(rw).Encode(UpdateUserApiResponse{
return // Message: fmt.Sprintf("Successfully added project %s to %s", newproj, id),
} // })
//
rw.Header().Add("Content-Type", "application/json") // } else {
rw.WriteHeader(http.StatusOK) // handleError(errors.New("Not Add [role|project]?"), http.StatusBadRequest, rw)
json.NewEncoder(rw).Encode(UpdateUserApiResponse{ // }
Message: fmt.Sprintf("Successfully added project %s to %s", newproj, id), // }
})
} else {
handleError(errors.New("Not Add [role|project]?"), http.StatusBadRequest, rw)
}
}
func (api *RestApi) updateConfiguration(rw http.ResponseWriter, r *http.Request) { func (api *RestApi) updateConfiguration(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("Content-Type", "text/plain") rw.Header().Set("Content-Type", "text/plain")

14
internal/util/array.go Normal file
View File

@ -0,0 +1,14 @@
// Copyright (C) 2023 NHR@FAU, University Erlangen-Nuremberg.
// All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package util
func Contains[T comparable](items []T, item T) bool {
for _, v := range items {
if v == item {
return true
}
}
return false
}

View File

@ -70,8 +70,8 @@ type ProgramConfig struct {
// Address where the http (or https) server will listen on (for example: 'localhost:80'). // Address where the http (or https) server will listen on (for example: 'localhost:80').
Addr string `json:"addr"` Addr string `json:"addr"`
// Addresses from which the /secured/* API endpoints can be reached // Addresses from which the /api/secured/* API endpoints can be reached
ApiAllowedAddrs []string `json:"apiAllowedAddrs"` ApiAllowedIPs []string `json:"apiAllowedIPs"`
// Drop root permissions once .env was read and the port was taken. // Drop root permissions once .env was read and the port was taken.
User string `json:"user"` User string `json:"user"`