mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 04:14:39 +03:00
Add basic username/password authentication
This commit is contained in:
@@ -3,10 +3,11 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/stashapp/stash/pkg/manager/config"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.ConfigGeneralInput) (*models.ConfigGeneralResult, error) {
|
||||
@@ -35,6 +36,20 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
|
||||
config.Set(config.Generated, input.GeneratedPath)
|
||||
}
|
||||
|
||||
if input.Username != nil {
|
||||
config.Set(config.Username, input.Username)
|
||||
}
|
||||
|
||||
if input.Password != nil {
|
||||
// bit of a hack - check if the passed in password is the same as the stored hash
|
||||
// and only set if they are different
|
||||
currentPWHash := config.GetPasswordHash()
|
||||
|
||||
if *input.Password != currentPWHash {
|
||||
config.SetPassword(*input.Password)
|
||||
}
|
||||
}
|
||||
|
||||
if err := config.Write(); err != nil {
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/pkg/manager/config"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
@@ -30,5 +31,7 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
|
||||
Stashes: config.GetStashPaths(),
|
||||
DatabasePath: config.GetDatabasePath(),
|
||||
GeneratedPath: config.GetGeneratedPath(),
|
||||
Username: config.GetUsername(),
|
||||
Password: config.GetPasswordHash(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,15 @@ import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/99designs/gqlgen/handler"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
@@ -16,14 +25,6 @@ import (
|
||||
"github.com/stashapp/stash/pkg/manager/paths"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var uiBox *packr.Box
|
||||
@@ -31,6 +32,32 @@ var uiBox *packr.Box
|
||||
//var legacyUiBox *packr.Box
|
||||
var setupUIBox *packr.Box
|
||||
|
||||
func authenticateHandler() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// only do this if credentials have been configured
|
||||
if !config.HasCredentials() {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
authUser, authPW, ok := r.BasicAuth()
|
||||
|
||||
if !ok || !config.ValidateCredentials(authUser, authPW) {
|
||||
unauthorized(w)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func unauthorized(w http.ResponseWriter) {
|
||||
w.Header().Add("WWW-Authenticate", `Basic realm=\"Stash\"`)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func Start() {
|
||||
uiBox = packr.New("UI Box", "../../ui/v2/build")
|
||||
//legacyUiBox = packr.New("UI Box", "../../ui/v1/dist/stash-frontend")
|
||||
@@ -38,6 +65,7 @@ func Start() {
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(authenticateHandler())
|
||||
r.Use(middleware.Recoverer)
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(middleware.DefaultCompress)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
@@ -9,6 +11,8 @@ const Cache = "cache"
|
||||
const Generated = "generated"
|
||||
const Metadata = "metadata"
|
||||
const Downloads = "downloads"
|
||||
const Username = "username"
|
||||
const Password = "password"
|
||||
|
||||
const Database = "database"
|
||||
|
||||
@@ -19,6 +23,10 @@ func Set(key string, value interface{}) {
|
||||
viper.Set(key, value)
|
||||
}
|
||||
|
||||
func SetPassword(value string) {
|
||||
Set(Password, hashPassword(value))
|
||||
}
|
||||
|
||||
func Write() error {
|
||||
return viper.WriteConfig()
|
||||
}
|
||||
@@ -51,8 +59,48 @@ func GetPort() int {
|
||||
return viper.GetInt(Port)
|
||||
}
|
||||
|
||||
func GetUsername() string {
|
||||
return viper.GetString(Username)
|
||||
}
|
||||
|
||||
func GetPasswordHash() string {
|
||||
return viper.GetString(Password)
|
||||
}
|
||||
|
||||
func GetCredentials() (string, string) {
|
||||
if HasCredentials() {
|
||||
return viper.GetString(Username), viper.GetString(Password)
|
||||
}
|
||||
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func HasCredentials() bool {
|
||||
return viper.IsSet(Username) && viper.IsSet(Password)
|
||||
}
|
||||
|
||||
func hashPassword(password string) string {
|
||||
hash, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost)
|
||||
|
||||
return string(hash)
|
||||
}
|
||||
|
||||
func ValidateCredentials(username string, password string) bool {
|
||||
if !HasCredentials() {
|
||||
// don't need to authenticate if no credentials saved
|
||||
return true
|
||||
}
|
||||
|
||||
authUser, authPWHash := GetCredentials()
|
||||
|
||||
err := bcrypt.CompareHashAndPassword([]byte(authPWHash), []byte(password))
|
||||
|
||||
return username == authUser && err == nil
|
||||
}
|
||||
|
||||
func IsValid() bool {
|
||||
setPaths := viper.IsSet(Stash) && viper.IsSet(Cache) && viper.IsSet(Generated) && viper.IsSet(Metadata)
|
||||
|
||||
// TODO: check valid paths
|
||||
return setPaths
|
||||
}
|
||||
|
||||
@@ -53,7 +53,9 @@ type ComplexityRoot struct {
|
||||
ConfigGeneralResult struct {
|
||||
DatabasePath func(childComplexity int) int
|
||||
GeneratedPath func(childComplexity int) int
|
||||
Password func(childComplexity int) int
|
||||
Stashes func(childComplexity int) int
|
||||
Username func(childComplexity int) int
|
||||
}
|
||||
|
||||
ConfigResult struct {
|
||||
@@ -413,6 +415,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
|
||||
return e.complexity.ConfigGeneralResult.GeneratedPath(childComplexity), true
|
||||
|
||||
case "ConfigGeneralResult.password":
|
||||
if e.complexity.ConfigGeneralResult.Password == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.complexity.ConfigGeneralResult.Password(childComplexity), true
|
||||
|
||||
case "ConfigGeneralResult.stashes":
|
||||
if e.complexity.ConfigGeneralResult.Stashes == nil {
|
||||
break
|
||||
@@ -420,6 +429,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
|
||||
return e.complexity.ConfigGeneralResult.Stashes(childComplexity), true
|
||||
|
||||
case "ConfigGeneralResult.username":
|
||||
if e.complexity.ConfigGeneralResult.Username == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.complexity.ConfigGeneralResult.Username(childComplexity), true
|
||||
|
||||
case "ConfigResult.general":
|
||||
if e.complexity.ConfigResult.General == nil {
|
||||
break
|
||||
@@ -1869,6 +1885,10 @@ schema {
|
||||
databasePath: String
|
||||
"""Path to generated files"""
|
||||
generatedPath: String
|
||||
"""Username"""
|
||||
username: String
|
||||
"""Password"""
|
||||
password: String
|
||||
}
|
||||
|
||||
type ConfigGeneralResult {
|
||||
@@ -1878,6 +1898,10 @@ type ConfigGeneralResult {
|
||||
databasePath: String!
|
||||
"""Path to generated files"""
|
||||
generatedPath: String!
|
||||
"""Username"""
|
||||
username: String!
|
||||
"""Password"""
|
||||
password: String!
|
||||
}
|
||||
|
||||
"""All configuration settings"""
|
||||
@@ -2851,6 +2875,60 @@ func (ec *executionContext) _ConfigGeneralResult_generatedPath(ctx context.Conte
|
||||
return ec.marshalNString2string(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _ConfigGeneralResult_username(ctx context.Context, field graphql.CollectedField, obj *ConfigGeneralResult) graphql.Marshaler {
|
||||
ctx = ec.Tracer.StartFieldExecution(ctx, field)
|
||||
defer func() { ec.Tracer.EndFieldExecution(ctx) }()
|
||||
rctx := &graphql.ResolverContext{
|
||||
Object: "ConfigGeneralResult",
|
||||
Field: field,
|
||||
Args: nil,
|
||||
IsMethod: false,
|
||||
}
|
||||
ctx = graphql.WithResolverContext(ctx, rctx)
|
||||
ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
|
||||
resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return obj.Username, nil
|
||||
})
|
||||
if resTmp == nil {
|
||||
if !ec.HasError(rctx) {
|
||||
ec.Errorf(ctx, "must not be null")
|
||||
}
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(string)
|
||||
rctx.Result = res
|
||||
ctx = ec.Tracer.StartFieldChildExecution(ctx)
|
||||
return ec.marshalNString2string(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _ConfigGeneralResult_password(ctx context.Context, field graphql.CollectedField, obj *ConfigGeneralResult) graphql.Marshaler {
|
||||
ctx = ec.Tracer.StartFieldExecution(ctx, field)
|
||||
defer func() { ec.Tracer.EndFieldExecution(ctx) }()
|
||||
rctx := &graphql.ResolverContext{
|
||||
Object: "ConfigGeneralResult",
|
||||
Field: field,
|
||||
Args: nil,
|
||||
IsMethod: false,
|
||||
}
|
||||
ctx = graphql.WithResolverContext(ctx, rctx)
|
||||
ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
|
||||
resTmp := ec.FieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return obj.Password, nil
|
||||
})
|
||||
if resTmp == nil {
|
||||
if !ec.HasError(rctx) {
|
||||
ec.Errorf(ctx, "must not be null")
|
||||
}
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(string)
|
||||
rctx.Result = res
|
||||
ctx = ec.Tracer.StartFieldChildExecution(ctx)
|
||||
return ec.marshalNString2string(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _ConfigResult_general(ctx context.Context, field graphql.CollectedField, obj *ConfigResult) graphql.Marshaler {
|
||||
ctx = ec.Tracer.StartFieldExecution(ctx, field)
|
||||
defer func() { ec.Tracer.EndFieldExecution(ctx) }()
|
||||
@@ -7909,6 +7987,18 @@ func (ec *executionContext) unmarshalInputConfigGeneralInput(ctx context.Context
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
case "username":
|
||||
var err error
|
||||
it.Username, err = ec.unmarshalOString2ᚖstring(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
case "password":
|
||||
var err error
|
||||
it.Password, err = ec.unmarshalOString2ᚖstring(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8687,6 +8777,16 @@ func (ec *executionContext) _ConfigGeneralResult(ctx context.Context, sel ast.Se
|
||||
if out.Values[i] == graphql.Null {
|
||||
invalids++
|
||||
}
|
||||
case "username":
|
||||
out.Values[i] = ec._ConfigGeneralResult_username(ctx, field, obj)
|
||||
if out.Values[i] == graphql.Null {
|
||||
invalids++
|
||||
}
|
||||
case "password":
|
||||
out.Values[i] = ec._ConfigGeneralResult_password(ctx, field, obj)
|
||||
if out.Values[i] == graphql.Null {
|
||||
invalids++
|
||||
}
|
||||
default:
|
||||
panic("unknown field " + strconv.Quote(field.Name))
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@ type ConfigGeneralInput struct {
|
||||
DatabasePath *string `json:"databasePath"`
|
||||
// Path to generated files
|
||||
GeneratedPath *string `json:"generatedPath"`
|
||||
// Username
|
||||
Username *string `json:"username"`
|
||||
// Password
|
||||
Password *string `json:"password"`
|
||||
}
|
||||
|
||||
type ConfigGeneralResult struct {
|
||||
@@ -24,6 +28,10 @@ type ConfigGeneralResult struct {
|
||||
DatabasePath string `json:"databasePath"`
|
||||
// Path to generated files
|
||||
GeneratedPath string `json:"generatedPath"`
|
||||
// Username
|
||||
Username string `json:"username"`
|
||||
// Password
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// All configuration settings
|
||||
|
||||
Reference in New Issue
Block a user