Merge pull request #1440 from ansible-semaphore/roles

Roles
This commit is contained in:
Denis Gukov 2023-08-27 01:00:18 +02:00 committed by GitHub
commit 36570ae15c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 320 additions and 163 deletions

View File

@ -150,10 +150,10 @@ var pathSubPatterns = []func() string{
func() string { return strconv.Itoa(userProject.ID) },
func() string { return strconv.Itoa(userPathTestUser.ID) },
func() string { return strconv.Itoa(userKey.ID) },
func() string { return strconv.Itoa(int(repoID)) },
func() string { return strconv.Itoa(int(inventoryID)) },
func() string { return strconv.Itoa(int(environmentID)) },
func() string { return strconv.Itoa(int(templateID)) },
func() string { return strconv.Itoa(repoID) },
func() string { return strconv.Itoa(inventoryID) },
func() string { return strconv.Itoa(environmentID) },
func() string { return strconv.Itoa(templateID) },
func() string { return strconv.Itoa(task.ID) },
func() string { return strconv.Itoa(schedule.ID) },
func() string { return strconv.Itoa(view.ID) },

View File

@ -104,6 +104,7 @@ func main() {
h.Before("project > /api/project/{project_id}/tasks/{task_id} > Get a single task > 200 > application/json", capabilityWrapper("task"))
h.Before("project > /api/project/{project_id}/tasks/{task_id} > Deletes task (including output) > 204 > application/json", capabilityWrapper("task"))
h.Before("project > /api/project/{project_id}/tasks/{task_id}/output > Get task output > 200 > application/json", capabilityWrapper("task"))
h.Before("project > /api/project/{project_id}/tasks/{task_id}/stop > Stop a job > 204 > application/json", capabilityWrapper("task"))
h.Before("schedule > /api/project/{project_id}/schedules/{schedule_id} > Get schedule > 200 > application/json", capabilityWrapper("schedule"))
h.Before("schedule > /api/project/{project_id}/schedules/{schedule_id} > Updates schedule > 204 > application/json", capabilityWrapper("schedule"))

View File

@ -928,6 +928,28 @@ paths:
204:
description: Project deleted
/project/{project_id}/role:
parameters:
- $ref: "#/parameters/project_id"
get:
tags:
- project
summary: Fetch permissions of the current user for project
responses:
200:
description: Permissions
schema:
type: object
properties:
role:
type: string
example: owner
permissions:
type: number
example: 0
/project/{project_id}/events:
parameters:
- $ref: '#/parameters/project_id'
@ -1487,7 +1509,6 @@ paths:
description: view removed
# tasks
/project/{project_id}/tasks:
parameters:
@ -1533,6 +1554,8 @@ paths:
description: Task queued
schema:
$ref: "#/definitions/Task"
/project/{project_id}/tasks/last:
parameters:
- $ref: "#/parameters/project_id"
@ -1547,6 +1570,22 @@ paths:
type: array
items:
$ref: '#/definitions/Task'
/project/{project_id}/tasks/{task_id}/stop:
parameters:
- $ref: "#/parameters/project_id"
- $ref: '#/parameters/task_id'
post:
tags:
- project
summary: Stop a job
responses:
204:
description: Task queued
/project/{project_id}/tasks/{task_id}:
parameters:
- $ref: "#/parameters/project_id"
@ -1567,6 +1606,9 @@ paths:
responses:
204:
description: task deleted
/project/{project_id}/tasks/{task_id}/output:
parameters:
- $ref: '#/parameters/project_id'

View File

@ -24,7 +24,7 @@ func ProjectMiddleware(next http.Handler) http.Handler {
}
// check if user in project's team
_, err = helpers.Store(r).GetProjectUser(projectID, user.ID)
projectUser, err := helpers.Store(r).GetProjectUser(projectID, user.ID)
if err != nil {
helpers.WriteError(w, err)
@ -38,36 +38,22 @@ func ProjectMiddleware(next http.Handler) http.Handler {
return
}
context.Set(r, "projectUserRole", projectUser.Role)
context.Set(r, "project", project)
next.ServeHTTP(w, r)
})
}
// GetMustCanMiddlewareFor ensures that the user has administrator rights
func GetMustCanMiddlewareFor(permissions db.ProjectUserPermission) mux.MiddlewareFunc {
// GetMustCanMiddleware ensures that the user has administrator rights
func GetMustCanMiddleware(permissions db.ProjectUserPermission) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
project := context.Get(r, "project").(db.Project)
user := context.Get(r, "user").(*db.User)
projectUserRole := context.Get(r, "projectUserRole").(db.ProjectUserRole)
if !user.Admin {
// check if user in project's team
projectUser, err := helpers.Store(r).GetProjectUser(project.ID, user.ID)
if err == db.ErrNotFound {
w.WriteHeader(http.StatusForbidden)
return
}
if err != nil {
helpers.WriteError(w, err)
return
}
if r.Method != "GET" && r.Method != "HEAD" && !projectUser.Can(permissions) {
w.WriteHeader(http.StatusForbidden)
return
}
if !user.Admin && r.Method != "GET" && r.Method != "HEAD" && !projectUserRole.Can(permissions) {
w.WriteHeader(http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
@ -80,6 +66,16 @@ func GetProject(w http.ResponseWriter, r *http.Request) {
helpers.WriteJSON(w, http.StatusOK, context.Get(r, "project"))
}
func GetUserRole(w http.ResponseWriter, r *http.Request) {
var permissions struct {
Role db.ProjectUserRole `json:"role"`
Permissions db.ProjectUserPermission `json:"permissions"`
}
permissions.Role = context.Get(r, "projectUserRole").(db.ProjectUserRole)
permissions.Permissions = permissions.Role.GetPermissions()
helpers.WriteJSON(w, http.StatusOK, permissions)
}
// UpdateProject saves updated project details to the database
func UpdateProject(w http.ResponseWriter, r *http.Request) {
project := context.Get(r, "project").(db.Project)

View File

@ -128,17 +128,19 @@ func Route() *mux.Router {
//
// Start and Stop tasks
projectTaskStart := authenticatedAPI.PathPrefix("/project/{project_id}").Subrouter()
projectTaskStart.Use(projects.ProjectMiddleware, projects.GetMustCanMiddlewareFor(db.CanRunProjectTasks))
projectTaskStart.Use(projects.ProjectMiddleware, projects.GetMustCanMiddleware(db.CanRunProjectTasks))
projectTaskStart.Path("/tasks").HandlerFunc(projects.AddTask).Methods("POST")
projectTaskStop := authenticatedAPI.PathPrefix("/project/{project_id}").Subrouter()
projectTaskStop.Use(projects.ProjectMiddleware, projects.GetTaskMiddleware, projects.GetMustCanMiddlewareFor(db.CanRunProjectTasks))
projectTaskStop.Use(projects.ProjectMiddleware, projects.GetTaskMiddleware, projects.GetMustCanMiddleware(db.CanRunProjectTasks))
projectTaskStop.HandleFunc("/tasks/{task_id}/stop", projects.StopTask).Methods("POST")
//
// Project resources CRUD
projectUserAPI := authenticatedAPI.PathPrefix("/project/{project_id}").Subrouter()
projectUserAPI.Use(projects.ProjectMiddleware, projects.GetMustCanMiddlewareFor(db.CanManageProjectResources))
projectUserAPI.Use(projects.ProjectMiddleware, projects.GetMustCanMiddleware(db.CanManageProjectResources))
projectUserAPI.Path("/role").HandlerFunc(projects.GetUserRole).Methods("GET", "HEAD")
projectUserAPI.Path("/events").HandlerFunc(getAllEvents).Methods("GET", "HEAD")
projectUserAPI.HandleFunc("/events/last", getLastEvents).Methods("GET", "HEAD")
@ -173,14 +175,14 @@ func Route() *mux.Router {
//
// Updating and deleting project
projectAdminAPI := authenticatedAPI.Path("/project/{project_id}").Subrouter()
projectAdminAPI.Use(projects.ProjectMiddleware, projects.GetMustCanMiddlewareFor(db.CanUpdateProject))
projectAdminAPI.Use(projects.ProjectMiddleware, projects.GetMustCanMiddleware(db.CanUpdateProject))
projectAdminAPI.Methods("PUT").HandlerFunc(projects.UpdateProject)
projectAdminAPI.Methods("DELETE").HandlerFunc(projects.DeleteProject)
//
// Manage project users
projectAdminUsersAPI := authenticatedAPI.PathPrefix("/project/{project_id}").Subrouter()
projectAdminUsersAPI.Use(projects.ProjectMiddleware, projects.GetMustCanMiddlewareFor(db.CanManageProjectUsers))
projectAdminUsersAPI.Use(projects.ProjectMiddleware, projects.GetMustCanMiddleware(db.CanManageProjectUsers))
projectAdminUsersAPI.Path("/users").HandlerFunc(projects.AddUser).Methods("POST")
projectUserManagement := projectAdminUsersAPI.PathPrefix("/users").Subrouter()

View File

@ -5,6 +5,7 @@ import (
"encoding/base64"
"github.com/ansible-semaphore/semaphore/api/helpers"
"github.com/ansible-semaphore/semaphore/db"
"github.com/ansible-semaphore/semaphore/util"
"github.com/gorilla/context"
"github.com/gorilla/mux"
"io"
@ -18,7 +19,15 @@ func getUser(w http.ResponseWriter, r *http.Request) {
return
}
helpers.WriteJSON(w, http.StatusOK, context.Get(r, "user"))
var user struct {
db.User
CanCreateProject bool `json:"can_create_project"`
}
user.User = *context.Get(r, "user").(*db.User)
user.CanCreateProject = user.Admin || util.Config.NonAdminCanCreateProject
helpers.WriteJSON(w, http.StatusOK, user)
}
func getAPITokens(w http.ResponseWriter, r *http.Request) {

View File

@ -19,8 +19,8 @@ const (
)
var rolePermissions = map[ProjectUserRole]ProjectUserPermission{
ProjectOwner: CanRunProjectTasks | CanUpdateProject | CanManageProjectResources,
ProjectManager: CanRunProjectTasks | CanManageProjectResources,
ProjectOwner: CanRunProjectTasks | CanManageProjectResources | CanUpdateProject | CanManageProjectUsers,
ProjectManager: CanRunProjectTasks | CanManageProjectResources | CanManageProjectUsers,
ProjectTaskRunner: CanRunProjectTasks,
ProjectGuest: 0,
}
@ -39,5 +39,13 @@ type ProjectUser struct {
func (u *ProjectUser) Can(permissions ProjectUserPermission) bool {
userPermissions := rolePermissions[u.Role]
return (userPermissions & userPermissions) == permissions
return (userPermissions & permissions) == permissions
}
func (r ProjectUserRole) Can(permissions ProjectUserPermission) bool {
return (rolePermissions[r] & permissions) == permissions
}
func (r ProjectUserRole) GetPermissions() ProjectUserPermission {
return rolePermissions[r]
}

15
db/ProjectUser_test.go Normal file
View File

@ -0,0 +1,15 @@
package db
import (
"testing"
)
func TestProjectUsers_RoleCan(t *testing.T) {
if !ProjectManager.Can(CanManageProjectResources) {
t.Fatal()
}
if ProjectManager.Can(CanUpdateProject) {
t.Fatal()
}
}

View File

@ -1,56 +1,56 @@
<template>
<v-app v-if="state === 'success'" class="app">
<EditDialog
v-model="passwordDialog"
save-button-text="Save"
:title="$t('changePassword')"
v-if="user"
event-name="i-user"
v-model="passwordDialog"
save-button-text="Save"
:title="$t('changePassword')"
v-if="user"
event-name="i-user"
>
<template v-slot:form="{ onSave, onError, needSave, needReset }">
<ChangePasswordForm
:project-id="projectId"
:item-id="user.id"
@save="onSave"
@error="onError"
:need-save="needSave"
:need-reset="needReset"
:project-id="projectId"
:item-id="user.id"
@save="onSave"
@error="onError"
:need-save="needSave"
:need-reset="needReset"
/>
</template>
</EditDialog>
<EditDialog
v-model="userDialog"
save-button-text="Save"
:title="$t('editUser')"
v-if="user"
event-name="i-user"
v-model="userDialog"
save-button-text="Save"
:title="$t('editUser')"
v-if="user"
event-name="i-user"
>
<template v-slot:form="{ onSave, onError, needSave, needReset }">
<UserForm
:project-id="projectId"
:item-id="user.id"
@save="onSave"
@error="onError"
:need-save="needSave"
:need-reset="needReset"
:project-id="projectId"
:item-id="user.id"
@save="onSave"
@error="onError"
:need-save="needSave"
:need-reset="needReset"
/>
</template>
</EditDialog>
<EditDialog
v-model="taskLogDialog"
save-button-text="Delete"
:max-width="1000"
:hide-buttons="true"
@close="onTaskLogDialogClosed()"
v-model="taskLogDialog"
save-button-text="Delete"
:max-width="1000"
:hide-buttons="true"
@close="onTaskLogDialogClosed()"
>
<template v-slot:title={}>
<div class="text-truncate" style="max-width: calc(100% - 36px);">
<router-link
class="breadcrumbs__item breadcrumbs__item--link"
:to="`/project/${projectId}/templates/${template ? template.id : null}`"
@click="taskLogDialog = false"
class="breadcrumbs__item breadcrumbs__item--link"
:to="`/project/${projectId}/templates/${template ? template.id : null}`"
@click="taskLogDialog = false"
>{{ template ? template.name : null }}
</router-link>
<v-icon>mdi-chevron-right</v-icon>
@ -59,8 +59,8 @@
<v-spacer></v-spacer>
<v-btn
icon
@click="taskLogDialog = false; onTaskLogDialogClosed()"
icon
@click="taskLogDialog = false; onTaskLogDialogClosed()"
>
<v-icon>mdi-close</v-icon>
</v-btn>
@ -71,61 +71,61 @@
</EditDialog>
<EditDialog
v-model="newProjectDialog"
save-button-text="Create"
:title="$t('newProject')"
event-name="i-project"
v-model="newProjectDialog"
save-button-text="Create"
:title="$t('newProject')"
event-name="i-project"
>
<template v-slot:form="{ onSave, onError, needSave, needReset }">
<ProjectForm
item-id="new"
@save="onSave"
@error="onError"
:need-save="needSave"
:need-reset="needReset"
item-id="new"
@save="onSave"
@error="onError"
:need-save="needSave"
:need-reset="needReset"
/>
</template>
</EditDialog>
<v-snackbar
v-model="snackbar"
:color="snackbarColor"
:timeout="3000"
top
v-model="snackbar"
:color="snackbarColor"
:timeout="3000"
top
>
{{ snackbarText }}
<v-btn
text
@click="snackbar = false"
text
@click="snackbar = false"
>
{{ $t('close') }}
</v-btn>
</v-snackbar>
<v-navigation-drawer
app
dark
:color="darkMode ? '#003236' : '#005057'"
fixed
width="260"
v-model="drawer"
mobile-breakpoint="960"
v-if="$route.path.startsWith('/project/')"
app
dark
:color="darkMode ? '#003236' : '#005057'"
fixed
width="260"
v-model="drawer"
mobile-breakpoint="960"
v-if="$route.path.startsWith('/project/')"
>
<v-menu bottom max-width="235" max-height="100%" v-if="project">
<template v-slot:activator="{ on, attrs }">
<v-list class="pa-0 overflow-y-auto">
<v-list-item
key="project"
class="app__project-selector"
v-bind="attrs"
v-on="on"
key="project"
class="app__project-selector"
v-bind="attrs"
v-on="on"
>
<v-list-item-icon>
<v-avatar
:color="getProjectColor(project)"
size="24"
style="font-size: 13px; font-weight: bold;"
:color="getProjectColor(project)"
size="24"
style="font-size: 13px; font-weight: bold;"
>
<span class="white--text">{{ getProjectInitials(project) }}</span>
</v-avatar>
@ -135,6 +135,7 @@
<v-list-item-title class="app__project-selector-title">
{{ project.name }}
</v-list-item-title>
<v-list-item-subtitle>{{ userRole.role }}</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-icon>
@ -145,16 +146,16 @@
</template>
<v-list>
<v-list-item
v-for="(item, i) in projects"
:key="i"
:to="`/project/${item.id}`"
@click="selectProject(item.id)"
v-for="(item, i) in projects"
:key="i"
:to="`/project/${item.id}`"
@click="selectProject(item.id)"
>
<v-list-item-icon>
<v-avatar
:color="getProjectColor(item)"
size="24"
style="font-size: 13px; font-weight: bold;"
:color="getProjectColor(item)"
size="24"
style="font-size: 13px; font-weight: bold;"
>
<span class="white--text">{{ getProjectInitials(item) }}</span>
</v-avatar>
@ -162,7 +163,7 @@
<v-list-item-content>{{ item.name }}</v-list-item-content>
</v-list-item>
<v-list-item @click="newProjectDialog = true" v-if="user.admin">
<v-list-item @click="newProjectDialog = true" v-if="user.can_create_project">
<v-list-item-icon>
<v-icon>mdi-plus</v-icon>
</v-list-item-icon>
@ -283,9 +284,9 @@
></v-switch>
</v-list-item>
<v-list-item
key="project"
v-bind="attrs"
v-on="on"
key="project"
v-bind="attrs"
v-on="on"
>
<v-list-item-icon>
<v-icon>mdi-account</v-icon>
@ -341,14 +342,14 @@
</v-list-item>
<v-list-item key="password" @click="passwordDialog = true">-->
<!-- <v-list-item-icon>-->
<!-- <v-icon>mdi-lock</v-icon>-->
<!-- </v-list-item-icon>-->
<!-- <v-list-item-icon>-->
<!-- <v-icon>mdi-lock</v-icon>-->
<!-- </v-list-item-icon>-->
<!-- <v-list-item-content>-->
<!-- Change Password-->
<!-- </v-list-item-content>-->
<!-- </v-list-item>-->
<!-- <v-list-item-content>-->
<!-- Change Password-->
<!-- </v-list-item-content>-->
<!-- </v-list-item>-->
<v-list-item key="sign_out" @click="signOut()">
<v-list-item-icon>
@ -365,23 +366,27 @@
</v-navigation-drawer>
<v-main>
<router-view :projectId="projectId" :userId="user ? user.id : null"></router-view>
<router-view
:projectId="projectId"
:userPermissions="userRole.permissions"
:userId="user ? user.id : null"
></router-view>
</v-main>
</v-app>
<v-app v-else-if="state === 'loading'">
<v-main>
<v-container
fluid
fill-height
align-center
justify-center
class="pa-0"
fluid
fill-height
align-center
justify-center
class="pa-0"
>
<v-progress-circular
:size="70"
color="primary"
indeterminate
:size="70"
color="primary"
indeterminate
></v-progress-circular>
</v-container>
</v-main>
@ -389,12 +394,12 @@
<v-app v-else-if="state === 'error'">
<v-main>
<v-container
fluid
flex-column
fill-height
align-center
justify-center
class="pa-0 text-center"
fluid
flex-column
fill-height
align-center
justify-center
class="pa-0 text-center"
>
<v-alert text color="error" class="d-inline-block">
<h3 class="headline">
@ -421,6 +426,7 @@
.v-dialog > .v-card > .v-card__title {
flex-wrap: nowrap;
overflow: hidden;
& * {
white-space: nowrap;
}
@ -489,7 +495,7 @@
}
& > td:first-child {
//font-weight: bold !important;
//font-weight: bold !important;
}
}
@ -560,6 +566,7 @@ export default {
return {
drawer: null,
user: null,
userRole: 0,
systemInfo: null,
state: 'loading',
snackbar: false,
@ -580,8 +587,8 @@ export default {
watch: {
async projects(val) {
if (val.length === 0
&& this.$route.path.startsWith('/project/')
&& this.$route.path !== '/project/new') {
&& this.$route.path.startsWith('/project/')
&& this.$route.path !== '/project/new') {
await this.$router.push({ path: '/project/new' });
}
},
@ -783,8 +790,8 @@ export default {
// try to find project and switch to it if URL not pointing to any project
if (this.$route.path === '/'
|| this.$route.path === '/project'
|| (this.$route.path.startsWith('/project/'))) {
|| this.$route.path === '/project'
|| (this.$route.path.startsWith('/project/'))) {
await this.trySelectMostSuitableProject();
}
@ -812,7 +819,7 @@ export default {
}
if ((projectId == null || !this.projects.some((p) => p.id === projectId))
&& localStorage.getItem('projectId')) {
&& localStorage.getItem('projectId')) {
projectId = parseInt(localStorage.getItem('projectId'), 10);
}
@ -826,10 +833,17 @@ export default {
},
async selectProject(projectId) {
this.userRole = (await axios({
method: 'get',
url: `/api/project/${projectId}/role`,
responseType: 'json',
})).data;
localStorage.setItem('projectId', projectId);
if (this.projectId === projectId) {
return;
}
await this.$router.push({ path: `/project/${projectId}` });
},
@ -861,7 +875,7 @@ export default {
getProjectColor(projectData) {
const projectIndex = this.projects.length
- this.projects.findIndex((p) => p.id === projectData.id);
- this.projects.findIndex((p) => p.id === projectData.id);
return PROJECT_COLORS[projectIndex % PROJECT_COLORS.length];
},

View File

@ -5,6 +5,7 @@ import YesNoDialog from '@/components/YesNoDialog.vue';
import ObjectRefsDialog from '@/components/ObjectRefsDialog.vue';
import { getErrorMessage } from '@/lib/error';
import { USER_PERMISSIONS } from '@/lib/constants';
export default {
components: {
@ -16,11 +17,16 @@ export default {
props: {
projectId: Number,
userId: Number,
userPermissions: Number,
},
data() {
const allowActions = this.allowActions();
const headers = this.getHeaders().filter((header) => allowActions || header.value !== 'actions');
return {
headers: this.getHeaders(),
headers,
items: null,
itemId: null,
@ -29,6 +35,8 @@ export default {
itemRefs: null,
itemRefsDialog: null,
USER_PERMISSIONS,
};
},
@ -38,6 +46,15 @@ export default {
},
methods: {
allowActions() {
return this.can(USER_PERMISSIONS.manageProjectResources);
},
can(permission) {
// eslint-disable-next-line no-bitwise
return (this.userPermissions & permission) === permission;
},
// eslint-disable-next-line no-empty-function
async beforeLoadItems() {
},

View File

@ -22,15 +22,22 @@
:disabled="formSaving"
></v-select>
<v-checkbox
v-model="item.admin"
:label="$t('administrator')"
></v-checkbox>
<v-select
v-model="item.role"
:label="$t('role')"
:items="USER_ROLES"
item-value="slug"
item-text="title"
:rules="[v => !!v || $t('user_required')]"
required
:disabled="formSaving"
></v-select>
</v-form>
</template>
<script>
import ItemFormBase from '@/components/ItemFormBase';
import axios from 'axios';
import { USER_ROLES } from '@/lib/constants';
export default {
mixins: [ItemFormBase],
@ -40,6 +47,7 @@ export default {
users: null,
userId: null,
teamMembers: null,
USER_ROLES,
};
},

View File

@ -15,3 +15,24 @@ export const TEMPLATE_TYPE_ACTION_TITLES = {
build: 'Build',
deploy: 'Deploy',
};
export const USER_PERMISSIONS = {
runProjectTasks: 1,
updateProject: 2,
manageProjectResources: 4,
manageProjectUsers: 8,
};
export const USER_ROLES = [{
slug: 'owner',
title: 'Owner',
}, {
slug: 'manager',
title: 'Manager',
}, {
slug: 'task_runner',
title: 'Task Runner',
}, {
slug: 'guest',
title: 'Guest',
}];

View File

@ -1,6 +1,6 @@
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
<div v-if="items">
<v-toolbar flat >
<v-toolbar flat>
<v-app-bar-nav-icon @click="showDrawer()"></v-app-bar-nav-icon>
<v-toolbar-title>{{ $t('dashboard') }}</v-toolbar-title>
<v-spacer></v-spacer>
@ -8,7 +8,12 @@
<v-tabs centered>
<v-tab key="history" :to="`/project/${projectId}/history`">{{ $t('history') }}</v-tab>
<v-tab key="activity" :to="`/project/${projectId}/activity`">{{ $t('activity') }}</v-tab>
<v-tab key="settings" :to="`/project/${projectId}/settings`">{{ $t('settings') }}</v-tab>
<v-tab
v-if="can(USER_PERMISSIONS.updateProject)"
key="settings"
:to="`/project/${projectId}/settings`"
>{{ $t('settings') }}
</v-tab>
</v-tabs>
</div>
</v-toolbar>
@ -27,8 +32,14 @@
</template>
<script>
import ItemListPageBase from '@/components/ItemListPageBase';
import { USER_PERMISSIONS } from '@/lib/constants';
export default {
computed: {
USER_PERMISSIONS() {
return USER_PERMISSIONS;
},
},
mixins: [ItemListPageBase],
methods: {

View File

@ -40,6 +40,7 @@
<v-btn
color="primary"
@click="editItem('new')"
v-if="can(USER_PERMISSIONS.manageProjectResources)"
>{{ $t('newEnvironment') }}
</v-btn>
</v-toolbar>

View File

@ -23,7 +23,12 @@
<v-tabs centered>
<v-tab key="history" :to="`/project/${projectId}/history`">{{ $t('history') }}</v-tab>
<v-tab key="activity" :to="`/project/${projectId}/activity`">{{ $t('activity') }}</v-tab>
<v-tab key="settings" :to="`/project/${projectId}/settings`">{{ $t('settings') }}</v-tab>
<v-tab
v-if="can(USER_PERMISSIONS.updateProject)"
key="settings"
:to="`/project/${projectId}/settings`"
>{{ $t('settings') }}
</v-tab>
</v-tabs>
</div>
</v-toolbar>

View File

@ -40,6 +40,7 @@
<v-btn
color="primary"
@click="editItem('new')"
v-if="can(USER_PERMISSIONS.manageProjectResources)"
>{{ $t('newInventory') }}</v-btn>
</v-toolbar>

View File

@ -41,6 +41,7 @@
<v-btn
color="primary"
@click="editItem('new')"
v-if="can(USER_PERMISSIONS.manageProjectResources)"
>{{ $t('newKey') }}</v-btn>
</v-toolbar>

View File

@ -39,6 +39,7 @@
<v-btn
color="primary"
@click="editItem('new')"
v-if="can(USER_PERMISSIONS.manageProjectResources)"
>{{ $t('newRepository') }}</v-btn>
</v-toolbar>

View File

@ -32,7 +32,9 @@
<v-btn
color="primary"
@click="editItem('new')"
>{{ $t('newTeamMember') }}</v-btn>
v-if="can(USER_PERMISSIONS.manageProjectUsers)"
>{{ $t('newTeamMember') }}
</v-btn>
</v-toolbar>
<v-data-table
@ -45,10 +47,11 @@
<template v-slot:item.role="{ item }">
<v-select
v-model="item.role"
:items="roles"
:items="USER_ROLES"
item-value="slug"
item-text="title"
:style="{width: '200px'}"
:disabled="!can(USER_PERMISSIONS.manageProjectUsers)"
@change="updateProjectUser(item)"
/>
</template>
@ -58,6 +61,7 @@
icon
:disabled="!isUserAdmin()"
@click="askDeleteItem(item.id)"
v-if="can(USER_PERMISSIONS.manageProjectUsers)"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
@ -70,25 +74,14 @@
import ItemListPageBase from '@/components/ItemListPageBase';
import TeamMemberForm from '@/components/TeamMemberForm.vue';
import axios from 'axios';
import { USER_PERMISSIONS, USER_ROLES } from '@/lib/constants';
export default {
components: { TeamMemberForm },
mixins: [ItemListPageBase],
data() {
return {
roles: [{
slug: 'owner',
title: 'Owner',
}, {
slug: 'manager',
title: 'Manager',
}, {
slug: 'task_runner',
title: 'Task Runner',
}, {
slug: 'guest',
title: 'Guest',
}],
USER_ROLES,
};
},
@ -103,6 +96,10 @@ export default {
await this.loadItems();
},
allowActions() {
return this.can(USER_PERMISSIONS.manageProjectUsers);
},
getHeaders() {
return [
{
@ -129,6 +126,7 @@ export default {
sortable: false,
}];
},
getSingleItemUrl() {
return `/api/project/${this.projectId}/users/${this.itemId}`;
},

View File

@ -78,6 +78,7 @@
color="primary"
@click="editItem('new')"
class="mr-1"
v-if="can(USER_PERMISSIONS.manageProjectResources)"
>{{ $t('newTemplate') }}
</v-btn>
@ -97,7 +98,12 @@
>{{ view.title }}
</v-tab>
<v-btn icon class="mt-2 ml-4" @click="editViewsDialog = true">
<v-btn
icon
class="mt-2 ml-4"
@click="editViewsDialog = true"
v-if="can(USER_PERMISSIONS.manageProjectResources)"
>
<v-icon>mdi-pencil</v-icon>
</v-btn>
</v-tabs>
@ -212,7 +218,7 @@
</div>
</template>
<style lang="scss">
@import '~vuetify/src/styles/settings/_variables';
@import '~vuetify/src/styles/settings/variables';
.templates-table .text-start:first-child {
padding-right: 0 !important;