rename api userconfig to frontend, return json on api auth error

This commit is contained in:
Christoph Kluge 2024-07-05 11:48:06 +02:00
parent 9d4767539c
commit 3afe40083d
8 changed files with 80 additions and 69 deletions

View File

@ -374,7 +374,7 @@ func main() {
securedapi := r.PathPrefix("/api").Subrouter() securedapi := r.PathPrefix("/api").Subrouter()
userapi := r.PathPrefix("/userapi").Subrouter() userapi := r.PathPrefix("/userapi").Subrouter()
configapi := r.PathPrefix("/config").Subrouter() configapi := r.PathPrefix("/config").Subrouter()
userconfigapi := r.PathPrefix("/userconfig").Subrouter() frontendapi := r.PathPrefix("/frontend").Subrouter()
if !config.Keys.DisableAuthentication { if !config.Keys.DisableAuthentication {
r.Handle("/login", authentication.Login( r.Handle("/login", authentication.Login(
@ -447,15 +447,13 @@ func main() {
// On success; // On success;
next, next,
// On failure: // On failure: JSON Response
func(rw http.ResponseWriter, r *http.Request, err error) { func(rw http.ResponseWriter, r *http.Request, err error) {
rw.Header().Add("Content-Type", "application/json")
rw.WriteHeader(http.StatusUnauthorized) rw.WriteHeader(http.StatusUnauthorized)
web.RenderTemplate(rw, "login.tmpl", &web.Page{ json.NewEncoder(rw).Encode(map[string]string{
Title: "Authentication failed - ClusterCockpit", "status": http.StatusText(http.StatusUnauthorized),
MsgType: "alert-danger", "error": err.Error(),
Message: err.Error(),
Build: buildInfo,
Infos: info,
}) })
}) })
}) })
@ -465,15 +463,13 @@ func main() {
// On success; // On success;
next, next,
// On failure: // On failure: JSON Response
func(rw http.ResponseWriter, r *http.Request, err error) { func(rw http.ResponseWriter, r *http.Request, err error) {
rw.Header().Add("Content-Type", "application/json")
rw.WriteHeader(http.StatusUnauthorized) rw.WriteHeader(http.StatusUnauthorized)
web.RenderTemplate(rw, "login.tmpl", &web.Page{ json.NewEncoder(rw).Encode(map[string]string{
Title: "Authentication failed - ClusterCockpit", "status": http.StatusText(http.StatusUnauthorized),
MsgType: "alert-danger", "error": err.Error(),
Message: err.Error(),
Build: buildInfo,
Infos: info,
}) })
}) })
}) })
@ -483,33 +479,29 @@ func main() {
// On success; // On success;
next, next,
// On failure: // On failure: JSON Response
func(rw http.ResponseWriter, r *http.Request, err error) { func(rw http.ResponseWriter, r *http.Request, err error) {
rw.Header().Add("Content-Type", "application/json")
rw.WriteHeader(http.StatusUnauthorized) rw.WriteHeader(http.StatusUnauthorized)
web.RenderTemplate(rw, "login.tmpl", &web.Page{ json.NewEncoder(rw).Encode(map[string]string{
Title: "Authentication failed - ClusterCockpit", "status": http.StatusText(http.StatusUnauthorized),
MsgType: "alert-danger", "error": err.Error(),
Message: err.Error(),
Build: buildInfo,
Infos: info,
}) })
}) })
}) })
userconfigapi.Use(func(next http.Handler) http.Handler { frontendapi.Use(func(next http.Handler) http.Handler {
return authentication.AuthUserConfigApi( return authentication.AuthFrontendApi(
// On success; // On success;
next, next,
// On failure: // On failure: JSON Response
func(rw http.ResponseWriter, r *http.Request, err error) { func(rw http.ResponseWriter, r *http.Request, err error) {
rw.Header().Add("Content-Type", "application/json")
rw.WriteHeader(http.StatusUnauthorized) rw.WriteHeader(http.StatusUnauthorized)
web.RenderTemplate(rw, "login.tmpl", &web.Page{ json.NewEncoder(rw).Encode(map[string]string{
Title: "Authentication failed - ClusterCockpit", "status": http.StatusText(http.StatusUnauthorized),
MsgType: "alert-danger", "error": err.Error(),
Message: err.Error(),
Build: buildInfo,
Infos: info,
}) })
}) })
}) })
@ -532,7 +524,7 @@ func main() {
api.MountApiRoutes(securedapi) api.MountApiRoutes(securedapi)
api.MountUserApiRoutes(userapi) api.MountUserApiRoutes(userapi)
api.MountConfigApiRoutes(configapi) api.MountConfigApiRoutes(configapi)
api.MountUserConfigApiRoutes(userconfigapi) api.MountFrontendApiRoutes(frontendapi)
if config.Keys.EmbedStaticFiles { if config.Keys.EmbedStaticFiles {
if i, err := os.Stat("./var/img"); err == nil { if i, err := os.Stat("./var/img"); err == nil {

View File

@ -106,12 +106,13 @@ func (api *RestApi) MountConfigApiRoutes(r *mux.Router) {
} }
} }
func (api *RestApi) MountUserConfigApiRoutes(r *mux.Router) { func (api *RestApi) MountFrontendApiRoutes(r *mux.Router) {
r.StrictSlash(true) r.StrictSlash(true)
if api.Authentication != nil { if api.Authentication != nil {
r.HandleFunc("/jwt/", api.getJWT).Methods(http.MethodGet) // Role:Admin Check in r.HandleFunc("/jwt/", api.getJWT).Methods(http.MethodGet)
r.HandleFunc("/configuration/", api.updateConfiguration).Methods(http.MethodPost) r.HandleFunc("/configuration/", api.updateConfiguration).Methods(http.MethodPost)
r.HandleFunc("/jobs/metrics/{id}", api.getJobMetrics).Methods(http.MethodGet) // Fetched in Job.svelte: Needs All-User-Access-Session-Auth
} }
} }

View File

@ -219,27 +219,25 @@ func (auth *Authentication) Auth(
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
user, err := auth.JwtAuth.AuthViaJWT(rw, r) user, err := auth.JwtAuth.AuthViaJWT(rw, r)
if err != nil { if err != nil {
log.Infof("authentication failed: %s", err.Error()) log.Infof("auth -> authentication failed: %s", err.Error())
http.Error(rw, err.Error(), http.StatusUnauthorized) http.Error(rw, err.Error(), http.StatusUnauthorized)
return return
} }
if user == nil { if user == nil {
user, err = auth.AuthViaSession(rw, r) user, err = auth.AuthViaSession(rw, r)
if err != nil { if err != nil {
log.Infof("authentication failed: %s", err.Error()) log.Infof("auth -> authentication failed: %s", err.Error())
http.Error(rw, err.Error(), http.StatusUnauthorized) http.Error(rw, err.Error(), http.StatusUnauthorized)
return return
} }
} }
if user != nil { if user != nil {
ctx := context.WithValue(r.Context(), repository.ContextUserKey, user) ctx := context.WithValue(r.Context(), repository.ContextUserKey, user)
onsuccess.ServeHTTP(rw, r.WithContext(ctx)) onsuccess.ServeHTTP(rw, r.WithContext(ctx))
return return
} }
log.Debug("authentication failed") log.Info("auth -> authentication failed")
onfailure(rw, r, errors.New("unauthorized (please login first)")) onfailure(rw, r, errors.New("unauthorized (please login first)"))
}) })
} }
@ -251,8 +249,8 @@ func (auth *Authentication) AuthApi(
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
user, err := auth.JwtAuth.AuthViaJWT(rw, r) user, err := auth.JwtAuth.AuthViaJWT(rw, r)
if err != nil { if err != nil {
log.Infof("authentication failed: %s", err.Error()) log.Infof("auth api -> authentication failed: %s", err.Error())
http.Error(rw, err.Error(), http.StatusUnauthorized) onfailure(rw, r, err)
return return
} }
if user != nil { if user != nil {
@ -270,12 +268,12 @@ func (auth *Authentication) AuthApi(
return return
} }
default: default:
log.Debug("authentication failed") log.Info("auth api -> authentication failed: missing role")
onfailure(rw, r, errors.New("unauthorized (missing role)")) onfailure(rw, r, errors.New("unauthorized"))
} }
} }
log.Debug("authentication failed") log.Info("auth api -> authentication failed: no auth")
onfailure(rw, r, errors.New("unauthorized (no auth)")) onfailure(rw, r, errors.New("unauthorized"))
}) })
} }
@ -286,8 +284,8 @@ func (auth *Authentication) AuthUserApi(
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
user, err := auth.JwtAuth.AuthViaJWT(rw, r) user, err := auth.JwtAuth.AuthViaJWT(rw, r)
if err != nil { if err != nil {
log.Infof("authentication failed: %s", err.Error()) log.Infof("auth user api -> authentication failed: %s", err.Error())
http.Error(rw, err.Error(), http.StatusUnauthorized) onfailure(rw, r, err)
return return
} }
if user != nil { if user != nil {
@ -305,12 +303,12 @@ func (auth *Authentication) AuthUserApi(
return return
} }
default: default:
log.Debug("authentication failed") log.Info("auth user api -> authentication failed: missing role")
onfailure(rw, r, errors.New("unauthorized (missing role)")) onfailure(rw, r, errors.New("unauthorized"))
} }
} }
log.Debug("authentication failed") log.Info("auth user api -> authentication failed: no auth")
onfailure(rw, r, errors.New("unauthorized (no auth)")) onfailure(rw, r, errors.New("unauthorized"))
}) })
} }
@ -321,8 +319,8 @@ func (auth *Authentication) AuthConfigApi(
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
user, err := auth.AuthViaSession(rw, r) user, err := auth.AuthViaSession(rw, r)
if err != nil { if err != nil {
log.Infof("authentication failed: %s", err.Error()) log.Infof("auth config api -> authentication failed: %s", err.Error())
http.Error(rw, err.Error(), http.StatusUnauthorized) onfailure(rw, r, err)
return return
} }
if user != nil && user.HasRole(schema.RoleAdmin) { if user != nil && user.HasRole(schema.RoleAdmin) {
@ -330,20 +328,20 @@ func (auth *Authentication) AuthConfigApi(
onsuccess.ServeHTTP(rw, r.WithContext(ctx)) onsuccess.ServeHTTP(rw, r.WithContext(ctx))
return return
} }
log.Debug("authentication failed") log.Info("auth config api -> authentication failed: no auth")
onfailure(rw, r, errors.New("unauthorized (no auth)")) onfailure(rw, r, errors.New("unauthorized"))
}) })
} }
func (auth *Authentication) AuthUserConfigApi( func (auth *Authentication) AuthFrontendApi(
onsuccess http.Handler, onsuccess http.Handler,
onfailure func(rw http.ResponseWriter, r *http.Request, authErr error), onfailure func(rw http.ResponseWriter, r *http.Request, authErr error),
) http.Handler { ) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
user, err := auth.AuthViaSession(rw, r) user, err := auth.AuthViaSession(rw, r)
if err != nil { if err != nil {
log.Infof("authentication failed: %s", err.Error()) log.Infof("auth frontend api -> authentication failed: %s", err.Error())
http.Error(rw, err.Error(), http.StatusUnauthorized) onfailure(rw, r, err)
return return
} }
if user != nil { if user != nil {
@ -351,8 +349,8 @@ func (auth *Authentication) AuthUserConfigApi(
onsuccess.ServeHTTP(rw, r.WithContext(ctx)) onsuccess.ServeHTTP(rw, r.WithContext(ctx))
return return
} }
log.Debug("authentication failed") log.Info("auth frontend api -> authentication failed: no auth")
onfailure(rw, r, errors.New("unauthorized (no auth)")) onfailure(rw, r, errors.New("unauthorized"))
}) })
} }

