Skip to content

Commit

Permalink
Merge pull request #404 from iberryful/roles
Browse files Browse the repository at this point in the history
feat: implement user roles
meranos authored Apr 28, 2020
2 parents fd1fc76 + 5e41477 commit 22c18ef
Showing 14 changed files with 288 additions and 66 deletions.
27 changes: 24 additions & 3 deletions src/api/handlers/admin/users.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,37 @@
from flask import g
from flask_restplus import Resource
from flask import g, abort, request
from flask_restplus import Resource, fields
from pyinfraboxutils.ibflask import OK

from pyinfraboxutils.ibrestplus import api

user_role_setting_model = api.model('UserRoleUpdate', {
'id': fields.String(required=True),
'role': fields.String(required=True, enum=['user', 'devops', 'admin']),
})

@api.route('/api/v1/admin/users', doc=False)
class Users(Resource):

def get(self):
users = g.db.execute_many_dict('''
SELECT name, username, email, avatar_url
SELECT id, name, username, email, avatar_url, role
FROM "user"
ORDER BY name
''')

return users

@api.expect(user_role_setting_model, validate=True)
def post(self):
if g.token['user']['role'] != 'admin':
abort(403, "updating user role is only allowed for admin user")
body = request.get_json()
if body['id'] == '00000000-0000-0000-0000-000000000000':
abort(403, "can't change role for Admin")
g.db.execute('''
UPDATE "user"
SET role=%s
WHERE id=%s
''', [body['role'], body['id']])
g.db.commit()
return OK("OK")
3 changes: 2 additions & 1 deletion src/api/handlers/user/user.py
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@
'name': fields.String,
'email': fields.String,
'id': fields.String,
'role': fields.String(enum=['user', 'devops', 'admin'])
})

