Gestion des autorisations côté serveur #20

Manually merged
wpetit merged 4 commits from feature/authorization into develop 2020-09-08 10:18:10 +02:00
10 changed files with 160 additions and 7 deletions
Showing only changes of commit 9c6ebae9bc - Show all commits

View File

@ -1,5 +1,6 @@
import React, { useState, ChangeEvent, useEffect } from 'react'; import React, { useState, ChangeEvent, useEffect } from 'react';
import { Workgroup } from '../../types/workgroup'; import { Workgroup } from '../../types/workgroup';
import { useIsAuthorized } from '../../gql/queries/authorization';
export interface InfoFormProps { export interface InfoFormProps {
workgroup: Workgroup workgroup: Workgroup
@ -17,6 +18,15 @@ export function InfoForm({ workgroup, onChange }: InfoFormProps) {
} }
}); });
const { isAuthorized } = useIsAuthorized({
variables: {
action: 'update',
object: {
workgroupId: state.workgroup.id,
}
}
}, state.workgroup.id === '' ? true : false);
useEffect(() => { useEffect(() => {
setState({ setState({
changed: false, changed: false,
@ -61,6 +71,7 @@ export function InfoForm({ workgroup, onChange }: InfoFormProps) {
<label className="label">Nom du groupe</label> <label className="label">Nom du groupe</label>
<div className="control"> <div className="control">
<input type="text" className="input" value={state.workgroup.name} <input type="text" className="input" value={state.workgroup.name}
disabled={!isAuthorized}
onChange={onWorkgroupAttrChange.bind(null, "name")} /> onChange={onWorkgroupAttrChange.bind(null, "name")} />
</div> </div>
</div> </div>
@ -85,7 +96,7 @@ export function InfoForm({ workgroup, onChange }: InfoFormProps) {
null null
} }
<div className="buttons is-right"> <div className="buttons is-right">
<button disabled={!state.changed} <button disabled={!state.changed || !isAuthorized}
className="button is-success" onClick={onSaveClick}> className="button is-success" onClick={onSaveClick}>
<span>Enregistrer</span> <span>Enregistrer</span>
<span className="icon"><i className="fa fa-save"></i></span> <span className="icon"><i className="fa fa-save"></i></span>

View File

@ -20,9 +20,11 @@ export function WorkgroupPage() {
} }
}); });
const userProfileQuery = useUserProfileQuery(); const userProfileQuery = useUserProfileQuery();
const [ joinWorkgroup, joinWorkgroupMutation ] = useJoinWorkgroupMutation(); const [ joinWorkgroup, joinWorkgroupMutation ] = useJoinWorkgroupMutation();
const [ leaveWorkgroup, leaveWorkgroupMutation ] = useLeaveWorkgroupMutation(); const [ leaveWorkgroup, leaveWorkgroupMutation ] = useLeaveWorkgroupMutation();
const [ closeWorkgroup, closeWorkgroupMutation ] = useCloseWorkgroupMutation(); const [ closeWorkgroup, closeWorkgroupMutation ] = useCloseWorkgroupMutation();
const [ state, setState ] = useState({ const [ state, setState ] = useState({
userProfileId: '', userProfileId: '',
workgroup: { workgroup: {

View File

@ -34,13 +34,17 @@ export const client = new ApolloClient<any>({
function mergeArrayByField<T>(fieldName: string) { function mergeArrayByField<T>(fieldName: string) {
return (existing: T[] = [], incoming: T[], { readField, mergeObjects }) => { return (existing: T[] = [], incoming: T[], { readField, mergeObjects }) => {
if (incoming.length === 0) return [];
const merged: any[] = existing ? existing.slice(0) : []; const merged: any[] = existing ? existing.slice(0) : [];
const objectFieldToIndex: Record<string, number> = Object.create(null); const objectFieldToIndex: Record<string, number> = Object.create(null);
if (existing) { if (existing) {
existing.forEach((obj, index) => { existing.forEach((obj, index) => {
objectFieldToIndex[readField(fieldName, obj)] = index; objectFieldToIndex[readField(fieldName, obj)] = index;
}); });
} }
incoming.forEach(obj => { incoming.forEach(obj => {
const field = readField(fieldName, obj); const field = readField(fieldName, obj);
const index = objectFieldToIndex[field]; const index = objectFieldToIndex[field];
@ -51,6 +55,7 @@ function mergeArrayByField<T>(fieldName: string) {
merged.push(obj); merged.push(obj);
} }
}); });
return merged; return merged;
} }
} }

View File

@ -1,5 +1,6 @@
import { gql, useQuery, useMutation } from '@apollo/client'; import { gql, useQuery, useMutation, FetchResult } from '@apollo/client';
import { QUERY_WORKGROUP } from '../queries/workgroups'; import { QUERY_WORKGROUP } from '../queries/workgroups';
import { QUERY_IS_AUTHORIZED } from '../queries/authorization';
export const MUTATION_UPDATE_WORKGROUP = gql` export const MUTATION_UPDATE_WORKGROUP = gql`
mutation updateWorkgroup($workgroupId: ID!, $changes: WorkgroupChanges!) { mutation updateWorkgroup($workgroupId: ID!, $changes: WorkgroupChanges!) {
@ -57,7 +58,19 @@ mutation joinWorkgroup($workgroupId: ID!) {
}`; }`;
export function useJoinWorkgroupMutation() { export function useJoinWorkgroupMutation() {
return useMutation(MUTATION_JOIN_WORKGROUP); return useMutation(MUTATION_JOIN_WORKGROUP, {
refetchQueries: ({ data }: FetchResult) => {
return [{
query: QUERY_IS_AUTHORIZED,
variables: {
action: 'update',
object: {
workgroupId: data.joinWorkgroup.id,
}
}
}]
}
});
} }
const MUTATION_LEAVE_WORKGROUP = gql` const MUTATION_LEAVE_WORKGROUP = gql`
@ -76,7 +89,27 @@ mutation leaveWorkgroup($workgroupId: ID!) {
}`; }`;
export function useLeaveWorkgroupMutation() { export function useLeaveWorkgroupMutation() {
return useMutation(MUTATION_LEAVE_WORKGROUP); return useMutation(MUTATION_LEAVE_WORKGROUP, {
refetchQueries: ({ data }: FetchResult) => {
return [{
query: QUERY_WORKGROUP,
variables: {
filter: {
ids: [data.leaveWorkgroup.id],
}
}
},
{
query: QUERY_IS_AUTHORIZED,
variables: {
action: 'update',
object: {
workgroupId: data.leaveWorkgroup.id,
}
}
}]
}
});
} }
const MUTATION_CLOSE_WORKGROUP = gql` const MUTATION_CLOSE_WORKGROUP = gql`

View File

@ -0,0 +1,19 @@
import { gql, useQuery } from '@apollo/client';
import { useGraphQLData } from './helper';
export const QUERY_IS_AUTHORIZED = gql`
query isAuthorized($action: String!, $object: AuthorizationObject!) {
isAuthorized(action: $action, object: $object)
}
`;
export function useIsAuthorizedQuery(options = {}) {
return useQuery(QUERY_IS_AUTHORIZED, options);
}
export function useIsAuthorized(options = {}, defaultValue = false) {
const { data, loading, error } = useGraphQLData<boolean>(
QUERY_IS_AUTHORIZED, 'isAuthorized', defaultValue, options
);
return { isAuthorized: data, loading, error };
}

View File

@ -0,0 +1,59 @@
package graph
import (
"context"
"forge.cadoles.com/Cadoles/daddy/internal/model"
errs "github.com/pkg/errors"
)
func handleIsAuthorized(ctx context.Context, action string, obj model.AuthorizationObject) (bool, error) {
db, err := getDB(ctx)
if err != nil {
return false, errs.WithStack(err)
}
var object interface{}
switch {
case obj.WorkgroupID != nil:
repo := model.NewWorkgroupRepository(db)
workgroup, err := repo.Find(ctx, *obj.WorkgroupID)
if err != nil {
return false, errs.WithStack(err)
}
object = workgroup
case obj.DecisionSupportFileID != nil:
repo := model.NewDSFRepository(db)
dsf, err := repo.Find(ctx, *obj.DecisionSupportFileID)
if err != nil {
return false, errs.WithStack(err)
}
object = dsf
case obj.UserID != nil:
repo := model.NewUserRepository(db)
user, err := repo.Find(ctx, *obj.UserID)
if err != nil {
return false, errs.WithStack(err)
}
object = user
default:
return false, errs.WithStack(ErrInvalidInput)
}
authorized, err := isAuthorized(ctx, object, model.Action(action))
if err != nil {
return false, errs.WithStack(err)
}
return authorized, nil
}

View File

@ -3,5 +3,6 @@ package graph
import "errors" import "errors"
var ( var (
ErrForbidden = errors.New("forbidden") ErrForbidden = errors.New("forbidden")
ErrInvalidInput = errors.New("invalid input")
) )

View File

@ -38,8 +38,15 @@ input DecisionSupportFileFilter {
ids: [ID] ids: [ID]
} }
input AuthorizationObject {
workgroupId: ID
userId: ID
decisionSupportFileId: ID
}
type Query { type Query {
userProfile: User userProfile: User
workgroups(filter: WorkgroupsFilter): [Workgroup]! workgroups(filter: WorkgroupsFilter): [Workgroup]!
decisionSupportFiles(filter: DecisionSupportFileFilter): [DecisionSupportFile]! decisionSupportFiles(filter: DecisionSupportFileFilter): [DecisionSupportFile]!
isAuthorized(action: String!, object: AuthorizationObject!): Boolean!
} }

View File

@ -31,6 +31,10 @@ func (r *queryResolver) DecisionSupportFiles(ctx context.Context, filter *model1
return handleDecisionSupportFiles(ctx, filter) return handleDecisionSupportFiles(ctx, filter)
} }
func (r *queryResolver) IsAuthorized(ctx context.Context, action string, object model1.AuthorizationObject) (bool, error) {
return handleIsAuthorized(ctx, action, object)
}
func (r *userResolver) ID(ctx context.Context, obj *model1.User) (string, error) { func (r *userResolver) ID(ctx context.Context, obj *model1.User) (string, error) {
return strconv.FormatUint(uint64(obj.ID), 10), nil return strconv.FormatUint(uint64(obj.ID), 10), nil
} }

View File

@ -7,6 +7,7 @@ import (
"forge.cadoles.com/Cadoles/daddy/internal/orm" "forge.cadoles.com/Cadoles/daddy/internal/orm"
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
"github.com/pkg/errors" "github.com/pkg/errors"
errs "github.com/pkg/errors"
) )
type UserRepository struct { type UserRepository struct {
@ -68,6 +69,17 @@ func (r *UserRepository) UpdateUserByEmail(ctx context.Context, email string, ch
return user, nil return user, nil
} }
func (r *UserRepository) Find(ctx context.Context, id string) (*User, error) {
user := &User{}
query := r.db.Model(user).Where("id = ?", id)
if err := query.First(&user).Error; err != nil {
return nil, errs.WithStack(err)
}
return user, nil
}
func NewUserRepository(db *gorm.DB) *UserRepository { func NewUserRepository(db *gorm.DB) *UserRepository {
return &UserRepository{db} return &UserRepository{db}
} }