diff --git a/db/dbinfo/user_.bob.go b/db/dbinfo/user_.bob.go index 9bbd0e95..0eebc3b3 100644 --- a/db/dbinfo/user_.bob.go +++ b/db/dbinfo/user_.bob.go @@ -132,6 +132,15 @@ var Users = Table[ Generated: false, AutoIncr: false, }, + Avatar: column{ + Name: "avatar", + DBType: "uuid", + Default: "NULL", + Comment: "", + Nullable: true, + Generated: false, + AutoIncr: false, + }, IsActive: column{ Name: "is_active", DBType: "boolean", @@ -144,27 +153,18 @@ var Users = Table[ IsDronePilot: column{ Name: "is_drone_pilot", DBType: "boolean", - Default: "NULL", + Default: "", Comment: "", - Nullable: true, + Nullable: false, Generated: false, AutoIncr: false, }, IsWarrant: column{ Name: "is_warrant", DBType: "boolean", - Default: "NULL", + Default: "", Comment: "", - Nullable: true, - Generated: false, - AutoIncr: false, - }, - Avatar: column{ - Name: "avatar", - DBType: "uuid", - Default: "NULL", - Comment: "", - Nullable: true, + Nullable: false, Generated: false, AutoIncr: false, }, @@ -246,15 +246,15 @@ type userColumns struct { PasswordHashType column PasswordHash column Role column + Avatar column IsActive column IsDronePilot column IsWarrant column - Avatar column } func (c userColumns) AsSlice() []column { return []column{ - c.ID, c.ArcgisAccessToken, c.ArcgisLicense, c.ArcgisRefreshToken, c.ArcgisRefreshTokenExpires, c.ArcgisRole, c.DisplayName, c.Email, c.OrganizationID, c.Username, c.PasswordHashType, c.PasswordHash, c.Role, c.IsActive, c.IsDronePilot, c.IsWarrant, c.Avatar, + c.ID, c.ArcgisAccessToken, c.ArcgisLicense, c.ArcgisRefreshToken, c.ArcgisRefreshTokenExpires, c.ArcgisRole, c.DisplayName, c.Email, c.OrganizationID, c.Username, c.PasswordHashType, c.PasswordHash, c.Role, c.Avatar, c.IsActive, c.IsDronePilot, c.IsWarrant, } } diff --git a/db/migrations/00125_user_props.sql b/db/migrations/00125_user_props.sql index b34d896a..e6e7a6d7 100644 --- a/db/migrations/00125_user_props.sql +++ b/db/migrations/00125_user_props.sql @@ -1,14 +1,17 @@ -- +goose Up ALTER TABLE user_ + ADD COLUMN avatar UUID, ADD COLUMN is_active BOOLEAN, ADD COLUMN is_drone_pilot BOOLEAN, - ADD COLUMN is_warrant BOOLEAN, - ADD COLUMN avatar UUID; -UPDATE user_ SET is_active = TRUE; -ALTER TABLE user_ ALTER COLUMN is_active SET NOT NULL; + ADD COLUMN is_warrant BOOLEAN; +UPDATE user_ SET avatar=NULL, is_active = TRUE, is_drone_pilot=FALSE, is_warrant=FALSE; +ALTER TABLE user_ + ALTER COLUMN is_active SET NOT NULL, + ALTER COLUMN is_drone_pilot SET NOT NULL, + ALTER COLUMN is_warrant SET NOT NULL; -- +goose Down ALTER TABLE user_ + DROP COLUMN avatar, DROP COLUMN is_active, DROP COLUMN is_drone_pilot, - DROP COLUMN is_warrant, - DROP COLUMN avatar; + DROP COLUMN is_warrant; diff --git a/db/models/user_.bob.go b/db/models/user_.bob.go index a7fbf9ac..a469bdaf 100644 --- a/db/models/user_.bob.go +++ b/db/models/user_.bob.go @@ -40,10 +40,10 @@ type User struct { PasswordHashType enums.Hashtype `db:"password_hash_type" ` PasswordHash string `db:"password_hash" ` Role enums.Userrole `db:"role" ` - IsActive bool `db:"is_active" ` - IsDronePilot null.Val[bool] `db:"is_drone_pilot" ` - IsWarrant null.Val[bool] `db:"is_warrant" ` Avatar null.Val[uuid.UUID] `db:"avatar" ` + IsActive bool `db:"is_active" ` + IsDronePilot bool `db:"is_drone_pilot" ` + IsWarrant bool `db:"is_warrant" ` R userR `db:"-" ` } @@ -93,7 +93,7 @@ type userR struct { func buildUserColumns(alias string) userColumns { return userColumns{ ColumnsExpr: expr.NewColumnsExpr( - "id", "arcgis_access_token", "arcgis_license", "arcgis_refresh_token", "arcgis_refresh_token_expires", "arcgis_role", "display_name", "email", "organization_id", "username", "password_hash_type", "password_hash", "role", "is_active", "is_drone_pilot", "is_warrant", "avatar", + "id", "arcgis_access_token", "arcgis_license", "arcgis_refresh_token", "arcgis_refresh_token_expires", "arcgis_role", "display_name", "email", "organization_id", "username", "password_hash_type", "password_hash", "role", "avatar", "is_active", "is_drone_pilot", "is_warrant", ).WithParent("user_"), tableAlias: alias, ID: psql.Quote(alias, "id"), @@ -109,10 +109,10 @@ func buildUserColumns(alias string) userColumns { PasswordHashType: psql.Quote(alias, "password_hash_type"), PasswordHash: psql.Quote(alias, "password_hash"), Role: psql.Quote(alias, "role"), + Avatar: psql.Quote(alias, "avatar"), IsActive: psql.Quote(alias, "is_active"), IsDronePilot: psql.Quote(alias, "is_drone_pilot"), IsWarrant: psql.Quote(alias, "is_warrant"), - Avatar: psql.Quote(alias, "avatar"), } } @@ -132,10 +132,10 @@ type userColumns struct { PasswordHashType psql.Expression PasswordHash psql.Expression Role psql.Expression + Avatar psql.Expression IsActive psql.Expression IsDronePilot psql.Expression IsWarrant psql.Expression - Avatar psql.Expression } func (c userColumns) Alias() string { @@ -163,10 +163,10 @@ type UserSetter struct { PasswordHashType omit.Val[enums.Hashtype] `db:"password_hash_type" ` PasswordHash omit.Val[string] `db:"password_hash" ` Role omit.Val[enums.Userrole] `db:"role" ` - IsActive omit.Val[bool] `db:"is_active" ` - IsDronePilot omitnull.Val[bool] `db:"is_drone_pilot" ` - IsWarrant omitnull.Val[bool] `db:"is_warrant" ` Avatar omitnull.Val[uuid.UUID] `db:"avatar" ` + IsActive omit.Val[bool] `db:"is_active" ` + IsDronePilot omit.Val[bool] `db:"is_drone_pilot" ` + IsWarrant omit.Val[bool] `db:"is_warrant" ` } func (s UserSetter) SetColumns() []string { @@ -210,18 +210,18 @@ func (s UserSetter) SetColumns() []string { if s.Role.IsValue() { vals = append(vals, "role") } + if !s.Avatar.IsUnset() { + vals = append(vals, "avatar") + } if s.IsActive.IsValue() { vals = append(vals, "is_active") } - if !s.IsDronePilot.IsUnset() { + if s.IsDronePilot.IsValue() { vals = append(vals, "is_drone_pilot") } - if !s.IsWarrant.IsUnset() { + if s.IsWarrant.IsValue() { vals = append(vals, "is_warrant") } - if !s.Avatar.IsUnset() { - vals = append(vals, "avatar") - } return vals } @@ -265,17 +265,17 @@ func (s UserSetter) Overwrite(t *User) { if s.Role.IsValue() { t.Role = s.Role.MustGet() } + if !s.Avatar.IsUnset() { + t.Avatar = s.Avatar.MustGetNull() + } if s.IsActive.IsValue() { t.IsActive = s.IsActive.MustGet() } - if !s.IsDronePilot.IsUnset() { - t.IsDronePilot = s.IsDronePilot.MustGetNull() + if s.IsDronePilot.IsValue() { + t.IsDronePilot = s.IsDronePilot.MustGet() } - if !s.IsWarrant.IsUnset() { - t.IsWarrant = s.IsWarrant.MustGetNull() - } - if !s.Avatar.IsUnset() { - t.Avatar = s.Avatar.MustGetNull() + if s.IsWarrant.IsValue() { + t.IsWarrant = s.IsWarrant.MustGet() } } @@ -364,26 +364,26 @@ func (s *UserSetter) Apply(q *dialect.InsertQuery) { vals[12] = psql.Raw("DEFAULT") } - if s.IsActive.IsValue() { - vals[13] = psql.Arg(s.IsActive.MustGet()) + if !s.Avatar.IsUnset() { + vals[13] = psql.Arg(s.Avatar.MustGetNull()) } else { vals[13] = psql.Raw("DEFAULT") } - if !s.IsDronePilot.IsUnset() { - vals[14] = psql.Arg(s.IsDronePilot.MustGetNull()) + if s.IsActive.IsValue() { + vals[14] = psql.Arg(s.IsActive.MustGet()) } else { vals[14] = psql.Raw("DEFAULT") } - if !s.IsWarrant.IsUnset() { - vals[15] = psql.Arg(s.IsWarrant.MustGetNull()) + if s.IsDronePilot.IsValue() { + vals[15] = psql.Arg(s.IsDronePilot.MustGet()) } else { vals[15] = psql.Raw("DEFAULT") } - if !s.Avatar.IsUnset() { - vals[16] = psql.Arg(s.Avatar.MustGetNull()) + if s.IsWarrant.IsValue() { + vals[16] = psql.Arg(s.IsWarrant.MustGet()) } else { vals[16] = psql.Raw("DEFAULT") } @@ -490,6 +490,13 @@ func (s UserSetter) Expressions(prefix ...string) []bob.Expression { }}) } + if !s.Avatar.IsUnset() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "avatar")...), + psql.Arg(s.Avatar), + }}) + } + if s.IsActive.IsValue() { exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ psql.Quote(append(prefix, "is_active")...), @@ -497,27 +504,20 @@ func (s UserSetter) Expressions(prefix ...string) []bob.Expression { }}) } - if !s.IsDronePilot.IsUnset() { + if s.IsDronePilot.IsValue() { exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ psql.Quote(append(prefix, "is_drone_pilot")...), psql.Arg(s.IsDronePilot), }}) } - if !s.IsWarrant.IsUnset() { + if s.IsWarrant.IsValue() { exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ psql.Quote(append(prefix, "is_warrant")...), psql.Arg(s.IsWarrant), }}) } - if !s.Avatar.IsUnset() { - exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ - psql.Quote(append(prefix, "avatar")...), - psql.Arg(s.Avatar), - }}) - } - return exprs } @@ -3314,10 +3314,10 @@ type userWhere[Q psql.Filterable] struct { PasswordHashType psql.WhereMod[Q, enums.Hashtype] PasswordHash psql.WhereMod[Q, string] Role psql.WhereMod[Q, enums.Userrole] - IsActive psql.WhereMod[Q, bool] - IsDronePilot psql.WhereNullMod[Q, bool] - IsWarrant psql.WhereNullMod[Q, bool] Avatar psql.WhereNullMod[Q, uuid.UUID] + IsActive psql.WhereMod[Q, bool] + IsDronePilot psql.WhereMod[Q, bool] + IsWarrant psql.WhereMod[Q, bool] } func (userWhere[Q]) AliasedAs(alias string) userWhere[Q] { @@ -3339,10 +3339,10 @@ func buildUserWhere[Q psql.Filterable](cols userColumns) userWhere[Q] { PasswordHashType: psql.Where[Q, enums.Hashtype](cols.PasswordHashType), PasswordHash: psql.Where[Q, string](cols.PasswordHash), Role: psql.Where[Q, enums.Userrole](cols.Role), - IsActive: psql.Where[Q, bool](cols.IsActive), - IsDronePilot: psql.WhereNull[Q, bool](cols.IsDronePilot), - IsWarrant: psql.WhereNull[Q, bool](cols.IsWarrant), Avatar: psql.WhereNull[Q, uuid.UUID](cols.Avatar), + IsActive: psql.Where[Q, bool](cols.IsActive), + IsDronePilot: psql.Where[Q, bool](cols.IsDronePilot), + IsWarrant: psql.Where[Q, bool](cols.IsWarrant), } } diff --git a/http/error_with_status.go b/http/error_with_status.go index fa6b5aee..44fec097 100644 --- a/http/error_with_status.go +++ b/http/error_with_status.go @@ -33,3 +33,6 @@ func NewErrorStatus(status int, mesg_format string, args ...any) *ErrorWithStatu Status: status, } } +func NewForbidden(mesg_format string, args ...any) *ErrorWithStatus { + return NewErrorStatus(http.StatusForbidden, mesg_format, args...) +} diff --git a/platform/user.go b/platform/user.go index 1f1ac223..4adac22d 100644 --- a/platform/user.go +++ b/platform/user.go @@ -83,6 +83,8 @@ func CreateUser(ctx context.Context, username string, name string, password_hash u_setter := models.UserSetter{ DisplayName: omit.From(name), IsActive: omit.From(true), + IsDronePilot: omit.From(false), + IsWarrant: omit.From(false), OrganizationID: omit.From(o.ID), PasswordHash: omit.From(password_hash), PasswordHashType: omit.From(enums.HashtypeBcrypt14), diff --git a/resource/user.go b/resource/user.go index 31a8f4d0..fca8e235 100644 --- a/resource/user.go +++ b/resource/user.go @@ -86,6 +86,9 @@ func (res *userR) ByIDPut(ctx context.Context, r *http.Request, user platform.Us return "", nhttp.NewErrorStatus(http.StatusBadRequest, "user id conversion: %w", err) } user_changes := &models.UserSetter{} + if !(user.HasRoot() || user.Role == enums.UserroleAccountOwner || user.ID == user_id) { + return "", nhttp.NewForbidden("Only account owners can change other users") + } if updates.Avatar.IsValue() { avatar_uuid, err := res.router.UUIDFromURI("avatar.ByUUIDGet", updates.Avatar.MustGet()) if err != nil { @@ -98,6 +101,29 @@ func (res *userR) ByIDPut(ctx context.Context, r *http.Request, user platform.Us if updates.DisplayName.IsValue() { user_changes.DisplayName = updates.DisplayName } + if updates.Role.IsValue() { + // Don't allow privilege escalation + if user.HasRoot() || user.Role == enums.UserroleAccountOwner { + user_changes.Role = updates.Role.MustGet() + } else { + return "", nhttp.NewBadRequest("you aren't allowed to change roles") + } + } + if updates.Tags.IsValue() { + for i, v := range updates.Tags.MustGet() { + user_changes.IsDronePilot = omit.From(false) + user_changes.IsWarrant = omit.From(false) + switch v { + case "drone pilot": + user_changes.IsDronePilot = omit.From(true) + case "warrant": + user_changes.IsWarrant = omit.From(true) + default: + return "", nhttp.NewBadRequest("'%s' (item %d) is not a valid tag", v, i) + } + } + } + err = platform.UserUpdate(ctx, user, user_id, user_changes) if err != nil { return "", nhttp.NewError("user update: %w", err) diff --git a/ts/view/configuration/User.vue b/ts/view/configuration/User.vue index bcad51b1..5dce3923 100644 --- a/ts/view/configuration/User.vue +++ b/ts/view/configuration/User.vue @@ -81,7 +81,7 @@