Nooul / spring-boot-rest-api-helpers

Java Spring REST API helpers for quick query building through JSON inspired by react-admin and offering an alternative to RSQL / FIQL
MIT License
36 stars 21 forks source link

Spring security issue #6

Closed mettok closed 3 years ago

mettok commented 4 years ago

Hi Thank for making this code available. It was a big help for me. there are some issue though. Spring security is included because of one exception handler. if you import this api into a spring boot app without spring security you will have authorize first. and i ran into another problem with Swagger which not working anymore. I needed to download the source code and and comment out the line which uses spring security. I tried to use simpleRestProvider, but this api is not compatible. i needed to do some modification and put the data into the root element of json response instead of wrap it into content, maybe i do something wrong, but i needed to do that to make it work.

zifnab87 commented 4 years ago

I understand! could you use exclusions in your pom/gradle when importing spring-boot-rest-api-helpers ? assuming you won't use spring security. Another option would be to configure the spring security so that it permits all requests and doesnt need authorization/authentication..

I would suggest to keep the wrapper that puts everything in content since this is compatible with pagination and on top of that protects from some old security issues when returning a json array directly.

mettok commented 4 years ago

I tried exclusion but i ran into some compiling problem, since the package is imported to the context. Yeah i would keep the wrapper but then i couldnt use simplerestDataprovider on react admin. Do you suggest any other dataprovider which is compatible?

zifnab87 commented 4 years ago

I have made one dataprovider specifically for these helpers in most of my projects I can share it with you the next days. Try to add spring security but permitAll() for all requests or create your own wrapper (controlleradvice) with just the things you need, so you dont have a problem with it

On Tue, Feb 18, 2020, 09:36 mettok notifications@github.com wrote:

I tried exclusion but i ran into some compiling problem, since the package is imported to the context. Yeah i would keep the wrapper but then i couldnt use simplerestDataprovider on react admin. Do you suggest any other dataprovider which is compatible?

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/zifnab87/spring-boot-rest-api-helpers/issues/6?email_source=notifications&email_token=AAEAN3TV6UO4PEJJPJ2HAILRDOFYLA5CNFSM4KW55BLKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEMA5EEA#issuecomment-587321872, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAEAN3TJEGTYSDJ7XZXZ72DRDOFYLANCNFSM4KW55BLA .

zifnab87 commented 4 years ago

Sorry for the delay when I tried 2-3 days ago comments were not working .. Here is a sample of what I usually use as a dataProvider:

import {
    GET_LIST,
    GET_ONE,
    GET_MANY,
    GET_MANY_REFERENCE,
    CREATE,
    UPDATE,
    DELETE,
    DELETE_MANY,
    fetchUtils,
} from 'react-admin';
import { stringify } from 'query-string';
import config from './config/config';
console.log(config);
const API_URL = config.baseUrl;
const HOST = config.apiHost;

/**
 * @param {String} type One of the constants appearing at the top if this file, e.g. 'UPDATE'
 * @param {String} resource Name of the resource to fetch, e.g. 'posts'
 * @param {Object} params The Data Provider request params, depending on the type
 * @returns {Object} { url, options } The HTTP request parameters
 */

let keysContainingFilePaths = ['avatarPhotos', 'galleryPhotos'];
let referenceManyToOneKeys = {
    "wines": ["wineSupplier","wineColor","wineType", "wineDryness", "wineBottleVolume",
            "wineSpecialType", "wineOrigin"]
}

let referenceManyToManyKeys = {
    "wines": ["varieties","foodCombinations"]
}

const convertDataProviderRequestToHTTP = (type, resource, params) => {
    switch (type) {
    case GET_LIST: {
        const { page, perPage } = params.pagination;
        const { field, order } = params.sort;
        const query = {
            sort: JSON.stringify([field, order]),
            range: JSON.stringify([page - 1,perPage]),
            filter: JSON.stringify(params.filter),
        };
        return { url: `${API_URL}/${resource}?${stringify(query)}` };
    }
    case GET_ONE:
        return { url: `${API_URL}/${resource}/${params.id}` };
    case GET_MANY: {
        const query = {
            filter: JSON.stringify({ id: params.ids }),
        };
        return { url: `${API_URL}/${resource}?${stringify(query)}` };
    }
    case GET_MANY_REFERENCE: {
        const { page, perPage } = params.pagination;
        const { field, order } = params.sort;
        const query = {
            sort: JSON.stringify([field, order]),
            range: JSON.stringify([page - 1 , perPage]),
            filter: JSON.stringify({ ...params.filter, [params.target]: params.id }),
        };
        return { url: `${API_URL}/${resource}?${stringify(query)}` };
    }
    case UPDATE:
        return {
            url: `${API_URL}/${resource}/${params.id}`,
            options: { method: 'PUT', body: JSON.stringify( upsertOneRequestHelper(params.data, keysContainingFilePaths, resource)) },
        };
    case CREATE:
        return {
            url: `${API_URL}/${resource}`,
            options: { method: 'POST', body: JSON.stringify( upsertOneRequestHelper(params.data, keysContainingFilePaths, resource)) },
        };
    case DELETE:
        return {
            url: `${API_URL}/${resource}/${params.id}`,
            options: { method: 'DELETE' },
        };
    case DELETE_MANY:
        return {
            url: `${API_URL}/${resource}/${params.ids[0]}`,
            options: { method: 'DELETE' },
        };
    default:
        throw new Error(`Unsupported fetch action type ${type}`);
    }
};

