From c067d3e9ade59cc7600396583766b5d3870dd86a Mon Sep 17 00:00:00 2001 From: Aaron Date: Wed, 20 Feb 2019 11:35:47 -0600 Subject: [PATCH 01/12] user is_admin field with working migration --- backend/grant/user/models.py | 1 + backend/migrations/versions/4e5d9f481f22_.py | 28 ++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 backend/migrations/versions/4e5d9f481f22_.py diff --git a/backend/grant/user/models.py b/backend/grant/user/models.py index 74ce874b..5f347c2e 100644 --- a/backend/grant/user/models.py +++ b/backend/grant/user/models.py @@ -106,6 +106,7 @@ class User(db.Model, UserMixin): display_name = db.Column(db.String(255), unique=False, nullable=True) title = db.Column(db.String(255), unique=False, nullable=True) active = db.Column(db.Boolean, default=True) + is_admin = db.Column(db.Boolean, default=False, nullable=False, server_default=db.text("FALSE")) # moderation silenced = db.Column(db.Boolean, default=False) diff --git a/backend/migrations/versions/4e5d9f481f22_.py b/backend/migrations/versions/4e5d9f481f22_.py new file mode 100644 index 00000000..845bd15a --- /dev/null +++ b/backend/migrations/versions/4e5d9f481f22_.py @@ -0,0 +1,28 @@ +"""user is_admin field + +Revision ID: 4e5d9f481f22 +Revises: 27975c4a04a4 +Create Date: 2019-02-20 11:30:30.376869 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '4e5d9f481f22' +down_revision = '27975c4a04a4' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('user', sa.Column('is_admin', sa.Boolean(), server_default=sa.text('FALSE'), nullable=False)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('user', 'is_admin') + # ### end Alembic commands ### From cc07fb77979eb55985586296ee801f783f1a588f Mon Sep 17 00:00:00 2001 From: Aaron Date: Wed, 20 Feb 2019 12:04:25 -0600 Subject: [PATCH 02/12] admin: edit user admin status --- admin/src/components/UserDetail/index.tsx | 44 +++++++++++++++++++++++ admin/src/types.ts | 1 + backend/grant/admin/views.py | 13 +++---- backend/grant/user/models.py | 7 ++++ 4 files changed, 59 insertions(+), 6 deletions(-) diff --git a/admin/src/components/UserDetail/index.tsx b/admin/src/components/UserDetail/index.tsx index 02426dcc..6fa12059 100644 --- a/admin/src/components/UserDetail/index.tsx +++ b/admin/src/components/UserDetail/index.tsx @@ -99,6 +99,33 @@ class UserDetailNaked extends React.Component { ); + const renderAdminControl = () => ( +
+ {u.isAdmin ? 'Remove admin privileges?' : 'Add admin privileges?'}} + okText="ok" + cancelText="cancel" + > + {' '} + + + Admin{' '} + + Admin User +
User will be able to log into this (admin) interface with full + privileges. +
+ } + /> + +
+ ); + const renderBanControl = () => (
{ {renderDelete()} {renderSilenceControl()} {renderBanControl()} + {renderAdminControl()} @@ -306,6 +334,22 @@ class UserDetailNaked extends React.Component { } }; + private handleToggleAdmin = async () => { + if (store.userDetail) { + const ud = store.userDetail; + const newAdmin = !ud.isAdmin; + await store.editUser(ud.userid, { isAdmin: newAdmin }); + if (store.userSaved) { + message.success( + <> + {ud.displayName} {newAdmin ? 'made admin' : 'no longer admin'} + , + 2, + ); + } + } + }; + private handleToggleBan = () => { if (store.userDetail) { const ud = store.userDetail; diff --git a/admin/src/types.ts b/admin/src/types.ts index 1b7bf6ca..981e2169 100644 --- a/admin/src/types.ts +++ b/admin/src/types.ts @@ -161,6 +161,7 @@ export interface User { silenced: boolean; banned: boolean; bannedReason: string; + isAdmin: boolean; } export interface EmailExample { diff --git a/backend/grant/admin/views.py b/backend/grant/admin/views.py index 15f49ac4..cf9e95c0 100644 --- a/backend/grant/admin/views.py +++ b/backend/grant/admin/views.py @@ -150,23 +150,24 @@ def get_user(id): parameter('silenced', type=bool, required=False), parameter('banned', type=bool, required=False), parameter('bannedReason', type=str, required=False), + parameter('isAdmin', type=bool, required=False) ) @admin_auth_required -def edit_user(user_id, silenced, banned, banned_reason): +def edit_user(user_id, silenced, banned, banned_reason, is_admin): user = User.query.filter(User.id == user_id).first() if not user: return {"message": f"Could not find user with id {id}"}, 404 if silenced is not None: - user.silenced = silenced - db.session.add(user) + user.set_silenced(silenced) if banned is not None: if banned and not banned_reason: # if banned true, provide reason return {"message": "Please include reason for banning"}, 417 - user.banned = banned - user.banned_reason = banned_reason - db.session.add(user) + user.set_banned(banned, banned_reason) + + if is_admin is not None: + user.set_admin(is_admin) db.session.commit() return admin_user_schema.dump(user) diff --git a/backend/grant/user/models.py b/backend/grant/user/models.py index 5f347c2e..5f477632 100644 --- a/backend/grant/user/models.py +++ b/backend/grant/user/models.py @@ -245,6 +245,12 @@ class User(db.Model, UserMixin): db.session.add(self) db.session.flush() + def set_admin(self, is_admin: bool): + # TODO: audit entry & possibly email user + self.is_admin = is_admin + db.session.add(self) + db.session.flush() + class SelfUserSchema(ma.Schema): class Meta: @@ -262,6 +268,7 @@ class SelfUserSchema(ma.Schema): "silenced", "banned", "banned_reason", + "is_admin", ) social_medias = ma.Nested("SocialMediaSchema", many=True) From f49b58dca4a05d9c5f2293042da642877c5915e6 Mon Sep 17 00:00:00 2001 From: Aaron Date: Wed, 20 Feb 2019 16:35:13 -0600 Subject: [PATCH 03/12] totp basics + remove old admin env key stuff --- admin/src/Routes.tsx | 4 +- admin/src/components/Login/index.tsx | 65 +++++++++++++++------------- admin/src/store.ts | 19 +++++--- backend/.env.example | 3 -- backend/grant/admin/__init__.py | 1 - backend/grant/admin/commands.py | 20 --------- backend/grant/admin/views.py | 17 ++++++-- backend/grant/app.py | 1 - backend/grant/settings.py | 2 - backend/grant/utils/admin.py | 46 ++++++++++---------- backend/grant/utils/totp_2fa.py | 33 ++++++++++++++ backend/requirements/prod.txt | 3 ++ 12 files changed, 121 insertions(+), 93 deletions(-) delete mode 100644 backend/grant/admin/commands.py create mode 100644 backend/grant/utils/totp_2fa.py diff --git a/admin/src/Routes.tsx b/admin/src/Routes.tsx index 3a670359..77541b76 100644 --- a/admin/src/Routes.tsx +++ b/admin/src/Routes.tsx @@ -25,13 +25,13 @@ type Props = RouteComponentProps; class Routes extends React.Component { render() { - const { hasCheckedLogin, isLoggedIn } = store; + const { hasCheckedLogin, isLoggedIn, is2faAuthed } = store; if (!hasCheckedLogin) { return
checking auth status...
; } return (