diff --git a/app/cmd.go b/app/cmd.go index f795b3bb1a..5cf263c06b 100644 --- a/app/cmd.go +++ b/app/cmd.go @@ -569,7 +569,7 @@ Migration: %s (#%d) fmt.Fprintln(os.Stderr) } - pw, err := basicStore.NewHashedPassword(pass) + pw, err := basicStore.NewHashedPassword(ctx, pass) if err != nil { return errors.Wrap(err, "hash password") } diff --git a/auth/basic/db.go b/auth/basic/db.go index f630cf6725..d499f178ab 100644 --- a/auth/basic/db.go +++ b/auth/basic/db.go @@ -5,8 +5,10 @@ import ( "database/sql" "sync" + "github.com/target/goalert/config" "github.com/target/goalert/permission" "github.com/target/goalert/util" + "github.com/target/goalert/validation" "github.com/target/goalert/validation/validate" "github.com/pkg/errors" @@ -18,6 +20,8 @@ import ( type Store struct { insert *sql.Stmt getByUsername *sql.Stmt + getByID *sql.Stmt + update *sql.Stmt mx sync.Mutex } @@ -31,6 +35,8 @@ func NewStore(ctx context.Context, db *sql.DB) (*Store, error) { return &Store{ insert: p.P("INSERT INTO auth_basic_users (user_id, username, password_hash) VALUES ($1, $2, $3)"), getByUsername: p.P("SELECT user_id, password_hash FROM auth_basic_users WHERE username = $1"), + getByID: p.P("SELECT password_hash FROM auth_basic_users WHERE user_id = $1"), + update: p.P("UPDATE auth_basic_users SET password_hash = $2 WHERE user_id = $1"), }, p.Err } @@ -46,9 +52,40 @@ type hashed []byte func (h hashed) Hash() string { return string(h) } func (h hashed) _private() {} +// ValidatedPassword represents a validated password for a UserID. +type ValidatedPassword interface { + UserID() string + + _private() // prevent external implementations +} + +type validated string + +func (v validated) UserID() string { return string(v) } +func (v validated) _private() {} + +// ValidateBasicAuth returns an access denied error for non-admins when basic auth is disabled in configs. +func ValidateBasicAuth(ctx context.Context) error { + if permission.Admin(ctx) { + return nil + } + + cfg := config.FromContext(ctx) + if cfg.Auth.DisableBasic { + return permission.NewAccessDenied("Basic auth is disabled by administrator.") + } + + return nil +} + // NewHashedPassword will hash the given password and return a Password object. -func (b *Store) NewHashedPassword(password string) (HashedPassword, error) { - err := validate.Text("Password", password, 8, 200) +func (b *Store) NewHashedPassword(ctx context.Context, password string) (HashedPassword, error) { + err := ValidateBasicAuth(ctx) + if err != nil { + return nil, err + } + + err = validate.Text("Password", password, 8, 200) if err != nil { return nil, err } @@ -67,7 +104,12 @@ func (b *Store) NewHashedPassword(password string) (HashedPassword, error) { // An error is returned if the username is not unique or the userID is invalid. // Must have same user or admin role. func (b *Store) CreateTx(ctx context.Context, tx *sql.Tx, userID, username string, password HashedPassword) error { - err := permission.LimitCheckAny(ctx, permission.System, permission.Admin, permission.MatchUser(userID)) + err := ValidateBasicAuth(ctx) + if err != nil { + return err + } + + err = permission.LimitCheckAny(ctx, permission.System, permission.Admin, permission.MatchUser(userID)) if err != nil { return err } @@ -84,6 +126,47 @@ func (b *Store) CreateTx(ctx context.Context, tx *sql.Tx, userID, username strin return err } +// UpdateTx updates a user's password. oldPass is required if the current context is not an admin. +func (b *Store) UpdateTx(ctx context.Context, tx *sql.Tx, userID string, oldPass ValidatedPassword, newPass HashedPassword) error { + err := ValidateBasicAuth(ctx) + if err != nil { + return err + } + + err = permission.LimitCheckAny(ctx, permission.Admin, permission.MatchUser(userID)) + if err != nil { + return err + } + + err = validate.UUID("UserID", userID) + if err != nil { + return err + } + + if oldPass != nil && oldPass.UserID() != userID { + return validation.NewFieldError("OldPassword", "Password does not match User") + } + if (!permission.Admin(ctx) || permission.UserID(ctx) == userID) && oldPass == nil { + return validation.NewFieldError("OldPassword", "Previous password required") + } + + res, err := tx.StmtContext(ctx, b.update).ExecContext(ctx, userID, newPass.Hash()) + if err != nil { + return err + } + + count, err := res.RowsAffected() + if err != nil { + return err + } + + if count == 0 { + return validation.NewFieldError("UserID", "does not have basic auth configured") + } + + return nil +} + // Validate should return a userID if the username and password match. func (b *Store) Validate(ctx context.Context, username, password string) (string, error) { err := validate.Many( @@ -115,3 +198,45 @@ func (b *Store) Validate(ctx context.Context, username, password string) (string return userID, nil } + +// ValidatePassword will validate the password of the currently authenticated user. +func (b *Store) ValidatePassword(ctx context.Context, password string) (ValidatedPassword, error) { + err := ValidateBasicAuth(ctx) + if err != nil { + return nil, err + } + + err = permission.LimitCheckAny(ctx, permission.User) + if err != nil { + return nil, err + } + + userID := permission.UserID(ctx) + + err = validate.Many( + validate.UUID("UserID", userID), + validate.Text("OldPassword", password, 8, 200), + ) + if err != nil { + return nil, err + } + + var hash string + err = b.getByID.QueryRowContext(ctx, userID).Scan(&hash) + if errors.Is(err, sql.ErrNoRows) { + return nil, errors.New("unknown userID") + } + if err != nil { + return nil, errors.WithMessage(err, "user lookup failure") + } + + b.mx.Lock() + defer b.mx.Unlock() + + err = bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + if err != nil { + return nil, validation.NewFieldError("OldPassword", "invalid password") + } + + return validated(userID), nil +} diff --git a/graphql2/generated.go b/graphql2/generated.go index f08b52d1af..03d1738ab6 100644 --- a/graphql2/generated.go +++ b/graphql2/generated.go @@ -283,6 +283,7 @@ type ComplexityRoot struct { AddAuthSubject func(childComplexity int, input user.AuthSubject) int ClearTemporarySchedules func(childComplexity int, input ClearTemporarySchedulesInput) int CreateAlert func(childComplexity int, input CreateAlertInput) int + CreateBasicAuth func(childComplexity int, input CreateBasicAuthInput) int CreateEscalationPolicy func(childComplexity int, input CreateEscalationPolicyInput) int CreateEscalationPolicyStep func(childComplexity int, input CreateEscalationPolicyStepInput) int CreateHeartbeatMonitor func(childComplexity int, input CreateHeartbeatMonitorInput) int @@ -313,6 +314,7 @@ type ComplexityRoot struct { TestContactMethod func(childComplexity int, id string) int UpdateAlerts func(childComplexity int, input UpdateAlertsInput) int UpdateAlertsByService func(childComplexity int, input UpdateAlertsByServiceInput) int + UpdateBasicAuth func(childComplexity int, input UpdateBasicAuthInput) int UpdateEscalationPolicy func(childComplexity int, input UpdateEscalationPolicyInput) int UpdateEscalationPolicyStep func(childComplexity int, input UpdateEscalationPolicyStepInput) int UpdateHeartbeatMonitor func(childComplexity int, input UpdateHeartbeatMonitorInput) int @@ -742,6 +744,8 @@ type MutationResolver interface { UpdateAlertsByService(ctx context.Context, input UpdateAlertsByServiceInput) (bool, error) SetConfig(ctx context.Context, input []ConfigValueInput) (bool, error) SetSystemLimits(ctx context.Context, input []SystemLimitInput) (bool, error) + CreateBasicAuth(ctx context.Context, input CreateBasicAuthInput) (bool, error) + UpdateBasicAuth(ctx context.Context, input UpdateBasicAuthInput) (bool, error) } type OnCallNotificationRuleResolver interface { Target(ctx context.Context, obj *schedule.OnCallNotificationRule) (*assignment.RawTarget, error) @@ -1672,6 +1676,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.CreateAlert(childComplexity, args["input"].(CreateAlertInput)), true + case "Mutation.createBasicAuth": + if e.complexity.Mutation.CreateBasicAuth == nil { + break + } + + args, err := ec.field_Mutation_createBasicAuth_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.CreateBasicAuth(childComplexity, args["input"].(CreateBasicAuthInput)), true + case "Mutation.createEscalationPolicy": if e.complexity.Mutation.CreateEscalationPolicy == nil { break @@ -2027,6 +2043,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.UpdateAlertsByService(childComplexity, args["input"].(UpdateAlertsByServiceInput)), true + case "Mutation.updateBasicAuth": + if e.complexity.Mutation.UpdateBasicAuth == nil { + break + } + + args, err := ec.field_Mutation_updateBasicAuth_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.UpdateBasicAuth(childComplexity, args["input"].(UpdateBasicAuthInput)), true + case "Mutation.updateEscalationPolicy": if e.complexity.Mutation.UpdateEscalationPolicy == nil { break @@ -3861,6 +3889,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputClearTemporarySchedulesInput, ec.unmarshalInputConfigValueInput, ec.unmarshalInputCreateAlertInput, + ec.unmarshalInputCreateBasicAuthInput, ec.unmarshalInputCreateEscalationPolicyInput, ec.unmarshalInputCreateEscalationPolicyStepInput, ec.unmarshalInputCreateHeartbeatMonitorInput, @@ -3903,6 +3932,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputTimeZoneSearchOptions, ec.unmarshalInputUpdateAlertsByServiceInput, ec.unmarshalInputUpdateAlertsInput, + ec.unmarshalInputUpdateBasicAuthInput, ec.unmarshalInputUpdateEscalationPolicyInput, ec.unmarshalInputUpdateEscalationPolicyStepInput, ec.unmarshalInputUpdateHeartbeatMonitorInput, @@ -4070,6 +4100,21 @@ func (ec *executionContext) field_Mutation_createAlert_args(ctx context.Context, return args, nil } +func (ec *executionContext) field_Mutation_createBasicAuth_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 CreateBasicAuthInput + if tmp, ok := rawArgs["input"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + arg0, err = ec.unmarshalNCreateBasicAuthInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐCreateBasicAuthInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_createEscalationPolicyStep_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -4505,6 +4550,21 @@ func (ec *executionContext) field_Mutation_updateAlerts_args(ctx context.Context return args, nil } +func (ec *executionContext) field_Mutation_updateBasicAuth_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 UpdateBasicAuthInput + if tmp, ok := rawArgs["input"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + arg0, err = ec.unmarshalNUpdateBasicAuthInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐUpdateBasicAuthInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_updateEscalationPolicyStep_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -13035,6 +13095,116 @@ func (ec *executionContext) fieldContext_Mutation_setSystemLimits(ctx context.Co return fc, nil } +func (ec *executionContext) _Mutation_createBasicAuth(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_createBasicAuth(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().CreateBasicAuth(rctx, fc.Args["input"].(CreateBasicAuthInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_createBasicAuth(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_createBasicAuth_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return + } + return fc, nil +} + +func (ec *executionContext) _Mutation_updateBasicAuth(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_updateBasicAuth(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().UpdateBasicAuth(rctx, fc.Args["input"].(UpdateBasicAuthInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_updateBasicAuth(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_updateBasicAuth_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return + } + return fc, nil +} + func (ec *executionContext) _Notice_type(ctx context.Context, field graphql.CollectedField, obj *notice.Notice) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Notice_type(ctx, field) if err != nil { @@ -26173,6 +26343,53 @@ func (ec *executionContext) unmarshalInputCreateAlertInput(ctx context.Context, return it, nil } +func (ec *executionContext) unmarshalInputCreateBasicAuthInput(ctx context.Context, obj interface{}) (CreateBasicAuthInput, error) { + var it CreateBasicAuthInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"username", "password", "userID"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "username": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("username")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Username = data + case "password": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("password")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Password = data + case "userID": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("userID")) + data, err := ec.unmarshalNID2string(ctx, v) + if err != nil { + return it, err + } + it.UserID = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputCreateEscalationPolicyInput(ctx context.Context, obj interface{}) (CreateEscalationPolicyInput, error) { var it CreateEscalationPolicyInput asMap := map[string]interface{}{} @@ -28691,6 +28908,53 @@ func (ec *executionContext) unmarshalInputUpdateAlertsInput(ctx context.Context, return it, nil } +func (ec *executionContext) unmarshalInputUpdateBasicAuthInput(ctx context.Context, obj interface{}) (UpdateBasicAuthInput, error) { + var it UpdateBasicAuthInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"password", "oldPassword", "userID"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "password": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("password")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Password = data + case "oldPassword": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("oldPassword")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.OldPassword = data + case "userID": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("userID")) + data, err := ec.unmarshalNID2string(ctx, v) + if err != nil { + return it, err + } + it.UserID = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputUpdateEscalationPolicyInput(ctx context.Context, obj interface{}) (UpdateEscalationPolicyInput, error) { var it UpdateEscalationPolicyInput asMap := map[string]interface{}{} @@ -31515,6 +31779,24 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) return ec._Mutation_setSystemLimits(ctx, field) }) + if out.Values[i] == graphql.Null { + invalids++ + } + case "createBasicAuth": + + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_createBasicAuth(ctx, field) + }) + + if out.Values[i] == graphql.Null { + invalids++ + } + case "updateBasicAuth": + + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_updateBasicAuth(ctx, field) + }) + if out.Values[i] == graphql.Null { invalids++ } @@ -35712,6 +35994,11 @@ func (ec *executionContext) unmarshalNCreateAlertInput2githubᚗcomᚋtargetᚋg return res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) unmarshalNCreateBasicAuthInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐCreateBasicAuthInput(ctx context.Context, v interface{}) (CreateBasicAuthInput, error) { + res, err := ec.unmarshalInputCreateBasicAuthInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNCreateEscalationPolicyInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐCreateEscalationPolicyInput(ctx context.Context, v interface{}) (CreateEscalationPolicyInput, error) { res, err := ec.unmarshalInputCreateEscalationPolicyInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) @@ -37610,6 +37897,11 @@ func (ec *executionContext) unmarshalNUpdateAlertsInput2githubᚗcomᚋtargetᚋ return res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) unmarshalNUpdateBasicAuthInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐUpdateBasicAuthInput(ctx context.Context, v interface{}) (UpdateBasicAuthInput, error) { + res, err := ec.unmarshalInputUpdateBasicAuthInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNUpdateEscalationPolicyInput2githubᚗcomᚋtargetᚋgoalertᚋgraphql2ᚐUpdateEscalationPolicyInput(ctx context.Context, v interface{}) (UpdateEscalationPolicyInput, error) { res, err := ec.unmarshalInputUpdateEscalationPolicyInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/graphql2/graphqlapp/user.go b/graphql2/graphqlapp/user.go index a493bb342a..350a02794d 100644 --- a/graphql2/graphqlapp/user.go +++ b/graphql2/graphqlapp/user.go @@ -5,7 +5,9 @@ import ( "database/sql" "github.com/target/goalert/auth" + "github.com/target/goalert/auth/basic" "github.com/target/goalert/calsub" + "github.com/target/goalert/validation" "github.com/target/goalert/validation/validate" "github.com/pkg/errors" @@ -68,6 +70,49 @@ func (a *User) OnCallSteps(ctx context.Context, obj *user.User) ([]escalation.St return a.PolicyStore.FindAllOnCallStepsForUserTx(ctx, nil, obj.ID) } +func (a *Mutation) CreateBasicAuth(ctx context.Context, input graphql2.CreateBasicAuthInput) (bool, error) { + pw, err := a.AuthBasicStore.NewHashedPassword(ctx, input.Password) + if err != nil { + return false, err + } + + err = withContextTx(ctx, a.DB, func(ctx context.Context, tx *sql.Tx) error { + return a.AuthBasicStore.CreateTx(ctx, tx, input.UserID, input.Username, pw) + }) + if err != nil { + return false, err + } + + return true, nil +} + +func (a *Mutation) UpdateBasicAuth(ctx context.Context, input graphql2.UpdateBasicAuthInput) (bool, error) { + var validatedPW basic.ValidatedPassword + var err error + if input.OldPassword != nil { + if *input.OldPassword == input.Password { + return false, validation.NewFieldError("Password", "Cannot match OldPassword") + } + validatedPW, err = a.AuthBasicStore.ValidatePassword(ctx, *input.OldPassword) + if err != nil { + return false, err + } + } + pw, err := a.AuthBasicStore.NewHashedPassword(ctx, input.Password) + if err != nil { + return false, err + } + + err = withContextTx(ctx, a.DB, func(ctx context.Context, tx *sql.Tx) error { + return a.AuthBasicStore.UpdateTx(ctx, tx, input.UserID, validatedPW, pw) + }) + if err != nil { + return false, err + } + + return true, nil +} + func (a *Mutation) CreateUser(ctx context.Context, input graphql2.CreateUserInput) (*user.User, error) { var newUser *user.User @@ -78,7 +123,7 @@ func (a *Mutation) CreateUser(ctx context.Context, input graphql2.CreateUserInpu return nil, err } - pass, err := a.AuthBasicStore.NewHashedPassword(input.Password) + pass, err := a.AuthBasicStore.NewHashedPassword(ctx, input.Password) if err != nil { return nil, err } diff --git a/graphql2/models_gen.go b/graphql2/models_gen.go index cef0f4c9c0..99d0e3a10a 100644 --- a/graphql2/models_gen.go +++ b/graphql2/models_gen.go @@ -117,6 +117,12 @@ type CreateAlertInput struct { Sanitize *bool `json:"sanitize,omitempty"` } +type CreateBasicAuthInput struct { + Username string `json:"username"` + Password string `json:"password"` + UserID string `json:"userID"` +} + type CreateEscalationPolicyInput struct { Name string `json:"name"` Description *string `json:"description,omitempty"` @@ -552,6 +558,12 @@ type UpdateAlertsInput struct { NewStatus AlertStatus `json:"newStatus"` } +type UpdateBasicAuthInput struct { + Password string `json:"password"` + OldPassword *string `json:"oldPassword,omitempty"` + UserID string `json:"userID"` +} + type UpdateEscalationPolicyInput struct { ID string `json:"id"` Name *string `json:"name,omitempty"` diff --git a/graphql2/schema.graphql b/graphql2/schema.graphql index 9054eabd68..c8dc1639c9 100644 --- a/graphql2/schema.graphql +++ b/graphql2/schema.graphql @@ -562,6 +562,21 @@ type Mutation { setConfig(input: [ConfigValueInput!]): Boolean! setSystemLimits(input: [SystemLimitInput!]!): Boolean! + + createBasicAuth(input: CreateBasicAuthInput!): Boolean! + updateBasicAuth(input: UpdateBasicAuthInput!): Boolean! +} + +input CreateBasicAuthInput { + username: String! + password: String! + userID: ID! +} + +input UpdateBasicAuthInput { + password: String! + oldPassword: String + userID: ID! } input UpdateAlertsByServiceInput { diff --git a/util/errutil/maperror.go b/util/errutil/maperror.go index ee9d21f92a..9d7cec01b3 100644 --- a/util/errutil/maperror.go +++ b/util/errutil/maperror.go @@ -39,6 +39,8 @@ func MapDBError(err error) error { return validation.NewFieldError("TargetID", "user does not exist") case "rotation_participants_user_id_fkey": return validation.NewFieldError("UserID", "user does not exist") + case "auth_basic_users_user_id_fkey": + return validation.NewFieldError("UserID", "user does not exist") } case "23505": // unique constraint if dbErr.ConstraintName == "auth_basic_users_username_key" { @@ -59,6 +61,9 @@ func MapDBError(err error) error { if dbErr.ConstraintName == "idx_no_alert_duplicates" { return validation.NewFieldError("", "duplicate alert already exists") } + if dbErr.ConstraintName == "auth_basic_users_pkey" { + return validation.NewFieldError("UserID", "already has a basic auth username configured") + } case "23514": // check constraint newErr := mapLimitError(dbErr) if newErr != nil { diff --git a/web/src/schema.d.ts b/web/src/schema.d.ts index 49b8552501..8b21e7b3a0 100644 --- a/web/src/schema.d.ts +++ b/web/src/schema.d.ts @@ -413,6 +413,20 @@ export interface Mutation { updateAlertsByService: boolean setConfig: boolean setSystemLimits: boolean + createBasicAuth: boolean + updateBasicAuth: boolean +} + +export interface CreateBasicAuthInput { + username: string + password: string + userID: string +} + +export interface UpdateBasicAuthInput { + password: string + oldPassword?: null | string + userID: string } export interface UpdateAlertsByServiceInput {