const upsertOneRequestHelper = (row, fileKeys, resource) => {
    let request = {};
    Object.keys(row).forEach(function(key) {
        let valIsFilePath = fileKeys.indexOf(key) > -1;
        let isManyToOneKey = (referenceManyToOneKeys[resource])?referenceManyToOneKeys[resource].indexOf(key) > -1: false;
        let isManyToManyKey = (referenceManyToManyKeys[resource])?referenceManyToManyKeys[resource].indexOf(key) > -1: false;
        if (valIsFilePath && row[key] instanceof Array) {
            let values = row[key];
            request[key] = [];
            for (let val of values) {
                request[key].push({ "id": val });
            }
        }
        else if(isManyToOneKey) {
            request[key] = (row[key])? {id: row[key]} : null;
        }
        else if(isManyToManyKey) {
            request[key] = (row[key])? row[key].map((id)=> { return {id: id}}) : [];
        }
        else {
            request[key] = row[key];
        }
    });
    return request;

}

const getOneRowResponseHelper = (row, fileKeys, rowInList, resource) => {
    let result = {};

    Object.keys(row).forEach(function(key) {
        let valIsFilePath = fileKeys.indexOf(key) > -1;
        let isManyToOneKey = (referenceManyToOneKeys[resource])?referenceManyToOneKeys[resource].indexOf(key) > -1: false;
        let isManyToManyKey = (referenceManyToManyKeys[resource])?referenceManyToManyKeys[resource].indexOf(key) > -1: false;
        if (valIsFilePath && row[key] instanceof Array) {
            let values = row[key];
            result[key] = [];
            for (let val of values) {
                result[key].push({ "id": val.id, "path": HOST +'/'+ val.relativePath +'/'+ val.fileName });
            }
        }
        else if(isManyToOneKey) {
            result[key] = (row[key])? row[key].id : null;
        }
        else if(isManyToManyKey) {
            result[key] = row[key]? row[key].map((obj)=> {return obj.id}) : [];
        }
        else {
            result[key] = row[key];
        }
    });

    return result;
}

const prepareGetListResponse = (rows, resource) => {

    let result =  rows.map(function (row) {
        return getOneRowResponseHelper(row, keysContainingFilePaths, true, resource);
    });
    return result;
}

/**
 * @param {Object} response HTTP response from fetch()
 * @param {String} type One of the constants appearing at the top if this file, e.g. 'UPDATE'
 * @param {String} resource Name of the resource to fetch, e.g. 'posts'
 * @param {Object} params The Data Provider request params, depending on the type
 * @returns {Object} Data Provider response
 */
const convertHTTPResponseToDataProvider = (response, type, resource, params) => {
    const { headers, json } = response;

    switch (type) {

        case GET_LIST:
        case GET_MANY:
            return {
                data: prepareGetListResponse(json.content.map(x => x), resource),
                total: parseInt(json.totalElements, 10)
            };
        case CREATE:
            return { data: { ...params.data, id: json.id } };
        default:
            return { data: getOneRowResponseHelper(json, keysContainingFilePaths, false, resource)};
    }
};

/**
 * @param {string} type Request type, e.g GET_LIST
 * @param {string} resource Resource name, e.g. "posts"
 * @param {Object} payload Request parameters. Depends on the request type
 * @returns {Promise} the Promise for response
 */
export default (type, resource, params) => {
    const { fetchJson } = fetchUtils;
    let { url, options } = convertDataProviderRequestToHTTP(type, resource, params);
    if (!options) {
        options = {};
    }

    if (!options.headers) {
        options.headers = new Headers({ Accept: 'application/json' });
    }

    let jwt_token = localStorage.getItem('jwt_token');
    options.headers.append('Authorization','Bearer ' + jwt_token);
    return fetchJson(url, options)
        .then(response => convertHTTPResponseToDataProvider(response, type, resource, params));
};
zifnab87 commented 3 years ago

spring security shouldn't be a problem anymore