20 Commits

Author SHA1 Message Date
Jan Eitzinger
ae79f3e98a Merge pull request #188 from ClusterCockpit/hotfix
Prepare minor release 1.1.0
2023-07-20 08:39:29 +02:00
f81ffbe83d Prepare minor release 1.1.0 2023-07-20 08:33:42 +02:00
Jan Eitzinger
c0ab5de2f1 Merge pull request #182 from ClusterCockpit/179_fix_frontend_apiusers
Fix frontend render for users with api role
2023-07-20 07:42:15 +02:00
Jan Eitzinger
25f5a889d0 Merge pull request #183 from ClusterCockpit/180_fix_render_acc_nodescope
fix: check if acc metrics are acc scope by default
2023-07-20 07:41:09 +02:00
Jan Eitzinger
71739e301c Merge pull request #187 from fodinabor/feature/arrayJobIdSearch
Add arrayJobId searchbar option.
2023-07-20 07:39:31 +02:00
Joachim Meyer
650bcae6be Add arrayJobId searchbar option. 2023-07-19 09:46:48 +02:00
Jan Eitzinger
1062989686 Merge pull request #184 from ClusterCockpit/177_add_scrambling
fix: add scrambling to user names and projectIds
2023-07-19 09:30:09 +02:00
Jan Eitzinger
536a51b93f Merge pull request #186 from ClusterCockpit/185-add-notification-banner
185 add notification banner
2023-07-19 09:13:15 +02:00
Jan Eitzinger
19f2e16bae Merge pull request #178 from ClusterCockpit/hotfix
Hotfix
2023-07-19 09:12:30 +02:00
5923070191 make distclean target phony 2023-07-19 09:04:46 +02:00
04e8279ae4 Change log level for JWT Cross login warning to debug 2023-07-19 09:04:27 +02:00
aab50775d8 Improve message layout and styling 2023-07-19 08:47:42 +02:00
c6a0d442cc feat: Add optional notification banner on homepage
Fixes #185
2023-07-19 08:25:14 +02:00
2674f2a769 Add message banner to template 2023-07-19 07:51:04 +02:00
Christoph Kluge
eed8bb2d44 fix: add scrambling to user names and projectIds 2023-07-17 15:45:40 +02:00
Christoph Kluge
58c7b0d1b4 fix: check if acc metrics are acc scope by default
- Fixes #180
2023-07-17 14:26:24 +02:00
Christoph Kluge
55943cacbf Fix frontend render for users with api role 2023-07-17 12:19:49 +02:00
b25ceccae9 Minor typos 2023-07-05 10:15:12 +02:00
c5633e9e6d Remove typos 2023-07-05 10:01:46 +02:00
df9fd77d06 Refactor auth and add docs
Cleanup and reformat
2023-07-05 09:50:44 +02:00
13 changed files with 253 additions and 52 deletions

View File