@ns.route('')
@@ -29,7 +30,7 @@ def get(self):
'''

user = g.db.execute_one_dict('''
SELECT github_id, username, avatar_url, name, email, id
SELECT github_id, username, avatar_url, name, email, id, role
FROM "user"
WHERE id = %s
''', [g.token['user']['id']])
2 changes: 1 addition & 1 deletion src/dashboard-client/src/App.vue
Original file line number Diff line number Diff line change
@@ -80,7 +80,7 @@
</a>
</md-list-item>

<md-list-item v-if="$store.state.user.isAdmin()">
<md-list-item v-if="$store.state.user.hasWriteAccess()">
<md-icon><i class="fa fa-fw fa-unlock"></i></md-icon>
<span>Admin</span>
<md-list-expand>
230 changes: 186 additions & 44 deletions src/dashboard-client/src/components/admin/AdminUsers.vue
Original file line number Diff line number Diff line change
@@ -1,73 +1,215 @@
<template>
<md-card class="main-card">
<md-card-header class="main-card-header fix-padding">
<md-card-header-text>
<h3 class="md-title card-title">
<md-layout>
<md-layout md-vertical-align="center">Users</md-layout>
</md-layout>
</h3>
</md-card-header-text>
</md-card-header>

<md-table-card class="clean-card">
<md-table>
<md-table-header>
<md-table-row>
<md-table-head>Name</md-table-head>
<md-table-head>Username</md-table-head>
<md-table-head>Email</md-table-head>
</md-table-row>
</md-table-header>
<md-table-body>
<md-table-row v-for="u in users" :key="u.id">
<md-table-cell>
<md-card class="main-card">
<md-card-header class="main-card-header fix-padding">
<md-card-header-text>
<h3 class="md-title card-title">
<md-layout>
<md-layout md-vertical-align="center">Users</md-layout>
</md-layout>
</h3>
</md-card-header-text>
</md-card-header>

<md-card md-theme="white" class="full-height clean-card">
<md-card-area>
<md-list class="m-t-md m-b-md">
<md-list-item>
<md-input-container class="m-r-sm">
<label>Name</label>
<md-input v-model="form.name"></md-input>
</md-input-container>
<md-input-container class="m-r-sm">
<label>Username</label>
<md-input v-model="form.username"></md-input>
</md-input-container>
<md-input-container class="m-r-sm">
<label>Email</label>
<md-input v-model="form.email"></md-input>
</md-input-container>
<md-input-container>
<label>Role</label>
<md-select name="role" id="role" v-model="form.role">
<md-option value="" class="bg-white">Any</md-option>
<md-option value="user" class="bg-white">user</md-option>
<md-option value="devops" class="bg-white">devops</md-option>
<md-option value="admin" class="bg-white">admin</md-option>
</md-select>
</md-input-container>
<md-button class="md-icon-button md-list-action" @click="doSearch()">
<md-icon md-theme="running" class="md-primary">search</md-icon>
<md-tooltip>Search</md-tooltip>
</md-button>
<md-button class="md-icon-button md-list-action" @click="resetSearch()">
<md-icon md-theme="running" class="md-primary">clear</md-icon>
<md-tooltip>Clear</md-tooltip>
</md-button>
</md-list-item>
</md-list>
</md-card-area>
</md-card>

<md-table-card class="clean-card">
<md-table>
<md-table-header>
<md-table-row>
<md-table-head>Name</md-table-head>
<md-table-head>User</md-table-head>
<md-table-head>Role</md-table-head>
<md-table-head>Email</md-table-head>
</md-table-row>
</md-table-header>
<md-table-body>
<md-table-row v-for="u in users" :key="u.id">
<md-table-cell>
<img :src="u.avatar_url" /> {{ u.name }}
</md-table-cell>
<md-table-cell>
<md-table-cell>
{{ u.username }}
</md-table-cell>
<md-table-cell>
</md-table-cell>
<md-table-cell>
<md-select v-model="u.role" name="role" id="u.id" v-on:change="setUserRole(u.id, $event)" :disabled="!isAdmin()">
<md-option value="user">User</md-option>
<md-option value="devops">Devops</md-option>
<md-option value="admin">Admin</md-option>
</md-select>
</md-table-cell>
<md-table-cell>
{{ u.email }}
</md-table-cell>
</md-table-row>
</md-table-body>
</md-table>

<md-table-pagination
:md-size="size"
:md-total="total"
:md-page="page"
md-label="Projects"
md-separator="of"
:md-page-options="[20, 50]"
@pagination="onPagination"></md-table-pagination>
</md-table-card>
</md-card>
</md-table-cell>
</md-table-row>
</md-table-body>
</md-table>

<md-table-pagination v-if="!this.search.search"
:md-size="size"
:md-total="total"
:md-page="page"
md-label="Users"
md-separator="of"
:md-page-options="[20, 50]"
@pagination="onPagination">
</md-table-pagination>
</md-table-card>
</md-card>
</template>

<script>
import store from '../../store'
import AdminService from '../../services/AdminService'
import NotificationService from '../../services/NotificationService'
import Notification from '../../models/Notification'
export default {
name: 'AdminProejcts',
name: 'AdminUsers',
store,
data: () => {
return {
users: [],
roles: {},
page: 1,
size: 20,
total: 0
total: 0,
form: {
name: null,
username: null,
email: null,
role: null
},
search: {
name: null,
username: null,
email: null,
role: null,
search: false
}
}
},
created () {
AdminService.loadUsers().then(() => {
this.onPagination({ size: this.size, page: this.page })
for (let u of store.state.admin.users) {
this.roles[u.id] = u.role
}
this.users = store.state.admin.users
this.total = store.state.admin.users.length
this.onPagination({ size: this.size, page: this.page })
})
},
methods: {
isAdmin () {
return store.state.user.isAdmin()
},
setUserRole (id, role) {
if (role === this.roles[id]) {
return
}
console.log(id, role)
const user = this.users.find(c => c.id === id)
AdminService.setUserRole(id, role).then(() => {
this.roles[id] = role
NotificationService.$emit('NOTIFICATION', new Notification({ message: `user ${user.name} role is set to ${role}` }))
}).catch((err) => {
user.role = this.roles[id]
NotificationService.$emit('NOTIFICATION', new Notification(err))
})
},
resetSearch () {
this.search.search = false
this.users = store.state.admin.users
},
searchUsers () {
if (store.state.admin.users.length === 0) {
return []
}
let users = []
for (let u of store.state.admin.users) {
if (!this.search.search) {
users.push(u)
continue
}
if (this.search.name && u.name.toLowerCase().search(this.search.name.toLowerCase()) === -1) {
continue
}
if (this.search.username && u.username.toLowerCase().search(this.search.username.toLowerCase()) === -1) {
continue
}
if (this.search.email && u.email.toLowerCase().search(this.search.email.toLowerCase()) === -1) {
continue
}
if (this.search.role && u.role !== this.search.role) {
continue
}
users.push(u)
}
return users
},
doSearch () {
this.search = {
username: this.form.username,
name: this.form.name,
role: this.form.role,
email: this.form.email,
search: true
}
this.users = this.searchUsers()
},
onPagination (opt) {
if (this.size !== opt.size) {
this.page = 1
2 changes: 1 addition & 1 deletion src/dashboard-client/src/models/Project.js
Original file line number Diff line number Diff line change
@@ -35,7 +35,7 @@ export default class Project {
}

userHasAdminRights () {
return this.userHasOwnerRights() || this.userrole === 'Administrator' || store.state.user.isAdmin()
return this.userHasOwnerRights() || this.userrole === 'Administrator' || store.state.user.hasWriteAccess()
}

userHasDevRights () {
9 changes: 7 additions & 2 deletions src/dashboard-client/src/models/User.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
export default class User {
constructor (username, avatarUrl, name, email, githubId, id) {
constructor (username, avatarUrl, name, email, githubId, id, role) {
this.githubRepos = []
this.username = username
this.avatarUrl = avatarUrl
this.name = name
this.email = email
this.githubId = githubId
this.id = id
this.role = role
}

hasGithubAccount () {
return this.githubId != null
}

isAdmin () {
return this.id === '00000000-0000-0000-0000-000000000000'
return this.role === 'admin'
}

hasWriteAccess () {
return this.role === 'devops' || this.isAdmin()
}
}
14 changes: 14 additions & 0 deletions src/dashboard-client/src/services/AdminService.js
Original file line number Diff line number Diff line change
@@ -34,6 +34,20 @@ class AdminService {
})
}

setUserRole (userId, role) {
const payload = {
id: userId,
role: role
}
return NewAPIService.post(`admin/users/`, payload)
.then(() => {
store.commit('setAdminUserRole', payload)
})
.catch((err) => {
NotificationService.$emit('NOTIFICATION', new Notification(err))
})
}

updateCluster (name, enabled) {
const payload = {
name: name,
3 changes: 2 additions & 1 deletion src/dashboard-client/src/services/UserService.js
Original file line number Diff line number Diff line change
@@ -61,7 +61,8 @@ class UserService {
d.name,
d.email,
d.github_id,
d.id)
d.id,
d.role)
store.commit('setUser', u)
ProjectService.init()
})
7 changes: 7 additions & 0 deletions src/dashboard-client/src/store.js
Original file line number Diff line number Diff line change
@@ -327,6 +327,12 @@ function updateAdminCluster (state, payload) {
cluster.enabled = enabled
}

function setAdminUserRole (state, payload) {
const { id, role } = payload
const user = state.admin.users.find(c => c.id === id)
user.role = role
}

const mutations = {
addProjects,
addJobs,
@@ -348,6 +354,7 @@ const mutations = {
setStats,
setTabs,
setAdminUsers,
setAdminUserRole,
setAdminProjects,
setAdminClusters,
updateAdminCluster,
4 changes: 2 additions & 2 deletions src/db/migrate.py
Original file line number Diff line number Diff line change
@@ -108,8 +108,8 @@ def configure_admin(conn):

cur = conn.cursor()
cur.execute('''
INSERT into "user" (id, username, name, email, password)
VALUES ('00000000-0000-0000-0000-000000000000', 'Admin', 'Admin', %s, %s)
INSERT into "user" (id, username, name, email, password, role)
VALUES ('00000000-0000-0000-0000-000000000000', 'Admin', 'Admin', %s, %s, 'admin')
ON CONFLICT (id) DO UPDATE
SET email = %s,
password = %s
8 changes: 8 additions & 0 deletions src/db/migrations/00034.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TYPE system_role AS ENUM (
'user',
'devops',
'admin'
);