View File

@ -305,16 +305,36 @@ func (r *JobRepository) FindByIdDirect(jobId int64) (*schema.Job, error) {
return scanJob(q.RunWith(r.stmtCache).QueryRow()) return scanJob(q.RunWith(r.stmtCache).QueryRow())
} }
// FindByJobId executes a SQL query to find a specific batch job.
// The job is queried using the slurm id and the clustername.
// It returns a pointer to a schema.Job data structure and an error variable.
// To check if no job was found test err == sql.ErrNoRows
func (r *JobRepository) FindByJobId(ctx context.Context, jobId int64, startTime int64, cluster string) (*schema.Job, error) {
q := sq.Select(jobColumns...).
From("job").
Where("job.job_id = ?", jobId).
Where("job.cluster = ?", cluster).
Where("job.start_time = ?", startTime)
q, qerr := SecurityCheck(ctx, q)
if qerr != nil {
return nil, qerr
}
return scanJob(q.RunWith(r.stmtCache).QueryRow())
}
// IsJobOwner executes a SQL query to find a specific batch job. // IsJobOwner executes a SQL query to find a specific batch job.
// The job is queried using the slurm id,a username and the cluster. // The job is queried using the slurm id,a username and the cluster.
// It returns a bool. // It returns a bool.
// If job was found, user is owner: test err != sql.ErrNoRows // If job was found, user is owner: test err != sql.ErrNoRows
func (r *JobRepository) IsJobOwner(jobId int64, user string, cluster string) bool { func (r *JobRepository) IsJobOwner(jobId int64, startTime int64, user string, cluster string) bool {
q := sq.Select("id"). q := sq.Select("id").
From("job"). From("job").
Where("job.job_id = ?", jobId). Where("job.job_id = ?", jobId).
Where("job.user = ?", user). Where("job.user = ?", user).
Where("job.cluster = ?", cluster) Where("job.cluster = ?", cluster).
Where("job.start_time = ?", startTime)
_, err := scanJob(q.RunWith(r.stmtCache).QueryRow()) _, err := scanJob(q.RunWith(r.stmtCache).QueryRow())
return err != sql.ErrNoRows return err != sql.ErrNoRows

View File

@ -262,7 +262,7 @@
<form <form
id="colorscheme-form" id="colorscheme-form"
method="post" method="post"
action="/userconfig/configuration/" action="/frontend/configuration/"
class="card-body" class="card-body"
> >
<!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. --> <!-- Svelte 'class' directive only on DOMs directly, normal 'class="xxx"' does not work, so style-array it is. -->

View File

@ -30,7 +30,7 @@
<form <form
id="line-width-form" id="line-width-form"
method="post" method="post"
action="/userconfig/configuration/" action="/frontend/configuration/"
class="card-body" class="card-body"
on:submit|preventDefault={() => on:submit|preventDefault={() =>
updateSetting("#line-width-form", "lw")} updateSetting("#line-width-form", "lw")}
@ -76,7 +76,7 @@
<form <form
id="plots-per-row-form" id="plots-per-row-form"
method="post" method="post"
action="/userconfig/configuration/" action="/frontend/configuration/"
class="card-body" class="card-body"
on:submit|preventDefault={() => on:submit|preventDefault={() =>
updateSetting("#plots-per-row-form", "ppr")} updateSetting("#plots-per-row-form", "ppr")}
@ -122,7 +122,7 @@
<form <form
id="backgrounds-form" id="backgrounds-form"
method="post" method="post"
action="/userconfig/configuration/" action="/frontend/configuration/"
class="card-body" class="card-body"
on:submit|preventDefault={() => on:submit|preventDefault={() =>
updateSetting("#backgrounds-form", "bg")} updateSetting("#backgrounds-form", "bg")}

View File

@ -51,7 +51,7 @@
<form <form
id="paging-form" id="paging-form"
method="post" method="post"
action="/userconfig/configuration/" action="/frontend/configuration/"
class="card-body" class="card-body"
on:submit|preventDefault={() => on:submit|preventDefault={() =>
updateSetting("#paging-form", "pag")} updateSetting("#paging-form", "pag")}

View File

@ -239,7 +239,7 @@ export async function fetchMetrics(job, metrics, scopes) {
try { try {
let res = await fetch( let res = await fetch(
`/api/jobs/metrics/${job.id}${query.length > 0 ? "?" : ""}${query.join( `/frontend/jobs/metrics/${job.id}${query.length > 0 ? "?" : ""}${query.join(
"&" "&"
)}` )}`
); );
@ -434,7 +434,7 @@ export function transformPerNodeDataForRoofline(nodes) {
} }
export async function fetchJwt(username) { export async function fetchJwt(username) {
const raw = await fetch(`/userconfig/jwt/?username=${username}`); const raw = await fetch(`/frontend/jwt/?username=${username}`);
if (!raw.ok) { if (!raw.ok) {
const message = `An error has occured: ${response.status}`; const message = `An error has occured: ${response.status}`;