Add basic username/password authentication

This commit is contained in:
WithoutPants
2019-07-28 19:36:52 +10:00
parent 4f016ab3c9
commit 5a891d00cf
18 changed files with 1043 additions and 11 deletions

View File

@@ -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
}

View File

@@ -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(),
}
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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))
}

View File

@@ -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