ALTER TABLE "user" ADD COLUMN role system_role DEFAULT 'user' NOT NULL;
UPDATE "user" SET role = 'admin' WHERE id = '00000000-0000-0000-0000-000000000000';
29 changes: 26 additions & 3 deletions src/openpolicyagent/policies/admin.rego
Original file line number Diff line number Diff line change
@@ -3,12 +3,24 @@ package infrabox
# HTTP API request
import input as api

# Allow administrators access to everything
user_roles = {"user": 10, "devops": 20, "admin": 30}

default authz = false

# deny will overwrite allow
authz {
allow
not deny
}


# Allow admin and devops access to everything
allow {
api.token.type = "user"
api.token.user.id = "00000000-0000-0000-0000-000000000000"
user_roles[api.token.user.role] >= 20
}


# Allow GET access to /api/v1/admin/clusters for users logged in
allow {
api.method = "GET"
@@ -21,5 +33,16 @@ allow {
api.method = "POST"
api.path = ["api", "v1", "admin", "clusters"]
api.token.type = "user"
api.token.user.id = "00000000-0000-0000-0000-000000000000"
user_roles[api.token.user.role] >= 20
}


# Deny POST access to /api/v1/admin/clusters for devops and user, only allowed for admin
deny {
api.method = "POST"
api.path = ["api", "v1", "admin", "users"]
api.token.type = "user"
user_roles[api.token.user.role] <= 20
}


14 changes: 7 additions & 7 deletions src/pyinfraboxutils/ibflask.py
Original file line number Diff line number Diff line change
@@ -187,11 +187,10 @@ def normalize_token(token):

# Validate user token
if token["type"] == "user":
if not validate_user_token(token):
return None
return validate_user_token(token)

# Validate project_token
if token["type"] == "project":
elif token["type"] == "project":
if not validate_project_token(token):
return None

@@ -220,15 +219,16 @@ def enrich_job_token(token):

def validate_user_token(token):
if not ("user" in token and "id" in token["user"] and validate_uuid(token['user']['id'])):
return False
return None

u = g.db.execute_one('''
SELECT id FROM "user" WHERE id = %s
SELECT id, role FROM "user" WHERE id = %s
''', [token['user']['id']])
if not u:
logger.warn('user not found')
return False
return True
return None
token['user']['role'] = u[1]
return token

def validate_project_token(token):
if not ("project" in token and "id" in token['project'] and validate_uuid(token['project']['id'])
2 changes: 1 addition & 1 deletion src/pyinfraboxutils/ibopa.py
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@

logger = get_logger('OPA')

OPA_AUTH_URL = "http://%s:%s/v1/data/infrabox/allow" % (get_env('INFRABOX_OPA_HOST'), get_env('INFRABOX_OPA_PORT'))
OPA_AUTH_URL = "http://%s:%s/v1/data/infrabox/authz" % (get_env('INFRABOX_OPA_HOST'), get_env('INFRABOX_OPA_PORT'))
COLLABORATOR_DATA_DEST_URL = "http://%s:%s/v1/data/infrabox/collaborators" % (get_env('INFRABOX_OPA_HOST'), get_env('INFRABOX_OPA_PORT'))
PROJECT_DATA_DEST_URL = "http://%s:%s/v1/data/infrabox/projects" % (get_env('INFRABOX_OPA_HOST'), get_env('INFRABOX_OPA_PORT'))

0 comments on commit 22c18ef

Please sign in to comment.