@@ -2,7 +2,7 @@ TARGET = ./cc-backend
VAR = ./var VAR = ./var
CFG = config.json .env CFG = config.json .env
FRONTEND = ./web/frontend FRONTEND = ./web/frontend
VERSION = 1.0.0 VERSION = 1.1.0
GIT_HASH := $(shell git rev-parse --short HEAD || echo 'development') GIT_HASH := $(shell git rev-parse --short HEAD || echo 'development')
CURRENT_TIME = $(shell date +"%Y-%m-%d:T%H:%M:%S") CURRENT_TIME = $(shell date +"%Y-%m-%d:T%H:%M:%S")
LD_FLAGS = '-s -X main.date=${CURRENT_TIME} -X main.version=${VERSION} -X main.commit=${GIT_HASH}' LD_FLAGS = '-s -X main.date=${CURRENT_TIME} -X main.version=${VERSION} -X main.commit=${GIT_HASH}'
@@ -28,7 +28,7 @@ SVELTE_SRC = $(wildcard $(FRONTEND)/src/*.svelte) \
$(wildcard $(FRONTEND)/src/plots/*.svelte) \ $(wildcard $(FRONTEND)/src/plots/*.svelte) \
$(wildcard $(FRONTEND)/src/joblist/*.svelte) $(wildcard $(FRONTEND)/src/joblist/*.svelte)
.PHONY: clean test tags frontend $(TARGET) .PHONY: clean distclean test tags frontend $(TARGET)
.NOTPARALLEL: .NOTPARALLEL:

View File

@@ -1,11 +1,11 @@
# `cc-backend` version 1.0.0 # `cc-backend` version 1.1.0
Supports job archive version 1 and database version 6. Supports job archive version 1 and database version 6.
This is the initial release of `cc-backend`, the API backend and frontend This is a minor release of `cc-backend`, the API backend and frontend
implementation of ClusterCockpit. implementation of ClusterCockpit.
** Breaking changes ** ** Breaking changes v1 **
The aggregate job statistic core hours is now computed using the job table The aggregate job statistic core hours is now computed using the job table
column `num_hwthreads`. In a future release this column will be renamed to column `num_hwthreads`. In a future release this column will be renamed to
@@ -25,12 +25,3 @@ sqlite> PRAGMA foreign_keys = ON;
``` ```
Otherwise if you delete jobs the jobtag relation table will not be updated accordingly! Otherwise if you delete jobs the jobtag relation table will not be updated accordingly!
**Notable changes**
* Supports user roles admin, support, manager, user, and api.
* Unified search bar supports job id, job name, project id, user name, and name
* Performance improvements for sqlite db backend
* Extended REST api supports to query job metrics
* Better support for shared jobs
* More flexible metric list configuration
* Versioning and migration for database and job archive

156
docs/dev-authentication.md Normal file
View File

@@ -0,0 +1,156 @@
# Overview
The implementation of authentication is not easy to understand by just looking
at the code. The authentication is implemented in `internal/auth/`. In `auth.go`
an interface is defined that any authentication provider must fulfill. It also
acts as a dispatcher to delegate the calls to the available authentication
providers.
The most important routine are:
* `CanLogin()` Check if the authentication method is supported for login attempt
* `Login()` Handle POST request to login user and start a new session
* `Auth()` Authenticate user and put User Object in context of the request
The http router calls auth in the following cases:
* `r.Handle("/login", authentication.Login( ... )).Methods(http.MethodPost)`:
The POST request on the `/login` route will call the Login callback.
* Any route in the secured subrouter will always call Auth(), on success it will
call the next handler in the chain, on failure it will render the login
template.
```
secured.Use(func(next http.Handler) http.Handler {
return authentication.Auth(
// On success;
next,
// On failure:
func(rw http.ResponseWriter, r *http.Request, err error) {
// Render login form
})
})
```
For non API routes a JWT token can be used to initiate an authenticated user
session. This can either happen by calling the login route with a token
provided in a header or query URL or via the `Auth()` method on first access
to a secured URL via a special cookie containing the JWT token.
For API routes the access is authenticated on every request using the JWT token
and no session is initiated.
# Login
The Login function (located in `auth.go`):
* Extracts the user name and gets the user from the user database table. In case the
user is not found the user object is set to nil.
* Iterates over all authenticators and:
- Calls the `CanLogin` function which checks if the authentication method is
supported for this user and the user object is valid.
- Calls the `Login` function to authenticate the user. On success a valid user
object is returned.
- Creates a new session object, stores the user attributes in the session and
saves the session.
- Starts the `onSuccess` http handler
## Local authenticator
This authenticator is applied if
```
return user != nil && user.AuthSource == AuthViaLocalPassword
```
Compares the password provided by the login form to the password hash stored in
the user database table:
```
if e := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(r.FormValue("password"))); e != nil {
log.Errorf("AUTH/LOCAL > Authentication for user %s failed!", user.Username)
return nil, fmt.Errorf("AUTH/LOCAL > Authentication failed")
}
```
## LDAP authenticator
This authenticator is applied if
```
return user != nil && user.AuthSource == AuthViaLDAP
```
Gets the LDAP connection and tries a bind with the provided credentials:
```
if err := l.Bind(userDn, r.FormValue("password")); err != nil {
log.Errorf("AUTH/LOCAL > Authentication for user %s failed: %v", user.Username, err)
return nil, fmt.Errorf("AUTH/LDAP > Authentication failed")
}
```
## JWT authenticator
Login via JWT token will create a session without password.
For login the `X-Auth-Token` header is not supported.
This authenticator is applied if either user is not nil and auth source is
`AuthViaToken` or the Authorization header is present or the URL query key
login-token is present:
```
return (user != nil && user.AuthSource == AuthViaToken) ||
r.Header.Get("Authorization") != "" ||
r.URL.Query().Get("login-token") != ""
```
The Login function:
* Parses the token
* Check if the signing method is EdDSA or HS256 or HS512
* Check if claims are valid and extracts the claims
* The following claims have to be present:
- `sub`: The subject, in this case this is the username
- `exp`: Expiration in Unix epoch time
- `roles`: String array with roles of user
* In case user is not yet set, which is usually the case:
- Try to fetch user from database
- In case user is not yet present add user to user database table with `AuthViaToken` AuthSource.
* Return valid user object
# Auth
The Auth function (located in `auth.go`):
* Returns a new http handler function that is defined right away
* This handler iterates over all authenticators
* Calls `Auth()` on every authenticator
* If err is not nil and the user object is valid it puts the user object in the
request context and starts the onSuccess http handler
* Otherwise it calls the onFailure handler
## Local
Calls the `AuthViaSession()` function in `auth.go`. This will extract username,
projects and roles from the session and initialize a user object with those
values.
## LDAP
Calls the `AuthViaSession()` function in `auth.go`. This will extract username,
projects and roles from the session and initialize a user object with those
values.
# JWT
Check for JWT token:
* Is token passed in the `X-Auth-Token` or `Authorization` header
* If no token is found in a header it tries to read the token from a configured
cookie.
Finally it calls AuthViaSession in `auth.go` if a valid session exists. This is
true if a JWT token was previously used to initiate a session. In this case the
user object initialized with the session is returned right away.
In case a token was found extract and parse the token:
* Check if signing method is Ed25519/EdDSA
* In case publicKeyCrossLogin is configured:
- Check if `iss` issuer claim matched trusted issuer from configuration
- Return public cross login key
- Otherwise return standard public key
* Check if claims are valid
* Depending on the option `ForceJWTValidationViaDatabase ` the roles are
extracted from JWT token or taken from user object fetched from database
* In case the token was extracted from cookie create a new session and ask the
browser to delete the JWT cookie
* Return valid user object

13
docs/dev-release.md Normal file
View File

@@ -0,0 +1,13 @@
# Steps to prepare a release
1. On `hotfix` branch:
* Update ReleaseNotes.md
* Update version in Makefile
* Commit, push, and pull request
* Merge in master
2. On Linux host:
* Pull master
* Ensure that GitHub Token environment variable `GITHUB_TOKEN` is set
* Create release tag: `git tag v1.1.0 -m release`
* Execute `goreleaser release`

View File

@@ -75,10 +75,7 @@ func getRoleEnum(roleStr string) Role {
} }
func isValidRole(role string) bool { func isValidRole(role string) bool {
if getRoleEnum(role) == RoleError { return getRoleEnum(role) != RoleError
return false
}
return true
} }
func (u *User) HasValidRole(role string) (hasRole bool, isValid bool) { func (u *User) HasValidRole(role string) (hasRole bool, isValid bool) {
@@ -166,16 +163,16 @@ func GetValidRoles(user *User) ([]string, error) {
return vals, fmt.Errorf("%s: only admins are allowed to fetch a list of roles", user.Username) return vals, fmt.Errorf("%s: only admins are allowed to fetch a list of roles", user.Username)
} }
// Called by routerConfig web.page setup in backend: Only requires known user and/or not API user // Called by routerConfig web.page setup in backend: Only requires known user
func GetValidRolesMap(user *User) (map[string]Role, error) { func GetValidRolesMap(user *User) (map[string]Role, error) {
named := make(map[string]Role) named := make(map[string]Role)
if user.HasNotRoles([]Role{RoleApi, RoleAnonymous}) { if user.HasNotRoles([]Role{RoleAnonymous}) {
for i := RoleApi; i < RoleError; i++ { for i := RoleApi; i < RoleError; i++ {
named[GetRoleString(i)] = i named[GetRoleString(i)] = i
} }
return named, nil return named, nil
} }
return named, fmt.Errorf("Only known users are allowed to fetch a list of roles") return named, fmt.Errorf("only known users are allowed to fetch a list of roles")
} }
// Find highest role // Find highest role
@@ -300,6 +297,7 @@ func (auth *Authentication) AuthViaSession(
return nil, nil return nil, nil
} }
// TODO Check if keys are present in session?
username, _ := session.Values["username"].(string) username, _ := session.Values["username"].(string)
projects, _ := session.Values["projects"].([]string) projects, _ := session.Values["projects"].([]string)
roles, _ := session.Values["roles"].([]string) roles, _ := session.Values["roles"].([]string)
@@ -320,11 +318,9 @@ func (auth *Authentication) Login(
err := errors.New("no authenticator applied") err := errors.New("no authenticator applied")
username := r.FormValue("username") username := r.FormValue("username")
user := (*User)(nil) user := (*User)(nil)
if username != "" { if username != "" {
if user, _ = auth.GetUser(username); err != nil { user, _ = auth.GetUser(username)
// log.Warnf("login of unkown user %v", username)
_ = err
}
} }
for _, authenticator := range auth.authenticators { for _, authenticator := range auth.authenticators {

View File

@@ -92,7 +92,7 @@ func (ja *JWTAuthenticator) Init(auth *Authentication, conf interface{}) error {
} }
} else { } else {
ja.publicKeyCrossLogin = nil ja.publicKeyCrossLogin = nil
log.Warn("environment variable 'CROSS_LOGIN_JWT_PUBLIC_KEY' not set (cross login token based authentication will not work)") log.Debug("environment variable 'CROSS_LOGIN_JWT_PUBLIC_KEY' not set (cross login token based authentication will not work)")
} }
return nil return nil
@@ -103,7 +103,9 @@ func (ja *JWTAuthenticator) CanLogin(
rw http.ResponseWriter, rw http.ResponseWriter,
r *http.Request) bool { r *http.Request) bool {
return (user != nil && user.AuthSource == AuthViaToken) || r.Header.Get("Authorization") != "" || r.URL.Query().Get("login-token") != "" return (user != nil && user.AuthSource == AuthViaToken) ||
r.Header.Get("Authorization") != "" ||
r.URL.Query().Get("login-token") != ""
} }
func (ja *JWTAuthenticator) Login( func (ja *JWTAuthenticator) Login(
@@ -111,13 +113,9 @@ func (ja *JWTAuthenticator) Login(
rw http.ResponseWriter, rw http.ResponseWriter,
r *http.Request) (*User, error) { r *http.Request) (*User, error) {
rawtoken := r.Header.Get("X-Auth-Token") rawtoken := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
if rawtoken == "" { if rawtoken == "" {
rawtoken = r.Header.Get("Authorization") rawtoken = r.URL.Query().Get("login-token")
rawtoken = strings.TrimPrefix(rawtoken, "Bearer ")
if rawtoken == "" {
rawtoken = r.URL.Query().Get("login-token")
}
} }
token, err := jwt.Parse(rawtoken, func(t *jwt.Token) (interface{}, error) { token, err := jwt.Parse(rawtoken, func(t *jwt.Token) (interface{}, error) {
@@ -134,7 +132,7 @@ func (ja *JWTAuthenticator) Login(
return nil, err return nil, err
} }
if err := token.Claims.Valid(); err != nil { if err = token.Claims.Valid(); err != nil {
log.Warn("jwt token claims are not valid") log.Warn("jwt token claims are not valid")
return nil, err return nil, err
} }
@@ -220,7 +218,10 @@ func (ja *JWTAuthenticator) Auth(
} }
// Is there more than one public key? // Is there more than one public key?
if ja.publicKeyCrossLogin != nil && ja.config != nil && ja.config.TrustedExternalIssuer != "" { if ja.publicKeyCrossLogin != nil &&
ja.config != nil &&
ja.config.TrustedExternalIssuer != "" {
// Determine whether to use the external public key // Determine whether to use the external public key
unvalidatedIssuer, success := t.Claims.(jwt.MapClaims)["iss"].(string) unvalidatedIssuer, success := t.Claims.(jwt.MapClaims)["iss"].(string)
if success && unvalidatedIssuer == ja.config.TrustedExternalIssuer { if success && unvalidatedIssuer == ja.config.TrustedExternalIssuer {

View File

@@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"os"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -15,6 +16,7 @@ import (
"github.com/ClusterCockpit/cc-backend/internal/auth" "github.com/ClusterCockpit/cc-backend/internal/auth"
"github.com/ClusterCockpit/cc-backend/internal/graph/model" "github.com/ClusterCockpit/cc-backend/internal/graph/model"
"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/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/web" "github.com/ClusterCockpit/cc-backend/web"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@@ -60,6 +62,16 @@ func setupHomeRoute(i InfoType, r *http.Request) InfoType {
} }
i["clusters"] = stats i["clusters"] = stats
if util.CheckFileExists("./var/notice.txt") {
msg, err := os.ReadFile("./var/notice.txt")
if err != nil {
log.Warnf("failed to read notice.txt file: %s", err.Error())
} else {
i["message"] = string(msg)
}
}
return i return i
} }
@@ -294,6 +306,8 @@ func HandleSearchBar(rw http.ResponseWriter, r *http.Request, buildInfo web.Buil
http.Redirect(rw, r, "/monitoring/jobs/?jobName="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery http.Redirect(rw, r, "/monitoring/jobs/?jobName="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
case "projectId": case "projectId":
http.Redirect(rw, r, "/monitoring/jobs/?projectMatch=eq&project="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery http.Redirect(rw, r, "/monitoring/jobs/?projectMatch=eq&project="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
case "arrayJobId":
http.Redirect(rw, r, "/monitoring/jobs/?arrayJobId="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) // All Users: Redirect to Tablequery
case "username": case "username":
if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) { if user.HasAnyRole([]auth.Role{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) {
http.Redirect(rw, r, "/monitoring/users/?user="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound) http.Redirect(rw, r, "/monitoring/users/?user="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusFound)

View File

@@ -93,7 +93,7 @@
<InputGroup> <InputGroup>
<Input type="text" placeholder="Search 'type:<query>' ..." name="searchId"/> <Input type="text" placeholder="Search 'type:<query>' ..." name="searchId"/>
<Button outline type="submit"><Icon name="search"/></Button> <Button outline type="submit"><Icon name="search"/></Button>
<InputGroupText style="cursor:help;" title={(authlevel >= roles.support) ? "Example: 'projectId:a100cd', Types are: jobId | jobName | projectId | username | name" : "Example: 'jobName:myjob', Types are jobId | jobName | projectId"}><Icon name="info-circle"/></InputGroupText> <InputGroupText style="cursor:help;" title={(authlevel >= roles.support) ? "Example: 'projectId:a100cd', Types are: jobId | jobName | projectId | arrayJobId | username | name" : "Example: 'jobName:myjob', Types are jobId | jobName | projectId | arrayJobId "}><Icon name="info-circle"/></InputGroupText>
</InputGroup> </InputGroup>
</form> </form>
{#if username} {#if username}

View File

@@ -48,7 +48,8 @@
`); `);
const ccconfig = getContext("cc-config"), const ccconfig = getContext("cc-config"),
clusters = getContext("clusters"); clusters = getContext("clusters"),
metrics = getContext("metrics")
let isMetricsSelectionOpen = false, let isMetricsSelectionOpen = false,
selectedMetrics = [], selectedMetrics = [],
@@ -74,16 +75,26 @@
ccconfig[`job_view_nodestats_selectedMetrics`]), ccconfig[`job_view_nodestats_selectedMetrics`]),
]); ]);
// Select default Scopes to load // Select default Scopes to load: Check before if accelerator metrics are not on accelerator scope by default
if (job.numAcc === 0) { const accMetrics = ['acc_utilization', 'acc_mem_used', 'acc_power', 'nv_mem_util', 'nv_sm_clock', 'nv_temp']
// No Accels const accNodeOnly = [...toFetch].some(function(m) {
if (accMetrics.includes(m)) {
const mc = metrics(job.cluster, m)
return mc.scope !== 'accelerator'
} else {
return false
}
})
if (job.numAcc === 0 || accNodeOnly === true) {
// No Accels or Accels on Node Scope
startFetching( startFetching(
job, job,
[...toFetch], [...toFetch],
job.numNodes > 2 ? ["node"] : ["node", "core"] job.numNodes > 2 ? ["node"] : ["node", "core"]
); );
} else { } else {
// Accels // Accels and not on node scope
startFetching( startFetching(
job, job,
[...toFetch], [...toFetch],

View File

@@ -119,10 +119,10 @@
<thead> <thead>
<tr> <tr>
<th scope="col"> <th scope="col">
<!-- {({ --> {({
<!-- USER: "Username", --> USER: "Username",
<!-- PROJECT: "Project Name", --> PROJECT: "Project Name",
<!-- })[type]} --> })[type]}
<Button <Button
color={sorting.field == "id" ? "primary" : "light"} color={sorting.field == "id" ? "primary" : "light"}
size="sm" size="sm"
@@ -216,14 +216,14 @@
> >
{:else if type == "PROJECT"} {:else if type == "PROJECT"}
<a href="/monitoring/jobs/?project={row.id}" <a href="/monitoring/jobs/?project={row.id}"
>{row.id}</a >{scrambleNames ? scramble(row.id) : row.id}</a
> >
{:else} {:else}
{row.id} {row.id}
{/if} {/if}
</td> </td>
{#if type == "USER"} {#if type == "USER"}
<td>{row?.name ? row.name : ""}</td> <td>{scrambleNames ? scramble(row?.name?row.name:"-") : row?.name?row.name:"-"}</td>
{/if} {/if}
<td>{row.totalJobs}</td> <td>{row.totalJobs}</td>
<td>{row.totalWalltime}</td> <td>{row.totalWalltime}</td>

View File

@@ -7,7 +7,10 @@
--> -->
<script context="module"> <script context="module">
export const scrambleNames = window.localStorage.getItem("cc-scramble-names") export const scrambleNames = window.localStorage.getItem("cc-scramble-names")
export const scramble = (str) => [...str].reduce((x, c, i) => x * 7 + c.charCodeAt(0) * i * 21, 5).toString(32) export const scramble = function(str) {
if (str === '-') return str
else return [...str].reduce((x, c, i) => x * 7 + c.charCodeAt(0) * i * 21, 5).toString(32).substr(0, 6)
}
</script> </script>
<script> <script>
import Tag from '../Tag.svelte'; import Tag from '../Tag.svelte';

View File

@@ -1,4 +1,20 @@
{{define "content"}} {{define "content"}}
{{if .Infos.message }}
<div class="row justify-content-center">
<div class="col-6">
<div class="alert alert-info p-3" role="alert">
<div class="row align-items-center">
<div class="col-2">
<h2><i class="bi-info-circle-fill m-3"></i></h2>
</div>
<div class="col-10">
{{.Infos.message}}
</div>
</div>
</div>
</div>
</div>
{{end}}
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h2>Clusters</h2> <h2>Clusters</h2>

View File

@@ -9,11 +9,11 @@ import (
"html/template" "html/template"
"io/fs" "io/fs"
"net/http" "net/http"
"os"
"strings" "strings"
"github.com/ClusterCockpit/cc-backend/internal/auth" "github.com/ClusterCockpit/cc-backend/internal/auth"
"github.com/ClusterCockpit/cc-backend/internal/config" "github.com/ClusterCockpit/cc-backend/internal/config"
"github.com/ClusterCockpit/cc-backend/internal/util"
"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"
) )
@@ -48,7 +48,7 @@ func init() {
} }
if path == "templates/login.tmpl" { if path == "templates/login.tmpl" {
if _, err := os.Stat("./var/login.tmpl"); err == nil { if util.CheckFileExists("./var/login.tmpl") {
log.Info("overwrite login.tmpl with local file") log.Info("overwrite login.tmpl with local file")
templates[strings.TrimPrefix(path, "templates/")] = templates[strings.TrimPrefix(path, "templates/")] =
template.Must(template.Must(base.Clone()).ParseFiles("./var/login.tmpl")) template.Must(template.Must(base.Clone()).ParseFiles("./var/login.tmpl"))
@@ -56,7 +56,7 @@ func init() {
} }
} }
if path == "templates/imprint.tmpl" { if path == "templates/imprint.tmpl" {
if _, err := os.Stat("./var/imprint.tmpl"); err == nil { if util.CheckFileExists("./var/imprint.tmpl") {
log.Info("overwrite imprint.tmpl with local file") log.Info("overwrite imprint.tmpl with local file")
templates[strings.TrimPrefix(path, "templates/")] = templates[strings.TrimPrefix(path, "templates/")] =
template.Must(template.Must(base.Clone()).ParseFiles("./var/imprint.tmpl")) template.Must(template.Must(base.Clone()).ParseFiles("./var/imprint.tmpl"))
@@ -64,7 +64,7 @@ func init() {
} }
} }
if path == "templates/privacy.tmpl" { if path == "templates/privacy.tmpl" {
if _, err := os.Stat("./var/privacy.tmpl"); err == nil { if util.CheckFileExists("./var/privacy.tmpl") {
log.Info("overwrite privacy.tmpl with local file") log.Info("overwrite privacy.tmpl with local file")
templates[strings.TrimPrefix(path, "templates/")] = templates[strings.TrimPrefix(path, "templates/")] =
template.Must(template.Must(base.Clone()).ParseFiles("./var/privacy.tmpl")) template.Must(template.Must(base.Clone()).ParseFiles("./var/privacy.tmpl"))