diff --git a/demos/jans-tarp/.gitignore b/demos/jans-tarp/.gitignore index 22b9f913967..5683ef3b5e6 100644 --- a/demos/jans-tarp/.gitignore +++ b/demos/jans-tarp/.gitignore @@ -1,4 +1,5 @@ node_modules dist release -package-lock.json \ No newline at end of file +package-lock.json +src/wasm \ No newline at end of file diff --git a/demos/jans-tarp/README.md b/demos/jans-tarp/README.md index d4a06130d05..3a53ce5fe8d 100644 --- a/demos/jans-tarp/README.md +++ b/demos/jans-tarp/README.md @@ -2,9 +2,10 @@ ## Relying Party tool in form of a Browser Extension. -[Demo Video](https://www.loom.com/share/6bfe8c5556a94abea05467e3deead8a2?sid=b65c81d9-c1a1-475c-b89b-c105887d31ad) +[Demo Video](https://www.loom.com/share/b112b9c7214a4920812a2ebe9c36dbf5?sid=7a15d2e5-881e-4002-9b8c-902dd1d80cec) -This extension is for convenient testing of authentication flows on browser. +- This extension is for convenient testing of authentication flows on browser. +- [Cedarling](https://docs.jans.io/head/cedarling/cedarling-overview/) is an embeddable stateful Policy Decision Point, or "PDP". Cedarling is integrated with Jans Tarp to make authorization decision post-authentication. ## Supporting Browser @@ -18,9 +19,10 @@ This extension is for convenient testing of authentication flows on browser. ## Build 1. Change directory to the project directory (`/jans-tarp`). -2. Run `npm install`. -3. Run `npm run build`. It will create Chrome and Firefox build in `/jans-tarp/dist/chrome` and `/jans-tarp/dist/firefox` directories respectively. -4. To pack the build into a zip file run `npm run pack`. This command will pack Chrome and Firefox builds in zip files at `/jans-tarp/release`. +2. Download and extract Cedarling WASM bindings from https://github.com/JanssenProject/jans/releases/download/nightly/cedarling_wasm_{version}_pkg.tar.gz to `/jans-tarp/src/wasm`. +3. Run `npm install`. +4. Run `npm run build`. It will create Chrome and Firefox build in `/jans-tarp/dist/chrome` and `/jans-tarp/dist/firefox` directories respectively. +5. To pack the build into a zip file run `npm run pack`. This command will pack Chrome and Firefox builds in zip files at `/jans-tarp/release`. ## Releases diff --git a/demos/jans-tarp/package.json b/demos/jans-tarp/package.json index 392bcd3777a..e32fd55220e 100644 --- a/demos/jans-tarp/package.json +++ b/demos/jans-tarp/package.json @@ -7,6 +7,7 @@ "scripts": { "watch": "webpack --watch --progress --config webpack.dev.js", "build": "webpack --mode=production --progress --config webpack.prod.js", + "build-dev": "webpack --mode=development --progress --config webpack.dev.js", "pack": "node pack.js" }, "devDependencies": { @@ -38,6 +39,7 @@ "autoprefixer": "^10.4.7", "axios": "^1.4.0", "dayjs": "^1.11.10", + "json-edit-react": "^1.19.2", "jwt-decode": "^4.0.0", "moment": "^2.29.4", "postcss": "^8.4.14", @@ -47,6 +49,7 @@ "react-select": "^5.7.3", "react-spinner-overlay": "^0.1.33", "styled-components": "^6.1.0", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "wasm": "file:src/wasm" } } diff --git a/demos/jans-tarp/src/options/addCedarlingConfig.tsx b/demos/jans-tarp/src/options/addCedarlingConfig.tsx new file mode 100644 index 00000000000..bf63ecfb533 --- /dev/null +++ b/demos/jans-tarp/src/options/addCedarlingConfig.tsx @@ -0,0 +1,200 @@ +import * as React from 'react'; +import Button from '@mui/material/Button'; +import TextField from '@mui/material/TextField'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; +import CircularProgress from "@mui/material/CircularProgress"; +import Stack from '@mui/material/Stack'; +import Alert from '@mui/material/Alert'; +import __wbg_init, { init, Cedarling } from "wasm"; +import { v4 as uuidv4 } from 'uuid'; +import Radio from '@mui/material/Radio'; +import RadioGroup from '@mui/material/RadioGroup'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import { JsonEditor } from 'json-edit-react'; +import axios from 'axios'; + +export default function AddCedarlingConfig({ isOpen, handleDialog, newData }) { + const [open, setOpen] = React.useState(isOpen); + const [bootstrap, setBootstrap] = React.useState(newData); + const [errorMessage, setErrorMessage] = React.useState("") + const [loading, setLoading] = React.useState(false); + const [inputSelection, setInputSelection] = React.useState("json"); + + const ADD_BOOTSTRAP_ERROR = 'Error in adding bootstrap. Check web console for logs.' + + React.useEffect(() => { + if (isOpen) { + handleOpen(); + } else { + handleClose(); + } + }, [isOpen]); + + React.useEffect(() => { + setBootstrap(newData) + }, [newData]); + + const handleClose = () => { + setInputSelection('json') + handleDialog(false) + setOpen(false); + }; + + const handleOpen = () => { + setErrorMessage(''); + setLoading(false); + handleDialog(true) + setOpen(true); + }; + + const validateBootstrap = async (e) => { + let bootstrap = e.target.value; + setErrorMessage(''); + if (inputSelection === 'url') { + let bootstrapUrl = e.target.value; + if (bootstrapUrl === '') { + setErrorMessage('URL is required.'); + return false; + } + const oidcConfigOptions = { + method: 'GET', + url: bootstrapUrl, + }; + const response = await axios(oidcConfigOptions); + bootstrap = response.data; + + } else if (inputSelection === 'json') { + bootstrap = e.target.value; + } + if (isEmpty(bootstrap) || Object.keys(bootstrap).length === 0) { + setErrorMessage('Empty authorization request not allowed.'); + return false; + } + isJsonValid(bootstrap); + }; + + const isJsonValid = async (bootstrap) => { + setErrorMessage(''); + try { + setBootstrap(JSON.parse(JSON.stringify(bootstrap))); + return true; + } catch (err) { + console.error(err) + setErrorMessage(`Invalid input: ${err}`); + return false; + } + }; + + const saveBootstrap = async () => { + try { + setLoading(true); + if (!isJsonValid(bootstrap)) { + return; + } + + await __wbg_init(); + let instance: Cedarling = await init(bootstrap); + + chrome.storage.local.get(["cedarlingConfig"], (result) => { + let bootstrapArr = [] + + let idObj = { id: uuidv4() }; + + bootstrapArr.push({ ...bootstrap, ...idObj }); + chrome.storage.local.set({ cedarlingConfig: bootstrapArr }); + handleClose(); + }); + } catch (err) { + console.error(err) + setErrorMessage(ADD_BOOTSTRAP_ERROR + err) + } + setLoading(false); + } + + const isEmpty = (value) => { + return (value == null || value.length === 0); + } + return ( + + { + event.preventDefault(); + }, + }} + className="form-container" + > + Add Cedarling Configuration + {loading ? ( +
+ +
+ ) : ( + "" + )} + + + Submit below details. + + + {(!!errorMessage || errorMessage !== '') ? + {errorMessage} : '' + } + + { setErrorMessage(''); setInputSelection("json"); }} color="success" />} label="JSON" /> + { setErrorMessage(''); setInputSelection("url") }} />} label="URL" /> + + {inputSelection === 'json' ? + + : ''} + {inputSelection === 'url' ? + { + validateBootstrap(e); + }} + /> : ''} + + + + + + +
+
+ ); +} \ No newline at end of file diff --git a/demos/jans-tarp/src/options/authFlowInputs.tsx b/demos/jans-tarp/src/options/authFlowInputs.tsx index 20c88d10840..72f80ce5f81 100644 --- a/demos/jans-tarp/src/options/authFlowInputs.tsx +++ b/demos/jans-tarp/src/options/authFlowInputs.tsx @@ -448,6 +448,7 @@ export default function AuthFlowInputs({ isOpen, handleDialog, client, notifyOnD )} /> + setDisplayToken(!displayToken)}/>} label="Display Access Token and ID Token after authentication" /> diff --git a/demos/jans-tarp/src/options/cedarling.tsx b/demos/jans-tarp/src/options/cedarling.tsx new file mode 100644 index 00000000000..c4d5d1991f8 --- /dev/null +++ b/demos/jans-tarp/src/options/cedarling.tsx @@ -0,0 +1,148 @@ +import * as React from 'react'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell, { tableCellClasses } from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Edit from '@mui/icons-material/Edit'; +import { pink, green } from '@mui/material/colors'; +import Grid from '@mui/material/Grid'; +import { styled } from '@mui/material/styles'; +import Paper from '@mui/material/Paper'; +import AddIcon from '@mui/icons-material/Add'; +import Container from '@mui/material/Container'; +import Button from '@mui/material/Button'; +import Stack from '@mui/material/Stack'; +import AddCedarlingConfig from './addCedarlingConfig' +import IconButton from '@mui/material/IconButton'; +import Tooltip from '@mui/material/Tooltip'; +import DeleteForeverOutlinedIcon from '@mui/icons-material/DeleteForeverOutlined'; +import HelpDrawer from './helpDrawer' +import Alert from '@mui/material/Alert'; +import { JsonEditor } from 'json-edit-react' + +const StyledTableCell = styled(TableCell)(({ theme }) => ({ + [`&.${tableCellClasses.head}`]: { + backgroundColor: theme.palette.common.black, + color: theme.palette.common.white, + }, + [`&.${tableCellClasses.body}`]: { + fontSize: 14, + }, +})); + +function Row(props: { row: any, notifyOnDataChange }) { + const { row, notifyOnDataChange } = props; + const [open, setOpen] = React.useState(false); + + const handleDialog = (isOpen) => { + setOpen(isOpen); + notifyOnDataChange(); + }; + + async function resetBootstrap() { + chrome.storage.local.get(["cedarlingConfig"], (result) => { + let cedarlingConfigArr = [] + chrome.storage.local.set({ cedarlingConfig: cedarlingConfigArr }); + }); + notifyOnDataChange(); + } + + return ( + + + *': { borderBottom: 'unset' } }}> + + + + + + + + + + + + + + + + { + setOpen(true); + notifyOnDataChange(); + }} /> + + + + + + + ); +} + +export default function Cedarling({ data, notifyOnDataChange, isOidcClientRegistered }) { + const [modelOpen, setModelOpen] = React.useState(false); + const [drawerOpen, setDrawerOpen] = React.useState(false); + const [oidcClientRegistered, setOidcClientRegistered] = React.useState(false); + + + React.useEffect(() => { + setOidcClientRegistered(isOidcClientRegistered) + }, [isOidcClientRegistered]); + + const handleDialog = (isOpen) => { + setModelOpen(isOpen); + notifyOnDataChange(); + }; + + const handleDrawer = (isOpen) => { + setDrawerOpen(isOpen); + }; + + return ( + + {oidcClientRegistered ? + <> + + + + + {(data === undefined || data?.length == 0) ? + : ''} + + + + + + + Bootstrap Configuration + Action + + + + {(data === undefined || data?.length == 0) ? + No Records to show. : + data.map((row, index) => ()) + } + +
+
+
+ : + At least one OIDC client must be registered in Jans-TARP to add Cedarling configuration. + } +
+ ); +} \ No newline at end of file diff --git a/demos/jans-tarp/src/options/homePage.tsx b/demos/jans-tarp/src/options/homePage.tsx new file mode 100644 index 00000000000..804908777ee --- /dev/null +++ b/demos/jans-tarp/src/options/homePage.tsx @@ -0,0 +1,86 @@ +import * as React from 'react'; +import Container from '@mui/material/Container'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; +import Box from '@mui/material/Box'; +import Password from '@mui/icons-material/Password'; +import LockPerson from '@mui/icons-material/LockPerson'; +import OIDCClients from './OIDCClients'; +import Cedarling from './cedarling'; +import Grid from '@mui/material/Grid'; +import Paper from '@mui/material/Paper'; +import { styled } from '@mui/material/styles'; + +interface TabPanelProps { + children?: React.ReactNode; + index: number; + value: number; +} + +function CustomTabPanel(props: TabPanelProps) { + const { children, value, index, ...other } = props; + + return ( + + ); +} + +function a11yProps(index: number) { + return { + id: `simple-tab-${index}`, + 'aria-controls': `simple-tabpanel-${index}`, + }; +} + +export default function HomePage({ data, notifyOnDataChange }) { + + const [value, setValue] = React.useState(0); + + const handleChange = (event: React.SyntheticEvent, newValue: number) => { + setValue(newValue); + }; + + return ( + + + + } /> + } /> + + + + + + + + + + + + + + ); +} \ No newline at end of file diff --git a/demos/jans-tarp/src/options/oidcClients.tsx b/demos/jans-tarp/src/options/oidcClients.tsx index d18ea9a298c..af33fea6a05 100644 --- a/demos/jans-tarp/src/options/oidcClients.tsx +++ b/demos/jans-tarp/src/options/oidcClients.tsx @@ -84,7 +84,6 @@ function Row(props: { row: ReturnType, notifyOnDataChange }) let clientArr = [] if (!!result.oidcClients) { clientArr = result.oidcClients; - chrome.storage.local.set({ oidcClients: clientArr.filter(obj => obj.clientId !== row.clientId) }); } }); @@ -93,7 +92,7 @@ function Row(props: { row: ReturnType, notifyOnDataChange }) return ( - + *': { borderBottom: 'unset' } }}> diff --git a/demos/jans-tarp/src/options/options.tsx b/demos/jans-tarp/src/options/options.tsx index cc76c137fc2..ff0e2288658 100644 --- a/demos/jans-tarp/src/options/options.tsx +++ b/demos/jans-tarp/src/options/options.tsx @@ -1,8 +1,9 @@ import React, { useState, useEffect } from 'react' import './options.css' import Header from './header' -import OIDCClients from './oidcClients' +import HomePage from './homePage' import UserDetails from './userDetails' +import { ILooseObject } from './ILooseObject' const Options = () => { @@ -15,17 +16,26 @@ const Options = () => { if (!isEmpty(oidcClientResults) && Object.keys(oidcClientResults).length !== 0) { - chrome.storage.local.get(["loginDetails"], (loginDetailsResult) => { + chrome.storage.local.get(["loginDetails"], async (loginDetailsResult) => { if (!isEmpty(loginDetailsResult) && Object.keys(loginDetailsResult).length !== 0) { setOptionType('loginPage'); setdata(loginDetailsResult); } else { - setOptionType('oidcClientPage'); - setdata(oidcClientResults); + let collectedData = {}; + setOptionType('homePage'); + collectedData = { ...data, ...oidcClientResults }; + + let cedarlingConfig: ILooseObject = await new Promise((resolve, reject) => { chrome.storage.local.get(["cedarlingConfig"], (result) => { resolve(result); }) }); + + if (!isEmpty(cedarlingConfig) && Object.keys(cedarlingConfig).length !== 0) { + collectedData = { ...collectedData, ...cedarlingConfig }; + } + + setdata(collectedData); } }); } else { - setOptionType('oidcClientPage'); + setOptionType('homePage'); setdata({}); } setDataChanged(false); @@ -42,9 +52,9 @@ const Options = () => { function renderPage({ optionType, data }) { switch (optionType) { - case 'oidcClientPage': - return case 'loginPage': diff --git a/demos/jans-tarp/src/options/registerClient.tsx b/demos/jans-tarp/src/options/registerClient.tsx index 79a4770e51b..d908e821a2b 100644 --- a/demos/jans-tarp/src/options/registerClient.tsx +++ b/demos/jans-tarp/src/options/registerClient.tsx @@ -54,7 +54,7 @@ export default function RegisterClient({ isOpen, handleDialog }) { }; const validateIssuer = async (e) => { - + setIssuerError(''); let issuer = e.target.value; if (issuer.length === 0) { @@ -221,7 +221,7 @@ export default function RegisterClient({ isOpen, handleDialog }) { event.preventDefault(); }, }} - className="form-container" + className="form-container" > Register OIDC Client {loading ? ( diff --git a/demos/jans-tarp/src/options/userDetails.tsx b/demos/jans-tarp/src/options/userDetails.tsx index 3aea22ce430..351f4f842ef 100644 --- a/demos/jans-tarp/src/options/userDetails.tsx +++ b/demos/jans-tarp/src/options/userDetails.tsx @@ -3,12 +3,113 @@ import { v4 as uuidv4 } from 'uuid'; import './options.css' import './alerts.css'; import { WindmillSpinner } from 'react-spinner-overlay' +import { JsonEditor } from 'json-edit-react' +import TextField from '@mui/material/TextField'; +import InputLabel from '@mui/material/InputLabel'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Checkbox from '@mui/material/Checkbox'; +import Button from '@mui/material/Button'; +import Accordion from '@mui/material/Accordion'; +import AccordionSummary from '@mui/material/AccordionSummary'; +import AccordionDetails from '@mui/material/AccordionDetails'; +import Typography from '@mui/material/Typography'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -const UserDetails = ({data, notifyOnDataChange}) => { +import __wbg_init, { init, Cedarling, AuthorizeResult } from "wasm"; + +const UserDetails = ({ data, notifyOnDataChange }) => { const [loading, setLoading] = useState(false); const [showMoreIdToken, setShowMoreIdToken] = useState(false); const [showMoreAT, setShowMoreAT] = useState(false); const [showMoreUI, setShowMoreUI] = useState(false); + const [context, setContext] = React.useState({}); + const [action, setAction] = React.useState(""); + const [accessToken, setAccessToken] = React.useState(false); + const [userInfoToken, setUserInfoToken] = React.useState(false); + const [idToken, setIdToken] = React.useState(false); + const [resource, setResource] = React.useState({}); + const [cedarlingBootstrapPresent, setCedarlingBootstrapPresent] = React.useState(false); + const [errorMessage, setErrorMessage] = React.useState("") + const [authzResult, setAuthzResult] = React.useState("") + const [authzLogs, setAuthzLogs] = React.useState("") + + + React.useEffect(() => { + chrome.storage.local.get(["authzRequest"], (authzRequest) => { + if (!isEmpty(authzRequest) && Object.keys(authzRequest).length !== 0) { + setContext(authzRequest.authzRequest.context); + setAction(authzRequest.authzRequest.action); + setResource(authzRequest.authzRequest.resource); + } + }); + chrome.storage.local.get(["cedarlingConfig"], async (cedarlingConfig) => { + setCedarlingBootstrapPresent(false); + if (Object.keys(cedarlingConfig).length !== 0 && !isEmpty(cedarlingConfig?.cedarlingConfig)) { + setCedarlingBootstrapPresent(true); + } + }); + }, []) + + const triggerCedarlingAuthzRequest = async () => { + setAuthzResult(""); + setAuthzLogs(""); + let reqObj = await createCedarlingAuthzRequestObj(); + chrome.storage.local.get(["cedarlingConfig"], async (cedarlingConfig) => { + let instance: Cedarling; + try { + if (Object.keys(cedarlingConfig).length !== 0) { + await __wbg_init(); + instance = await init(!isEmpty(cedarlingConfig?.cedarlingConfig) ? cedarlingConfig?.cedarlingConfig[0] : undefined); + let result: AuthorizeResult = await instance.authorize(reqObj); + setAuthzResult(result.json_string()) + console.log("result:", result); + let logs = await instance.pop_logs(); + if (logs.length != 0) { + let pretty_logs = logs.map(log => JSON.stringify(log, null, 2)); + setAuthzLogs(pretty_logs.toString()); + } + + } + } catch (err) { + setAuthzResult(err); + console.log("err:", err); + let logs = await instance.pop_logs(); + if (logs.length != 0) { + let pretty_logs = logs.map(log => JSON.stringify(log, null, 2)); + setAuthzLogs(pretty_logs.toString()); + } + } + + }); + + } + + const createCedarlingAuthzRequestObj = async () => { + let reqObj = { tokens: { access_token: '', id_token: '', userinfo_token: '' }, action: "", resource: {}, context: {} }; + if (accessToken) { + reqObj.tokens.access_token = (!!data ? data?.access_token : ''); + } + + if (idToken) { + reqObj.tokens.id_token = (!!data ? data?.id_token : ''); + } + + if (userInfoToken) { + reqObj.tokens.userinfo_token = (!!data ? data?.userDetails : ''); + } + + //reqObj.tokens.access_token = 'eyJraWQiOiJjb25uZWN0X2Y5YTAwN2EyLTZkMGItNDkyYS05MGNkLWYwYzliMWMyYjVkYl9zaWdfcnMyNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJxenhuMVNjcmI5bFd0R3hWZWRNQ2t5LVFsX0lMc3BaYVFBNmZ5dVlrdHcwIiwiY29kZSI6IjNlMmEyMDEyLTA5OWMtNDY0Zi04OTBiLTQ0ODE2MGMyYWIyNSIsImlzcyI6Imh0dHBzOi8vYWNjb3VudC5nbHV1Lm9yZyIsInRva2VuX3R5cGUiOiJCZWFyZXIiLCJjbGllbnRfaWQiOiJkN2Y3MWJlYS1jMzhkLTRjYWYtYTFiYS1lNDNjNzRhMTFhNjIiLCJhdWQiOiJkN2Y3MWJlYS1jMzhkLTRjYWYtYTFiYS1lNDNjNzRhMTFhNjIiLCJhY3IiOiJzaW1wbGVfcGFzc3dvcmRfYXV0aCIsIng1dCNTMjU2IjoiIiwibmJmIjoxNzMxOTUzMDMwLCJzY29wZSI6WyJyb2xlIiwib3BlbmlkIiwicHJvZmlsZSIsImVtYWlsIl0sImF1dGhfdGltZSI6MTczMTk1MzAyNywiZXhwIjoxNzMyMTIxNDYwLCJpYXQiOjE3MzE5NTMwMzAsImp0aSI6InVaVWgxaERVUW82UEZrQlBud3BHemciLCJ1c2VybmFtZSI6IkRlZmF1bHQgQWRtaW4gVXNlciIsInN0YXR1cyI6eyJzdGF0dXNfbGlzdCI6eyJpZHgiOjMwNiwidXJpIjoiaHR0cHM6Ly9qYW5zLnRlc3QvamFucy1hdXRoL3Jlc3R2MS9zdGF0dXNfbGlzdCJ9fX0.Pt-Y7F-hfde_WP7ZYwyvvSS11rKYQWGZXTzjH_aJKC5VPxzOjAXqI3Igr6gJLsP1aOd9WJvOPchflZYArctopXMWClbX_TxpmADqyCMsz78r4P450TaMKj-WKEa9cL5KtgnFa0fmhZ1ZWolkDTQ_M00Xr4EIvv4zf-92Wu5fOrdjmsIGFot0jt-12WxQlJFfs5qVZ9P-cDjxvQSrO1wbyKfHQ_txkl1GDATXsw5SIpC5wct92vjAVm5CJNuv_PE8dHAY-KfPTxOuDYBuWI5uA2Yjd1WUFyicbJgcmYzUSVt03xZ0kQX9dxKExwU2YnpDorfwebaAPO7G114Bkw208g'; + //reqObj.tokens.id_token = 'eyJraWQiOiJjb25uZWN0X2Y5YTAwN2EyLTZkMGItNDkyYS05MGNkLWYwYzliMWMyYjVkYl9zaWdfcnMyNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdF9oYXNoIjoiYnhhQ1QwWlFYYnY0c2J6alNEck5pQSIsInN1YiI6InF6eG4xU2NyYjlsV3RHeFZlZE1Da3ktUWxfSUxzcFphUUE2Znl1WWt0dzAiLCJhbXIiOltdLCJpc3MiOiJodHRwczovL2FjY291bnQuZ2x1dS5vcmciLCJub25jZSI6IjI1YjJiMTZiLTMyYTItNDJkNi04YThlLWU1ZmE5YWI4ODhjMCIsInNpZCI6IjZkNDQzNzM0LWI3YTItNGVkOC05ZDNhLTE2MDZkMmY5OTI0NCIsImphbnNPcGVuSURDb25uZWN0VmVyc2lvbiI6Im9wZW5pZGNvbm5lY3QtMS4wIiwiYXVkIjoiZDdmNzFiZWEtYzM4ZC00Y2FmLWExYmEtZTQzYzc0YTExYTYyIiwiYWNyIjoic2ltcGxlX3Bhc3N3b3JkX2F1dGgiLCJjX2hhc2giOiJWOGg0c085Tnp1TEthd1BPLTNETkxBIiwibmJmIjoxNzMxOTUzMDMwLCJhdXRoX3RpbWUiOjE3MzE5NTMwMjcsImV4cCI6MTczMTk1NjYzMCwiZ3JhbnQiOiJhdXRob3JpemF0aW9uX2NvZGUiLCJpYXQiOjE3MzE5NTMwMzAsImp0aSI6ImlqTFpPMW9vUnlXcmdJbjdjSWROeUEiLCJzdGF0dXMiOnsic3RhdHVzX2xpc3QiOnsiaWR4IjozMDcsInVyaSI6Imh0dHBzOi8vamFucy50ZXN0L2phbnMtYXV0aC9yZXN0djEvc3RhdHVzX2xpc3QifX19.Nw7MRaJ5LtDak_LdEjrICgVOxDwd1p1I8WxD7IYw0_mKlIJ-J_78rGPski9p3L5ZNCpXiHtVbnhc4lJdmbh-y6mrD3_EY_AmjK50xpuf6YuUuNVtFENCSkj_irPLkIDG65HeZherWsvH0hUn4FVGv8Sw9fjny9Doi-HGHnKg9Qvphqre1U8hCphCVLQlzXAXmBkbPOC8tDwId5yigBKXP50cdqDcT-bjXf9leIdGgq0jxb57kYaFSElprLN9nUygM4RNCn9mtmo1l4IsdTlvvUb3OMAMQkRLfMkiKBjjeSF3819mYRLb3AUBaFH16ZdHFBzTSB6oA22TYpUqOLihMg'; + //reqObj.tokens.userinfo_token = 'eyJraWQiOiJjb25uZWN0X2Y5YTAwN2EyLTZkMGItNDkyYS05MGNkLWYwYzliMWMyYjVkYl9zaWdfcnMyNTYiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJxenhuMVNjcmI5bFd0R3hWZWRNQ2t5LVFsX0lMc3BaYVFBNmZ5dVlrdHcwIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInJvbGUiOlsiQ2FzYUFkbWluIl0sImlzcyI6Imh0dHBzOi8vYWNjb3VudC5nbHV1Lm9yZyIsImdpdmVuX25hbWUiOiJBZG1pbiIsIm1pZGRsZV9uYW1lIjoiQWRtaW4iLCJpbnVtIjoiYTZhNzAzMDEtYWY0OS00OTAxLTk2ODctMGJjZGNmNGUzNGZhIiwiY2xpZW50X2lkIjoiZDdmNzFiZWEtYzM4ZC00Y2FmLWExYmEtZTQzYzc0YTExYTYyIiwiYXVkIjoiZDdmNzFiZWEtYzM4ZC00Y2FmLWExYmEtZTQzYzc0YTExYTYyIiwidXBkYXRlZF9hdCI6MTczMTY5ODEzNSwibmFtZSI6IkRlZmF1bHQgQWRtaW4gVXNlciIsIm5pY2tuYW1lIjoiQWRtaW4iLCJmYW1pbHlfbmFtZSI6IlVzZXIiLCJqdGkiOiJPSW4zZzFTUFNEU0tBWUR6RU5Wb3VnIiwiZW1haWwiOiJhZG1pbkBqYW5zLnRlc3QiLCJqYW5zQWRtaW5VSVJvbGUiOlsiYXBpLWFkbWluIl19.CIahQtRpoTkIQx8KttLPIKH7gvGG8OmYCMzz7wch6k792DVYQG1R7q3sS9Ema1rO5Fm_GgjOsR0yTTMKsyhHDLBwkDd3cnMLgsh2AwVFZvxtpafTlUAPfjvMAy9YTtkPcY6rNUhsYLSSOA83kt6pHdIv5nI-G6ybqgg-bLBRpwZDoOV0TulRhmuukdiuugTXHT6Bb-K3ZeYs8CwewztnxoFTSDghSzq7VZIraV8SLTBLx5_xswn9mefamyB2XNN3o6vXuMyf4BEbYSCuJ3pu6YtNgfyWwt9cF8PYe4PVLoXZuJKN-cy4qrtgy43QXPCg96jSQUJqgLb5ZL5_3udm2Q'; + + reqObj.action = action; + reqObj.context = context; + reqObj.resource = resource; + + chrome.storage.local.set({ authzRequest: reqObj }); + return reqObj; + } + async function logout() { setLoading(true); try { @@ -54,6 +155,10 @@ const UserDetails = ({data, notifyOnDataChange}) => { notifyOnDataChange("true"); } + function isEmpty(value) { + return (value == null || value.length === 0); + } + return (
@@ -64,25 +169,155 @@ const UserDetails = ({data, notifyOnDataChange}) => {
{data?.displayToken ? <> + + } + aria-controls="panel1-content" + id="panel1-header" + > + Access Token + + + +
+

{showMoreAT ? (!!data ? data?.access_token : '') : (!!data ? data?.access_token.substring(0, 250).concat(' ...') : '')}

+ setShowMoreAT(!showMoreAT)}>{showMoreAT ? "Show less" : "Show more"} +
+
+
+ + } + aria-controls="panel1-content" + id="panel1-header" + > + Id Token + + + +
+

{showMoreIdToken ? (!!data ? data?.id_token : '') : (!!data ? data?.id_token.substring(0, 250).concat(' ...') : '')}

+ setShowMoreIdToken(!showMoreIdToken)}>{showMoreIdToken ? "Show less" : "Show more"} +
+
+
+ + : ''} + + } + aria-controls="panel1-content" + id="panel1-header" + > + User Details + +
- Access Token -

{showMoreAT ? (!!data ? data?.access_token : '') : (!!data ? data?.access_token.substring(0, 250).concat(' ...') : '')}

- setShowMoreAT(!showMoreAT)}>{showMoreAT ? "Show less" : "Show more"} + User Details +

{showMoreUI ? (!!data ? data?.userDetails : '') : (!!data ? data?.userDetails.substring(0, 250).concat(' ...') : '')}

+ setShowMoreUI(!showMoreUI)}>{showMoreUI ? "Show less" : "Show more"}
-
- Id Token -

{showMoreIdToken ? (!!data ? data?.id_token : '') : (!!data ? data?.id_token.substring(0, 250).concat(' ...') : '')}

- setShowMoreIdToken(!showMoreIdToken)}>{showMoreIdToken ? "Show less" : "Show more"} + + + {cedarlingBootstrapPresent ? + + } + aria-controls="panel1-content" + id="panel1-header" + > + Cedarling Authz Request Form + + +
+ Principal + setAccessToken(!accessToken)} />} label="Access Token" /> + setUserInfoToken(!userInfoToken)} />} label="Userinfo Token" /> + setIdToken(!idToken)} />} label="Id Token" /> + + { + setAction(e.target.value); + }} + /> + Resource + + Context + +
+
- - : ''} -
- User Details -

{showMoreUI ? (!!data ? data?.userDetails : '') : (!!data ? data?.userDetails.substring(0, 250).concat(' ...') : '')}

- setShowMoreUI(!showMoreUI)}>{showMoreUI ? "Show less" : "Show more"} -
+
+
: ''} + {!!authzResult ? + + } + aria-controls="panel1-content" + id="panel1-header" + > + Cedarling Authz Result + + + + + + : ''} + {!!authzLogs ? + + } + aria-controls="panel2-content" + id="panel2-header" + > + Cedarling Authz Logs + + + + + + : ''} +
- +
) }; diff --git a/demos/jans-tarp/src/static/chrome/manifest.json b/demos/jans-tarp/src/static/chrome/manifest.json index 2cb920ca3c3..ce82d4a8486 100644 --- a/demos/jans-tarp/src/static/chrome/manifest.json +++ b/demos/jans-tarp/src/static/chrome/manifest.json @@ -24,5 +24,8 @@ "*://*/*" ], "options_page": "options.html", - "incognito": "split" + "incognito": "split", + "content_security_policy": { + "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';" + } } \ No newline at end of file diff --git a/demos/jans-tarp/src/static/firefox/manifest.json b/demos/jans-tarp/src/static/firefox/manifest.json index e30988c67a1..0ace852b544 100644 --- a/demos/jans-tarp/src/static/firefox/manifest.json +++ b/demos/jans-tarp/src/static/firefox/manifest.json @@ -27,5 +27,8 @@ "gecko": { "id": "jans-tarp@gluu.org" } + }, + "content_security_policy": { + "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';" } } \ No newline at end of file diff --git a/docs/cedarling/cedarling-authz.md b/docs/cedarling/cedarling-authz.md index 89f3a5c7619..70445076942 100644 --- a/docs/cedarling/cedarling-authz.md +++ b/docs/cedarling/cedarling-authz.md @@ -83,14 +83,14 @@ decision_result = await cedarling(input) ## Automatically Adding Entity References to the Context -Cedarling simplifies context creation by automatically including certain entities. This means you don't need to manually pass their references when using them in your policies. The following entities are automatically added to the context, along with their naming conventions in `lower_snake_case` format: - -- **Workload Entity**: `workload` -- **User Entity**: `user` -- **Resource Entity**: `resource` -- **Access Token Entity**: `access_token` -- **ID Token Entity**: `id_token` -- **Userinfo Token Entity**: `userinfo_token` +Cedarling simplifies context creation by automatically including certain entities. This means you don't need to manually pass their references when using them in your policies. The following entities are automatically added to the context. + +- Workload Entity +- User Entity +- Resource Entity +- Access Token Entity +- ID Token Entity +- Userinfo Token Entity ### Example Policy diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeAction.java b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeAction.java index 35384a724db..9433e274ce3 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeAction.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/authorize/ws/rs/AuthorizeAction.java @@ -86,6 +86,7 @@ @Named public class AuthorizeAction { + public static final String UNKNOWN = "Unknown"; @Inject private Logger log; @@ -985,7 +986,7 @@ public String getClientDisplayName() { log.trace("client {}", clientId); if (StringUtils.isBlank(clientId)) { - return "Unknown"; + return UNKNOWN; } final Client client = clientService.getClient(clientId); @@ -994,15 +995,19 @@ public String getClientDisplayName() { public String getClientDisplayName(final Client client) { log.trace("client {}", client); - + if (client == null) { - getClientDisplayName(); + return UNKNOWN; } return getCheckedClientDisplayName(client); } private String getCheckedClientDisplayName(final Client client) { + if (client == null) { + return UNKNOWN; + } + if (StringUtils.isNotBlank(client.getClientName())) { return client.getClientName(); } @@ -1011,7 +1016,7 @@ private String getCheckedClientDisplayName(final Client client) { return client.getClientId(); } - return "Unknown"; + return UNKNOWN; } public String getAuthReqId() { diff --git a/jans-auth-server/server/src/main/webapp/WEB-INF/incl/layout/authorize-extended-template.xhtml b/jans-auth-server/server/src/main/webapp/WEB-INF/incl/layout/authorize-extended-template.xhtml index 37ca0a66712..57a4ac12f27 100644 --- a/jans-auth-server/server/src/main/webapp/WEB-INF/incl/layout/authorize-extended-template.xhtml +++ b/jans-auth-server/server/src/main/webapp/WEB-INF/incl/layout/authorize-extended-template.xhtml @@ -56,8 +56,8 @@
  • - +
    @@ -132,10 +132,6 @@
  • -
    - #{msgs['common.copyright']} Gluu. - #{msgs['common.allRightsReserved']} -
    diff --git a/jans-auth-server/server/src/main/webapp/authz/authorize.xhtml b/jans-auth-server/server/src/main/webapp/authz/authorize.xhtml index a3dfe21e879..c17d933890d 100644 --- a/jans-auth-server/server/src/main/webapp/authz/authorize.xhtml +++ b/jans-auth-server/server/src/main/webapp/authz/authorize.xhtml @@ -7,6 +7,7 @@ xmlns:h="http://xmlns.jcp.org/jsf/html" template="/WEB-INF/incl/layout/authorize-extended-template.xhtml"> + diff --git a/jans-auth-server/server/src/main/webapp/authz/transaction.xhtml b/jans-auth-server/server/src/main/webapp/authz/transaction.xhtml index 47c99994584..6fb7f4fd943 100644 --- a/jans-auth-server/server/src/main/webapp/authz/transaction.xhtml +++ b/jans-auth-server/server/src/main/webapp/authz/transaction.xhtml @@ -7,6 +7,7 @@ xmlns:h="http://xmlns.jcp.org/jsf/html" template="/WEB-INF/incl/layout/authorize-extended-template.xhtml"> + diff --git a/jans-auth-server/server/src/test/java/io/jans/as/server/comp/db/UserJansExtUidAttributeTest.java b/jans-auth-server/server/src/test/java/io/jans/as/server/comp/db/UserJansExtUidAttributeTest.java index aeb3cdb8d41..800133aa225 100644 --- a/jans-auth-server/server/src/test/java/io/jans/as/server/comp/db/UserJansExtUidAttributeTest.java +++ b/jans-auth-server/server/src/test/java/io/jans/as/server/comp/db/UserJansExtUidAttributeTest.java @@ -10,6 +10,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import java.io.File; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Properties; @@ -86,7 +88,9 @@ public static void init() { @Produces @ApplicationScoped public StringEncrypter getStringEncrypter() throws EncryptionException { - FileConfiguration cryptoConfiguration = new FileConfiguration(".\\target\\conf\\salt"); + String saltFilePath = Paths.get(Paths.get("").toAbsolutePath().toString(), "target/conf/salt").toAbsolutePath().toString(); + FileConfiguration cryptoConfiguration = new FileConfiguration(saltFilePath); + String encodeSalt = cryptoConfiguration.getString("encodeSalt"); return StringEncrypter.instance(encodeSalt); diff --git a/jans-auth-server/server/src/test/java/io/jans/as/server/comp/db/UserServiceTest.java b/jans-auth-server/server/src/test/java/io/jans/as/server/comp/db/UserServiceTest.java index f437694ee64..8fd1c8a8d41 100644 --- a/jans-auth-server/server/src/test/java/io/jans/as/server/comp/db/UserServiceTest.java +++ b/jans-auth-server/server/src/test/java/io/jans/as/server/comp/db/UserServiceTest.java @@ -10,6 +10,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import java.nio.file.Paths; import java.util.Arrays; import java.util.Properties; @@ -87,7 +88,8 @@ public static void init() { @Produces @ApplicationScoped public StringEncrypter getStringEncrypter() throws EncryptionException { - FileConfiguration cryptoConfiguration = new FileConfiguration(".\\target\\conf\\salt"); + String saltFilePath = Paths.get(Paths.get("").toAbsolutePath().toString(), "target/conf/salt").toAbsolutePath().toString(); + FileConfiguration cryptoConfiguration = new FileConfiguration(saltFilePath); String encodeSalt = cryptoConfiguration.getString("encodeSalt"); return StringEncrypter.instance(encodeSalt); diff --git a/jans-cedarling/bindings/cedarling_python/PYTHON_TYPES.md b/jans-cedarling/bindings/cedarling_python/PYTHON_TYPES.md index f0875a6f06d..436c8f5d727 100644 --- a/jans-cedarling/bindings/cedarling_python/PYTHON_TYPES.md +++ b/jans-cedarling/bindings/cedarling_python/PYTHON_TYPES.md @@ -209,30 +209,14 @@ ___ Exception raised by authorize_errors ___ -# authorize_errors.CreateAccessTokenEntityError -Error encountered while creating access_token entity +# authorize_errors.BuildEntitiesError +Error encountered while building entities into context ___ # authorize_errors.CreateContextError Error encountered while validating context according to the schema ___ -# authorize_errors.CreateIdTokenEntityError -Error encountered while creating id token entities -___ - -# authorize_errors.CreateUserEntityError -Error encountered while creating User entity -___ - -# authorize_errors.CreateUserinfoTokenEntityError -Error encountered while creating Userinfo_token entity -___ - -# authorize_errors.CreateWorkloadEntityError -Error encountered while creating workload entity -___ - # authorize_errors.EntitiesError Error encountered while collecting all entities ___ @@ -245,14 +229,6 @@ ___ Error encountered while processing JWT token data ___ -# authorize_errors.ResourceEntityError -Error encountered while creating resource entity -___ - -# authorize_errors.RoleEntityError -Error encountered while creating role entity -___ - # authorize_errors.UserRequestValidationError Error encountered while creating cedar_policy::Request for user entity principal ___ diff --git a/jans-cedarling/bindings/cedarling_python/src/authorize/errors.rs b/jans-cedarling/bindings/cedarling_python/src/authorize/errors.rs index 89ad49ba9a9..feda288a4ad 100644 --- a/jans-cedarling/bindings/cedarling_python/src/authorize/errors.rs +++ b/jans-cedarling/bindings/cedarling_python/src/authorize/errors.rs @@ -30,54 +30,6 @@ create_exception!( "Error encountered while processing JWT token data" ); -create_exception!( - authorize_errors, - CreateIdTokenEntityError, - AuthorizeError, - "Error encountered while creating id token entities" -); - -create_exception!( - authorize_errors, - CreateUserinfoTokenEntityError, - AuthorizeError, - "Error encountered while creating Userinfo_token entity" -); -create_exception!( - authorize_errors, - CreateAccessTokenEntityError, - AuthorizeError, - "Error encountered while creating access_token entity" -); - -create_exception!( - authorize_errors, - CreateUserEntityError, - AuthorizeError, - "Error encountered while creating User entity" -); - -create_exception!( - authorize_errors, - CreateWorkloadEntityError, - AuthorizeError, - "Error encountered while creating workload entity" -); - -create_exception!( - authorize_errors, - ResourceEntityError, - AuthorizeError, - "Error encountered while creating resource entity" -); - -create_exception!( - authorize_errors, - RoleEntityError, - AuthorizeError, - "Error encountered while creating role entity" -); - create_exception!( authorize_errors, ActionError, @@ -120,6 +72,13 @@ create_exception!( "Error encountered while parsing all entities to json for logging" ); +create_exception!( + authorize_errors, + BuildEntitiesError, + AuthorizeError, + "Error encountered while building entities into context" +); + create_exception!( authorize_errors, AddEntitiesIntoContextError, @@ -166,17 +125,11 @@ macro_rules! errors_functions { // For each possible case of `AuthorizeError`, we have created a corresponding Python exception that inherits from `cedarling::AuthorizeError`. errors_functions! { ProcessTokens => ProcessTokens, - CreateIdTokenEntity => CreateIdTokenEntityError, - CreateUserinfoTokenEntity => CreateUserinfoTokenEntityError, - CreateAccessTokenEntity => CreateAccessTokenEntityError, - CreateUserEntity => CreateUserEntityError, - CreateWorkloadEntity => CreateWorkloadEntityError, - ResourceEntity => ResourceEntityError, - RoleEntity => RoleEntityError, Action => ActionError, CreateContext => CreateContextError, WorkloadRequestValidation => WorkloadRequestValidationError, UserRequestValidation => UserRequestValidationError, + BuildEntity => BuildEntitiesError, BuildContext => AddEntitiesIntoContextError, Entities => EntitiesError, EntitiesToJson => EntitiesToJsonError diff --git a/jans-cedarling/bindings/cedarling_python/tests/test_authorize.py b/jans-cedarling/bindings/cedarling_python/tests/test_authorize.py index 92926ccfccd..b1e8971f1ce 100644 --- a/jans-cedarling/bindings/cedarling_python/tests/test_authorize.py +++ b/jans-cedarling/bindings/cedarling_python/tests/test_authorize.py @@ -186,8 +186,8 @@ def test_resource_entity_error(): ''' try: raise_authorize_error(load_bootstrap_config()) - except authorize_errors.ResourceEntityError as e: - assert str(e) == "could not create resource entity: could not get attribute value from payload: type mismatch for key 'org_id'. expected: 'String', but found: 'number'" + except authorize_errors.BuildEntitiesError as e: + assert str(e) == "failed to build resource entity: failed to build `org_id` attribute: failed to build restricted expression: type mismatch for key 'org_id'. expected: 'string', but found: 'number'" def test_authorize_error(): @@ -199,4 +199,4 @@ def test_authorize_error(): try: raise_authorize_error(load_bootstrap_config()) except authorize_errors.AuthorizeError as e: - assert str(e) == "could not create resource entity: could not get attribute value from payload: type mismatch for key 'org_id'. expected: 'String', but found: 'number'" + assert str(e) == "failed to build resource entity: failed to build `org_id` attribute: failed to build restricted expression: type mismatch for key 'org_id'. expected: 'string', but found: 'number'" diff --git a/jans-cedarling/cedarling/Cargo.toml b/jans-cedarling/cedarling/Cargo.toml index 689991b1919..2e924cad531 100644 --- a/jans-cedarling/cedarling/Cargo.toml +++ b/jans-cedarling/cedarling/Cargo.toml @@ -16,7 +16,7 @@ serde_yml = "0.0.12" thiserror = { workspace = true } sparkv = { workspace = true } uuid7 = { version = "1.1.0", features = ["serde", "uuid"] } -cedar-policy = "4.2" +cedar-policy = { version = "4.2", features = ["partial-eval"] } base64 = "0.22.1" url = "2.5.2" lazy_static = "1.5.0" diff --git a/jans-cedarling/cedarling/src/authz/build_ctx.rs b/jans-cedarling/cedarling/src/authz/build_ctx.rs new file mode 100644 index 00000000000..265a4f7e7aa --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/build_ctx.rs @@ -0,0 +1,191 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use std::collections::HashMap; + +use super::{AuthorizeEntitiesData, AuthzConfig}; +use crate::common::cedar_schema::cedar_json::attribute::Attribute; +use crate::common::cedar_schema::cedar_json::CedarSchemaJson; +use crate::common::cedar_schema::CEDAR_NAMESPACE_SEPARATOR; +use cedar_policy::ContextJsonError; +use serde_json::{json, map::Entry, Value}; + +/// Constructs the authorization context by adding the built entities from the tokens +pub fn build_context( + config: &AuthzConfig, + request_context: Value, + entities_data: &AuthorizeEntitiesData, + schema: &cedar_policy::Schema, + action: &cedar_policy::EntityUid, +) -> Result { + let namespace = config.policy_store.namespace(); + let action_name = &action.id().escaped(); + let json_schema = &config.policy_store.schema.json; + let action_schema = json_schema + .get_action(namespace, action_name) + .ok_or(BuildContextError::UnknownAction(action_name.to_string()))?; + + // Get the entities required for the context + let mut ctx_entity_refs = json!({}); + let type_ids = entities_data.type_ids(); + if let Some(ctx) = action_schema.applies_to.context.as_ref() { + match ctx { + Attribute::Record { attrs, .. } => { + for (key, attr) in attrs.iter() { + if let Some(entity_ref) = + build_entity_refs_from_attr(namespace, attr, &type_ids, json_schema)? + { + ctx_entity_refs[key] = entity_ref; + } + } + }, + Attribute::EntityOrCommon { name, .. } => { + // TODO: handle potential namespace collisions when Cedarling starts + // supporting multiple namespaces + if let Some((_namespace, attr)) = json_schema.get_common_type(name) { + match attr { + Attribute::Record { attrs, .. } => { + for (key, attr) in attrs.iter() { + if let Some(entity_ref) = build_entity_refs_from_attr( + namespace, + attr, + &type_ids, + json_schema, + )? { + ctx_entity_refs[key] = entity_ref; + } + } + }, + attr => { + return Err(BuildContextError::InvalidKind( + attr.kind_str().to_string(), + "record".to_string(), + )) + }, + } + } + }, + attr => { + return Err(BuildContextError::InvalidKind( + attr.kind_str().to_string(), + "record or common".to_string(), + )) + }, + } + } + + let context = merge_json_values(request_context, ctx_entity_refs)?; + let context: cedar_policy::Context = + cedar_policy::Context::from_json_value(context, Some((schema, action)))?; + + Ok(context) +} + +/// Builds the JSON entity references from a given attribute. +/// +/// Returns `Ok(None)` if the attr is not an Entity Reference +fn build_entity_refs_from_attr( + namespace: &str, + attr: &Attribute, + type_ids: &HashMap, + schema: &CedarSchemaJson, +) -> Result, BuildContextError> { + match attr { + Attribute::Entity { name, .. } => map_entity_id(namespace, name, type_ids), + Attribute::EntityOrCommon { name, .. } => { + if let Some((entity_namespace, _)) = schema.get_entity_from_base_name(name) { + if namespace == entity_namespace { + return map_entity_id(namespace, name, type_ids); + } + } + Ok(None) + }, + _ => Ok(None), + } +} + +/// Maps a known entity ID to the entity reference +fn map_entity_id( + namespace: &str, + name: &str, + type_ids: &HashMap, +) -> Result, BuildContextError> { + if let Some(type_id) = type_ids.get(name).as_ref() { + let name = join_namespace(namespace, name); + Ok(Some(json!({"type": name, "id": type_id}))) + } else { + Err(BuildContextError::MissingEntityId(name.to_string())) + } +} + +/// Joins the given type name with the given namespace if it's not an empty string. +fn join_namespace(namespace: &str, type_name: &str) -> String { + if namespace.is_empty() { + return type_name.to_string(); + } + [namespace, type_name].join(CEDAR_NAMESPACE_SEPARATOR) +} + +#[derive(Debug, thiserror::Error)] +pub enum BuildContextError { + /// Error encountered while validating context according to the schema + #[error("failed to merge JSON objects due to conflicting keys: {0}")] + KeyConflict(String), + /// Error encountered while deserializing the Context from JSON + #[error(transparent)] + DeserializeFromJson(#[from] ContextJsonError), + /// Error encountered if the action being used as the reference to build the Context + /// is not in the schema + #[error("failed to find the action `{0}` in the schema")] + UnknownAction(String), + /// Error encountered while building entity references in the Context + #[error("failed to build entity reference for `{0}` since an entity id was not provided")] + MissingEntityId(String), + #[error("invalid action context type: {0}. expected: {1}")] + InvalidKind(String, String), +} + +pub fn merge_json_values(mut base: Value, other: Value) -> Result { + if let (Some(base_map), Some(additional_map)) = (base.as_object_mut(), other.as_object()) { + for (key, value) in additional_map { + if let Entry::Vacant(entry) = base_map.entry(key) { + entry.insert(value.clone()); + } else { + return Err(BuildContextError::KeyConflict(key.clone())); + } + } + } + Ok(base) +} + +#[cfg(test)] +mod test { + use super::*; + use serde_json::json; + + #[test] + fn can_merge_json_objects() { + let obj1 = json!({ "a": 1, "b": 2 }); + let obj2 = json!({ "c": 3, "d": 4 }); + let expected = json!({"a": 1, "b": 2, "c": 3, "d": 4}); + + let result = merge_json_values(obj1, obj2).expect("Should merge JSON objects"); + + assert_eq!(result, expected); + } + + #[test] + fn errors_on_same_keys() { + // Test for only two objects + let obj1 = json!({ "a": 1, "b": 2 }); + let obj2 = json!({ "b": 3, "c": 4 }); + let result = merge_json_values(obj1, obj2); + + assert!( + matches!(result, Err(BuildContextError::KeyConflict(key)) if key.as_str() == "b"), + "Expected an error due to conflicting keys" + ); + } +} diff --git a/jans-cedarling/cedarling/src/authz/entities/create.rs b/jans-cedarling/cedarling/src/authz/entities/create.rs deleted file mode 100644 index 517cae03e69..00000000000 --- a/jans-cedarling/cedarling/src/authz/entities/create.rs +++ /dev/null @@ -1,440 +0,0 @@ -// This software is available under the Apache-2.0 license. -// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. -// -// Copyright (c) 2024, Gluu, Inc. - -use std::collections::{HashMap, HashSet}; -use std::str::FromStr; - -use cedar_policy::{EntityId, EntityTypeName, EntityUid, RestrictedExpression}; - -use super::trait_as_expression::AsExpression; -use crate::common::cedar_schema::CedarSchemaJson; -use crate::common::cedar_schema::cedar_json::{ - CedarSchemaEntityShape, CedarSchemaRecord, CedarType, GetCedarTypeError, SchemaDefinedType, -}; -use crate::common::policy_store::ClaimMappings; -use crate::jwt::{Token, TokenClaim, TokenClaimTypeError, TokenClaims}; - -pub const CEDAR_POLICY_SEPARATOR: &str = "::"; - -/// Meta information about an entity type. -/// Is used to store in `static` variable. -#[derive(Debug)] -pub(crate) struct EntityMetadata<'a> { - pub entity_type: EntityParsedTypeName<'a>, - pub entity_id_data_key: &'a str, -} - -impl<'a> EntityMetadata<'a> { - /// create new instance of EntityMetadata. - pub fn new(entity_type: EntityParsedTypeName<'a>, entity_id_data_key: &'a str) -> Self { - Self { - entity_type, - entity_id_data_key, - } - } - - /// Create entity from token data. - // we also can create entity using the ['create_entity'] function. - pub fn create_entity( - &'a self, - schema: &'a CedarSchemaJson, - token: &Token, - parents: HashSet, - claim_mapping: &ClaimMappings, - ) -> Result { - let entity_uid = build_entity_uid( - self.entity_type.full_type_name().as_str(), - token - .get_claim(self.entity_id_data_key) - .ok_or(CreateCedarEntityError::MissingClaim( - self.entity_id_data_key.to_string(), - ))? - .as_str()?, - )?; - - create_entity( - entity_uid, - &self.entity_type, - schema, - token.claims(), - parents, - claim_mapping, - ) - } -} - -/// build [`EntityUid`] based on input parameters -pub(crate) fn build_entity_uid( - entity_type: &str, - entity_id: &str, -) -> Result { - let entity_uid = EntityUid::from_type_name_and_id( - EntityTypeName::from_str(entity_type) - .map_err(|err| CreateCedarEntityError::EntityTypeName(entity_type.to_string(), err))?, - EntityId::new(entity_id), - ); - - Ok(entity_uid) -} - -/// Parsed result of entity type name and namespace. -/// Analog to the internal cedar_policy type `InternalName` -#[derive(Debug)] -pub(crate) struct EntityParsedTypeName<'a> { - pub type_name: &'a str, - pub namespace: &'a str, -} -impl<'a> EntityParsedTypeName<'a> { - pub fn new(typename: &'a str, namespace: &'a str) -> Self { - EntityParsedTypeName { - type_name: typename, - namespace, - } - } - - pub fn full_type_name(&self) -> String { - if self.namespace.is_empty() { - self.type_name.to_string() - } else { - [self.namespace, self.type_name].join(CEDAR_POLICY_SEPARATOR) - } - } -} - -/// Parse entity type name and namespace from entity type string. -/// return (typename, namespace) -pub fn parse_namespace_and_typename(raw_entity_type: &str) -> (&str, String) { - let mut raw_path: Vec<&str> = raw_entity_type.split(CEDAR_POLICY_SEPARATOR).collect(); - let typename = raw_path.pop().unwrap_or_default(); - let namespace = raw_path.join(CEDAR_POLICY_SEPARATOR); - (typename, namespace) -} - -/// fetch the schema record for a given entity type from the cedar schema json -fn fetch_schema_record<'a>( - entity_info: &EntityParsedTypeName, - schema: &'a CedarSchemaJson, -) -> Result<&'a CedarSchemaEntityShape, CreateCedarEntityError> { - let entity_shape = schema - .entity_schema(entity_info.namespace, entity_info.type_name) - .ok_or(CreateCedarEntityError::CouldNotFindEntity( - entity_info.type_name.to_string(), - ))?; - - if let Some(entity_record) = &entity_shape.shape { - if !entity_record.is_record() { - return Err(CreateCedarEntityError::NotRecord( - entity_info.type_name.to_string(), - )); - }; - } - - Ok(entity_shape) -} - -/// get mapping of the entity attributes -fn entity_meta_attributes( - schema_record: &CedarSchemaRecord, -) -> Result, GetCedarTypeError> { - schema_record - .attributes - .iter() - .map(|(attribute_name, attribute)| { - attribute - .get_type() - .map(|attr_type| EntityAttributeMetadata { - attribute_name: attribute_name.as_str(), - cedar_policy_type: attr_type, - is_required: attribute.is_required(), - }) - }) - .collect::, _>>() -} - -/// Build attributes for the entity -fn build_entity_attributes( - schema: &CedarSchemaJson, - parsed_typename: &EntityParsedTypeName, - tkn_data: &TokenClaims, - claim_mapping: &ClaimMappings, -) -> Result, CreateCedarEntityError> { - // fetch the schema entity shape from the json-schema. - let schema_shape = fetch_schema_record(parsed_typename, schema)?; - - if let Some(schema_record) = &schema_shape.shape { - let attr_vec = entity_meta_attributes(schema_record)? - .into_iter() - .filter_map(|attr: EntityAttributeMetadata| { - let attr_name = attr.attribute_name; - let cedar_exp_result = token_attribute_to_cedar_exp( - &attr, - tkn_data, - parsed_typename, - schema, - claim_mapping, - ); - match (cedar_exp_result, attr.is_required) { - (Ok(cedar_exp), _) => Some(Ok((attr_name.to_string(), cedar_exp))), - ( - Err(CreateCedarEntityError::MissingClaim(_)), - false, - // when the attribute is not required and not found in token data we skip it - ) => None, - (Err(err), _) => Some(Err(err)), - } - }) - .collect::, CreateCedarEntityError>>()?; - Ok(HashMap::from_iter(attr_vec)) - } else { - Ok(HashMap::new()) - } -} - -/// Create entity from token payload data. -pub fn create_entity( - entity_uid: EntityUid, - parsed_typename: &EntityParsedTypeName, - schema: &CedarSchemaJson, - tkn_data: &TokenClaims, - parents: HashSet, - claim_mapping: &ClaimMappings, -) -> Result { - let attrs = build_entity_attributes(schema, parsed_typename, tkn_data, claim_mapping)?; - - let entity_uid_string = entity_uid.to_string(); - cedar_policy::Entity::new(entity_uid, attrs, parents) - .map_err(|err| CreateCedarEntityError::CreateEntity(entity_uid_string, err)) -} - -/// Meta information about an attribute for cedar policy. -pub struct EntityAttributeMetadata<'a> { - // The name of the attribute in the cedar policy - // mapped one-to-one with the attribute in the token data. - pub attribute_name: &'a str, - // The type of the cedar policy attribute. - pub cedar_policy_type: CedarType, - // if this attribute is required - pub is_required: bool, -} - -/// Get the cedar policy expression value for a given type. -fn token_attribute_to_cedar_exp( - attribute_metadata: &EntityAttributeMetadata, - tkn_data: &TokenClaims, - entity_typename: &EntityParsedTypeName, - schema: &CedarSchemaJson, - claim_mapping: &ClaimMappings, -) -> Result { - let token_claim_key = attribute_metadata.attribute_name; - - let token_claim_value = - tkn_data - .get_claim(token_claim_key) - .ok_or(CreateCedarEntityError::MissingClaim( - token_claim_key.to_string(), - ))?; - - get_expression( - &attribute_metadata.cedar_policy_type, - &token_claim_value, - entity_typename, - schema, - claim_mapping, - ) -} - -/// Build [`RestrictedExpression`] based on input parameters. -fn get_expression( - cedar_type: &CedarType, - claim: &TokenClaim, - base_entity_typename: &EntityParsedTypeName, - schema: &CedarSchemaJson, - claim_mapping: &ClaimMappings, -) -> Result { - match cedar_type { - CedarType::String => Ok(claim.as_str()?.to_string().to_expression()), - CedarType::Long => Ok(claim.as_i64()?.to_expression()), - CedarType::Boolean => Ok(claim.as_bool()?.to_expression()), - CedarType::TypeName(cedar_typename) => { - match schema.find_type(cedar_typename, base_entity_typename.namespace) { - Some(SchemaDefinedType::Entity(_)) => { - get_entity_expression(cedar_typename, base_entity_typename, claim) - }, - Some(SchemaDefinedType::CommonType(record)) => { - let record_typename = - EntityParsedTypeName::new(cedar_typename, base_entity_typename.namespace); - - get_record_expression(record, &record_typename, claim, schema, claim_mapping) - .map_err(|err| { - CreateCedarEntityError::CreateRecord( - record_typename.full_type_name(), - Box::new(err), - ) - }) - }, - None => Err(CreateCedarEntityError::FindType( - EntityParsedTypeName::new(cedar_typename, base_entity_typename.namespace) - .full_type_name(), - )), - } - }, - CedarType::Set(cedar_type) => { - let vec_of_expression = claim - .as_array()? - .into_iter() - .map(|payload| { - get_expression( - cedar_type, - &payload, - base_entity_typename, - schema, - claim_mapping, - ) - }) - .collect::, _>>()?; - - Ok(RestrictedExpression::new_set(vec_of_expression)) - }, - } -} - -/// Create [`RestrictedExpression`] with entity UID as token_claim_value -fn get_entity_expression( - cedar_typename: &str, - base_entity_typename: &EntityParsedTypeName<'_>, - token_claim: &TokenClaim, -) -> Result { - let restricted_expression = { - let entity_full_type_name = - EntityParsedTypeName::new(cedar_typename, base_entity_typename.namespace) - .full_type_name(); - - let uid = EntityUid::from_type_name_and_id( - EntityTypeName::from_str(entity_full_type_name.as_str()).map_err(|err| { - CreateCedarEntityError::EntityTypeName(entity_full_type_name.to_string(), err) - })?, - EntityId::new(token_claim.as_str()?), - ); - RestrictedExpression::new_entity_uid(uid) - }; - Ok(restricted_expression) -} - -/// Build [`RestrictedExpression`] based on token_claim_value. -/// It tries to find mapping and apply it to `token_claim` json value. -fn get_record_expression( - record: &CedarSchemaRecord, - cedar_record_type: &EntityParsedTypeName<'_>, - token_claim: &TokenClaim, - schema: &CedarSchemaJson, - claim_mapping: &ClaimMappings, -) -> Result { - // map json value of `token_claim` to TokenPayload object (HashMap) - let mapped_claim: TokenClaims = - match claim_mapping.get_mapping(token_claim.key(), &cedar_record_type.full_type_name()) { - Some(m) => m.apply_mapping(token_claim.value()).into(), - // if we do not have mapping, and value is json object, return TokenPayload based on it. - // if value is not json object, return empty value - None => { - if let Some(map) = token_claim.value().as_object() { - TokenClaims::from_json_map(map.to_owned()) - } else { - TokenClaims::default() - } - }, - }; - - let mut record_restricted_exps = Vec::new(); - - for (attribute_key, entity_attribute) in record.attributes.iter() { - let attribute_type = entity_attribute.get_type()?; - - let mapped_claim_value = - mapped_claim - .get_claim(attribute_key) - .ok_or(CreateCedarEntityError::MissingClaim( - attribute_key.to_string(), - ))?; - - let exp = get_expression( - &attribute_type, - &mapped_claim_value, - cedar_record_type, - schema, - claim_mapping, - ) - .map_err(|err| { - CreateCedarEntityError::BuildAttribute( - cedar_record_type.full_type_name(), - attribute_key.to_string(), - Box::new(err), - ) - })?; - - record_restricted_exps.push((attribute_key.to_string(), exp)); - } - - let restricted_expression = - RestrictedExpression::new_record(record_restricted_exps.into_iter()) - .map_err(CreateCedarEntityError::CreateRecordFromIter)?; - Ok(restricted_expression) -} - -/// Describe errors on creating entity -#[derive(thiserror::Error, Debug)] -pub enum CreateCedarEntityError { - /// Could not parse entity type - #[error("could not parse entity type name: {0}, error: {1}")] - EntityTypeName(String, cedar_policy::ParseErrors), - - /// Could find entity type in the `cedar-policy` schema - #[error("could find entity type: {0} in the schema")] - CouldNotFindEntity(String), - - /// Type in the schema is not record - #[error("type: {0} in the schema is not record")] - NotRecord(String), - - /// Could create entity - #[error("could create entity with uid: {0}, error: {1}")] - CreateEntity(String, cedar_policy::EntityAttrEvaluationError), - - /// Could not get attribute value from payload - #[error("could not get attribute value from payload: {0}")] - GetTokenClaim(#[from] TokenClaimTypeError), - - /// Could not retrieve attribute from cedar-policy schema - #[error("could not retrieve attribute from cedar-policy schema: {0}")] - GetCedarType(#[from] GetCedarTypeError), - - /// Error on cedar-policy type attribute - #[error("err build cedar-policy type: {0}, mapped JWT attribute `{1}`: {2}")] - BuildAttribute(String, String, Box), - - /// Error on creating `cedar-policy` record, in schema it is named as type - #[error("could not create `cedar-policy` record/type {0} : {1}")] - CreateRecord(String, Box), - - /// Wrapped error on [`RestrictedExpression::new_record`] - // this error probably newer happen - #[error("could not build expression from list of expressions: {0}")] - CreateRecordFromIter(cedar_policy::ExpressionConstructionError), - - /// Cause when cannot find record/type in json schema. - #[error("could find record/type: {0}")] - FindType(String), - - /// Error when using the transaction token. Its usage is currently not implemented. - #[error("transaction token not implemented")] - TransactionToken, - - /// Indicates that the creation of an Entity failed due to the absence of available tokens. - #[error("no available token to build the entity from")] - UnavailableToken, - - /// Missing claim - #[error("missing claim: {0}")] - MissingClaim(String), -} diff --git a/jans-cedarling/cedarling/src/authz/entities/mod.rs b/jans-cedarling/cedarling/src/authz/entities/mod.rs deleted file mode 100644 index 6d9310e791d..00000000000 --- a/jans-cedarling/cedarling/src/authz/entities/mod.rs +++ /dev/null @@ -1,278 +0,0 @@ -// This software is available under the Apache-2.0 license. -// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. -// -// Copyright (c) 2024, Gluu, Inc. - -//! Module for creating cedar-policy entities - -mod create; -mod trait_as_expression; -mod user; -mod workload; - -#[cfg(test)] -mod test_create; - -use std::collections::HashSet; - -use cedar_policy::{Entity, EntityUid}; -pub use create::{CEDAR_POLICY_SEPARATOR, CreateCedarEntityError}; -use create::{ - EntityMetadata, EntityParsedTypeName, build_entity_uid, create_entity, - parse_namespace_and_typename, -}; -pub use user::*; -pub use workload::*; - -use super::AuthorizeError; -use super::request::ResourceData; -use crate::AuthorizationConfig; -use crate::common::cedar_schema::CedarSchemaJson; -use crate::common::policy_store::{ClaimMappings, PolicyStore, TokenKind}; -use crate::jwt::Token; - -const DEFAULT_ACCESS_TKN_ENTITY_TYPE_NAME: &str = "Access_token"; -const DEFAULT_ID_TKN_ENTITY_TYPE_NAME: &str = "id_token"; -const DEFAULT_USERINFO_TKN_ENTITY_TYPE_NAME: &str = "Userinfo_token"; -const DEFAULT_TKN_PRINCIPAL_IDENTIFIER: &str = "jti"; - -pub struct DecodedTokens<'a> { - pub access_token: Option>, - pub id_token: Option>, - pub userinfo_token: Option>, -} - -impl DecodedTokens<'_> { - pub fn iter(&self) -> impl Iterator { - [ - self.access_token.as_ref(), - self.id_token.as_ref(), - self.userinfo_token.as_ref(), - ] - .into_iter() - .flatten() - } -} - -pub struct TokenEntities { - pub access: Option, - pub id: Option, - pub userinfo: Option, -} - -pub fn create_token_entities( - conf: &AuthorizationConfig, - policy_store: &PolicyStore, - tokens: &DecodedTokens, -) -> Result { - let schema = &policy_store.schema.json; - let namespace = policy_store.namespace(); - - // create access token entity - let access = if let Some(token) = tokens.access_token.as_ref() { - let type_name = conf - .mapping_access_token - .as_deref() - .unwrap_or(DEFAULT_ACCESS_TKN_ENTITY_TYPE_NAME); - Some( - create_token_entity(token, schema, namespace, type_name) - .map_err(AuthorizeError::CreateAccessTokenEntity)?, - ) - } else { - None - }; - - // create id token entity - let id = if let Some(token) = tokens.id_token.as_ref() { - let type_name = conf - .mapping_id_token - .as_deref() - .unwrap_or(DEFAULT_ID_TKN_ENTITY_TYPE_NAME); - Some( - create_token_entity(token, schema, namespace, type_name) - .map_err(AuthorizeError::CreateIdTokenEntity)?, - ) - } else { - None - }; - - // create userinfo token entity - let userinfo = if let Some(token) = tokens.userinfo_token.as_ref() { - let type_name = conf - .mapping_userinfo_token - .as_deref() - .unwrap_or(DEFAULT_USERINFO_TKN_ENTITY_TYPE_NAME); - Some( - create_token_entity(token, schema, namespace, type_name) - .map_err(AuthorizeError::CreateUserinfoTokenEntity)?, - ) - } else { - None - }; - - Ok(TokenEntities { - access, - id, - userinfo, - }) -} - -fn create_token_entity( - token: &Token, - schema: &CedarSchemaJson, - namespace: &str, - type_name: &str, -) -> Result { - let claim_mapping = token.claim_mapping(); - let tkn_metadata = EntityMetadata::new( - EntityParsedTypeName { - type_name, - namespace, - }, - token - .metadata() - .principal_identifier - .as_deref() - .unwrap_or(DEFAULT_TKN_PRINCIPAL_IDENTIFIER), - ); - tkn_metadata.create_entity(schema, token, HashSet::new(), claim_mapping) -} - -/// Describe errors on creating resource entity -#[derive(thiserror::Error, Debug)] -pub enum ResourceEntityError { - #[error("could not create resource entity: {0}")] - Create(#[from] CreateCedarEntityError), -} - -/// Create entity from [`ResourceData`] -pub fn create_resource_entity( - resource: ResourceData, - schema: &CedarSchemaJson, -) -> Result { - let entity_uid = resource.entity_uid().map_err(|err| { - CreateCedarEntityError::EntityTypeName(resource.resource_type.clone(), err) - })?; - - let (typename, namespace) = parse_namespace_and_typename(&resource.resource_type); - - Ok(create_entity( - entity_uid, - &EntityParsedTypeName::new(typename, namespace.as_str()), - schema, - &resource.payload.into(), - HashSet::new(), - // we no need mapping for resource because user put json structure and it should be correct - &ClaimMappings::default(), - )?) -} - -/// Describe errors on creating role entity -#[derive(thiserror::Error, Debug)] -pub enum RoleEntityError { - #[error("could not create Jans::Role entity from {token_kind} token: {error}")] - Create { - error: CreateCedarEntityError, - token_kind: TokenKind, - }, - - /// Indicates that the creation of the Role Entity failed due to the absence of available tokens. - #[error("Role Entity creation failed: no available token to build the entity from")] - UnavailableToken, -} - -/// Create `Role` entites from based on `TrustedIssuer` role mapping for each token or default value of `RoleMapping` -pub fn create_role_entities( - policy_store: &PolicyStore, - tokens: &DecodedTokens, -) -> Result, RoleEntityError> { - let mut role_entities = Vec::new(); - - for token in tokens.iter() { - let mut entities = extract_roles_from_token(policy_store, token)?; - role_entities.append(&mut entities); - } - - Ok(role_entities) -} - -/// Extract `Role` entites based on single `RoleMapping` -fn extract_roles_from_token( - policy_store: &PolicyStore, - token: &Token, -) -> Result, RoleEntityError> { - let parsed_typename = EntityParsedTypeName::new("Role", policy_store.namespace()); - let role_entity_type = parsed_typename.full_type_name(); - - // get payload of role id in JWT token data - let Some(payload) = token.get_claim(token.role_mapping()) else { - // if key not found we return empty vector - return Ok(Vec::new()); - }; - - // it can be 2 scenario when field is array or field is string - let entity_uid_vec: Vec = if let Ok(payload_str) = payload.as_str() { - // case if it string - let entity_uid = - build_entity_uid(role_entity_type.as_str(), payload_str).map_err(|err| { - RoleEntityError::Create { - error: err, - token_kind: token.kind, - } - })?; - vec![entity_uid] - } else { - // case if it array of string - match payload - // get as array - .as_array() - { - Ok(payload_vec) => { - payload_vec - .iter() - .map(|payload_el| { - // get each element of array as `str` - payload_el.as_str().map_err(|err| RoleEntityError::Create { - error: err.into(), - token_kind: token.kind, - }) - // build entity uid - .and_then(|name| build_entity_uid(role_entity_type.as_str(), name) - .map_err(|err| RoleEntityError::Create { - error: err, - token_kind: token.kind, - })) - }) - .collect::, _>>()? - }, - Err(err) => { - // Handle the case where the payload is neither a string nor an array - return Err(RoleEntityError::Create { - error: err.into(), - token_kind: token.kind, - }); - }, - } - }; - - let schema = &policy_store.schema.json; - - // create role entity for each entity uid - entity_uid_vec - .into_iter() - .map(|entity_uid| { - create_entity( - entity_uid, - &parsed_typename, - schema, - token.claims(), - HashSet::new(), - token.claim_mapping(), - ) - .map_err(|err| RoleEntityError::Create { - error: err, - token_kind: token.kind, - }) - }) - .collect::, _>>() -} diff --git a/jans-cedarling/cedarling/src/authz/entities/test_create.rs b/jans-cedarling/cedarling/src/authz/entities/test_create.rs deleted file mode 100644 index 3cc5cc331ff..00000000000 --- a/jans-cedarling/cedarling/src/authz/entities/test_create.rs +++ /dev/null @@ -1,587 +0,0 @@ -// This software is available under the Apache-2.0 license. -// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. -// -// Copyright (c) 2024, Gluu, Inc. - -//! Testing the creating entities - -use std::collections::HashSet; - -use test_utils::{SortedJson, assert_eq}; - -use super::create::*; -use crate::common::cedar_schema::CedarSchemaJson; -use crate::jwt::{Token, TokenClaimTypeError, TokenClaims}; - -// test all successful cases -// with empty namespace -#[test] -fn successful_scenario_empty_namespace() { - let schema_json = include_str!("test_create_data/successful_scenario_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: "", - type_name: "Test", - }, - "test_id_key", - ); - - let json = serde_json::json!( { - "test_id_key": "test_id", - "string_key": "test string value", - "long_key": 12345, - "entity_uid_key": "unique_id", - "bool_key": true, - "set_key": ["some_string"], - "set_set_key": [["some_string"]] - }); - - let payload: TokenClaims = serde_json::from_value(json).unwrap(); - - let entity = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect("entity should be created"); - - let entity_json = entity.to_json_value().expect("should serialize to json"); - - let expected = serde_json::json!({ - "uid": { - "type": "Test", - "id": "test_id" - }, - "attrs": { - "entity_uid_key": { - "__entity": { - "type": "Test2", - "id": "unique_id" - } - }, - "long_key": 12345, - "string_key": "test string value", - "bool_key": true, - "set_key": ["some_string"], - "set_set_key": [["some_string"]] - }, - "parents": [] - }); - - assert_eq!(expected.sorted(), entity_json.sorted()); -} - -// test all successful cases -// with empty namespace -#[test] -fn successful_scenario_not_empty_namespace() { - let schema_json = include_str!("test_create_data/successful_scenario_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: "Jans", - type_name: "Test", - }, - "test_id_key", - ); - - let json = serde_json::json!( { - "test_id_key": "test_id", - "string_key": "test string value", - "long_key": 12345, - "entity_uid_key": "unique_id", - "bool_key": true, - }); - - let payload: TokenClaims = serde_json::from_value(json).unwrap(); - - let entity = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect("entity should be created"); - - let entity_json = entity.to_json_value().expect("should serialize to json"); - - let expected = serde_json::json!({ - "uid": { - "type": "Jans::Test", - "id": "test_id" - }, - "attrs": { - "entity_uid_key": { - "__entity": { - "type": "Jans::Test2", - "id": "unique_id" - } - }, - "long_key": 12345, - "string_key": "test string value", - "bool_key": true - }, - "parents": [] - }); - - assert_eq!(expected.sorted(), entity_json.sorted()); -} - -/// test wrong string type in token payload -#[test] -fn get_token_claim_type_string_error() { - let schema_json = include_str!("test_create_data/successful_scenario_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: "", - type_name: "Test", - }, - "test_id_key", - ); - - let test_key = "string_key"; - let json = serde_json::json!( { - "test_id_key": "test_id", - // This will trigger the type error, because it's not a String. - test_key: 123, - "long_key": 12345, - "entity_uid_key": "unique_id", - "bool_key": true, - "set_key": ["some_string"], - "set_set_key": [["some_string"]] - }); - - let payload: TokenClaims = serde_json::from_value(json.clone()).unwrap(); - - let entity_creation_error = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect_err("entity creating should throw error"); - - if let CreateCedarEntityError::GetTokenClaim(TokenClaimTypeError { - key, actual_type, .. - }) = entity_creation_error - { - let json_attr_value = json.as_object().unwrap().get(test_key).unwrap(); - let origin_type = TokenClaimTypeError::json_value_type_name(json_attr_value); - - assert!(key == test_key, "expected key: {test_key}, but got: {key}"); - assert!( - actual_type == origin_type, - "expected type: {origin_type}, but got: {actual_type}" - ); - } else { - panic!( - "expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}" - ); - } -} - -/// test wrong long type in token payload -#[test] -fn get_token_claim_type_long_error() { - let schema_json = include_str!("test_create_data/successful_scenario_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: "", - type_name: "Test", - }, - "test_id_key", - ); - - let test_key = "long_key"; - let json = serde_json::json!( { - "test_id_key": "test_id", - "string_key": "test string", - // This will trigger the type error, because it's not an i64. - "long_key": "str", - "entity_uid_key": "unique_id", - "bool_key": true, - "set_key": ["some_string"], - "set_set_key": [["some_string"]] - }); - - let payload: TokenClaims = serde_json::from_value(json.clone()).unwrap(); - - let entity_creation_error = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect_err("entity creating should throw error"); - - if let CreateCedarEntityError::GetTokenClaim(TokenClaimTypeError { - key, actual_type, .. - }) = entity_creation_error - { - let json_attr_value = json.as_object().unwrap().get(test_key).unwrap(); - let origin_type = TokenClaimTypeError::json_value_type_name(json_attr_value); - - assert!(key == test_key, "expected key: {test_key}, but got: {key}"); - assert!( - actual_type == origin_type, - "expected type: {origin_type}, but got: {actual_type}" - ); - } else { - panic!( - "expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}" - ); - } -} - -/// test wrong entity_uid type in token payload -#[test] -fn get_token_claim_type_entity_uid_error() { - let schema_json = include_str!("test_create_data/successful_scenario_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: "", - type_name: "Test", - }, - "test_id_key", - ); - - let test_key = "entity_uid_key"; - let json = serde_json::json!( { - "test_id_key": "test_id", - "string_key": "test string", - "long_key": 1234, - // This will trigger the type error, because it's not a String. - "entity_uid_key": 123, - "bool_key": true, - "set_key": ["some_string"], - "set_set_key": [["some_string"]] - }); - - let payload: TokenClaims = serde_json::from_value(json.clone()).unwrap(); - - let entity_creation_error = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect_err("entity creating should throw error"); - - if let CreateCedarEntityError::GetTokenClaim(TokenClaimTypeError { - key, actual_type, .. - }) = entity_creation_error - { - let json_attr_value = json.as_object().unwrap().get(test_key).unwrap(); - let origin_type = TokenClaimTypeError::json_value_type_name(json_attr_value); - - assert!(key == test_key, "expected key: {test_key}, but got: {key}"); - assert!( - actual_type == origin_type, - "expected type: {origin_type}, but got: {actual_type}" - ); - } else { - panic!( - "expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}" - ); - } -} - -/// test wrong boolean type in token payload -#[test] -fn get_token_claim_type_boolean_error() { - let schema_json = include_str!("test_create_data/successful_scenario_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: "", - type_name: "Test", - }, - "test_id_key", - ); - - let test_key = "bool_key"; - let json = serde_json::json!( { - "test_id_key": "test_id", - "string_key": "test string", - "long_key": 1234, - "entity_uid_key": "ff910f15-d5a4-4227-828e-11cb8463f1b7", - // This will trigger the type error, because it's not a bool. - "bool_key": 123, - "set_key": ["some_string"], - "set_set_key": [["some_string"]] - }); - - let payload: TokenClaims = serde_json::from_value(json.clone()).unwrap(); - - let entity_creation_error = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect_err("entity creating should throw error"); - - if let CreateCedarEntityError::GetTokenClaim(TokenClaimTypeError { - key, - actual_type, - expected_type, - }) = entity_creation_error - { - let json_attr_value = json.as_object().unwrap().get(test_key).unwrap(); - let origin_type = TokenClaimTypeError::json_value_type_name(json_attr_value); - - assert!( - key == test_key, - "expected key: {test_key}, but got: {key} with schema expected_type: {expected_type}" - ); - assert!( - actual_type == origin_type, - "expected type: {origin_type}, but got: {actual_type} with schema expected_type: \ - {expected_type}" - ); - } else { - panic!( - "expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}" - ); - } -} - -/// test wrong set type in token payload, should be array of string -#[test] -fn get_token_claim_type_set_error() { - let schema_json = include_str!("test_create_data/successful_scenario_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: "", - type_name: "Test", - }, - "test_id_key", - ); - - let test_key = "set_key"; - let json = serde_json::json!( { - "test_id_key": "test_id", - "string_key": "test string", - "long_key": 1234, - "entity_uid_key": "ff910f15-d5a4-4227-828e-11cb8463f1b7", - "bool_key": false, - // This will trigger the type error, because it's not a array of string. - "set_key": 1, - "set_set_key": [["some_string"]] - }); - - let payload: TokenClaims = serde_json::from_value(json.clone()).unwrap(); - - let entity_creation_error = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect_err("entity creating should throw error"); - - if let CreateCedarEntityError::GetTokenClaim(TokenClaimTypeError { - key, - actual_type, - expected_type, - }) = entity_creation_error - { - let json_attr_value = json.as_object().unwrap().get(test_key).unwrap(); - let origin_type = TokenClaimTypeError::json_value_type_name(json_attr_value); - - assert!( - key == test_key, - "expected key: {test_key}, but got: {key} with schema expected_type: {expected_type}" - ); - assert!( - actual_type == origin_type, - "expected type: {origin_type}, but got: {actual_type} with schema expected_type: \ - {expected_type}" - ); - } else { - panic!( - "expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}" - ); - } -} - -/// test wrong set type in token payload, should be array of array of string -#[test] -fn get_token_claim_type_set_of_set_error() { - let schema_json = include_str!("test_create_data/successful_scenario_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: "", - type_name: "Test", - }, - "test_id_key", - ); - - let json = serde_json::json!( { - "test_id_key": "test_id", - "string_key": "test string", - "long_key": 1234, - "entity_uid_key": "ff910f15-d5a4-4227-828e-11cb8463f1b7", - "bool_key": false, - "set_key": ["some_string"], - // This will trigger the type error, because it's not a array of array of string. - "set_set_key": ["some_string"] - }); - - let payload: TokenClaims = serde_json::from_value(json.clone()).unwrap(); - - let entity_creation_error = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect_err("entity creating should throw error"); - - if let CreateCedarEntityError::GetTokenClaim(TokenClaimTypeError { - key, - actual_type, - expected_type, - }) = entity_creation_error - { - let json_attr_value = json.as_object().unwrap().get("set_set_key").unwrap(); - let origin_type = - TokenClaimTypeError::json_value_type_name(&json_attr_value.as_array().unwrap()[0]); - - // key set_set_key and zero element in array - let test_key = "set_set_key[0]"; - - assert!( - key == test_key, - "expected key: {test_key}, but got: {key} with schema expected_type: {expected_type}" - ); - assert!( - actual_type == origin_type, - "expected type: {origin_type}, but got: {actual_type} with schema expected_type: \ - {expected_type}" - ); - } else { - panic!( - "expected error type: CedarPolicyCreateTypeError::TokenClaimTypeError(GetTokenClaimError::KeyNotCorrectType), but got: {entity_creation_error}" - ); - } -} - -/// create entity with wrong cedar typename -#[test] -fn get_token_claim_cedar_typename_error() { - let schema_json = include_str!("test_create_data/successful_scenario_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - // Mistake in entity type name, should be `"Jans::Test"`, it will trigger error - let (typename, namespace) = parse_namespace_and_typename("Jans:::Test"); - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: &namespace, - type_name: typename, - }, - "test_id_key", - ); - - let json = serde_json::json!( { - "test_id_key": "test_id", - "string_key": "test string value", - "long_key": 12345, - "entity_uid_key": "unique_id", - "bool_key": true, - }); - - let payload: TokenClaims = serde_json::from_value(json).unwrap(); - - let entity_creation_error = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect_err("entity creating should throw error"); - - if let CreateCedarEntityError::EntityTypeName(typename, _) = &entity_creation_error { - assert_eq!("Jans:::Test", typename); - } else { - panic!( - "error should be CedarPolicyCreateTypeError::EntityTypeName, but got {:?}", - entity_creation_error - ); - } -} - -/// create entity with wrong cedar typename in the attribute -// The JSON schema contains an error.r: -// -// "entity_uid_key": { -// "type": "EntityOrCommon", -// "name": ":Test2" -// }, -// -// ":Test2" is not correct type definition, it will trigger error -#[test] -fn get_token_claim_cedar_typename_in_attr_error() { - let schema_json = include_str!("test_create_data/type_error_schema.json"); - let schema: CedarSchemaJson = serde_json::from_str(schema_json).unwrap(); - - let metadata = EntityMetadata::new( - EntityParsedTypeName { - namespace: "Jans", - type_name: "Test", - }, - "test_id_key", - ); - - let json = serde_json::json!( { - "test_id_key": "test_id", - "string_key": "test string value", - "long_key": 12345, - "entity_uid_key": "unique_id", - "bool_key": true, - }); - - let payload: TokenClaims = serde_json::from_value(json).unwrap(); - - let entity_creation_error = metadata - .create_entity( - &schema, - &Token::new_id(payload, None), - HashSet::new(), - &Default::default(), - ) - .expect_err("entity creating should throw error"); - - if let CreateCedarEntityError::FindType(typename) = &entity_creation_error { - assert_eq!("Jans:::Test2", typename); - } else { - panic!( - "error should be CedarPolicyCreateTypeError::EntityTypeName, but got {:?}", - entity_creation_error - ); - } -} diff --git a/jans-cedarling/cedarling/src/authz/entities/test_create_data/successful_scenario.schema b/jans-cedarling/cedarling/src/authz/entities/test_create_data/successful_scenario.schema deleted file mode 100644 index ea5cd8c527a..00000000000 --- a/jans-cedarling/cedarling/src/authz/entities/test_create_data/successful_scenario.schema +++ /dev/null @@ -1,26 +0,0 @@ -entity Test = { - string_key: String, - long_key: Long, - bool_key: Boolean, - entity_uid_key: Test2, - optional_key?: String, - set_key: Set, - set_set_key: Set>, -}; - -entity Test2 = { -}; - - -namespace Jans{ - entity Test = { - string_key: String, - long_key: Long, - bool_key: Boolean, - entity_uid_key: Test2, - optional_key?: String - }; - - entity Test2 = { - }; -} \ No newline at end of file diff --git a/jans-cedarling/cedarling/src/authz/entities/test_create_data/successful_scenario_schema.json b/jans-cedarling/cedarling/src/authz/entities/test_create_data/successful_scenario_schema.json deleted file mode 100644 index 794637d08ee..00000000000 --- a/jans-cedarling/cedarling/src/authz/entities/test_create_data/successful_scenario_schema.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "Jans": { - "entityTypes": { - "Test2": {}, - "Test": { - "shape": { - "type": "Record", - "attributes": { - "bool_key": { - "type": "EntityOrCommon", - "name": "Boolean" - }, - "entity_uid_key": { - "type": "EntityOrCommon", - "name": "Test2" - }, - "long_key": { - "type": "EntityOrCommon", - "name": "Long" - }, - "optional_key": { - "type": "EntityOrCommon", - "name": "String", - "required": false - }, - "string_key": { - "type": "EntityOrCommon", - "name": "String" - } - } - } - } - }, - "actions": {} - }, - "": { - "entityTypes": { - "Test2": {}, - "Test": { - "shape": { - "type": "Record", - "attributes": { - "bool_key": { - "type": "EntityOrCommon", - "name": "Boolean" - }, - "entity_uid_key": { - "type": "EntityOrCommon", - "name": "Test2" - }, - "long_key": { - "type": "EntityOrCommon", - "name": "Long" - }, - "optional_key": { - "type": "EntityOrCommon", - "name": "String", - "required": false - }, - "set_key": { - "type": "Set", - "element": { - "type": "EntityOrCommon", - "name": "String" - } - }, - "set_set_key": { - "type": "Set", - "element": { - "type": "Set", - "element": { - "type": "EntityOrCommon", - "name": "String" - } - } - }, - "string_key": { - "type": "EntityOrCommon", - "name": "String" - } - } - } - } - }, - "actions": {} - } -} \ No newline at end of file diff --git a/jans-cedarling/cedarling/src/authz/entities/test_create_data/type_error_schema.json b/jans-cedarling/cedarling/src/authz/entities/test_create_data/type_error_schema.json deleted file mode 100644 index 7ee0fa03dbe..00000000000 --- a/jans-cedarling/cedarling/src/authz/entities/test_create_data/type_error_schema.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "Jans": { - "entityTypes": { - "Test": { - "shape": { - "type": "Record", - "attributes": { - "bool_key": { - "type": "EntityOrCommon", - "name": "Boolean" - }, - "entity_uid_key": { - "type": "EntityOrCommon", - "name": ":Test2" - }, - "long_key": { - "type": "EntityOrCommon", - "name": "Long" - }, - "optional_key": { - "type": "EntityOrCommon", - "name": "String", - "required": false - }, - "string_key": { - "type": "EntityOrCommon", - "name": "String" - } - } - } - }, - "Test2": {} - }, - "actions": {} - } -} \ No newline at end of file diff --git a/jans-cedarling/cedarling/src/authz/entities/trait_as_expression.rs b/jans-cedarling/cedarling/src/authz/entities/trait_as_expression.rs deleted file mode 100644 index cd66476a33a..00000000000 --- a/jans-cedarling/cedarling/src/authz/entities/trait_as_expression.rs +++ /dev/null @@ -1,29 +0,0 @@ -// This software is available under the Apache-2.0 license. -// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. -// -// Copyright (c) 2024, Gluu, Inc. - -use cedar_policy::RestrictedExpression; - -/// Trait to cast type to [`RestrictedExpression`] -pub(crate) trait AsExpression { - fn to_expression(self) -> RestrictedExpression; -} - -impl AsExpression for i64 { - fn to_expression(self) -> RestrictedExpression { - RestrictedExpression::new_long(self) - } -} - -impl AsExpression for String { - fn to_expression(self) -> RestrictedExpression { - RestrictedExpression::new_string(self) - } -} - -impl AsExpression for bool { - fn to_expression(self) -> RestrictedExpression { - RestrictedExpression::new_bool(self) - } -} diff --git a/jans-cedarling/cedarling/src/authz/entities/user.rs b/jans-cedarling/cedarling/src/authz/entities/user.rs deleted file mode 100644 index fd50773bad9..00000000000 --- a/jans-cedarling/cedarling/src/authz/entities/user.rs +++ /dev/null @@ -1,257 +0,0 @@ -// This software is available under the Apache-2.0 license. -// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. -// -// Copyright (c) 2024, Gluu, Inc. - -use std::collections::HashSet; -use std::fmt; - -use cedar_policy::EntityUid; - -use super::{CreateCedarEntityError, DecodedTokens, EntityMetadata, EntityParsedTypeName}; -use crate::common::cedar_schema::CedarSchemaJson; -use crate::common::policy_store::{PolicyStore, TokenKind}; -use crate::jwt::Token; - -/// Create user entity -pub fn create_user_entity( - entity_mapping: Option<&str>, - policy_store: &PolicyStore, - tokens: &DecodedTokens, - parents: HashSet, -) -> Result { - let schema: &CedarSchemaJson = &policy_store.schema.json; - let namespace = policy_store.namespace(); - let mut errors = Vec::new(); - - // helper closure to attempt entity creation from a token - let try_create_entity = |token_kind: TokenKind, token: Option<&Token>| { - if let Some(token) = token { - let claim_mapping = token.claim_mapping(); - let user_mapping = token.user_mapping(); - let entity_metadata = EntityMetadata::new( - EntityParsedTypeName { - type_name: entity_mapping.unwrap_or("User"), - namespace, - }, - user_mapping, - ); - entity_metadata - .create_entity(schema, token, parents.clone(), claim_mapping) - .map_err(|e| (token_kind, e)) - } else { - Err((token_kind, CreateCedarEntityError::UnavailableToken)) - } - }; - - // attempt entity creation for each token type that contains user info - for (token_kind, token) in [ - (TokenKind::Userinfo, tokens.userinfo_token.as_ref()), - (TokenKind::Id, tokens.id_token.as_ref()), - ] { - match try_create_entity(token_kind, token) { - Ok(entity) => return Ok(entity), - Err(e) => errors.push(e), - } - } - - Err(CreateUserEntityError { errors }) -} - -#[derive(Debug, thiserror::Error)] -pub struct CreateUserEntityError { - pub errors: Vec<(TokenKind, CreateCedarEntityError)>, -} - -impl fmt::Display for CreateUserEntityError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.errors.is_empty() { - writeln!( - f, - "Failed to create User Entity since no tokens were provided" - )?; - } else { - writeln!( - f, - "Failed to create User Entity due to the following errors:" - )?; - for (token_kind, error) in &self.errors { - writeln!(f, "- TokenKind {:?}: {}", token_kind, error)?; - } - } - Ok(()) - } -} - -#[cfg(test)] -mod test { - use std::collections::{HashMap, HashSet}; - use std::path::Path; - - use cedar_policy::{Entity, RestrictedExpression}; - use serde_json::json; - use test_utils::assert_eq; - use tokio::test; - - use super::create_user_entity; - use crate::authz::entities::DecodedTokens; - use crate::common::policy_store::TokenKind; - use crate::init::policy_store::load_policy_store; - use crate::jwt::Token; - use crate::{CreateCedarEntityError, PolicyStoreConfig, PolicyStoreSource}; - - #[test] - async fn can_create_from_id_token() { - let entity_mapping = None; - let policy_store = load_policy_store(&PolicyStoreConfig { - source: PolicyStoreSource::FileYaml( - Path::new("../test_files/policy-store_ok_2.yaml").into(), - ), - }) - .await - .expect("Should load policy store") - .store; - - let tokens = DecodedTokens { - access_token: None, - id_token: Some(Token::new_id( - HashMap::from([ - ("sub".to_string(), json!("user-1")), - ("country".to_string(), json!("US")), - ]) - .into(), - None, - )), - userinfo_token: None, - }; - let result = create_user_entity(entity_mapping, &policy_store, &tokens, HashSet::new()) - .expect("expected to create user entity"); - assert_eq!( - result, - Entity::new( - "Jans::User::\"user-1\"" - .parse() - .expect("expected to create user UID"), - HashMap::from([( - "country".to_string(), - RestrictedExpression::new_string("US".to_string()) - )]), - HashSet::new(), - ) - .expect("should create expected user entity") - ) - } - - #[test] - async fn can_create_from_userinfo_token() { - let entity_mapping = None; - let policy_store = load_policy_store(&PolicyStoreConfig { - source: PolicyStoreSource::FileYaml( - Path::new("../test_files/policy-store_ok_2.yaml").into(), - ), - }) - .await - .expect("Should load policy store") - .store; - - let tokens = DecodedTokens { - id_token: None, - access_token: None, - userinfo_token: Some(Token::new_userinfo( - HashMap::from([ - ("sub".to_string(), json!("user-1")), - ("country".to_string(), json!("US")), - ]) - .into(), - None, - )), - }; - let result = create_user_entity(entity_mapping, &policy_store, &tokens, HashSet::new()) - .expect("expected to create user entity"); - assert_eq!( - result, - Entity::new( - "Jans::User::\"user-1\"" - .parse() - .expect("expected to create user UID"), - HashMap::from([( - "country".to_string(), - RestrictedExpression::new_string("US".to_string()) - )]), - HashSet::new(), - ) - .expect("should create expected user entity") - ) - } - - #[test] - async fn errors_when_tokens_have_missing_claims() { - let entity_mapping = None; - let policy_store = load_policy_store(&PolicyStoreConfig { - source: PolicyStoreSource::FileYaml( - Path::new("../test_files/policy-store_ok_2.yaml").into(), - ), - }) - .await - .expect("Should load policy store") - .store; - - let tokens = DecodedTokens { - access_token: Some(Token::new_access(HashMap::from([]).into(), None)), - id_token: Some(Token::new_id(HashMap::from([]).into(), None)), - userinfo_token: Some(Token::new_userinfo(HashMap::from([]).into(), None)), - }; - - let result = create_user_entity(entity_mapping, &policy_store, &tokens, HashSet::new()) - .expect_err("expected to error while creating user entity"); - - for (tkn_kind, err) in result.errors.iter() { - match tkn_kind { - TokenKind::Access => assert!( - matches!(err, CreateCedarEntityError::MissingClaim(ref claim) if claim == "sub"), - "expected error MissingClaim(\"sub\")" - ), - TokenKind::Id => assert!( - matches!(err, CreateCedarEntityError::MissingClaim(ref claim) if claim == "sub"), - "expected error MissingClaim(\"sub\")" - ), - TokenKind::Userinfo => assert!( - matches!(err, CreateCedarEntityError::MissingClaim(ref claim) if claim == "sub"), - "expected error MissingClaim(\"sub\")" - ), - TokenKind::Transaction => (), // we don't support these yet - } - } - } - - #[test] - async fn errors_when_tokens_unavailable() { - let entity_mapping = None; - let policy_store = load_policy_store(&PolicyStoreConfig { - source: PolicyStoreSource::FileYaml( - Path::new("../test_files/policy-store_ok_2.yaml").into(), - ), - }) - .await - .expect("Should load policy store") - .store; - - let tokens = DecodedTokens { - access_token: None, - id_token: None, - userinfo_token: None, - }; - - let result = create_user_entity(entity_mapping, &policy_store, &tokens, HashSet::new()) - .expect_err("expected to error while creating user entity"); - - assert_eq!(result.errors.len(), 2); - for (_tkn_kind, err) in result.errors.iter() { - assert!( - matches!(err, CreateCedarEntityError::UnavailableToken), - "expected error UnavailableToken, got: {:?}", - err - ); - } - } -} diff --git a/jans-cedarling/cedarling/src/authz/entities/workload.rs b/jans-cedarling/cedarling/src/authz/entities/workload.rs deleted file mode 100644 index d10509c08d7..00000000000 --- a/jans-cedarling/cedarling/src/authz/entities/workload.rs +++ /dev/null @@ -1,249 +0,0 @@ -// This software is available under the Apache-2.0 license. -// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. -// -// Copyright (c) 2024, Gluu, Inc. - -use std::collections::HashSet; -use std::fmt; - -use super::{CreateCedarEntityError, DecodedTokens, EntityMetadata, EntityParsedTypeName}; -use crate::common::policy_store::{PolicyStore, TokenKind}; -use crate::jwt::Token; - -/// Create workload entity -pub fn create_workload_entity( - entity_mapping: Option<&str>, - policy_store: &PolicyStore, - tokens: &DecodedTokens, -) -> Result { - let namespace = policy_store.namespace(); - let schema = &policy_store.schema.json; - let mut errors = Vec::new(); - - // helper closure to attempt entity creation from a token - let try_create_entity = |token_kind: TokenKind, token: Option<&Token>, key: &str| { - if let Some(token) = token { - let claim_mapping = token.claim_mapping(); - let entity_metadta = EntityMetadata::new( - EntityParsedTypeName { - type_name: entity_mapping.unwrap_or("Workload"), - namespace, - }, - key, - ); - entity_metadta - .create_entity(schema, token, HashSet::new(), claim_mapping) - .map_err(|e| (token_kind, e)) - } else { - Err((token_kind, CreateCedarEntityError::UnavailableToken)) - } - }; - - // attempt entity creation for each token type - for (token_kind, token, key) in [ - (TokenKind::Access, tokens.access_token.as_ref(), "client_id"), - (TokenKind::Id, tokens.id_token.as_ref(), "aud"), - ] { - match try_create_entity(token_kind, token, key) { - Ok(entity) => return Ok(entity), - Err(e) => errors.push(e), - } - } - - Err(CreateWorkloadEntityError { errors }) -} - -#[derive(Debug, thiserror::Error)] -pub struct CreateWorkloadEntityError { - pub errors: Vec<(TokenKind, CreateCedarEntityError)>, -} - -impl fmt::Display for CreateWorkloadEntityError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.errors.is_empty() { - writeln!( - f, - "Failed to create Workload Entity since no tokens were provided" - )?; - } else { - writeln!( - f, - "Failed to create Workload Entity due to the following errors:" - )?; - for (token_kind, error) in &self.errors { - writeln!(f, "- TokenKind {:?}: {}", token_kind, error)?; - } - } - Ok(()) - } -} - -#[cfg(test)] -mod test { - use std::collections::{HashMap, HashSet}; - use std::path::Path; - - use cedar_policy::{Entity, RestrictedExpression}; - use serde_json::json; - use test_utils::assert_eq; - use tokio::test; - - use super::create_workload_entity; - use crate::authz::entities::DecodedTokens; - use crate::common::policy_store::TokenKind; - use crate::init::policy_store::load_policy_store; - use crate::jwt::Token; - use crate::{CreateCedarEntityError, PolicyStoreConfig, PolicyStoreSource}; - - #[test] - async fn can_create_from_id_token() { - let entity_mapping = None; - let policy_store = load_policy_store(&PolicyStoreConfig { - source: PolicyStoreSource::FileYaml( - Path::new("../test_files/policy-store_ok_2.yaml").into(), - ), - }) - .await - .expect("Should load policy store") - .store; - - let tokens = DecodedTokens { - access_token: None, - id_token: Some(Token::new_id( - HashMap::from([ - ("aud".to_string(), json!("workload-1")), - ("org_id".to_string(), json!("some-org-123")), - ]) - .into(), - None, - )), - userinfo_token: None, - }; - let result = create_workload_entity(entity_mapping, &policy_store, &tokens) - .expect("expected to create workload entity"); - assert_eq!( - result, - Entity::new( - "Jans::Workload::\"workload-1\"" - .parse() - .expect("expected to create workload UID"), - HashMap::from([( - "org_id".to_string(), - RestrictedExpression::new_string("some-org-123".to_string()) - )]), - HashSet::new(), - ) - .expect("should create expected workload entity") - ) - } - - #[test] - async fn can_create_from_access_token() { - let entity_mapping = None; - let policy_store = load_policy_store(&PolicyStoreConfig { - source: PolicyStoreSource::FileYaml( - Path::new("../test_files/policy-store_ok_2.yaml").into(), - ), - }) - .await - .expect("Should load policy store") - .store; - - let tokens = DecodedTokens { - access_token: Some(Token::new_access( - HashMap::from([ - ("client_id".to_string(), json!("workload-1")), - ("org_id".to_string(), json!("some-org-123")), - ]) - .into(), - None, - )), - id_token: None, - userinfo_token: None, - }; - let result = create_workload_entity(entity_mapping, &policy_store, &tokens) - .expect("expected to create workload entity"); - assert_eq!( - result, - Entity::new( - "Jans::Workload::\"workload-1\"" - .parse() - .expect("expected to create workload UID"), - HashMap::from([( - "org_id".to_string(), - RestrictedExpression::new_string("some-org-123".to_string()) - )]), - HashSet::new(), - ) - .expect("should create expected workload entity") - ) - } - - #[test] - async fn errors_when_tokens_have_missing_claims() { - let entity_mapping = None; - let policy_store = load_policy_store(&PolicyStoreConfig { - source: PolicyStoreSource::FileYaml( - Path::new("../test_files/policy-store_ok_2.yaml").into(), - ), - }) - .await - .expect("Should load policy store") - .store; - - let tokens = DecodedTokens { - access_token: Some(Token::new_access(HashMap::from([]).into(), None)), - id_token: Some(Token::new_id(HashMap::from([]).into(), None)), - userinfo_token: Some(Token::new_userinfo(HashMap::from([]).into(), None)), - }; - - let result = create_workload_entity(entity_mapping, &policy_store, &tokens) - .expect_err("expected to error while creating workload entity"); - - for (tkn_kind, err) in result.errors.iter() { - match tkn_kind { - TokenKind::Access => assert!( - matches!(err, CreateCedarEntityError::MissingClaim(ref claim) if claim == "client_id"), - "expected error MissingClaim(\"client_id\")" - ), - TokenKind::Id => assert!( - matches!(err, CreateCedarEntityError::MissingClaim(ref claim) if claim == "aud"), - "expected error MissingClaim(\"aud\")" - ), - _ => (), // we don't create workload tokens using other tokens - } - } - } - - #[test] - async fn errors_when_tokens_unavailable() { - let entity_mapping = None; - let policy_store = load_policy_store(&PolicyStoreConfig { - source: PolicyStoreSource::FileYaml( - Path::new("../test_files/policy-store_ok_2.yaml").into(), - ), - }) - .await - .expect("Should load policy store") - .store; - - // we can only create the workload from the access_token and id_token - let tokens = DecodedTokens { - access_token: None, - id_token: None, - userinfo_token: None, - }; - - let result = create_workload_entity(entity_mapping, &policy_store, &tokens) - .expect_err("expected to error while creating workload entity"); - - assert_eq!(result.errors.len(), 2); - for (_tkn_kind, err) in result.errors.iter() { - assert!( - matches!(err, CreateCedarEntityError::UnavailableToken), - "expected error UnavailableToken, got: {:?}", - err - ); - } - } -} diff --git a/jans-cedarling/cedarling/src/authz/entity_builder.rs b/jans-cedarling/cedarling/src/authz/entity_builder.rs new file mode 100644 index 00000000000..4e3d948a8a8 --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/entity_builder.rs @@ -0,0 +1,471 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +mod build_attrs; +mod build_expr; +mod build_resource_entity; +mod build_role_entity; +mod build_token_entities; +mod build_user_entity; +mod build_workload_entity; +mod mapping; + +use crate::common::cedar_schema::CEDAR_NAMESPACE_SEPARATOR; +use crate::common::cedar_schema::cedar_json::CedarSchemaJson; +use crate::common::policy_store::TokenKind; +use crate::jwt::{Token, TokenClaimTypeError}; +use crate::{AuthorizationConfig, ResourceData}; +use build_attrs::{BuildAttrError, ClaimAliasMap, build_entity_attrs_from_tkn}; +use build_expr::*; +use build_resource_entity::{BuildResourceEntityError, JsonTypeError}; +use build_role_entity::BuildRoleEntityError; +pub use build_token_entities::BuildTokenEntityError; +use build_user_entity::BuildUserEntityError; +use build_workload_entity::BuildWorkloadEntityError; +use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid}; +use serde_json::Value; +use std::collections::{HashMap, HashSet}; +use std::convert::Infallible; +use std::fmt; +use std::str::FromStr; + +use super::AuthorizeEntitiesData; + +const DEFAULT_WORKLOAD_ENTITY_NAME: &str = "Workload"; +const DEFAULT_USER_ENTITY_NAME: &str = "User"; +const DEFAULT_ACCESS_TKN_ENTITY_NAME: &str = "Access_token"; +const DEFAULT_ID_TKN_ENTITY_NAME: &str = "id_token"; +const DEFAULT_USERINFO_TKN_ENTITY_NAME: &str = "Userinfo_token"; +const DEFAULT_ROLE_ENTITY_NAME: &str = "Role"; + +pub struct DecodedTokens<'a> { + pub access: Option>, + pub id: Option>, + pub userinfo: Option>, +} + +/// The names of the entities in the schema +pub struct EntityNames { + user: String, + workload: String, + id_token: String, + access_token: String, + userinfo_token: String, + role: String, +} + +impl From<&AuthorizationConfig> for EntityNames { + fn from(config: &AuthorizationConfig) -> Self { + Self { + user: config + .mapping_user + .clone() + .unwrap_or_else(|| DEFAULT_USER_ENTITY_NAME.to_string()), + workload: config + .mapping_workload + .clone() + .unwrap_or_else(|| DEFAULT_WORKLOAD_ENTITY_NAME.to_string()), + id_token: config + .mapping_id_token + .clone() + .unwrap_or_else(|| DEFAULT_ID_TKN_ENTITY_NAME.to_string()), + access_token: config + .mapping_access_token + .clone() + .unwrap_or_else(|| DEFAULT_ACCESS_TKN_ENTITY_NAME.to_string()), + userinfo_token: config + .mapping_userinfo_token + .clone() + .unwrap_or_else(|| DEFAULT_USERINFO_TKN_ENTITY_NAME.to_string()), + // TODO: implement a bootstrap property to set the Role entity name + role: DEFAULT_ROLE_ENTITY_NAME.to_string(), + } + } +} + +impl Default for EntityNames { + fn default() -> Self { + Self { + user: DEFAULT_USER_ENTITY_NAME.to_string(), + workload: DEFAULT_WORKLOAD_ENTITY_NAME.to_string(), + id_token: DEFAULT_ID_TKN_ENTITY_NAME.to_string(), + access_token: DEFAULT_ACCESS_TKN_ENTITY_NAME.to_string(), + userinfo_token: DEFAULT_USERINFO_TKN_ENTITY_NAME.to_string(), + role: DEFAULT_ROLE_ENTITY_NAME.to_string(), + } + } +} + +pub struct EntityBuilder { + schema: CedarSchemaJson, + entity_names: EntityNames, + build_workload: bool, + build_user: bool, +} + +impl EntityBuilder { + pub fn new( + schema: CedarSchemaJson, + entity_names: EntityNames, + build_workload: bool, + build_user: bool, + ) -> Self { + Self { + schema, + entity_names, + build_workload, + build_user, + } + } + + pub fn build_entities( + &self, + tokens: &DecodedTokens, + resource: &ResourceData, + ) -> Result { + let workload = if self.build_workload { + Some(self.build_workload_entity(tokens)?) + } else { + None + }; + + let (user, roles) = if self.build_user { + let roles = self.try_build_role_entities(tokens)?; + let parents = roles + .iter() + .map(|role| role.uid()) + .collect::>(); + (Some(self.build_user_entity(tokens, parents)?), roles) + } else { + (None, vec![]) + }; + + let access_token = if let Some(token) = tokens.access.as_ref() { + Some( + self.build_access_tkn_entity(token) + .map_err(BuildCedarlingEntityError::AccessToken)?, + ) + } else { + None + }; + + let id_token = if let Some(token) = tokens.id.as_ref() { + Some( + self.build_id_tkn_entity(token) + .map_err(BuildCedarlingEntityError::IdToken)?, + ) + } else { + None + }; + + let userinfo_token = if let Some(token) = tokens.userinfo.as_ref() { + Some( + self.build_userinfo_tkn_entity(token) + .map_err(BuildCedarlingEntityError::UserinfoToken)?, + ) + } else { + None + }; + + let resource = self.build_resource_entity(resource)?; + + Ok(AuthorizeEntitiesData { + workload, + user, + access_token, + id_token, + userinfo_token, + resource, + roles, + }) + } +} + +/// Builds a Cedar Entity using a JWT +fn build_entity( + schema: &CedarSchemaJson, + entity_name: &str, + token: &Token, + id_src_claim: &str, + claim_aliases: Vec, + parents: HashSet, +) -> Result { + // Get entity Id from the specified token claim + let entity_id = token + .get_claim(id_src_claim) + .ok_or(BuildEntityError::MissingClaim(id_src_claim.to_string()))? + .as_str()? + .to_owned(); + + // Get entity namespace and type + let mut entity_name = entity_name.to_string(); + let (namespace, entity_type) = schema + .get_entity_from_base_name(&entity_name) + .ok_or(BuildEntityError::EntityNotInSchema(entity_name.to_string()))?; + if !namespace.is_empty() { + entity_name = [namespace.as_str(), &entity_name].join(CEDAR_NAMESPACE_SEPARATOR); + } + + // Build entity attributes + let entity_attrs = build_entity_attrs_from_tkn(schema, entity_type, token, claim_aliases) + .map_err(BuildEntityError::BuildAttribute)?; + + // Build cedar entity + let entity_type_name = + EntityTypeName::from_str(&entity_name).map_err(BuildEntityError::ParseEntityTypeName)?; + let entity_id = EntityId::from_str(&entity_id).map_err(BuildEntityError::ParseEntityId)?; + let entity_uid = EntityUid::from_type_name_and_id(entity_type_name, entity_id); + Ok(Entity::new(entity_uid, entity_attrs, parents)?) +} + +/// Errors encountered when building a Cedarling-specific entity +#[derive(Debug, thiserror::Error)] +pub enum BuildCedarlingEntityError { + #[error(transparent)] + Workload(#[from] BuildWorkloadEntityError), + #[error(transparent)] + User(#[from] BuildUserEntityError), + #[error(transparent)] + Role(#[from] BuildRoleEntityError), + #[error("failed to build resource entity: {0}")] + Resource(#[from] BuildResourceEntityError), + #[error("error while building Access Token entity: {0}")] + AccessToken(#[source] BuildTokenEntityError), + #[error("error while building Id Token entity: {0}")] + IdToken(#[source] BuildTokenEntityError), + #[error("error while building Userinfo Token entity: {0}")] + UserinfoToken(#[source] BuildTokenEntityError), +} + +#[derive(Debug, thiserror::Error)] +pub enum BuildEntityError { + #[error("failed to parse entity type name: {0}")] + ParseEntityTypeName(#[source] cedar_policy::ParseErrors), + #[error("failed to parse entity id: {0}")] + ParseEntityId(#[source] Infallible), + #[error("failed to evaluate entity or tag: {0}")] + AttrEvaluation(#[from] cedar_policy::EntityAttrEvaluationError), + #[error("failed to build entity since a token was not provided")] + TokenUnavailable, + #[error("the given token is missing a `{0}` claim")] + MissingClaim(String), + #[error(transparent)] + TokenClaimTypeMismatch(#[from] TokenClaimTypeError), + #[error(transparent)] + JsonTypeError(#[from] JsonTypeError), + #[error("the entity `{0}` is not defined in the schema")] + EntityNotInSchema(String), + #[error(transparent)] + BuildAttribute(#[from] BuildAttrError), + #[error("got {0} token, expected: {1}")] + InvalidToken(TokenKind, TokenKind), +} + +impl BuildEntityError { + pub fn json_type_err(expected_type_name: &str, got_value: &Value) -> Self { + Self::JsonTypeError(JsonTypeError::type_mismatch(expected_type_name, got_value)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + common::{cedar_schema::cedar_json::CedarSchemaJson, policy_store::TrustedIssuer}, + jwt::{Token, TokenClaims}, + }; + use cedar_policy::EvalResult; + use serde_json::json; + use std::collections::HashMap; + + #[test] + fn can_build_entity_using_jwt() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + "name": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let iss = TrustedIssuer::default(); + let token = Token::new_access( + TokenClaims::new(HashMap::from([ + ("client_id".to_string(), json!("workload-123")), + ("name".to_string(), json!("somename")), + ])), + Some(&iss), + ); + let entity = build_entity( + &schema, + "Workload", + &token, + "client_id", + Vec::new(), + HashSet::new(), + ) + .expect("should successfully build entity"); + + assert_eq!(entity.uid().to_string(), "Jans::Workload::\"workload-123\""); + assert_eq!( + entity + .attr("client_id") + .expect("expected workload entity to have a `client_id` attribute") + .unwrap(), + EvalResult::String("workload-123".to_string()), + ); + assert_eq!( + entity + .attr("name") + .expect("expected workload entity to have a `name` attribute") + .unwrap(), + EvalResult::String("somename".to_string()), + ); + } + + #[test] + fn errors_on_invalid_entity_type_name() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload!": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + "name": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let iss = TrustedIssuer::default(); + let token = Token::new_access( + TokenClaims::new(HashMap::from([ + ("client_id".to_string(), json!("workload-123")), + ("name".to_string(), json!("somename")), + ])), + Some(&iss), + ); + + let err = build_entity( + &schema, + "Workload!", + &token, + "client_id", + Vec::new(), + HashSet::new(), + ) + .expect_err("should error while parsing entity type name"); + + assert!( + matches!(err, BuildEntityError::ParseEntityTypeName(_)), + "expected ParseEntityTypeName error but got: {:?}", + err + ); + } + + #[test] + fn errors_when_token_is_missing_entity_id_claim() { + let schema = serde_json::from_value::(json!({})) + .expect("should successfully build schema"); + let iss = TrustedIssuer::default(); + let token = Token::new_access(TokenClaims::new(HashMap::new()), Some(&iss)); + + let err = build_entity( + &schema, + "Workload", + &token, + "client_id", + Vec::new(), + HashSet::new(), + ) + .expect_err("should error while parsing entity type name"); + + assert!( + matches!( + err, + BuildEntityError::MissingClaim(ref claim_name) + if claim_name =="client_id" + ), + "expected MissingClaim error but got: {}", + err + ); + } + + #[test] + fn errors_token_claim_has_unexpected_type() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let iss = TrustedIssuer::default(); + let token = Token::new_access( + TokenClaims::new(HashMap::from([("client_id".to_string(), json!(123))])), + Some(&iss), + ); + let err = build_entity( + &schema, + "Workload", + &token, + "client_id", + Vec::new(), + HashSet::new(), + ) + .expect_err("should error due to unexpected json type"); + + assert!( + matches!( + err, + BuildEntityError::TokenClaimTypeMismatch(ref err) + if err == &TokenClaimTypeError::type_mismatch("client_id", "String", &json!(123)) + ), + "expected TokenClaimTypeMismatch error but got: {:?}", + err + ); + } + + #[test] + fn errors_when_entity_not_in_schema() { + let schema = serde_json::from_value::(json!({})) + .expect("should successfully build schema"); + let iss = TrustedIssuer::default(); + let token = Token::new_access( + TokenClaims::new(HashMap::from([( + "client_id".to_string(), + json!("client-123"), + )])), + Some(&iss), + ); + + let err = build_entity( + &schema, + "Workload", + &token, + "client_id", + Vec::new(), + HashSet::new(), + ) + .expect_err("should error due to entity not being in the schema"); + assert!( + matches!( + err, + BuildEntityError::EntityNotInSchema(ref type_name) + if type_name == "Workload" + ), + "expected EntityNotInSchema error but got: {:?}", + err + ); + } +} diff --git a/jans-cedarling/cedarling/src/authz/entity_builder/build_attrs.rs b/jans-cedarling/cedarling/src/authz/entity_builder/build_attrs.rs new file mode 100644 index 00000000000..e56471cdbb8 --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/entity_builder/build_attrs.rs @@ -0,0 +1,318 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use super::*; +use crate::{common::cedar_schema::cedar_json::entity_type::EntityType, jwt::Token}; +use cedar_policy::RestrictedExpression; +use serde_json::Value; +use std::collections::HashMap; + +/// Builds Cedar entity attributes using a JWT. +/// +/// This uses claim mapping metadata to unwrap claims into their respective Cedar types +pub fn build_entity_attrs_from_tkn( + schema: &CedarSchemaJson, + entity_type: &EntityType, + token: &Token, + claim_aliases: Vec, +) -> Result, BuildAttrError> { + let mut entity_attrs = HashMap::new(); + + let shape = match entity_type.shape.as_ref() { + Some(shape) => shape, + None => return Ok(entity_attrs), + }; + + let mut claims = token.claims_value().clone(); + apply_claim_aliases(&mut claims, claim_aliases); + + for (attr_name, attr) in shape.attrs.iter() { + let expression = if let Some(mapping) = token.claim_mapping().get(attr_name) { + let claim = claims.get(attr_name).ok_or_else(|| { + BuildAttrError::new( + attr_name, + BuildAttrErrorKind::MissingSource(attr_name.to_string()), + ) + })?; + let mapped_claim = mapping.apply_mapping(claim); + attr.build_expr(&mapped_claim, attr_name, schema) + .map_err(|err| BuildAttrError::new(attr_name, err.into()))? + } else { + match attr.build_expr(&claims, attr_name, schema) { + Ok(expr) => expr, + Err(err) if attr.is_required() => Err(BuildAttrError::new(attr_name, err.into()))?, + // silently fail when attribute isn't required + Err(_) => continue, + } + }; + + if let Some(expr) = expression { + entity_attrs.insert(attr_name.to_string(), expr); + } + } + + Ok(entity_attrs) +} + +pub fn build_entity_attrs_from_values( + schema: &CedarSchemaJson, + entity_type: &EntityType, + src: &HashMap, +) -> Result, BuildAttrError> { + let mut entity_attrs = HashMap::new(); + + let shape = match entity_type.shape.as_ref() { + Some(shape) => shape, + None => return Ok(entity_attrs), + }; + + for (attr_name, attr) in shape.attrs.iter() { + let val = match src.get(attr_name) { + Some(val) => val, + None if attr.is_required() => { + return Err(BuildAttrError::new( + attr_name, + BuildAttrErrorKind::MissingSource(attr_name.to_string()), + )); + }, + _ => continue, + }; + + let mapped_src = serde_json::from_value::>(val.clone()); + let src = if let Ok(mapped_src) = mapped_src.as_ref() { + mapped_src + } else { + src + }; + + let expression = match attr.build_expr(src, attr_name, schema) { + Ok(expr) => expr, + Err(err) if attr.is_required() => { + return Err(BuildAttrError::new(attr_name, err.into()))?; + }, + // move on to the next attribute if this isn't required + Err(_) => continue, + }; + + if let Some(expr) = expression { + entity_attrs.insert(attr_name.to_string(), expr); + } + } + + Ok(entity_attrs) +} + +/// Describes how to rename a claim named `from` to `to` +pub struct ClaimAliasMap<'a> { + from: &'a str, + to: &'a str, +} + +impl<'a> ClaimAliasMap<'a> { + pub fn new(from: &'a str, to: &'a str) -> Self { + Self { from, to } + } +} + +fn apply_claim_aliases(claims: &mut HashMap, aliases: Vec) { + for map in aliases { + if let Some(claim) = claims.get(map.from) { + claims.insert(map.to.to_string(), claim.clone()); + } + } +} + +#[derive(Debug, thiserror::Error)] +#[error("failed to build `{attr_name}` attribute: {source}")] +pub struct BuildAttrError { + attr_name: String, + #[source] + source: BuildAttrErrorKind, +} + +impl BuildAttrError { + fn new(name: impl ToString, src: BuildAttrErrorKind) -> Self { + Self { + attr_name: name.to_string(), + source: src, + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum BuildAttrErrorKind { + #[error("missing attribute source: `{0}`")] + MissingSource(String), + #[error("failed to build restricted expression: {0}")] + BuildExpression(#[from] BuildExprError), +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + common::{ + cedar_schema::cedar_json::{ + attribute::Attribute, + entity_type::{EntityShape, EntityType}, + }, + policy_store::TrustedIssuer, + }, + jwt::TokenClaims, + }; + use serde_json::json; + use std::collections::HashMap; + + #[test] + fn can_build_entity_attrs_from_tkn() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let entity_type = EntityType { + member_of: None, + tags: None, + shape: Some(EntityShape { + required: true, + attrs: HashMap::from([("client_id".to_string(), Attribute::string())]), + }), + }; + let iss = TrustedIssuer::default(); + let token = Token::new_access( + TokenClaims::new(HashMap::from([( + "client_id".to_string(), + json!("workload-123"), + )])), + Some(&iss), + ); + + let attrs = build_entity_attrs_from_tkn(&schema, &entity_type, &token, Vec::new()) + .expect("should build entity attrs"); + // RestrictedExpression does not implement PartialEq so the best we can do is check + // if the attribute was created + assert!( + attrs.contains_key("client_id"), + "there should be a `client_id` attribute" + ); + } + + #[test] + fn errors_when_tkn_missing_src() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let entity_type = EntityType { + member_of: None, + tags: None, + shape: Some(EntityShape { + required: true, + attrs: HashMap::from([("client_id".to_string(), Attribute::string())]), + }), + }; + let iss = TrustedIssuer::default(); + let token = Token::new_access(TokenClaims::new(HashMap::new()), Some(&iss)); + + let err = build_entity_attrs_from_tkn(&schema, &entity_type, &token, Vec::new()) + .expect_err("should error due to missing source"); + assert!( + matches!( + err, + BuildAttrError { + attr_name: ref name, + source: BuildAttrErrorKind::BuildExpression(BuildExprError::MissingSource(ref src_name))} + if name == "client_id" && + src_name == "client_id" + ), + "expected MissingSource error but got: {:?}", + err, + ); + } + + #[test] + fn can_build_entity_attrs_from_value() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let entity_type = EntityType { + member_of: None, + tags: None, + shape: Some(EntityShape { + required: true, + attrs: HashMap::from([("client_id".to_string(), Attribute::string())]), + }), + }; + let src_values = HashMap::from([("client_id".to_string(), json!("workload-123"))]); + + let attrs = build_entity_attrs_from_values(&schema, &entity_type, &src_values) + .expect("should build entity attrs"); + // RestrictedExpression does not implement PartialEq so the best we can do is check + // if the attribute was created + assert!( + attrs.contains_key("client_id"), + "there should be a `client_id` attribute" + ); + } + + #[test] + fn errors_when_values_missing_src() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let entity_type = EntityType { + member_of: None, + tags: None, + shape: Some(EntityShape { + required: true, + attrs: HashMap::from([("client_id".to_string(), Attribute::string())]), + }), + }; + let src_values = HashMap::new(); + + let err = build_entity_attrs_from_values(&schema, &entity_type, &src_values) + .expect_err("should error due to missing source"); + assert!( + matches!( + err, + BuildAttrError{ + attr_name: ref name, + source: BuildAttrErrorKind::MissingSource(ref src_name)} + if name == "client_id" && + src_name == "client_id"), + "expected MissingSource error but got: {:?}", + err, + ); + } +} diff --git a/jans-cedarling/cedarling/src/authz/entity_builder/build_expr.rs b/jans-cedarling/cedarling/src/authz/entity_builder/build_expr.rs new file mode 100644 index 00000000000..2f8c67049db --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/entity_builder/build_expr.rs @@ -0,0 +1,586 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use crate::common::cedar_schema::cedar_json::CedarSchemaJson; +use crate::common::cedar_schema::cedar_json::attribute::Attribute; +use cedar_policy::{ + EntityId, EntityTypeName, EntityUid, ExpressionConstructionError, ParseErrors, + RestrictedExpression, +}; +use serde_json::Value; +use std::collections::HashMap; +use std::str::FromStr; + +use super::CEDAR_NAMESPACE_SEPARATOR; + +impl Attribute { + pub fn kind_str(&self) -> &str { + match self { + Attribute::String { .. } => "String", + Attribute::Long { .. } => "Long", + Attribute::Boolean { .. } => "Boolean", + Attribute::Record { .. } => "Record", + Attribute::Set { .. } => "Set", + Attribute::Entity { .. } => "Entity", + Attribute::Extension { .. } => "Extension", + Attribute::EntityOrCommon { .. } => "EntityOrCommon", + } + } + + /// Builds a [`RestrictedExpression`] while checking the schema + pub fn build_expr( + &self, + attr_src: &HashMap, + src_key: &str, + schema: &CedarSchemaJson, + ) -> Result, BuildExprError> { + match self { + // Handle String attributes + Attribute::String { required } => { + if let Some(claim) = attr_src.get(src_key) { + let claim = claim + .as_str() + .ok_or(KeyedJsonTypeError::type_mismatch(src_key, "string", claim))? + .to_string(); + Ok(Some(RestrictedExpression::new_string(claim))) + } else if *required { + Err(BuildExprError::MissingSource(src_key.to_string())) + } else { + Ok(None) + } + }, + + // Handle Long attributes + Attribute::Long { required } => { + if let Some(claim) = attr_src.get(src_key) { + let claim = claim + .as_i64() + .ok_or(KeyedJsonTypeError::type_mismatch(src_key, "number", claim))?; + Ok(Some(RestrictedExpression::new_long(claim))) + } else if *required { + Err(BuildExprError::MissingSource(src_key.to_string())) + } else { + Ok(None) + } + }, + + // Handle Boolean attributes + Attribute::Boolean { required } => { + if let Some(claim) = attr_src.get(src_key) { + let claim = claim + .as_bool() + .ok_or(KeyedJsonTypeError::type_mismatch(src_key, "bool", claim))?; + Ok(Some(RestrictedExpression::new_bool(claim))) + } else if *required { + Err(BuildExprError::MissingSource(src_key.to_string())) + } else { + Ok(None) + } + }, + + // Handle Record attributes + Attribute::Record { attrs, required } => { + let mut fields = HashMap::new(); + for (name, kind) in attrs.iter() { + if let Some(expr) = kind.build_expr(attr_src, name, schema)? { + fields.insert(name.to_string(), expr); + } + } + + if fields.is_empty() && !required { + Ok(None) + } else { + Ok(Some(RestrictedExpression::new_record(fields)?)) + } + }, + + // Handle Set attributes + Attribute::Set { required, element } => { + if let Some(claim) = attr_src.get(src_key) { + let claim = claim + .as_array() + .ok_or(KeyedJsonTypeError::type_mismatch(src_key, "array", claim))?; + + let mut values = Vec::new(); + for (i, val) in claim.iter().enumerate() { + let claim_name = i.to_string(); + if let Some(expr) = element.build_expr( + &HashMap::from([(claim_name.clone(), val.clone())]), + &claim_name, + schema, + )? { + values.push(expr); + } + } + Ok(Some(RestrictedExpression::new_set(values))) + } else if *required { + Err(BuildExprError::MissingSource(src_key.to_string())) + } else { + Ok(None) + } + }, + + // Handle Entity attributes + Attribute::Entity { required, name } => { + if let Some(claim) = attr_src.get(src_key) { + let claim = claim + .as_str() + .ok_or(KeyedJsonTypeError::type_mismatch(src_key, "string", claim))?; + + let mut name = name.to_string(); + if let Some((namespace, _)) = schema.get_entity_from_base_name(&name) { + if !namespace.is_empty() { + name = [namespace, name.as_str()].join(CEDAR_NAMESPACE_SEPARATOR); + } + } else if *required { + return Err(BuildExprError::EntityNotInSchema(name.to_string())); + } else { + return Ok(None); + } + + let type_name = EntityTypeName::from_str(&name) + .map_err(|e| BuildExprError::ParseEntityTypeName(name, e))?; + let type_id = EntityId::new(claim); + let uid = EntityUid::from_type_name_and_id(type_name, type_id); + Ok(Some(RestrictedExpression::new_entity_uid(uid))) + } else if *required { + Err(BuildExprError::MissingSource(src_key.to_string())) + } else { + Ok(None) + } + }, + + // Handle Extension attributes + Attribute::Extension { required, name } => { + if let Some(claim) = attr_src.get(src_key) { + let claim = claim + .as_str() + .ok_or(KeyedJsonTypeError::type_mismatch(src_key, "string", claim))?; + let expr = match name.as_str() { + "ipaddr" => RestrictedExpression::new_ip(claim), + "decimal" => RestrictedExpression::new_decimal(claim), + name => RestrictedExpression::new_unknown(name), + }; + Ok(Some(expr)) + } else if *required { + Err(BuildExprError::MissingSource(src_key.to_string())) + } else { + Ok(None) + } + }, + + // Handle EntityOrCommon attributes + Attribute::EntityOrCommon { required, name } => { + if let Some((_namespace_name, attr)) = schema.get_common_type(name) { + attr.build_expr(attr_src, src_key, schema) + } else if schema.get_entity_from_base_name(name).is_some() { + let attr = Attribute::Entity { + required: *required, + name: name.to_string(), + }; + attr.build_expr(attr_src, src_key, schema) + } else if let Some(attr) = str_to_primitive_type(*required, name) { + attr.build_expr(attr_src, src_key, schema) + } else if *required { + Err(BuildExprError::UnkownType(name.to_string())) + } else { + Ok(None) + } + }, + } + } +} + +fn str_to_primitive_type(required: bool, name: &str) -> Option { + let primitive_type = match name { + "String" => Attribute::String { required }, + "Long" => Attribute::Long { required }, + "Boolean" => Attribute::Boolean { required }, + _ => return None, + }; + Some(primitive_type) +} + +/// Errors when building a [`RestrictedExpression`] +#[derive(Debug, thiserror::Error)] +pub enum BuildExprError { + #[error("the given attribute source data is missing the key: {0}")] + MissingSource(String), + #[error(transparent)] + TypeMismatch(#[from] KeyedJsonTypeError), + #[error(transparent)] + ConstructionError(#[from] ExpressionConstructionError), + #[error("the type of `{0}` could not be determined")] + UnkownType(String), + #[error("the entity type `{0}` is not in the schema")] + EntityNotInSchema(String), + #[error("failed to parse entity type name \"{0}\": {1}")] + ParseEntityTypeName(String, ParseErrors), +} + +#[derive(Debug, thiserror::Error)] +#[error("type mismatch for key '{key}'. expected: '{expected_type}', but found: '{actual_type}'")] +pub struct KeyedJsonTypeError { + pub key: String, + pub expected_type: String, + pub actual_type: String, +} + +impl KeyedJsonTypeError { + /// Returns the JSON type name of the given value. + pub fn value_type_name(value: &Value) -> &'static str { + match value { + Value::Null => "null", + Value::Bool(_) => "bool", + Value::Number(_) => "number", + Value::String(_) => "string", + Value::Array(_) => "array", + Value::Object(_) => "object", + } + } + + /// Constructs a `TypeMismatch` error with detailed information about the expected and actual types. + pub fn type_mismatch(key: &str, expected_type_name: &str, got_value: &Value) -> Self { + let got_value_type_name = Self::value_type_name(got_value).to_string(); + + Self { + key: key.to_string(), + expected_type: expected_type_name.to_string(), + actual_type: got_value_type_name, + } + } +} + +#[cfg(test)] +mod test { + use crate::{ + authz::entity_builder::BuildExprError, + common::cedar_schema::cedar_json::{CedarSchemaJson, attribute::Attribute}, + }; + use serde_json::json; + use std::collections::HashMap; + + #[test] + fn can_build_string_expr() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::string(); + let src = HashMap::from([("src_key".to_string(), json!("attr-val"))]); + let expr = attr + .build_expr(&src, "src_key", &schema) + .expect("should not error"); + assert!(expr.is_some(), "a restricted expression should be built") + } + + #[test] + fn can_build_long_expr() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { "type": "Long" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::long(); + let src = HashMap::from([("src_key".to_string(), json!(123))]); + let expr = attr + .build_expr(&src, "src_key", &schema) + .expect("should not error"); + assert!(expr.is_some(), "a restricted expression should be built") + } + + #[test] + fn can_build_boolean_expr() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { "type": "Boolean" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::boolean(); + let src = HashMap::from([("src_key".to_string(), json!(true))]); + let expr = attr + .build_expr(&src, "src_key", &schema) + .expect("should not error"); + assert!(expr.is_some(), "a restricted expression should be built") + } + + #[test] + fn can_build_record_expr() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "outer_attr": { + "type": "Record", + "attributes": { + "inner_attr": { "type": "String" } + }, + }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::record(HashMap::from([( + "inner_attr".to_string(), + Attribute::string(), + )])); + let src = HashMap::from([("inner_attr".to_string(), json!("test"))]); + let expr = attr + .build_expr(&src, "src_key", &schema) + .expect("should not error"); + assert!(expr.is_some(), "a restricted expression should be built") + } + + #[test] + fn can_build_set_expr() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { + "type": "Set", + "element": { + "type": "String", + } + }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::set(Attribute::string()); + let src = HashMap::from([("src_key".to_string(), json!(["admin", "user"]))]); + let expr = attr + .build_expr(&src, "src_key", &schema) + .expect("should not error"); + assert!(expr.is_some(), "a restricted expression should be built") + } + + #[test] + fn errors_when_expected_set_has_different_types() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { + "type": "Set", + "element": { + "type": "String", + } + }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::set(Attribute::string()); + let src = HashMap::from([("src_key".to_string(), json!(["admin", 123]))]); + let err = attr + .build_expr(&src, "src_key", &schema) + .expect_err("should error"); + assert!( + matches!(err, BuildExprError::TypeMismatch(_)), + "should error due to type mismatch but got: {:?}", + err + ); + } + + #[test] + fn can_build_entity_expr() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { + "OtherEntity": {}, + "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { + "type": "Entity", + "name": "OtherEntity", + }, + }, + } + } + }} + })) + .expect("should successfully build schema"); + let attr = Attribute::entity("OtherEntity"); + let src = HashMap::from([("src_key".to_string(), json!("test"))]); + let expr = attr + .build_expr(&src, "src_key", &schema) + .expect("should not error"); + assert!(expr.is_some(), "a restricted expression should be built"); + } + + #[test] + fn can_build_entity_expr_from_entity_or_common() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { + "OtherEntity": {}, + "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { + "type": "EntityOrCommon", + "name": "OtherEntity", + }, + }, + } + } + }} + })) + .expect("should successfully build schema"); + let attr = Attribute::entity("OtherEntity"); + let src = HashMap::from([("src_key".to_string(), json!("test"))]); + let expr = attr + .build_expr(&src, "src_key", &schema) + .expect("should not error"); + assert!(expr.is_some(), "a restricted expression should be built"); + } + + #[test] + fn errors_when_entity_isnt_in_schema() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { + "type": "Entity", + "name": "OtherEntity", + }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::entity("OtherEntity"); + let src = HashMap::from([("src_key".to_string(), json!("test"))]); + let err = attr + .build_expr(&src, "src_key", &schema) + .expect_err("should error"); + assert!( + matches!( + err, + BuildExprError::EntityNotInSchema(ref entity_name) + if entity_name == "OtherEntity" + ), + "should error due to type mismatch but got: {:?}", + err + ); + } + + #[test] + fn can_build_ip_addr_extension_expr() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { "type": "Extension", "name": "ipaddr" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::string(); + let src = HashMap::from([("src_key".to_string(), json!("0.0.0.0"))]); + let expr = attr + .build_expr(&src, "src_key", &schema) + .expect("should not error"); + assert!(expr.is_some(), "a restricted expression should be built") + } + + #[test] + fn can_build_decimal_extension_expr() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { "type": "Extension", "name": "decimal" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::string(); + let src = HashMap::from([("src_key".to_string(), json!("1.1"))]); + let expr = attr + .build_expr(&src, "src_key", &schema) + .expect("should not error"); + assert!(expr.is_some(), "a restricted expression should be built") + } + + #[test] + fn can_skip_non_required_expr() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::String { required: false }; + let src = HashMap::new(); + let expr = attr + .build_expr(&src, "client_id", &schema) + .expect("should not error"); + assert!(expr.is_none(), "a restricted expression shouldn't built") + } + + #[test] + fn errors_on_type_mismatch() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Test": { + "shape": { + "type": "Record", + "attributes": { + "attr1": { "type": "String" }, + }, + } + }}} + })) + .expect("should successfully build schema"); + let attr = Attribute::string(); + let src = HashMap::from([("src_key".to_string(), json!(123))]); + let err = attr + .build_expr(&src, "src_key", &schema) + .expect_err("should error"); + assert!( + matches!(err, BuildExprError::TypeMismatch(_)), + "should error due to type mismatch but got: {:?}", + err + ); + } +} diff --git a/jans-cedarling/cedarling/src/authz/entity_builder/build_resource_entity.rs b/jans-cedarling/cedarling/src/authz/entity_builder/build_resource_entity.rs new file mode 100644 index 00000000000..6878749b1c1 --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/entity_builder/build_resource_entity.rs @@ -0,0 +1,331 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use build_attrs::build_entity_attrs_from_values; +use cedar_policy::{EntityAttrEvaluationError, ExpressionConstructionError, ParseErrors}; +use serde_json::Value; + +use super::*; +use crate::ResourceData; + +impl EntityBuilder { + pub fn build_resource_entity( + &self, + resource: &ResourceData, + ) -> Result { + let entity_type_name = EntityTypeName::from_str(&resource.resource_type)?; + let (_namespace_name, entity_type) = self + .schema + .get_entity_from_base_name(entity_type_name.basename()) + .ok_or(BuildEntityError::EntityNotInSchema( + entity_type_name.to_string(), + ))?; + + let entity_attrs = + build_entity_attrs_from_values(&self.schema, entity_type, &resource.payload)?; + + // Build cedar entity + let entity_id = + EntityId::from_str(&resource.id).map_err(BuildEntityError::ParseEntityId)?; + let entity_uid = EntityUid::from_type_name_and_id(entity_type_name, entity_id); + Ok(Entity::new(entity_uid, entity_attrs, HashSet::new())?) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum BuildResourceEntityError { + #[error(transparent)] + BuildEntity(#[from] BuildEntityError), + #[error(transparent)] + TypeMismatch(#[from] JsonTypeError), + #[error(transparent)] + ExpressionConstructExpression(#[from] ExpressionConstructionError), + #[error(transparent)] + EntityAttrEvaluationError(#[from] EntityAttrEvaluationError), + #[error(transparent)] + BuildAttr(#[from] BuildAttrError), + #[error("invalid entity name: {0}")] + InvalidEntityName(#[from] ParseErrors), +} + +#[derive(Debug, thiserror::Error, PartialEq)] +#[error("JSON value type mismatch: expected '{expected_type}', but found '{actual_type}'")] +pub struct JsonTypeError { + pub expected_type: String, + pub actual_type: String, +} + +impl JsonTypeError { + /// Returns the JSON type name of the given value. + pub fn value_type_name(value: &Value) -> String { + match value { + Value::Null => "null".to_string(), + Value::Bool(_) => "bool".to_string(), + Value::Number(_) => "number".to_string(), + Value::String(_) => "string".to_string(), + Value::Array(_) => "array".to_string(), + Value::Object(_) => "object".to_string(), + } + } + + /// Constructs a `TypeMismatch` error with detailed information about the expected and actual types. + pub fn type_mismatch(expected_type_name: &str, got_value: &Value) -> Self { + let got_value_type_name = Self::value_type_name(got_value); + + Self { + expected_type: expected_type_name.to_string(), + actual_type: got_value_type_name, + } + } +} + +#[cfg(test)] +mod test { + use super::super::*; + use super::*; + use crate::common::cedar_schema::cedar_json::CedarSchemaJson; + use cedar_policy::EvalResult; + use serde_json::json; + + #[test] + fn can_build_entity() { + let schema = serde_json::from_value::(json!({ + "Jans": { + "commonTypes": { + "Url": { + "type": "Record", + "attributes": { + "host": { "type": "String" }, + "path": { "type": "String" }, + "protocol": { "type": "String" }, + }, + }, + }, + "entityTypes": { + "Role": {}, + "HttpRequest": { + "shape": { + "type": "Record", + "attributes": { + "header": { + "type": "Record", + "attributes": { + "Accept": { "type": "EntityOrCommon", "name": "String" }, + }, + }, + "url": { "type": "EntityOrCommon", "name": "Url" }, + }, + } + } + } + } + })) + .expect("should successfully create test schema"); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let resource_data = ResourceData { + resource_type: "HttpRequest".to_string(), + id: "request-123".to_string(), + payload: HashMap::from([ + ("header".to_string(), json!({"Accept": "test"})), + ( + "url".to_string(), + json!({"host": "protected.host", "protocol": "http", "path": "/protected"}), + ), + ]), + }; + let entity = builder + .build_resource_entity(&resource_data) + .expect("expected to build resource entity"); + + let url = entity + .attr("url") + .expect("entity must have an `url` attribute") + .unwrap(); + if let EvalResult::Record(ref record) = url { + assert_eq!(record.len(), 3); + assert_eq!( + record + .get("host") + .expect("expected `url` to have a `host` attribute"), + &EvalResult::String("protected.host".to_string()) + ); + assert_eq!( + record + .get("protocol") + .expect("expected `url` to have a `domain` attribute"), + &EvalResult::String("http".to_string()) + ); + assert_eq!( + record + .get("path") + .expect("expected `url` to have a `path` attribute"), + &EvalResult::String("/protected".to_string()) + ); + } else { + panic!( + "expected the attribute `url` to be a record, got: {:?}", + url + ); + } + + let header = entity + .attr("header") + .expect("entity must have an `header` attribute") + .unwrap(); + if let EvalResult::Record(ref record) = header { + assert_eq!(record.len(), 1); + assert_eq!( + record + .get("Accept") + .expect("expected `url` to have an `Accept` attribute"), + &EvalResult::String("test".to_string()) + ); + } else { + panic!( + "expected the attribute `header` to be a record, got: {:?}", + header + ); + } + } + + #[test] + fn can_build_entity_with_optional_attr() { + let schema = serde_json::from_value::(json!({ + "Jans": { + "commonTypes": { + "Url": { + "type": "Record", + "attributes": { + "host": { "type": "String" }, + "path": { "type": "String" }, + "protocol": { "type": "String" }, + }, + }, + }, + "entityTypes": { + "Role": {}, + "HttpRequest": { + "shape": { + "type": "Record", + "attributes": { + "url": { "type": "EntityOrCommon", "name": "Url", "required": false}, + }, + } + } + } + } + })) + .expect("should successfully create test schema"); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let resource_data = ResourceData { + resource_type: "HttpRequest".to_string(), + id: "request-123".to_string(), + payload: HashMap::new(), + }; + let entity = builder + .build_resource_entity(&resource_data) + .expect("expected to build resource entity"); + + assert!( + entity.attr("url").is_none(), + "entity should not have a `url` attribute" + ); + } + + #[test] + fn can_build_entity_with_optional_record_attr() { + let schema = serde_json::from_value::(json!({ + "Jans": { + "commonTypes": { + "Url": { + "type": "Record", + "attributes": { + "host": { "type": "String" }, + "path": { "type": "String" }, + "protocol": { "type": "String" }, + }, + }, + }, + "entityTypes": { + "Role": {}, + "HttpRequest": { + "shape": { + "type": "Record", + "attributes": { + "header": { + "type": "Record", + "attributes": { + "Accept": { "type": "EntityOrCommon", "name": "String", "required": false }, + }, + }, + "url": { "type": "EntityOrCommon", "name": "Url" }, + }, + } + } + } + } + })) + .expect("should successfully create test schema"); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let resource_data = ResourceData { + resource_type: "HttpRequest".to_string(), + id: "request-123".to_string(), + payload: HashMap::from([ + ( + "url".to_string(), + json!({"host": "protected.host", "protocol": "http", "path": "/protected"}), + ), + ("header".to_string(), json!({})), + ]), + }; + let entity = builder + .build_resource_entity(&resource_data) + .expect("expected to build resource entity"); + + let url = entity + .attr("url") + .expect("entity must have an `url` attribute") + .unwrap(); + if let EvalResult::Record(ref record) = url { + assert_eq!(record.len(), 3); + assert_eq!( + record + .get("host") + .expect("expected `url` to have a `host` attribute"), + &EvalResult::String("protected.host".to_string()) + ); + assert_eq!( + record + .get("protocol") + .expect("expected `url` to have a `domain` attribute"), + &EvalResult::String("http".to_string()) + ); + assert_eq!( + record + .get("path") + .expect("expected `url` to have a `path` attribute"), + &EvalResult::String("/protected".to_string()) + ); + } else { + panic!( + "expected the attribute `url` to be a record, got: {:?}", + url + ); + } + + let header = entity + .attr("header") + .expect("entity must have an `header` attribute") + .unwrap(); + if let EvalResult::Record(ref record) = header { + assert_eq!(record.len(), 0, "the header attribute must be empty"); + } else { + panic!( + "expected the attribute `header` to be a record, got: {:?}", + header + ); + } + } +} diff --git a/jans-cedarling/cedarling/src/authz/entity_builder/build_role_entity.rs b/jans-cedarling/cedarling/src/authz/entity_builder/build_role_entity.rs new file mode 100644 index 00000000000..0be6ba7a284 --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/entity_builder/build_role_entity.rs @@ -0,0 +1,286 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use super::*; +use cedar_policy::{EntityId, EntityTypeName, EntityUid}; +use serde::Deserialize; + +#[derive(Debug, serde::Deserialize)] +#[serde(untagged)] +enum UnifyClaims { + Single(String), + Multiple(Vec), +} + +impl UnifyClaims { + fn iter<'a>(&'a self) -> Box + 'a> { + match self { + Self::Single(ref v) => Box::new(std::iter::once(v)), + Self::Multiple(ref vs) => Box::new(vs.iter()), + } + } +} + +impl EntityBuilder { + /// Tries to build role entities using each given token. Will return an empty Vec + /// if no entities were created. + pub fn try_build_role_entities( + &self, + tokens: &DecodedTokens, + ) -> Result, BuildRoleEntityError> { + // Get entity namespace and type + let mut entity_name = self.entity_names.role.to_string(); + if let Some((namespace, _entity_type)) = self.schema.get_entity_from_base_name(&entity_name) + { + if !namespace.is_empty() { + entity_name = [namespace.as_str(), &entity_name].join(CEDAR_NAMESPACE_SEPARATOR); + } + } + + let mut entities = HashMap::new(); + + let token_refs = [ + tokens.userinfo.as_ref(), + tokens.id.as_ref(), + tokens.access.as_ref(), + ]; + for token in token_refs.into_iter().flatten() { + let role_claim = token.role_mapping(); + if let Some(claim) = token.get_claim(role_claim).as_ref() { + let unified_claims = UnifyClaims::deserialize(claim.value()); + let claim_role_name_iter = match unified_claims { + Ok(ref unified_claims) => unified_claims.iter(), + Err(_) => { + return Err(BuildRoleEntityError::map_tkn_err( + token, + BuildEntityError::TokenClaimTypeMismatch( + TokenClaimTypeError::type_mismatch( + role_claim, + "String or Array", + claim.value(), + ), + ), + )) + }, + }; + + for claim_role_name in claim_role_name_iter { + if !entities.contains_key(claim_role_name) { + let entity = build_entity(&entity_name, claim_role_name) + .map_err(|e| BuildRoleEntityError::map_tkn_err(token, e))?; + entities.insert(claim_role_name.clone(), entity); + } + } + } + } + + Ok(entities.into_values().collect()) + } +} + +fn build_entity(name: &str, id: &str) -> Result { + let name = EntityTypeName::from_str(name).map_err(BuildEntityError::ParseEntityTypeName)?; + let id = EntityId::from_str(id).map_err(BuildEntityError::ParseEntityId)?; + let uid = EntityUid::from_type_name_and_id(name, id); + let entity = Entity::new(uid, HashMap::new(), HashSet::new())?; + Ok(entity) +} + +#[derive(Debug, thiserror::Error)] +pub enum BuildRoleEntityError { + #[error("failed to build role entity from access token: {0}")] + Access(#[source] BuildEntityError), + #[error("failed to build role entity from id token: {0}")] + Id(#[source] BuildEntityError), + #[error("failed to build role entity from userinfo token: {0}")] + Userinfo(#[source] BuildEntityError), +} + +impl BuildRoleEntityError { + pub fn map_tkn_err(token: &Token, err: BuildEntityError) -> Self { + match token.kind { + TokenKind::Access => BuildRoleEntityError::Access(err), + TokenKind::Id => BuildRoleEntityError::Id(err), + TokenKind::Userinfo => BuildRoleEntityError::Userinfo(err), + TokenKind::Transaction => unimplemented!("transaction tokens are not yet supported"), + } + } +} + +#[cfg(test)] +mod test { + use super::super::*; + use crate::common::cedar_schema::cedar_json::CedarSchemaJson; + use crate::common::policy_store::TrustedIssuer; + use crate::jwt::{Token, TokenClaims}; + use serde_json::json; + use std::collections::HashMap; + + fn test_schema() -> CedarSchemaJson { + serde_json::from_value::(json!({ + "Jans": { + "entityTypes": { + "Role": {}, + "User": { + "memberOfTypes": ["Role"], + "shape": { + "type": "Record", + "attributes": {}, + } + }}} + })) + .expect("should successfully create test schema") + } + + fn test_build_entity_from_str_claim(tokens: DecodedTokens) { + let schema = test_schema(); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let entity = builder + .try_build_role_entities(&tokens) + .expect("expected to build role entities"); + + assert_eq!(entity.len(), 1); + assert_eq!(entity[0].uid().to_string(), "Jans::Role::\"admin\""); + } + + #[test] + fn can_build_using_userinfo_tkn_vec_claim() { + let iss = TrustedIssuer::default(); + let userinfo_token = Token::new_userinfo( + TokenClaims::new(HashMap::from([( + "role".to_string(), + json!(["admin", "user"]), + )])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: None, + id: None, + userinfo: Some(userinfo_token), + }; + let schema = test_schema(); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let entity = builder + .try_build_role_entities(&tokens) + .expect("expected to build role entities"); + + assert_eq!(entity.len(), 2); + let entity_uids = entity + .iter() + .map(|e| e.uid().to_string()) + .collect::>(); + assert_eq!( + entity_uids, + HashSet::from(["Jans::Role::\"admin\"", "Jans::Role::\"user\""].map(|s| s.to_string())) + ); + } + + #[test] + fn can_build_using_userinfo_tkn_string_claim() { + let iss = TrustedIssuer::default(); + let userinfo_token = Token::new_userinfo( + TokenClaims::new(HashMap::from([("role".to_string(), json!("admin"))])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: None, + id: None, + userinfo: Some(userinfo_token), + }; + test_build_entity_from_str_claim(tokens); + } + + #[test] + fn can_build_using_id_tkn() { + let iss = TrustedIssuer::default(); + let id_token = Token::new_id( + TokenClaims::new(HashMap::from([("role".to_string(), json!("admin"))])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: None, + id: Some(id_token), + userinfo: None, + }; + test_build_entity_from_str_claim(tokens); + } + + #[test] + fn can_build_using_access_tkn() { + let iss = TrustedIssuer::default(); + let access_token = Token::new_access( + TokenClaims::new(HashMap::from([("role".to_string(), json!("admin"))])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: Some(access_token), + id: None, + userinfo: None, + }; + test_build_entity_from_str_claim(tokens); + } + + #[test] + fn ignores_duplicate_roles() { + let iss = TrustedIssuer::default(); + let access_token = Token::new_access( + TokenClaims::new(HashMap::from([("role".to_string(), json!("admin"))])), + Some(&iss), + ); + let id_token = Token::new_id( + TokenClaims::new(HashMap::from([("role".to_string(), json!("admin"))])), + Some(&iss), + ); + let userinfo_token = Token::new_userinfo( + TokenClaims::new(HashMap::from([("role".to_string(), json!("admin"))])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: Some(access_token), + id: Some(id_token), + userinfo: Some(userinfo_token), + }; + test_build_entity_from_str_claim(tokens); + } + + #[test] + fn can_create_multiple_different_roles_from_different_tokens() { + let iss = TrustedIssuer::default(); + let schema = test_schema(); + let access_token = Token::new_access( + TokenClaims::new(HashMap::from([("role".to_string(), json!("role1"))])), + Some(&iss), + ); + let id_token = Token::new_id( + TokenClaims::new(HashMap::from([("role".to_string(), json!("role2"))])), + Some(&iss), + ); + let userinfo_token = Token::new_userinfo( + TokenClaims::new(HashMap::from([( + "role".to_string(), + json!(["role3", "role4"]), + )])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: Some(access_token), + id: Some(id_token), + userinfo: Some(userinfo_token), + }; + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let entities = builder + .try_build_role_entities(&tokens) + .expect("expected to build role entities"); + + let entities = entities + .iter() + .map(|e| e.uid().to_string()) + .collect::>(); + let expected_entities = (1..=4) + .map(|x| format!("Jans::Role::\"role{}\"", x)) + .collect::>(); + assert_eq!(entities, expected_entities); + } +} diff --git a/jans-cedarling/cedarling/src/authz/entity_builder/build_token_entities.rs b/jans-cedarling/cedarling/src/authz/entity_builder/build_token_entities.rs new file mode 100644 index 00000000000..38be043fc10 --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/entity_builder/build_token_entities.rs @@ -0,0 +1,368 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use super::*; + +const DEFAULT_TKN_PRINCIPAL_IDENTIFIER: &str = "jti"; + +impl EntityBuilder { + pub fn build_access_tkn_entity(&self, token: &Token) -> Result { + if token.kind != TokenKind::Access { + return Err(BuildTokenEntityError { + token_kind: TokenKind::Access, + err: BuildEntityError::InvalidToken(token.kind, TokenKind::Access), + }); + } + let entity_name = self.entity_names.access_token.as_ref(); + self.build_tkn_entity(entity_name, token) + } + + pub fn build_id_tkn_entity(&self, token: &Token) -> Result { + if token.kind != TokenKind::Id { + return Err(BuildTokenEntityError { + token_kind: TokenKind::Id, + err: BuildEntityError::InvalidToken(token.kind, TokenKind::Id), + }); + } + let entity_name = self.entity_names.id_token.as_ref(); + self.build_tkn_entity(entity_name, token) + } + + pub fn build_userinfo_tkn_entity( + &self, + token: &Token, + ) -> Result { + if token.kind != TokenKind::Userinfo { + return Err(BuildTokenEntityError { + token_kind: TokenKind::Userinfo, + err: BuildEntityError::InvalidToken(token.kind, TokenKind::Userinfo), + }); + } + let entity_name = self.entity_names.userinfo_token.as_ref(); + self.build_tkn_entity(entity_name, token) + } + + fn build_tkn_entity( + &self, + entity_name: &str, + token: &Token, + ) -> Result { + let id_src_claim = token + .metadata() + .principal_identifier + .as_deref() + .unwrap_or(DEFAULT_TKN_PRINCIPAL_IDENTIFIER); + build_entity( + &self.schema, + entity_name, + token, + id_src_claim, + vec![], + HashSet::new(), + ) + .map_err(|err| BuildTokenEntityError { + token_kind: token.kind, + err, + }) + } +} + +#[derive(Debug, thiserror::Error)] +#[error("failed to create {token_kind} token entity: {err}")] +pub struct BuildTokenEntityError { + pub token_kind: TokenKind, + pub err: BuildEntityError, +} + +impl BuildTokenEntityError { + pub fn access_tkn_unavailable() -> Self { + Self { + token_kind: TokenKind::Access, + err: BuildEntityError::TokenUnavailable, + } + } + + pub fn id_tkn_unavailable() -> Self { + Self { + token_kind: TokenKind::Id, + err: BuildEntityError::TokenUnavailable, + } + } + + pub fn userinfo_tkn_unavailable() -> Self { + Self { + token_kind: TokenKind::Userinfo, + err: BuildEntityError::TokenUnavailable, + } + } +} + +#[cfg(test)] +mod test { + use super::super::*; + use crate::common::cedar_schema::cedar_json::CedarSchemaJson; + use crate::common::policy_store::{ClaimMappings, TokenEntityMetadata, TrustedIssuer}; + use crate::jwt::{Token, TokenClaims}; + use cedar_policy::EvalResult; + use serde_json::json; + use std::collections::HashMap; + use test_utils::assert_eq; + + fn test_schema() -> CedarSchemaJson { + serde_json::from_value::(json!({ + "Jans": { + "commonTypes": { + "Url": { + "type": "Record", + "attributes": { + "scheme": { "type": "String" }, + "path": { "type": "String" }, + "domain": { "type": "String" }, + }, + }, + }, + "entityTypes": { + "Access_token": { + "shape": { + "type": "Record", + "attributes": { + "jti": { "type": "String" }, + "trusted_issuer": { "type": "EntityOrCommon", "name": "TrustedIssuer" }, + }, + } + }, + "id_token": { + "shape": { + "type": "Record", + "attributes": { + "jti": { "type": "String" }, + "trusted_issuer": { "type": "EntityOrCommon", "name": "TrustedIssuer" }, + }, + } + }, + "Userinfo_token": { + "shape": { + "type": "Record", + "attributes": { + "jti": { "type": "String" }, + "trusted_issuer": { "type": "EntityOrCommon", "name": "TrustedIssuer" }, + }, + } + }, + "TrustedIssuer": { + "shape": { + "type": "Record", + "attributes": { + "issuer_entity_id": { "type": "EntityOrCommon", "name": "Url" } + }, + } + } + } + }})) + .expect("should deserialize schema") + } + + fn test_issusers() -> HashMap { + let token_entity_metadata = TokenEntityMetadata { + claim_mapping: serde_json::from_value::(json!({ + "url": { + "parser": "regex", + "type": "Jans::Url", + "regex_expression": r#"^(?P[a-zA-Z][a-zA-Z0-9+.-]*):\/\/(?P[^\/]+)(?P\/.*)?$"#, + "SCHEME": {"attr": "scheme", "type": "String"}, + "DOMAIN": {"attr": "domain", "type": "String"}, + "PATH": {"attr": "path", "type": "String"} + } + })) + .unwrap(), + ..Default::default() + }; + let iss = TrustedIssuer { + access_tokens: token_entity_metadata.clone(), + id_tokens: token_entity_metadata.clone(), + userinfo_tokens: token_entity_metadata, + ..Default::default() + }; + let issuers = HashMap::from([("test_iss".into(), iss.clone())]); + issuers + } + + fn test_build_entity(tkn_entity_type_name: &str, token: Token, build_tkn_entity_fn: F) + where + F: FnOnce(&Token) -> Result, + { + let entity = + build_tkn_entity_fn(&token).expect("expected to successfully build token entity"); + + assert_eq!( + entity.uid().to_string(), + format!("Jans::{}::\"tkn-123\"", tkn_entity_type_name) + ); + + assert_eq!( + entity + .attr("jti") + .expect("expected entity to have a `jti` attribute") + .unwrap(), + EvalResult::String("tkn-123".to_string()), + ); + + let trusted_iss = entity + .attr("trusted_issuer") + .expect("expected entity to have a `trusted_issuer` attribute") + .unwrap(); + if let EvalResult::EntityUid(ref uid) = trusted_iss { + assert_eq!(uid.type_name().basename(), "TrustedIssuer"); + assert_eq!( + uid.id().escaped(), + "https://some-iss.com/.well-known/openid-configuration" + ); + } else { + panic!( + "expected the attribute `trusted_issuer` to be an EntityUid, got: {:?}", + trusted_iss + ); + } + } + + #[test] + fn can_build_access_tkn_entity() { + let schema = test_schema(); + let issuers = test_issusers(); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let access_token = Token::new_access( + TokenClaims::new(HashMap::from([ + ("jti".to_string(), json!("tkn-123")), + ( + "trusted_issuer".to_string(), + json!("https://some-iss.com/.well-known/openid-configuration"), + ), + ])), + Some(&issuers.get("test_iss").unwrap()), + ); + test_build_entity("Access_token", access_token, |tkn| { + builder.build_access_tkn_entity(tkn) + }); + } + + #[test] + fn can_build_id_tkn_entity() { + let schema = test_schema(); + let issuers = test_issusers(); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let id_token = Token::new_id( + TokenClaims::new(HashMap::from([ + ("jti".to_string(), json!("tkn-123")), + ( + "trusted_issuer".to_string(), + json!("https://some-iss.com/.well-known/openid-configuration"), + ), + ])), + Some(&issuers.get("test_iss").unwrap()), + ); + test_build_entity("id_token", id_token, |tkn| builder.build_id_tkn_entity(tkn)); + } + + #[test] + fn can_build_userinfo_tkn_entity() { + let schema = test_schema(); + let issuers = test_issusers(); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let userinfo_token = Token::new_userinfo( + TokenClaims::new(HashMap::from([ + ("jti".to_string(), json!("tkn-123")), + ( + "trusted_issuer".to_string(), + json!("https://some-iss.com/.well-known/openid-configuration"), + ), + ])), + Some(&issuers.get("test_iss").unwrap()), + ); + test_build_entity("Userinfo_token", userinfo_token, |tkn| { + builder.build_userinfo_tkn_entity(tkn) + }); + } + + #[test] + fn errors_when_given_incorrect_tkn_kind() { + let schema = test_schema(); + let issuers = test_issusers(); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, false); + let tkn_claims = TokenClaims::new(HashMap::from([ + ("jti".to_string(), json!("tkn-123")), + ( + "trusted_issuer".to_string(), + json!("https://some-iss.com/.well-known/openid-configuration"), + ), + ])); + let iss = Some(issuers.get("test_iss").unwrap()); + let access_token = Token::new_access(tkn_claims.clone(), iss); + let id_token = Token::new_id(tkn_claims.clone(), iss); + let userinfo_token = Token::new_userinfo(tkn_claims, iss); + + for tkn in [&id_token, &userinfo_token].iter() { + let err = builder + .build_access_tkn_entity(tkn) + .expect_err("expected to error because a wrong token kind was supplied"); + assert!( + matches!( + err, + BuildTokenEntityError { + ref token_kind, + err: BuildEntityError::InvalidToken(ref got_kind, ref expected_kind) + } + if *token_kind == TokenKind::Access && + *got_kind == tkn.kind && + *expected_kind == TokenKind::Access + ), + "should match error for {} token but got: {:#?}", + tkn.kind, + err, + ); + } + + for tkn in [&access_token, &userinfo_token].iter() { + let err = builder + .build_id_tkn_entity(tkn) + .expect_err("expected to error because a wrong token kind was supplied"); + assert!( + matches!( + err, + BuildTokenEntityError { + ref token_kind, + err: BuildEntityError::InvalidToken(ref got_kind, ref expected_kind) + } + if *token_kind == TokenKind::Id && + *got_kind == tkn.kind && + *expected_kind == TokenKind::Id + ), + "should match error for {} token but got: {:#?}", + tkn.kind, + err, + ); + } + + for tkn in [&access_token, &id_token].iter() { + let err = builder + .build_userinfo_tkn_entity(tkn) + .expect_err("expected to error because a wrong token kind was supplied"); + assert!( + matches!( + err, + BuildTokenEntityError { + ref token_kind, + err: BuildEntityError::InvalidToken(ref got_kind, ref expected_kind) + } + if *token_kind == TokenKind::Userinfo && + *got_kind == tkn.kind && + *expected_kind == TokenKind::Userinfo + ), + "should match error for {} token but got: {:#?}", + tkn.kind, + err, + ); + } + } +} diff --git a/jans-cedarling/cedarling/src/authz/entity_builder/build_user_entity.rs b/jans-cedarling/cedarling/src/authz/entity_builder/build_user_entity.rs new file mode 100644 index 00000000000..d22e45b87f3 --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/entity_builder/build_user_entity.rs @@ -0,0 +1,291 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use super::*; +use cedar_policy::Entity; +use std::collections::HashSet; + +impl EntityBuilder { + pub fn build_user_entity( + &self, + tokens: &DecodedTokens, + parents: HashSet, + ) -> Result { + let entity_name = self.entity_names.user.as_ref(); + let mut errors = vec![]; + + for token in [tokens.userinfo.as_ref(), tokens.id.as_ref()] + .iter() + .flatten() + { + let user_id_claim = token.user_mapping(); + match build_entity( + &self.schema, + entity_name, + token, + user_id_claim, + vec![], + parents.clone(), + ) { + Ok(entity) => return Ok(entity), + Err(err) => errors.push((token.kind, err)), + } + } + + Err(BuildUserEntityError { errors }) + } +} + +#[derive(Debug, thiserror::Error)] +pub struct BuildUserEntityError { + pub errors: Vec<(TokenKind, BuildEntityError)>, +} + +impl fmt::Display for BuildUserEntityError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.errors.is_empty() { + writeln!( + f, + "failed to create User Entity since no tokens were provided" + )?; + } else { + writeln!( + f, + "failed to create User Entity due to the following errors:" + )?; + for (token_kind, error) in &self.errors { + writeln!(f, "- TokenKind {:?}: {}", token_kind, error)?; + } + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::super::*; + use crate::common::cedar_schema::cedar_json::CedarSchemaJson; + use crate::common::policy_store::{ClaimMappings, TokenEntityMetadata, TrustedIssuer}; + use crate::jwt::{Token, TokenClaims}; + use cedar_policy::EvalResult; + use serde_json::json; + use std::collections::HashMap; + use test_utils::assert_eq; + + fn test_iss() -> TrustedIssuer { + let token_entity_metadata = TokenEntityMetadata { + claim_mapping: serde_json::from_value::(json!({ + "email": { + "parser": "regex", + "type": "Jans::Email", + "regex_expression" : "^(?P[^@]+)@(?P.+)$", + "UID": {"attr": "uid", "type":"String"}, + "DOMAIN": {"attr": "domain", "type":"String"}, + }, + })) + .unwrap(), + ..Default::default() + }; + TrustedIssuer { + id_tokens: token_entity_metadata.clone(), + userinfo_tokens: token_entity_metadata, + ..Default::default() + } + } + + fn test_schema() -> CedarSchemaJson { + serde_json::from_value::(json!({ + "Jans": { + "commonTypes": { + "Email": { + "type": "Record", + "attributes": { + "uid": { "type": "String" }, + "domain": { "type": "String" }, + }, + }, + "Url": { + "type": "Record", + "attributes": { + "scheme": { "type": "String" }, + "path": { "type": "String" }, + "domain": { "type": "String" }, + }, + }, + }, + "entityTypes": { + "Role": {}, + "User": { + "memberOf": ["Role"], + "shape": { + "type": "Record", + "attributes": { + "email": { "type": "EntityOrCommon", "name": "Email" }, + "sub": { "type": "String" }, + }, + } + } + } + }})) + .expect("should successfully create test schema") + } + + fn test_successfully_building_user_entity(tokens: DecodedTokens) { + let schema = test_schema(); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, true); + let entity = builder + .build_user_entity(&tokens, HashSet::new()) + .expect("expected to build user entity"); + + assert_eq!(entity.uid().to_string(), "Jans::User::\"user-123\""); + + assert_eq!( + entity.attr("sub").unwrap().unwrap(), + EvalResult::String("user-123".to_string()), + ); + + let email = entity + .attr("email") + .expect("entity must have an `email` attribute") + .unwrap(); + if let EvalResult::Record(ref record) = email { + assert_eq!(record.len(), 2); + assert_eq!( + record.get("uid").unwrap(), + &EvalResult::String("test".to_string()) + ); + assert_eq!( + record.get("domain").unwrap(), + &EvalResult::String("email.com".to_string()) + ); + } else { + panic!( + "expected the attribute `email` to be a record, got: {:?}", + email + ); + } + } + + #[test] + fn can_build_using_userinfo_tkn() { + let iss = test_iss(); + let userinfo_token = Token::new_userinfo( + TokenClaims::new(HashMap::from([ + ("email".to_string(), json!("test@email.com")), + ("sub".to_string(), json!("user-123")), + ("role".to_string(), json!(["admin", "user"])), + ])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: None, + id: None, + userinfo: Some(userinfo_token), + }; + test_successfully_building_user_entity(tokens); + } + + #[test] + fn can_build_using_id_tkn() { + let iss = test_iss(); + let id_token = Token::new_id( + TokenClaims::new(HashMap::from([ + ("email".to_string(), json!("test@email.com")), + ("sub".to_string(), json!("user-123")), + ("role".to_string(), json!(["admin", "user"])), + ])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: None, + id: Some(id_token), + userinfo: None, + }; + test_successfully_building_user_entity(tokens); + } + + #[test] + fn errors_when_token_has_missing_claim() { + let iss = test_iss(); + let schema = test_schema(); + + let id_token = Token::new_id(TokenClaims::new(HashMap::new()), Some(&iss)); + let userinfo_token = Token::new_userinfo(TokenClaims::new(HashMap::new()), Some(&iss)); + let tokens = DecodedTokens { + access: None, + id: Some(id_token), + userinfo: Some(userinfo_token), + }; + + let builder = EntityBuilder::new(schema, EntityNames::default(), false, true); + let err = builder + .build_user_entity(&tokens, HashSet::new()) + .expect_err("expected to error while building the user entity"); + + assert_eq!(err.errors.len(), 2); + for (i, expected_kind) in [TokenKind::Userinfo, TokenKind::Id].iter().enumerate() { + assert!( + matches!( + err.errors[i], + (ref tkn_kind, BuildEntityError::MissingClaim(ref claim_name)) + if tkn_kind == expected_kind && + claim_name == "sub" + ), + "expected an error due to missing the `sub` claim, got: {:?}", + err.errors[i] + ); + } + } + + #[test] + fn errors_when_tokens_unavailable() { + let schema = test_schema(); + + let tokens = DecodedTokens { + access: None, + id: None, + userinfo: None, + }; + + let builder = EntityBuilder::new(schema, EntityNames::default(), false, true); + let err = builder + .build_user_entity(&tokens, HashSet::new()) + .expect_err("expected to error while building the user entity"); + + assert_eq!(err.errors.len(), 0); + } + + #[test] + fn can_build_entity_with_roles() { + let iss = test_iss(); + let userinfo_token = Token::new_userinfo( + TokenClaims::new(HashMap::from([ + ("sub".to_string(), json!("user-123")), + ("email".to_string(), json!("someone@email.com")), + ("role".to_string(), json!(["role1", "role2", "role3"])), + ])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: None, + id: None, + userinfo: Some(userinfo_token), + }; + let schema = test_schema(); + let builder = EntityBuilder::new(schema, EntityNames::default(), false, true); + let roles = HashSet::from([ + "Role::\"role1\"".parse().unwrap(), + "Role::\"role2\"".parse().unwrap(), + "Role::\"role3\"".parse().unwrap(), + ]); + + let user_entity = builder + .build_user_entity(&tokens, roles.clone()) + .expect("expected to build user entity"); + + let (_, _, parents) = user_entity.into_inner(); + assert_eq!(parents, roles,); + } +} diff --git a/jans-cedarling/cedarling/src/authz/entity_builder/build_workload_entity.rs b/jans-cedarling/cedarling/src/authz/entity_builder/build_workload_entity.rs new file mode 100644 index 00000000000..3f444413a16 --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/entity_builder/build_workload_entity.rs @@ -0,0 +1,463 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use super::*; +use cedar_policy::Entity; +use std::collections::HashSet; + +// Default claims to use for the Workload Entity's ID. +const DEFAULT_ACCESS_TKN_WORKLOAD_CLAIM: &str = "client_id"; +const DEFAULT_ID_TKN_WORKLOAD_CLAIM: &str = "aud"; + +impl EntityBuilder { + pub fn build_workload_entity( + &self, + tokens: &DecodedTokens, + ) -> Result { + let entity_name = self.entity_names.workload.as_ref(); + let mut errors = vec![]; + + for (workload_id_claim, token_option, claim_aliases) in [ + ( + DEFAULT_ACCESS_TKN_WORKLOAD_CLAIM, + tokens.access.as_ref(), + vec![], + ), + ( + DEFAULT_ID_TKN_WORKLOAD_CLAIM, + tokens.id.as_ref(), + vec![ClaimAliasMap::new("aud", "client_id")], + ), + ] + .into_iter() + { + if let Some(token) = token_option { + match build_entity( + &self.schema, + entity_name, + token, + workload_id_claim, + claim_aliases, + HashSet::new(), + ) { + Ok(entity) => return Ok(entity), + Err(err) => errors.push((token.kind, err)), + } + } + } + + Err(BuildWorkloadEntityError { errors }) + } +} + +#[derive(Debug, thiserror::Error)] +pub struct BuildWorkloadEntityError { + pub errors: Vec<(TokenKind, BuildEntityError)>, +} + +impl fmt::Display for BuildWorkloadEntityError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.errors.is_empty() { + writeln!( + f, + "failed to create Workload Entity since no tokens were provided" + )?; + } else { + writeln!( + f, + "failed to create Workload Entity due to the following errors:" + )?; + for (token_kind, error) in &self.errors { + writeln!(f, "- TokenKind {:?}: {}", token_kind, error)?; + } + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::super::*; + use crate::authz::entity_builder::BuildEntityError; + use crate::common::cedar_schema::cedar_json::CedarSchemaJson; + use crate::common::policy_store::{ + ClaimMappings, TokenEntityMetadata, TokenKind, TrustedIssuer, + }; + use crate::jwt::{Token, TokenClaims}; + use cedar_policy::EvalResult; + use serde_json::json; + use std::collections::HashMap; + + #[test] + fn can_build_using_access_tkn() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + "name": { "type": "String" }, + }, + } + }}} + })) + .unwrap(); + let iss = TrustedIssuer::default(); + let builder = EntityBuilder::new(schema, EntityNames::default(), true, false); + let access_token = Token::new_access( + TokenClaims::new(HashMap::from([ + ("client_id".to_string(), json!("workload-123")), + ("name".to_string(), json!("somename")), + ])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: Some(access_token), + id: None, + userinfo: None, + }; + let entity = builder + .build_workload_entity(&tokens) + .expect("expeted to successfully build workload entity"); + assert_eq!(entity.uid().to_string(), "Jans::Workload::\"workload-123\""); + assert_eq!( + entity + .attr("client_id") + .expect("expected workload entity to have a `client_id` attribute") + .unwrap(), + EvalResult::String("workload-123".to_string()), + ); + assert_eq!( + entity + .attr("name") + .expect("expected workload entity to have a `name` attribute") + .unwrap(), + EvalResult::String("somename".to_string()), + ); + } + + #[test] + fn can_build_using_id_tkn() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + "name": { "type": "String" }, + }, + } + }}} + })) + .unwrap(); + let iss = TrustedIssuer::default(); + let builder = EntityBuilder::new(schema, EntityNames::default(), true, false); + let id_token = Token::new_id( + TokenClaims::new(HashMap::from([ + ("aud".to_string(), json!("workload-123")), + ("name".to_string(), json!("somename")), + ])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: None, + id: Some(id_token), + userinfo: None, + }; + let entity = builder + .build_workload_entity(&tokens) + .expect("expected to successfully build workload entity"); + assert_eq!(entity.uid().to_string(), "Jans::Workload::\"workload-123\""); + assert_eq!( + entity + .attr("client_id") + .expect("expected workload entity to have a `client_id` attribute") + .unwrap(), + EvalResult::String("workload-123".to_string()), + ); + assert_eq!( + entity + .attr("name") + .expect("expected workload entity to have a `name` attribute") + .unwrap(), + EvalResult::String("somename".to_string()), + ); + } + + #[test] + fn can_build_expression_with_regex_mapping() { + let schema = serde_json::from_value::(json!({ + "Jans": { + "commonTypes": { + "Email": { + "type": "Record", + "attributes": { + "uid": { "type": "String" }, + "domain": { "type": "String" }, + }, + }, + "Url": { + "type": "Record", + "attributes": { + "scheme": { "type": "String" }, + "path": { "type": "String" }, + "domain": { "type": "String" }, + }, + }, + }, + "entityTypes": { + "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + "email": { "type": "EntityOrCommon", "name": "Email" }, + "url": { "type": "EntityOrCommon", "name": "Url" }, + }, + } + } + }} + })) + .unwrap(); + let iss = TrustedIssuer { + access_tokens: TokenEntityMetadata { + claim_mapping: serde_json::from_value::(json!({ + "email": { + "parser": "regex", + "type": "Jans::Email", + "regex_expression" : "^(?P[^@]+)@(?P.+)$", + "UID": {"attr": "uid", "type":"String"}, + "DOMAIN": {"attr": "domain", "type":"String"}, + }, + "url": { + "parser": "regex", + "type": "Jans::Url", + "regex_expression": r#"^(?P[a-zA-Z][a-zA-Z0-9+.-]*):\/\/(?P[^\/]+)(?P\/.*)?$"#, + "SCHEME": {"attr": "scheme", "type": "String"}, + "DOMAIN": {"attr": "domain", "type": "String"}, + "PATH": {"attr": "path", "type": "String"} + } + })) + .unwrap(), + ..Default::default() + }, + ..Default::default() + }; + let builder = EntityBuilder::new(schema, EntityNames::default(), true, false); + let access_token = Token::new_access( + TokenClaims::new(HashMap::from([ + ("client_id".to_string(), json!("workload-123")), + ("email".to_string(), json!("test@example.com")), + ("url".to_string(), json!("https://test.com/example")), + ])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: Some(access_token), + id: None, + userinfo: None, + }; + let entity = builder + .build_workload_entity(&tokens) + .expect("expected to successfully build workload entity"); + + assert_eq!(entity.uid().to_string(), "Jans::Workload::\"workload-123\""); + + assert_eq!( + entity + .attr("client_id") + .expect("expected to workload entity to have a `client_id` attribute") + .unwrap(), + EvalResult::String("workload-123".to_string()), + ); + + let email = entity + .attr("email") + .expect("expected workload entity to have an `email` attribute") + .unwrap(); + if let EvalResult::Record(ref record) = email { + assert_eq!(record.len(), 2); + assert_eq!( + record.get("uid").unwrap(), + &EvalResult::String("test".to_string()) + ); + assert_eq!( + record.get("domain").unwrap(), + &EvalResult::String("example.com".to_string()) + ); + } else { + panic!( + "expected the attribute `email` to be a record, got: {:?}", + email + ); + } + + let url = entity + .attr("url") + .expect("entity must have a `url` attribute") + .unwrap(); + if let EvalResult::Record(ref record) = url { + assert_eq!(record.len(), 3); + assert_eq!( + record.get("scheme").unwrap(), + &EvalResult::String("https".to_string()) + ); + assert_eq!( + record.get("domain").unwrap(), + &EvalResult::String("test.com".to_string()) + ); + assert_eq!( + record.get("path").unwrap(), + &EvalResult::String("/example".to_string()) + ); + } else { + panic!( + "expected the attribute `url` to be a record, got: {:?}", + email + ); + } + } + + #[test] + fn can_build_entity_with_entity_ref() { + let schema = serde_json::from_value::(json!({ + "Jans": { + "entityTypes": { + "TrustedIss": {}, + "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + "iss": { "type": "EntityOrCommon", "name": "TrustedIss" }, + }, + } + } + }} + })) + .unwrap(); + let iss = TrustedIssuer::default(); + let builder = EntityBuilder::new(schema, EntityNames::default(), true, false); + let access_token = Token::new_access( + TokenClaims::new(HashMap::from([ + ("client_id".to_string(), json!("workload-123")), + ( + "iss".to_string(), + json!("https://test.com/.well-known/openid-configuration"), + ), + ])), + Some(&iss), + ); + let tokens = DecodedTokens { + access: Some(access_token), + id: None, + userinfo: None, + }; + let entity = builder + .build_workload_entity(&tokens) + .expect("expected to successfully build workload entity"); + + assert_eq!(entity.uid().to_string(), "Jans::Workload::\"workload-123\""); + + assert_eq!( + entity + .attr("client_id") + .expect("expected to workload entity to have a `client_id` attribute") + .unwrap(), + EvalResult::String("workload-123".to_string()), + ); + + let iss = entity + .attr("iss") + .expect("entity must have a `iss` attribute") + .unwrap(); + if let EvalResult::EntityUid(uid) = iss { + assert_eq!(uid.type_name().namespace(), "Jans"); + assert_eq!(uid.type_name().basename(), "TrustedIss"); + assert_eq!( + uid.id().escaped(), + "https://test.com/.well-known/openid-configuration" + ); + } else { + panic!( + "expected the attribute `iss` to be an EntityUid, got: {:?}", + iss + ); + } + } + + #[test] + fn errors_when_token_has_missing_claim() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + "name": { "type": "String" }, + }, + } + }}} + })) + .unwrap(); + let iss = TrustedIssuer::default(); + let builder = EntityBuilder::new(schema, EntityNames::default(), true, false); + let access_token = Token::new_access(TokenClaims::new(HashMap::new()), Some(&iss)); + let id_token = Token::new_id(TokenClaims::new(HashMap::new()), Some(&iss)); + let tokens = DecodedTokens { + access: Some(access_token), + id: Some(id_token), + userinfo: None, + }; + let err = builder + .build_workload_entity(&tokens) + .expect_err("expected to error while building the workload entity"); + + assert_eq!(err.errors.len(), 2); + assert!( + matches!( + err.errors[0], + (ref tkn_kind, BuildEntityError::MissingClaim(ref claim_name)) + if tkn_kind == &TokenKind::Access && + claim_name == "client_id" + ), + "expected an error due to missing the `client_id` claim" + ); + assert!( + matches!( + err.errors[1], + (ref tkn_kind, BuildEntityError::MissingClaim(ref claim_name)) + if tkn_kind == &TokenKind::Id && + claim_name == "aud" + ), + "expected an error due to missing the `aud` claim" + ); + } + + #[test] + fn errors_when_tokens_unavailable() { + let schema = serde_json::from_value::(json!({ + "Jans": { "entityTypes": { "Workload": { + "shape": { + "type": "Record", + "attributes": { + "client_id": { "type": "String" }, + "name": { "type": "String" }, + }, + } + }}} + })) + .unwrap(); + let builder = EntityBuilder::new(schema, EntityNames::default(), true, false); + let tokens = DecodedTokens { + access: None, + id: None, + userinfo: None, + }; + let err = builder.build_workload_entity(&tokens).unwrap_err(); + + assert_eq!(err.errors.len(), 0); + } +} diff --git a/jans-cedarling/cedarling/src/authz/entity_builder/mapping.rs b/jans-cedarling/cedarling/src/authz/entity_builder/mapping.rs new file mode 100644 index 00000000000..0f1edb8d466 --- /dev/null +++ b/jans-cedarling/cedarling/src/authz/entity_builder/mapping.rs @@ -0,0 +1,61 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use crate::common::policy_store::ClaimMappings; +use serde_json::Value; +use std::collections::HashMap; + +impl ClaimMappings { + /// Creates new claims and adds it to the HashMap of the given claims + /// if a mapping exists + /// + /// * Note that this will overwrite existing names + pub fn apply_mapping(&self, claims: &HashMap) -> HashMap { + let mut mapped_claims = HashMap::new(); + for (name, claim) in claims.iter() { + if let Some(mapping) = self.get(name) { + let applied_mapping = mapping.apply_mapping(claim); + mapped_claims.extend(applied_mapping); + } + } + mapped_claims + } +} + +#[cfg(test)] +mod test { + use crate::common::policy_store::ClaimMappings; + use serde_json::json; + use std::collections::HashMap; + use test_utils::assert_eq; + + #[test] + fn can_apply_mapping() { + let claims = HashMap::from([ + ("email".to_string(), json!("test@test.com")), + ("url".to_string(), json!("https://example.com/test")), + ]); + let claim_mapping = serde_json::from_value::(json!({ + "url": { + "parser": "regex", + "type": "Jans::Url", + "regex_expression": r#"^(?P[a-zA-Z][a-zA-Z0-9+.-]*):\/\/(?P[^\/]+)(?P\/.*)?$"#, + "SCHEME": {"attr": "scheme", "type": "String"}, + "DOMAIN": {"attr": "domain", "type": "String"}, + "PATH": {"attr": "path", "type": "String"} + } + })) + .unwrap(); + let mapped_claims = claim_mapping.apply_mapping(&claims); + assert_eq!( + mapped_claims, + HashMap::from([ + ("scheme".to_string(), json!("https")), + ("domain".to_string(), json!("example.com")), + ("path".to_string(), json!("/test")), + ]) + ); + } +} diff --git a/jans-cedarling/cedarling/src/authz/merge_json.rs b/jans-cedarling/cedarling/src/authz/merge_json.rs deleted file mode 100644 index c60a2d032a3..00000000000 --- a/jans-cedarling/cedarling/src/authz/merge_json.rs +++ /dev/null @@ -1,56 +0,0 @@ -// This software is available under the Apache-2.0 license. -// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. -// -// Copyright (c) 2024, Gluu, Inc. - -use serde_json::Value; - -#[derive(Debug, thiserror::Error)] -pub enum MergeError { - #[error("Failed to merge JSON objects due to conflicting keys: {0}")] - KeyConflict(String), -} - -pub fn merge_json_values(mut base: Value, other: Value) -> Result { - if let (Some(base_map), Some(additional_map)) = (base.as_object_mut(), other.as_object()) { - for (key, value) in additional_map { - if base_map.contains_key(key) { - return Err(MergeError::KeyConflict(key.clone())); - } - base_map.insert(key.clone(), value.clone()); - } - } - Ok(base) -} - -#[cfg(test)] -mod test { - use serde_json::json; - - use super::merge_json_values; - use crate::authz::merge_json::MergeError; - - #[test] - fn can_merge_json_objects() { - let obj1 = json!({ "a": 1, "b": 2 }); - let obj2 = json!({ "c": 3, "d": 4 }); - let expected = json!({"a": 1, "b": 2, "c": 3, "d": 4}); - - let result = merge_json_values(obj1, obj2).expect("Should merge JSON objects"); - - assert_eq!(result, expected); - } - - #[test] - fn errors_on_same_keys() { - // Test for only two objects - let obj1 = json!({ "a": 1, "b": 2 }); - let obj2 = json!({ "b": 3, "c": 4 }); - let result = merge_json_values(obj1, obj2); - - assert!( - matches!(result, Err(MergeError::KeyConflict(key)) if key.as_str() == "b"), - "Expected an error due to conflicting keys" - ); - } -} diff --git a/jans-cedarling/cedarling/src/authz/mod.rs b/jans-cedarling/cedarling/src/authz/mod.rs index c92a1c84f18..7bf65920ad7 100644 --- a/jans-cedarling/cedarling/src/authz/mod.rs +++ b/jans-cedarling/cedarling/src/authz/mod.rs @@ -8,43 +8,34 @@ //! - evaluate if authorization is granted for *user* //! - evaluate if authorization is granted for *client* / *workload * -use std::collections::{HashMap, HashSet}; -use std::io::Cursor; -use std::str::FromStr; -use std::sync::Arc; - use crate::bootstrap_config::AuthorizationConfig; use crate::common::app_types; -use crate::common::cedar_schema::cedar_json::{BuildJsonCtxError, FindActionError}; use crate::common::policy_store::PolicyStoreWithID; use crate::jwt::{self, TokenStr}; - use crate::log::interface::LogWriter; use crate::log::{ AuthorizationLogInfo, BaseLogEntry, DecisionLogEntry, Diagnostics, DiagnosticsRefs, LogEntry, LogLevel, LogTokensInfo, LogType, Logger, PrincipalLogEntry, UserAuthorizeInfo, WorkloadAuthorizeInfo, }; +use build_ctx::*; +use cedar_policy::{Entities, Entity, EntityUid}; +use chrono::Utc; +use entity_builder::*; +use request::Request; +use std::collections::HashMap; +use std::io::Cursor; +use std::str::FromStr; +use std::sync::Arc; + +pub use authorize_result::AuthorizeResult; mod authorize_result; -mod merge_json; +mod build_ctx; -pub(crate) mod entities; +pub(crate) mod entity_builder; pub(crate) mod request; -pub use authorize_result::AuthorizeResult; -use cedar_policy::{ContextJsonError, Entities, Entity, EntityUid}; -use chrono::Utc; -use entities::{ - CEDAR_POLICY_SEPARATOR, CreateCedarEntityError, CreateUserEntityError, - CreateWorkloadEntityError, DecodedTokens, ResourceEntityError, RoleEntityError, - create_resource_entity, create_role_entities, create_token_entities, create_user_entity, - create_workload_entity, -}; -use merge_json::{MergeError, merge_json_values}; -use request::Request; -use serde_json::Value; - /// Configuration to Authz to initialize service without errors pub(crate) struct AuthzConfig { pub log_service: Logger, @@ -61,11 +52,23 @@ pub(crate) struct AuthzConfig { pub struct Authz { config: AuthzConfig, authorizer: cedar_policy::Authorizer, + entity_builder: EntityBuilder, } impl Authz { /// Create a new Authorization Service pub(crate) fn new(config: AuthzConfig) -> Self { + let json_schema = config.policy_store.schema.json.clone(); + let entity_names = EntityNames::from(&config.authorization); + let build_workload = config.authorization.use_workload_principal; + let build_user = config.authorization.use_user_principal; + let entity_builder = entity_builder::EntityBuilder::new( + json_schema, + entity_names, + build_workload, + build_user, + ); + config.log_service.log( LogEntry::new_with_data( config.pdp_id, @@ -80,6 +83,7 @@ impl Authz { Self { config, authorizer: cedar_policy::Authorizer::new(), + entity_builder, } } @@ -88,33 +92,31 @@ impl Authz { &'a self, request: &'a Request, ) -> Result, AuthorizeError> { - let access_token = if let Some(tkn) = request.tokens.access_token.as_ref() { + let access = if let Some(tkn) = request.tokens.access_token.as_ref() { Some( self.config .jwt_service - .process_token(TokenStr::Access(tkn.as_str())) + .process_token(TokenStr::Access(tkn)) .await?, ) } else { None }; - - let id_token = if let Some(tkn) = request.tokens.id_token.as_ref() { + let id = if let Some(tkn) = request.tokens.id_token.as_ref() { Some( self.config .jwt_service - .process_token(TokenStr::Id(tkn.as_str())) + .process_token(TokenStr::Id(tkn)) .await?, ) } else { None }; - - let userinfo_token = if let Some(tkn) = request.tokens.userinfo_token.as_ref() { + let userinfo = if let Some(tkn) = request.tokens.userinfo_token.as_ref() { Some( self.config .jwt_service - .process_token(TokenStr::Userinfo(tkn.as_str())) + .process_token(TokenStr::Userinfo(tkn)) .await?, ) } else { @@ -122,9 +124,9 @@ impl Authz { }; Ok(DecodedTokens { - access_token, - id_token, - userinfo_token, + access, + id, + userinfo, }) } @@ -143,7 +145,9 @@ impl Authz { .map_err(AuthorizeError::Action)?; // Parse [`cedar_policy::Entity`]-s to [`AuthorizeEntitiesData`] that hold all entities (for usability). - let entities_data: AuthorizeEntitiesData = self.build_entities(&request, &tokens).await?; + let entities_data = self + .entity_builder + .build_entities(&tokens, &request.resource)?; // Get entity UIDs what we will be used on authorize check let resource_uid = entities_data.resource.uid(); @@ -276,7 +280,7 @@ impl Authz { .map(|auth_info| &auth_info.diagnostics); let tokens_logging_info = LogTokensInfo { - access: tokens.access_token.as_ref().map(|tkn| { + access: tokens.access.as_ref().map(|tkn| { tkn.logging_info( self.config .authorization @@ -284,7 +288,7 @@ impl Authz { .as_str(), ) }), - id_token: tokens.access_token.as_ref().map(|tkn| { + id_token: tokens.id.as_ref().map(|tkn| { tkn.logging_info( self.config .authorization @@ -292,7 +296,7 @@ impl Authz { .as_str(), ) }), - userinfo: tokens.userinfo_token.as_ref().map(|tkn| { + userinfo: tokens.userinfo.as_ref().map(|tkn| { tkn.logging_info( self.config .authorization @@ -371,106 +375,18 @@ impl Authz { Ok(response) } - /// Build all the Cedar [`Entities`] from a [`Request`] - /// - /// [`Entities`]: Entity - pub async fn build_entities( + #[cfg(test)] + pub fn build_entities( &self, request: &Request, tokens: &DecodedTokens<'_>, ) -> Result { - let policy_store = &self.config.policy_store; - let auth_conf = &self.config.authorization; - - // build workload entity - let workload = if self.config.authorization.use_workload_principal { - Some(create_workload_entity( - auth_conf.mapping_workload.as_deref(), - policy_store, - tokens, - )?) - } else { - None - }; - - // build role entity - let roles = create_role_entities(policy_store, tokens)?; - - // build user entity - let user = if self.config.authorization.use_user_principal { - Some(create_user_entity( - auth_conf.mapping_user.as_deref(), - policy_store, - tokens, - HashSet::from_iter(roles.iter().map(|e| e.uid())), - )?) - } else { - None - }; - - let token_entities = create_token_entities(auth_conf, policy_store, tokens)?; - - // build resource entity - let resource = create_resource_entity( - request.resource.clone(), - &self.config.policy_store.schema.json, - )?; - - Ok(AuthorizeEntitiesData { - workload, - access_token: token_entities.access, - id_token: token_entities.id, - userinfo_token: token_entities.userinfo, - user, - resource, - roles, - }) + Ok(self + .entity_builder + .build_entities(tokens, &request.resource)?) } } -/// Constructs the authorization context by adding the built entities from the tokens -fn build_context( - config: &AuthzConfig, - request_context: Value, - entities_data: &AuthorizeEntitiesData, - schema: &cedar_policy::Schema, - action: &cedar_policy::EntityUid, -) -> Result { - let namespace = config.policy_store.namespace(); - let action_name = action.id().escaped().to_string(); - let action_schema = config - .policy_store - .schema - .json - .find_action(&action_name, namespace) - .map_err(|e| BuildContextError::FindActionSchema(action_name.clone(), e))? - .ok_or(BuildContextError::MissingActionSchema(action_name))?; - - let mut id_mapping = HashMap::new(); - for entity in entities_data.iter() { - // we strip the namespace from the type_name then make it lowercase - // example: 'Jans::Id_token' -> 'id_token' - let type_name = entity.uid().type_name().to_string(); - let type_name = type_name - .strip_prefix(&format!("{}{}", namespace, CEDAR_POLICY_SEPARATOR)) - .unwrap_or(&type_name) - .to_lowercase(); - let type_id = entity.uid().id().escaped(); - id_mapping.insert(type_name, type_id.to_string()); - } - - let entities_context = action_schema - .build_ctx_entity_refs_json(id_mapping) - .unwrap(); - - let context = merge_json_values(entities_context, request_context)?; - - let context: cedar_policy::Context = - cedar_policy::Context::from_json_value(context, Some((schema, action)))?; - - Ok(context) -} - /// Helper struct to hold named parameters for [`Authz::execute_authorize`] method. struct ExecuteAuthorizeParameters<'a> { entities: &'a Entities, @@ -493,6 +409,17 @@ pub struct AuthorizeEntitiesData { } impl AuthorizeEntitiesData { + // NOTE: the type ids created from these does not include the namespace + fn type_ids(&self) -> HashMap { + self.iter() + .map(|entity| { + let type_name = entity.uid().type_name().basename().to_string(); + let type_id = entity.uid().id().escaped().to_string(); + (type_name, type_id) + }) + .collect::>() + } + /// Create iterator to get all entities fn into_iter(self) -> impl Iterator { vec![self.resource].into_iter().chain(self.roles).chain( @@ -541,27 +468,6 @@ pub enum AuthorizeError { /// Error encountered while processing JWT token data #[error(transparent)] ProcessTokens(#[from] jwt::JwtProcessingError), - /// Error encountered while creating id token entity - #[error("could not create id_token entity: {0}")] - CreateIdTokenEntity(CreateCedarEntityError), - /// Error encountered while creating userinfo entity - #[error("could not create userinfo entity: {0}")] - CreateUserinfoTokenEntity(CreateCedarEntityError), - /// Error encountered while creating access_token entity - #[error("could not create access_token entity: {0}")] - CreateAccessTokenEntity(CreateCedarEntityError), - /// Error encountered while creating user entity - #[error("could not create User entity: {0}")] - CreateUserEntity(#[from] CreateUserEntityError), - /// Error encountered while creating workload - #[error(transparent)] - CreateWorkloadEntity(#[from] CreateWorkloadEntityError), - /// Error encountered while creating resource entity - #[error("{0}")] - ResourceEntity(#[from] ResourceEntityError), - /// Error encountered while creating role entity - #[error(transparent)] - RoleEntity(#[from] RoleEntityError), /// Error encountered while parsing Action to EntityUid #[error("could not parse action: {0}")] Action(cedar_policy::ParseErrors), @@ -583,25 +489,9 @@ pub enum AuthorizeError { /// Error encountered while building the context for the request #[error("Failed to build context: {0}")] BuildContext(#[from] BuildContextError), -} - -#[derive(Debug, thiserror::Error)] -pub enum BuildContextError { - /// Error encountered while validating context according to the schema - #[error(transparent)] - Merge(#[from] MergeError), - /// Error encountered while deserializing the Context from JSON - #[error(transparent)] - DeserializeFromJson(#[from] ContextJsonError), - /// Error encountered while deserializing the Context from JSON - #[error("Failed to find the action `{0}` in the schema: {0}")] - FindActionSchema(String, FindActionError), - /// Error encountered while deserializing the Context from JSON - #[error("The action `{0}` was not found in the schema")] - MissingActionSchema(String), - /// Error encountered while deserializing the Context from JSON + /// Error encountered while building Cedar Entities #[error(transparent)] - BuildJson(#[from] BuildJsonCtxError), + BuildEntity(#[from] BuildCedarlingEntityError), } #[derive(Debug, derive_more::Error, derive_more::Display)] diff --git a/jans-cedarling/cedarling/src/authz/request.rs b/jans-cedarling/cedarling/src/authz/request.rs index a5c80de20b9..daa225775f3 100644 --- a/jans-cedarling/cedarling/src/authz/request.rs +++ b/jans-cedarling/cedarling/src/authz/request.rs @@ -4,9 +4,6 @@ // Copyright (c) 2024, Gluu, Inc. use std::collections::HashMap; -use std::str::FromStr; - -use cedar_policy::{EntityId, EntityTypeName, EntityUid, ParseErrors}; /// Box to store authorization data #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -49,12 +46,3 @@ pub struct ResourceData { #[serde(flatten)] pub payload: HashMap, } - -impl ResourceData { - pub(crate) fn entity_uid(&self) -> Result { - Ok(EntityUid::from_type_name_and_id( - EntityTypeName::from_str(&self.resource_type)?, - EntityId::new(&self.id), - )) - } -} diff --git a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json.rs b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json.rs index 64f69ef1e9d..c1387ed61fb 100644 --- a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json.rs +++ b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json.rs @@ -3,369 +3,137 @@ // // Copyright (c) 2024, Gluu, Inc. -//! Module contains the JSON representation of a [cedar_policy::Schema] -//! Support translated schema from human representation to JSON via CLI version `cedar-policy-cli 4.1`. -//! To translate human redable format to JSON via CLI use next command: -//! `cedar translate-schema --direction cedar-to-json -s .\cedar.schema` -//! [cedar json schema grammar](https://docs.cedarpolicy.com/schema/json-schema-grammar.html) - documentation about json structure of cedar schema. - -mod action; -mod entity_types; - -use std::collections::HashMap; - -use action::ActionSchema; -pub use action::{BuildJsonCtxError, FindActionError}; -use derive_more::derive::Display; -pub use entity_types::{CedarSchemaEntityShape, CedarSchemaRecord}; - -/// Represent `cedar-policy` schema type for external usage. -#[derive(Debug, PartialEq, Hash, Eq, Display)] -pub enum CedarType { - Long, - String, - Boolean, - TypeName(String), - Set(Box), -} - -/// Possible errors that may occur when retrieving a [`CedarType`] from cedar-policy schema. -#[derive(Debug, thiserror::Error)] -pub enum GetCedarTypeError { - /// Error while getting `cedar-policy` schema not implemented type - #[error("could not get cedar-policy type {0}, it is not implemented")] - TypeNotImplemented(String), -} - -/// Enum to get info about type based on name. -/// Is used as a result in [`CedarSchemaJson::find_type`] -pub enum SchemaDefinedType<'a> { - Entity(&'a CedarSchemaEntityShape), - CommonType(&'a CedarSchemaRecord), -} - -/// JSON representation of a [`cedar_policy::Schema`] -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq)] +//! This module is responsible for deserializing the JSON Cedar schema + +use action::*; +use attribute::*; +use entity_type::*; +use serde::Deserialize; +use std::{collections::HashMap, str::FromStr}; + +pub(crate) mod action; +pub(crate) mod attribute; +pub(crate) mod entity_type; + +mod deserialize; + +pub type ActionName = String; +pub type ActionGroupName = String; +pub type AttributeName = String; +pub type CommonTypeName = String; +pub type EntityName = String; +pub type EntityTypeName = String; +pub type EntityOrCommonName = String; +pub type ExtensionName = String; +pub type NamespaceName = String; + +#[derive(Debug, Deserialize, PartialEq, Clone)] pub struct CedarSchemaJson { #[serde(flatten)] - pub namespace: HashMap, + namespaces: HashMap, } impl CedarSchemaJson { - /// Get schema record by namespace name and entity type name - pub fn entity_schema( - &self, - namespace: &str, - typename: &str, - ) -> Option<&CedarSchemaEntityShape> { - let namespace = self.namespace.get(namespace)?; - namespace.entity_types.get(typename) + pub fn get_action(&self, namespace: &str, name: &str) -> Option<&Action> { + self.namespaces + .get(namespace) + .and_then(|nmspce| nmspce.actions.get(name)) } - /// Find the typename if exist in the schema and return it definition - pub fn find_type(&self, type_name: &str, namespace: &str) -> Option { - let namespace = self.namespace.get(namespace)?; - - let schema_type = namespace - .common_types - .get(type_name) - .as_ref() - .map(|common_type| SchemaDefinedType::CommonType(common_type)); - - if schema_type.is_some() { - return schema_type; + pub fn get_common_type(&self, name: &str) -> Option<(&NamespaceName, &Attribute)> { + for (namespace_name, namespace) in self.namespaces.iter() { + if let Some(attr) = namespace.common_types.get(name) { + return Some((namespace_name, attr)); + } } + None + } - let schema_type = namespace - .entity_types - .get(type_name) - .as_ref() - .map(|entity| SchemaDefinedType::Entity(entity)); - if schema_type.is_some() { - return schema_type; + pub fn get_entity_from_base_name( + &self, + base_name: &str, + ) -> Option<(&NamespaceName, &EntityType)> { + for (namespace_name, namespace) in self.namespaces.iter() { + if let Some(entity_type) = namespace.entity_types.get(base_name) { + return Some((namespace_name, entity_type)); + } } + None + } + pub fn get_entity_from_full_name( + &self, + full_name: &str, + ) -> Option<(NamespaceName, &EntityType)> { + let full_name = cedar_policy::EntityTypeName::from_str(full_name).ok()?; + let namespace_name = full_name.namespace(); + if let Some(namespace) = self.namespaces.get(&namespace_name) { + let base_name = full_name.basename(); + if let Some(entity_type) = namespace.entity_types.get(base_name) { + return Some((namespace_name, entity_type)); + } + } None } } -/// CedarSchemaEntities hold all entities and their shapes in the namespace. -// It may contain more fields, but we don't need all of them. -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq)] -pub struct CedarSchemaEntities { +#[derive(Debug, Deserialize, PartialEq, Clone)] +pub struct Namespace { #[serde(rename = "entityTypes", default)] - pub entity_types: HashMap, + entity_types: HashMap, #[serde(rename = "commonTypes", default)] - pub common_types: HashMap, - pub actions: HashMap, + common_types: HashMap, + #[serde(default)] + actions: HashMap, } #[cfg(test)] -mod tests { - use std::collections::HashSet; - - use action::CtxAttribute; - use serde_json::json; - use test_utils::{SortedJson, assert_eq}; - - use super::entity_types::*; +mod test_deserialize_json_cedar_schema { use super::*; + use serde_json::json; + use std::collections::HashSet; - /// Test to parse the cedar json schema - /// to debug deserialize the schema #[test] - fn parse_correct_example() { - let json_value = include_str!("test_files/test_data_cedar.json"); - - let parsed_cedar_schema: CedarSchemaJson = - serde_json::from_str(json_value).expect("failed to parse json"); - - let entity_types = HashMap::from_iter(vec![ - ("Access_token".to_string(), CedarSchemaEntityShape { - shape: Some(CedarSchemaRecord { - entity_type: "Record".to_string(), - attributes: HashMap::from_iter(vec![ - ("aud".to_string(), CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "String".to_string(), - }), - required: true, - }), - ("exp".to_string(), CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "Long".to_string(), - }), - required: true, - }), - ("iat".to_string(), CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Primitive(PrimitiveType { - kind: PrimitiveTypeKind::Long, - }), - required: true, - }), - ("scope".to_string(), CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Set(Box::new(SetEntityType { - element: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "String".to_string(), - }), - })), - - required: false, - }), - ]), + fn can_deserialize_entity_types() { + let schema = json!({ + "Jans": { + "entityTypes": { + "User": { + "memberOfTypes": [ "UserGroup" ], + "shape": { + "type": "Record", + "attributes": { + "department": { "type": "String" }, + "jobLevel": { "type": "Long" } + } + } + }, + "UserGroup": {}, + }, + } + }); + let schema = serde_json::from_value::(schema).unwrap(); + let namespace = Namespace { + entity_types: HashMap::from([ + ("User".into(), EntityType { + member_of: Some(HashSet::from(["UserGroup".into()])), + shape: Some(EntityShape::required(HashMap::from([ + ("department".into(), Attribute::string()), + ("jobLevel".into(), Attribute::long()), + ]))), + tags: None, }), - }), - ("Role".to_string(), CedarSchemaEntityShape { shape: None }), - ("TrustedIssuer".to_string(), CedarSchemaEntityShape { - shape: Some(CedarSchemaRecord { - entity_type: "Record".to_string(), - attributes: HashMap::from_iter([( - "issuer_entity_id".to_string(), - CedarSchemaEntityAttribute { - required: true, - cedar_type: CedarSchemaEntityType::Typed(EntityType { - name: "Url".to_string(), - kind: "EntityOrCommon".to_string(), - }), - }, - )]), - }), - }), - ("Issue".to_string(), CedarSchemaEntityShape { shape: None }), - ]); - - let common_types = HashMap::from_iter([("Url".to_string(), CedarSchemaRecord { - entity_type: "Record".to_string(), - attributes: HashMap::from_iter([ - ("host".to_string(), CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "String".to_string(), - }), - required: true, - }), - ("path".to_string(), CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "String".to_string(), - }), - required: true, - }), - ("protocol".to_string(), CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "String".to_string(), - }), - required: true, + ("UserGroup".into(), EntityType { + member_of: None, + shape: None, + tags: None, }), ]), - })]); - - let actions = HashMap::from([("Update".to_string(), ActionSchema { - resource_types: HashSet::from(["Issue"].map(|x| x.to_string())), - principal_types: HashSet::from(["Access_token", "Role"].map(|x| x.to_string())), - context: None, - })]); - - let schema_to_compare = CedarSchemaJson { - namespace: HashMap::from_iter(vec![("Jans".to_string(), CedarSchemaEntities { - entity_types, - common_types, - actions, - })]), + common_types: HashMap::new(), + actions: HashMap::new(), }; - - assert_eq!( - serde_json::json!(parsed_cedar_schema).sorted(), - serde_json::json!(schema_to_compare).sorted() - ); - } - - /// test to check if we get error on parsing invalid `EntityOrCommon` type - #[test] - fn parse_error_entity_or_common() { - // In this file we skipped field `name` for `EntityOrCommon` - let json_value = include_str!("test_files/test_data_cedar_err_entity_or_common.json"); - - let parse_error = - serde_json::from_str::(json_value).expect_err("should fail to parse"); - assert_eq!( - parse_error.to_string(), - "could not deserialize CedarSchemaEntityType: failed to deserialize EntityOrCommon: \ - missing field `name` at line 17 column 1" - ) - } - - /// test to check if we get error on parsing invalid `PrimitiveType` type - #[test] - fn parse_error_primitive_type() { - // In this file we use `"type": 123` but in OK case should be `"type": "Long"` - let json_value = include_str!("test_files/test_data_cedar_err_primitive_type.json"); - - let parse_error = - serde_json::from_str::(json_value).expect_err("should fail to parse"); - assert_eq!( - parse_error.to_string(), - "could not deserialize CedarSchemaEntityType: invalid type: integer `123`, expected a \ - string at line 17 column 1" - ) - } - - /// test to check if we get error on parsing invalid nested Sets :`Set>` type - #[test] - fn parse_error_set_entity_or_common() { - // In this file we skipped field `name` for `EntityOrCommon` in the nested set - let json_value = include_str!("test_files/test_data_cedar_err_set.json"); - - let parse_error = - serde_json::from_str::(json_value).expect_err("should fail to parse"); - assert_eq!( - parse_error.to_string(), - "could not deserialize CedarSchemaEntityType: failed to deserialize Set: failed to \ - deserialize Set: failed to deserialize EntityOrCommon: missing field `name` at line \ - 24 column 1" - ) - } - - /// test to check if we get error on parsing invalid type in field `is_required` - #[test] - fn parse_error_field_is_required() { - // In this file we use ` "required": 1234` but in OK case should be ` "required": false` or omit - let json_value = include_str!("test_files/test_data_cedar_err_field_is_required.json"); - - let parse_error = - serde_json::from_str::(json_value).expect_err("should fail to parse"); - assert_eq!( - parse_error.to_string(), - "could not deserialize CedarSchemaEntityAttribute, field 'is_required': invalid type: \ - integer `1234`, expected a boolean at line 22 column 1" - ) - } - - #[test] - fn can_parse_action_with_ctx() { - let expected_principal_entities = - HashSet::from(["Jans::Workload".into(), "Jans::User".into()]); - let expected_resource_entities = HashSet::from(["Jans::Issue".into()]); - let expected_context_entities = Some(HashSet::from([ - CtxAttribute { - namespace: "Jans".into(), - key: "access_token".into(), - kind: CedarType::TypeName("Access_token".to_string()), - }, - CtxAttribute { - namespace: "Jans".into(), - key: "time".into(), - kind: CedarType::Long, - }, - CtxAttribute { - namespace: "Jans".into(), - key: "user".into(), - kind: CedarType::TypeName("User".to_string()), - }, - CtxAttribute { - namespace: "Jans".into(), - key: "workload".into(), - kind: CedarType::TypeName("Workload".to_string()), - }, - ])); - - // Test case where the context is a record: - // action "Update" appliesTo { - // principal: [Workload, User], - // resource: [Issue], - // context: { - // time: Long, - // user: User, - // workload: Workload, - // access_token: Access_token, - // }}; - let json_value = include_str!("./test_files/test_schema.json"); - let parsed_cedar_schema: CedarSchemaJson = - serde_json::from_str(json_value).expect("Should parse JSON schema"); - let action = parsed_cedar_schema - .find_action("UpdateWithRecordCtx", "Jans") - .expect("Should not error while finding action") - .expect("Action should not be none"); - assert_eq!(action.principal_entities, expected_principal_entities); - assert_eq!(action.resource_entities, expected_resource_entities); - assert_eq!(action.context_entities, expected_context_entities); - - // Test case where the context is a type: - // action "Update" appliesTo { - // principal: [Workload, User], - // resource: [Issue], - // context: Context - // }; - let json_value = include_str!("./test_files/test_schema.json"); - let parsed_cedar_schema: CedarSchemaJson = - serde_json::from_str(json_value).expect("Should parse JSON schema"); - let action = parsed_cedar_schema - .find_action("UpdateWithTypeCtx", "Jans") - .expect("Should not error while finding action") - .expect("Action should not be none"); - assert_eq!(action.principal_entities, expected_principal_entities); - assert_eq!(action.resource_entities, expected_resource_entities); - assert_eq!(action.context_entities, expected_context_entities); - - let id_mapping = HashMap::from([ - ("access_token".into(), "tkn-1".into()), - ("user".into(), "user-123".into()), - ("workload".into(), "workload-321".into()), - ]); - let ctx_json = action - .build_ctx_entity_refs_json(id_mapping) - .expect("Should build JSON context"); - assert_eq!( - ctx_json, - json!({ - "access_token": { "type": "Jans::Access_token", "id": "tkn-1" }, - "user": { "type": "Jans::User", "id": "user-123" }, - "workload": { "type": "Jans::Workload", "id": "workload-321" }, - }) - ) + assert_eq!(schema, CedarSchemaJson { + namespaces: HashMap::from([("Jans".into(), namespace)]) + }); } } diff --git a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/action.rs b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/action.rs index efc98a2ad7e..72859e180ec 100644 --- a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/action.rs +++ b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/action.rs @@ -3,450 +3,177 @@ // // Copyright (c) 2024, Gluu, Inc. -use std::collections::{HashMap, HashSet}; - -use serde::ser::SerializeMap; -use serde::{Deserialize, Serialize, de}; -use serde_json::{Value, json}; - -use super::entity_types::{ - CedarSchemaEntityAttribute, CedarSchemaEntityType, PrimitiveType, PrimitiveTypeKind, -}; -use super::{ - CedarSchemaEntities, CedarSchemaJson, CedarSchemaRecord, CedarType, GetCedarTypeError, -}; -use crate::authz::entities::CEDAR_POLICY_SEPARATOR; -use crate::common::cedar_schema::cedar_json::SchemaDefinedType; - -type AttrName = String; - -#[derive(Debug, Eq, Hash, PartialEq)] -pub struct CtxAttribute { - pub namespace: String, - pub key: String, - pub kind: CedarType, -} - -pub struct Action<'a> { - pub principal_entities: HashSet, - pub resource_entities: HashSet, - pub context_entities: Option>, - pub schema_entities: &'a CedarSchemaEntities, - pub schema: &'a ActionSchema, +use super::attribute::Attribute; +use super::*; +use serde::Deserialize; +use std::collections::HashSet; + +#[derive(Debug, Deserialize, PartialEq, Clone)] +pub struct Action { + #[serde(rename = "memberOf", default)] + member_of: Option>, + #[serde(rename = "appliesTo")] + pub applies_to: AppliesTo, } -impl Action<'_> { - /// Builds the JSON representation of context entities for a given action. +#[derive(Debug, Deserialize, Hash, PartialEq, Eq, Clone)] +pub struct ActionGroup { + id: EntityName, + /// Specifies membership for an action group in a different namespace. /// - /// This method processes the context attributes of the action and generates a - /// corresponding JSON value. The context may include entity references (with - /// `type` and `id`) and other values, which can be mapped through the provided - /// `id_mapping` and `value_mapping`. - /// - /// The `id_mapping` param is a A `HashMap` that maps context attribute keys - /// (like `"access_token"`) to their corresponding `id`s (like `"acs-tkn-1"`). - /// - /// # Usage Example - /// - /// ```rs - /// let id_mapping = HashMap::from([("access_token".to_string(), "acs-tkn-1".to_string())]); - /// let json = action.build_ctx_entities_json(id_mapping, value_mapping); - /// ``` - pub fn build_ctx_entity_refs_json( - &self, - id_mapping: HashMap, - ) -> Result { - let mut json = json!({}); - - if let Some(ctx_entities) = &self.context_entities { - for attr in ctx_entities.iter() { - if let CedarType::TypeName(type_name) = &attr.kind { - let id = match id_mapping.get(&attr.key) { - Some(val) => val, - None => Err(BuildJsonCtxError::MissingIdMapping(attr.key.clone()))?, - }; - let type_name = - [attr.namespace.as_str(), type_name].join(CEDAR_POLICY_SEPARATOR); - json[attr.key.as_str()] = json!({"type": type_name, "id": id}); - } - } - } - - Ok(json) - } -} - -#[derive(Debug, thiserror::Error)] -pub enum BuildJsonCtxError { - /// If an entity reference is provided but the ID is missing from `id_mapping`. - /// - /// This is usually caused by: - /// - disabling workload AuthZ but having a Workload entity in the context schema - /// - disabling user AuthZ but referencing User entity in the context schema - #[error( - "An entity reference for `{0}` is required by the schema but an ID was not provided via \ - the `id_mapping`" - )] - MissingIdMapping(String), - /// If a non-entity attribute is provided but the value is missing from `value_mapping`. - #[error( - "A non-entity attribute for `{0}` is required by the schema but a value was not provided \ - via the `value_mapping`" - )] - MissingValueMapping(String), -} - -impl CedarSchemaJson { - /// Find the action in the schema - pub fn find_action( - &self, - action_name: &str, - namespace: &str, - ) -> Result, FindActionError> { - let schema_entities = match self.namespace.get(namespace) { - Some(entities) => entities, - None => return Ok(None), - }; - - let action_schema = match schema_entities.actions.get(action_name) { - Some(schema) => schema, - None => return Ok(None), - }; - - let principal_entities = HashSet::from_iter( - action_schema - .principal_types - .iter() - .map(|principal_type| [namespace, principal_type].join(CEDAR_POLICY_SEPARATOR)), - ); - let resource_entities = HashSet::from_iter( - action_schema - .resource_types - .iter() - .map(|resource_type| [namespace, resource_type].join(CEDAR_POLICY_SEPARATOR)), - ); - let context_entities = action_schema - .context - .as_ref() - .map(|ctx| self.process_action_context(ctx, namespace)) - .transpose()?; - - Ok(Some(Action { - principal_entities, - resource_entities, - context_entities, - schema_entities, - schema: action_schema, - })) - } - - fn process_action_context( - &self, - ctx: &RecordOrType, - namespace: &str, - ) -> Result, FindActionError> { - let mut entities = HashSet::::new(); - - match ctx { - // Case: the context is defined as a record in the schema - // for example: - // Jans { - // action View appliesTo { - // principal: [User], - // resource: [File], - // context: { - // "status": String, - // "id_token": Id_token, - // }, - // }; - // } - RecordOrType::Record(record) => { - for (key, attr) in record.attributes.iter() { - entities.insert(CtxAttribute { - namespace: namespace.to_string(), - key: key.to_string(), - kind: attr.get_type()?, - }); - } - }, - // Case: the context is defined as a type in the schema - // for example: - // Jans { - // type Context = { - // "status": String, - // "id_token": Id_token, - // }; - // action View appliesTo { - // principal: [User], - // resource: [File], - // context: Context, - // }; - // } - RecordOrType::Type(entity_type) => match entity_type { - CedarSchemaEntityType::Primitive(primitive_type) => { - if let PrimitiveTypeKind::TypeName(type_name) = &primitive_type.kind { - let cedar_type = self.find_type(type_name, namespace).unwrap(); - match cedar_type { - SchemaDefinedType::CommonType(common) => { - for (key, attr) in common.attributes.iter() { - entities.insert(CtxAttribute { - namespace: namespace.to_string(), - key: key.to_string(), - kind: attr.get_type()?, - }); - } - }, - SchemaDefinedType::Entity(_) => { - Err(FindActionError::EntityContext(entity_type.clone()))? - }, - } - } - }, - CedarSchemaEntityType::Set(_) => { - Err(FindActionError::SetContext(entity_type.clone()))? - }, - CedarSchemaEntityType::Typed(_) => { - Err(FindActionError::TypedContext(entity_type.clone()))? - }, - }, - } - - Ok(entities) - } -} - -/// Represents an action in the Cedar JSON schema -#[derive(Default, Debug, PartialEq, Clone)] -pub struct ActionSchema { - pub resource_types: HashSet, - pub principal_types: HashSet, - pub context: Option, -} - -impl Serialize for ActionSchema { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let mut state = serializer.serialize_map(Some(1))?; - state.serialize_entry( - "appliesTo", - &json!({ - "resourceTypes": self.resource_types, - "principalTypes": self.principal_types, - "context": self.context, - }), - )?; - state.end() - } -} - -impl<'de> Deserialize<'de> for ActionSchema { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let mut action = HashMap::>::deserialize(deserializer)?; - let mut action = action - .remove("appliesTo") - .ok_or(de::Error::missing_field("appliesTo"))?; - - let resource_types = action - .remove("resourceTypes") - .map(|val| serde_json::from_value::>(val).map_err(de::Error::custom)) - .transpose()? - .ok_or(de::Error::missing_field("resourceTypes"))?; - - let principal_types = action - .remove("principalTypes") - .map(|val| serde_json::from_value::>(val).map_err(de::Error::custom)) - .transpose()? - .ok_or(de::Error::missing_field("principalTypes"))?; - - let context = action - .remove("context") - .map(|val| serde_json::from_value::(val).map_err(de::Error::custom)) - .transpose()?; - - Ok(Self { - resource_types, - principal_types, - context, - }) - } -} - -#[derive(Debug, PartialEq, Clone, Serialize)] -pub enum RecordOrType { - Record(CedarSchemaRecord), - Type(CedarSchemaEntityType), + /// e.g.: `kind: "My::Namespace::Action"` + #[serde(rename = "type", default)] + kind: Option, } -impl<'de> Deserialize<'de> for RecordOrType { - fn deserialize(deserializer: D) -> Result - where - D: de::Deserializer<'de>, - { - let mut context = HashMap::::deserialize(deserializer)?; - let context_type = context - .remove("type") - .map(|val| serde_json::from_value::(val).map_err(de::Error::custom)) - .transpose()? - .ok_or(de::Error::missing_field("type"))?; - - match context_type.as_str() { - "Record" => { - let attributes = context - .remove("attributes") - .map(|val| { - serde_json::from_value::>(val) - .map_err(de::Error::custom) - }) - .transpose()? - .ok_or(de::Error::missing_field("attributes"))?; - Ok(RecordOrType::Record(CedarSchemaRecord { - entity_type: "Record".to_string(), - attributes, - })) - }, - type_name => Ok(RecordOrType::Type(CedarSchemaEntityType::Primitive( - PrimitiveType { - kind: PrimitiveTypeKind::TypeName(type_name.to_string()), - }, - ))), - } - } -} - -#[derive(Debug, thiserror::Error)] -pub enum FindActionError { - #[error("Error while collecting entities from action schema: {0}")] - CollectEntities(#[from] GetCedarTypeError), - #[error("Using `Set` as the context type is unsupported: {0:#?}")] - SetContext(CedarSchemaEntityType), - #[error("Using `Entity` as the context type is unsupported: {0:#?}")] - EntityContext(CedarSchemaEntityType), - #[error("Using `Typed` as the context type is unsupported: {0:#?}")] - TypedContext(CedarSchemaEntityType), +#[derive(Debug, Deserialize, PartialEq, Clone)] +pub struct AppliesTo { + #[serde(rename = "principalTypes", default)] + pub principal_types: HashSet, + #[serde(rename = "resourceTypes", default)] + pub resource_types: HashSet, + #[serde(default)] + pub context: Option, } #[cfg(test)] -mod test { +mod test_deserialize_action { + use super::super::attribute::Attribute; + use super::{Action, ActionGroup, AppliesTo}; + use serde_json::json; use std::collections::{HashMap, HashSet}; + use test_utils::assert_eq; - use serde::Deserialize; - use serde_json::{Value, json}; - - use super::ActionSchema; - use crate::common::cedar_schema::cedar_json::CedarSchemaRecord; - use crate::common::cedar_schema::cedar_json::action::RecordOrType; - use crate::common::cedar_schema::cedar_json::entity_types::{ - CedarSchemaEntityAttribute, CedarSchemaEntityType, EntityType, PrimitiveType, - PrimitiveTypeKind, - }; - - type ActionType = String; - #[derive(Deserialize, Debug, PartialEq)] - struct MockJsonSchema { - actions: HashMap, - } - - fn build_schema(ctx: Option) -> Value { - let mut schema = json!({ - "actions": { - "Update": { - "appliesTo": { - "resourceTypes": ["Issue"], - "principalTypes": ["Workload", "User"] - } - } + #[test] + fn can_deserialize() { + // Case: both principal types and resource types is empty + let action = json!({ + "appliesTo": { + "principalTypes": [], + "resourceTypes": [], } }); - if let Some(ctx) = ctx { - schema["actions"]["Update"]["appliesTo"]["context"] = ctx; - } - schema - } - - fn build_expected(ctx: Option) -> MockJsonSchema { - MockJsonSchema { - actions: HashMap::from([("Update".to_string(), ActionSchema { - resource_types: HashSet::from(["Issue"].map(|s| s.to_string())), - principal_types: HashSet::from(["Workload", "User"].map(|s| s.to_string())), - context: ctx, - })]), - } - } - - #[test] - pub fn can_deserialize_empty_ctx() { - let schema = build_schema(None); - - let result = serde_json::from_value::(schema) - .expect("Value should be deserialized successfully"); + let action = serde_json::from_value::(action).unwrap(); + assert_eq!(action, Action { + member_of: None, + applies_to: AppliesTo { + principal_types: HashSet::new(), + resource_types: HashSet::new(), + context: None, + }, + }); - let expected = build_expected(None); + // Case: resource types is empty + let action = json!({ + "appliesTo": { + "principalTypes": ["PrincipalEntityType1"], + "resourceTypes": [], + } + }); + let action = serde_json::from_value::(action).unwrap(); + assert_eq!(action, Action { + member_of: None, + applies_to: AppliesTo { + principal_types: HashSet::from(["PrincipalEntityType1".into()]), + resource_types: HashSet::new(), + context: None, + }, + }); - assert_eq!(result, expected) + // Case: only principal types is empty + let action = json!({ + "appliesTo": { + "principalTypes": [], + "resourceTypes": ["ResourceEntityType1"], + } + }); + let action = serde_json::from_value::(action).unwrap(); + assert_eq!(action, Action { + member_of: None, + applies_to: AppliesTo { + principal_types: HashSet::new(), + resource_types: HashSet::from(["ResourceEntityType1".into()]), + context: None, + }, + }); } #[test] - pub fn can_deserialize_record_ctx() { - let schema = build_schema(Some(json!({ - "type": "Record", - "attributes": { - "token": { - "type": "EntityOrCommon", - "name": "Access_token" - }, - "username": { - "type": "EntityOrCommon", - "name": "String" - } + fn can_deserialize_with_member_of() { + // Case: action group type is not provided + let action = json!({ + "memberOf": [{"id": "read"}], + "appliesTo": { + "principalTypes": ["User"], + "resourceTypes": ["Photo"], } - }))); - - let result = serde_json::from_value::(schema) - .expect("Value should be deserialized successfully"); - - let expected = build_expected(Some(RecordOrType::Record(CedarSchemaRecord { - entity_type: "Record".to_string(), - attributes: HashMap::from([ - ("token".to_string(), CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "Access_token".to_string(), - }), - required: true, - }), - ("username".to_string(), CedarSchemaEntityAttribute { - cedar_type: CedarSchemaEntityType::Typed(EntityType { - kind: "EntityOrCommon".to_string(), - name: "String".to_string(), - }), - required: true, - }), - ]), - }))); + }); + let action = serde_json::from_value::(action).unwrap(); + assert_eq!(action, Action { + member_of: Some(HashSet::from([ActionGroup { + id: "read".into(), + kind: None + }])), + applies_to: AppliesTo { + principal_types: HashSet::from(["User".into()]), + resource_types: HashSet::from(["Photo".into()]), + context: None, + }, + }); - assert_eq!(result, expected) + // Case: an action group type is provided + let action = json!({ + "memberOf": [{ + "id": "read", + "type": "My::Namespace::Action", + }], + "appliesTo": { + "principalTypes": ["User"], + "resourceTypes": ["Photo"], + } + }); + let action = serde_json::from_value::(action).unwrap(); + assert_eq!(action, Action { + member_of: Some(HashSet::from([ActionGroup { + id: "read".into(), + kind: Some("My::Namespace::Action".into()), + }])), + applies_to: AppliesTo { + principal_types: HashSet::from(["User".into()]), + resource_types: HashSet::from(["Photo".into()]), + context: None, + }, + }); } #[test] - pub fn can_deserialize_entity_or_common_ctx() { - let schema = build_schema(Some(json!({ - "type": "Context", - }))); - - let result = serde_json::from_value::(schema) - .expect("Value should be deserialized successfully"); - - let expected = build_expected(Some(RecordOrType::Type(CedarSchemaEntityType::Primitive( - PrimitiveType { - kind: PrimitiveTypeKind::TypeName("Context".to_string()), + fn can_deserialize_with_context() { + let action = json!({ + "appliesTo": { + "principalTypes": ["PrincipalEntityType1"], + "resourceTypes": ["ResourceEntityType1"], + "context": { + "type": "Record", + "attributes": { + "field1": { "type": "Boolean" }, + "field2": { "type": "Long" }, + "field3": { "type": "String", "required": false }, + } + }, }, - )))); - - assert_eq!(result, expected) + }); + let action = serde_json::from_value::(action).unwrap(); + assert_eq!(action, Action { + member_of: None, + applies_to: AppliesTo { + principal_types: HashSet::from(["PrincipalEntityType1".into()]), + resource_types: HashSet::from(["ResourceEntityType1".into()]), + context: Some(Attribute::record(HashMap::from([ + ("field1".into(), Attribute::boolean()), + ("field2".into(), Attribute::long()), + ("field3".into(), Attribute::String { required: false }) + ]))), + }, + }); } } diff --git a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/attribute.rs b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/attribute.rs new file mode 100644 index 00000000000..91f6492f622 --- /dev/null +++ b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/attribute.rs @@ -0,0 +1,283 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use super::deserialize::*; +use super::*; +use serde::{Deserialize, de}; +use serde_json::Value; +use std::collections::HashMap; + +#[derive(Debug, PartialEq, Clone)] +pub enum Attribute { + String { + required: bool, + }, + Long { + required: bool, + }, + Boolean { + required: bool, + }, + Record { + required: bool, + attrs: HashMap, + }, + Set { + required: bool, + element: Box, + }, + Entity { + required: bool, + name: EntityName, + }, + Extension { + required: bool, + name: ExtensionName, + }, + EntityOrCommon { + required: bool, + name: EntityOrCommonName, + }, +} + +impl<'de> Deserialize<'de> for Attribute { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let mut attr = HashMap::::deserialize(deserializer)?; + let kind = attr + .remove("type") + .ok_or(de::Error::missing_field("type"))?; + let required = attr + .remove("required") + .map(serde_json::from_value::) + .transpose() + .map_err(|e| { + de::Error::custom(format!("error while deserializing JSON Value to bool: {e}")) + })? + .unwrap_or(true); + let kind = String::deserialize(&kind).map_err(de::Error::custom)?; + let attr = match kind.as_str() { + "String" => Attribute::String { required }, + "Long" => Attribute::Long { required }, + "Boolean" => Attribute::Boolean { required }, + "Record" => { + let attrs = attr + .remove("attributes") + .ok_or(de::Error::missing_field("attributes"))?; + let attrs = deserialize_record_attrs::(attrs)?; + Self::Record { required, attrs } + }, + "Set" => { + let element = attr + .remove("element") + .ok_or(de::Error::missing_field("element"))?; + let element = serde_json::from_value::(element).map_err(|e| { + de::Error::custom(format!( + "error while deserializing cedar element attribute: {e}" + )) + })?; + + Self::Set { + required, + element: Box::new(element), + } + }, + "Entity" => { + let name = attr + .remove("name") + .ok_or(de::Error::missing_field("name"))?; + let name = String::deserialize(&name).map_err(de::Error::custom)?; + Self::Entity { required, name } + }, + "Extension" => { + let name = attr + .remove("name") + .ok_or(de::Error::missing_field("name"))?; + let name = String::deserialize(&name).map_err(de::Error::custom)?; + Self::Extension { required, name } + }, + "EntityOrCommon" => { + let name = attr + .remove("name") + .ok_or(de::Error::missing_field("name"))?; + let name = String::deserialize(&name).map_err(de::Error::custom)?; + Self::EntityOrCommon { required, name } + }, + name => Self::EntityOrCommon { + required, + name: name.to_string(), + }, + }; + + Ok(attr) + } +} + +impl Attribute { + pub fn is_required(&self) -> bool { + *match self { + Attribute::String { required } => required, + Attribute::Long { required } => required, + Attribute::Boolean { required } => required, + Attribute::Record { required, .. } => required, + Attribute::Set { required, .. } => required, + Attribute::Entity { required, .. } => required, + Attribute::Extension { required, .. } => required, + Attribute::EntityOrCommon { required, .. } => required, + } + } +} + +#[cfg(test)] +/// Helper methods to easily create required attributes +impl Attribute { + pub fn string() -> Self { + Self::String { required: true } + } + + pub fn long() -> Self { + Self::Long { required: true } + } + + pub fn boolean() -> Self { + Self::Boolean { required: true } + } + + pub fn record(attrs: HashMap) -> Self { + Self::Record { + required: true, + attrs, + } + } + + pub fn set(element: Self) -> Self { + Self::Set { + required: true, + + element: Box::new(element), + } + } + + pub fn entity(name: &str) -> Self { + Self::Entity { + required: true, + name: name.into(), + } + } + + pub fn extension(name: &str) -> Self { + Self::Extension { + required: true, + name: name.into(), + } + } + + pub fn entity_or_common(name: &str) -> Self { + Self::EntityOrCommon { + required: true, + name: name.into(), + } + } +} + +#[cfg(test)] +mod test { + use super::Attribute; + use serde_json::json; + use std::collections::HashMap; + + #[test] + fn can_deserialize_string() { + let attr_json = json!({"type": "String"}); + let deserialized = serde_json::from_value::(attr_json).unwrap(); + assert_eq!(deserialized, Attribute::string()); + } + + #[test] + fn can_deserialize_long() { + let attr_json = json!({"type": "Long"}); + let deserialized = serde_json::from_value::(attr_json).unwrap(); + assert_eq!(deserialized, Attribute::long()); + } + + #[test] + fn can_deserialize_boolean() { + let attr_json = json!({"type": "Boolean"}); + let deserialized = serde_json::from_value::(attr_json).unwrap(); + assert_eq!(deserialized, Attribute::boolean()); + } + + #[test] + fn can_deserialize_record() { + let attr_json = json!({ + "type": "Record", + "attributes": { + "primary": { "type": "String" }, + "secondary": { "type": "String" }, + }, + }); + let deserialized = serde_json::from_value::(attr_json).unwrap(); + let expected = HashMap::from([ + ("primary".into(), Attribute::string()), + ("secondary".into(), Attribute::string()), + ]); + assert_eq!(deserialized, Attribute::record(expected)); + } + + #[test] + fn can_deserialize_set() { + let attr_json = json!({ + "type": "Set", + "element": { + "type": "EntityOrCommon", + "name": "Subscription" + } + }); + let deserialized = serde_json::from_value::(attr_json).unwrap(); + assert_eq!( + deserialized, + Attribute::set(Attribute::entity_or_common("Subscription")) + ); + } + + #[test] + fn can_deserialize_entity() { + let attr_json = json!({ + "type": "Entity", + "name": "Role", + }); + let deserialized = serde_json::from_value::(attr_json).unwrap(); + assert_eq!(deserialized, Attribute::entity("Role")); + } + + #[test] + fn can_deserialize_extension() { + let attr_json = json!({ + "type": "Extension", + "name": "decimal", + }); + let deserialized = serde_json::from_value::(attr_json).unwrap(); + assert_eq!(deserialized, Attribute::extension("decimal"),); + } + + #[test] + fn can_deserialize_entity_or_common() { + let attr_json = json!({ + "type": "EntityOrCommon", + "name": "String", + }); + let deserialized = serde_json::from_value::(attr_json).unwrap(); + assert_eq!(deserialized, Attribute::entity_or_common("String"),); + } + + #[test] + fn can_deserialize_non_required_attr() { + let attr_json = json!({"type": "String", "required": false}); + let deserialized = serde_json::from_value::(attr_json).unwrap(); + assert_eq!(deserialized, Attribute::String { required: false }); + } +} diff --git a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/deserialize.rs b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/deserialize.rs new file mode 100644 index 00000000000..55cf8cfd77d --- /dev/null +++ b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/deserialize.rs @@ -0,0 +1,36 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use super::*; +use serde::de; +use serde_json::Value; +use std::collections::HashMap; + +/// Deserialize a [`Value`] to a to the attrs of a [`AttributeKind::Record`] +pub fn deserialize_record_attrs<'de, D>( + attrs: Value, +) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let attrs_json = serde_json::from_value::>(attrs).map_err(|e| { + de::Error::custom(format!( + "error while deserializing cedar record attribute: {e}" + )) + })?; + + // loop through each attr then deserialize into Self + let mut attrs = HashMap::::new(); + for (key, val) in attrs_json.into_iter() { + let val = serde_json::from_value::(val).map_err(|e| { + de::Error::custom(format!( + "error while deserializing cedar record attribute: {e}" + )) + })?; + attrs.insert(key, val); + } + + Ok(attrs) +} diff --git a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/entity_type.rs b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/entity_type.rs new file mode 100644 index 00000000000..217d1c32791 --- /dev/null +++ b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/entity_type.rs @@ -0,0 +1,171 @@ +// This software is available under the Apache-2.0 license. +// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. +// +// Copyright (c) 2024, Gluu, Inc. + +use super::attribute::Attribute; +use super::deserialize::*; +use super::*; +use serde::{Deserialize, de}; +use serde_json::Value; +use std::collections::HashSet; + +#[derive(Debug, PartialEq, Clone)] +pub struct EntityShape { + pub required: bool, + pub attrs: HashMap, +} + +#[cfg(test)] +impl EntityShape { + pub fn required(attrs: HashMap) -> Self { + Self { + required: true, + attrs, + } + } +} + +#[derive(Debug, PartialEq, Deserialize, Clone)] +pub struct EntityType { + #[serde(rename = "memberOfTypes")] + pub member_of: Option>, + #[serde(deserialize_with = "deserialize_entity_shape", default)] + pub shape: Option, + #[serde(default)] + pub tags: Option, +} + +// Forces the `shape` field into the [`AttributeKind::Shape`] variant. +fn deserialize_entity_shape<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let mut attr = HashMap::::deserialize(deserializer)?; + let kind = attr + .remove("type") + .ok_or(de::Error::missing_field("type"))?; + let required = attr + .remove("required") + .map(serde_json::from_value::) + .transpose() + .map_err(|e| { + de::Error::custom(format!("error while deserializing JSON Value to bool: {e}")) + })? + .unwrap_or(true); + let kind = String::deserialize(&kind).map_err(de::Error::custom)?; + let attr = match kind.as_str() { + "Record" => { + let attrs = attr + .remove("attributes") + .ok_or(de::Error::missing_field("attributes"))?; + let attrs = deserialize_record_attrs::(attrs)?; + EntityShape { required, attrs } + }, + variant => { + return Err(de::Error::custom(format!( + "invalid type: {}, expected {}", + variant, "Record" + ))); + }, + }; + + Ok(Some(attr)) +} + +#[cfg(test)] +mod test_deserialize_entity_type { + use super::super::attribute::Attribute; + use super::*; + use serde_json::json; + use std::collections::{HashMap, HashSet}; + use test_utils::assert_eq; + + #[test] + fn can_deserialize() { + let entity_type = json!({ + "shape": { + "type": "Record", + "attributes": { + "name": {"type": "String"}, + "age": {"type": "Long"}, + }, + }, + }); + let entity_type = serde_json::from_value::(entity_type).unwrap(); + assert_eq!(entity_type, EntityType { + member_of: None, + shape: Some(EntityShape::required(HashMap::from([ + ("name".into(), Attribute::string()), + ("age".into(), Attribute::long()) + ]))), + tags: None, + }); + } + + #[test] + fn can_deserialize_with_member_of() { + let with_member_of = json!({ + "memberOfTypes": ["UserGroup"], + "shape": { + "type": "Record", + "attributes": { + "name": {"type": "String"}, + "age": {"type": "Long"}, + }, + }, + }); + let with_member_of = serde_json::from_value::(with_member_of).unwrap(); + assert_eq!(with_member_of, EntityType { + member_of: Some(HashSet::from(["UserGroup".into()])), + shape: Some(EntityShape::required(HashMap::from([ + ("name".into(), Attribute::string()), + ("age".into(), Attribute::long()) + ]))), + tags: None, + }); + } + + #[test] + fn can_deserialize_with_tags() { + let with_tags = json!({ + "shape": { + "type": "Record", + "attributes": { + "name": {"type": "String"}, + "age": {"type": "Long"}, + }, + }, + "tags": { + "type": "Set", + "element": { + "type": "EntityOrCommon", + "name": "String" + } + } + }); + let with_tags = serde_json::from_value::(with_tags).unwrap(); + assert_eq!(with_tags, EntityType { + member_of: None, + shape: Some(EntityShape::required(HashMap::from([ + ("name".into(), Attribute::string()), + ("age".into(), Attribute::long()) + ]))), + tags: Some(Attribute::set(Attribute::entity_or_common("String",))) + }); + } + + #[test] + fn errors_on_invalid_shape() { + let entity_type = json!({ + "shape": { + "type": "Set", + }, + }); + let err = serde_json::from_value::(entity_type).unwrap_err(); + assert!( + err.to_string() + .contains("invalid type: Set, expected Record") + ); + } +} diff --git a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/entity_types.rs b/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/entity_types.rs deleted file mode 100644 index 9d855ed6449..00000000000 --- a/jans-cedarling/cedarling/src/common/cedar_schema/cedar_json/entity_types.rs +++ /dev/null @@ -1,226 +0,0 @@ -// This software is available under the Apache-2.0 license. -// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text. -// -// Copyright (c) 2024, Gluu, Inc. - -use std::collections::HashMap; - -use super::{CedarType, GetCedarTypeError}; - -/// CedarSchemaEntityShape hold shape of an entity. -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq)] -pub struct CedarSchemaEntityShape { - pub shape: Option, -} - -/// CedarSchemaRecord defines type name and attributes for an entity. -/// Record ::= '"type": "Record", "attributes": {' [ RecordAttr { ',' RecordAttr } ] '}' -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq)] -pub struct CedarSchemaRecord { - #[serde(rename = "type")] - pub entity_type: String, - // represent RecordAttr - // RecordAttr ::= STR ': {' Type [',' '"required"' ':' ( true | false )] '}' - // attributes as key is used attribute name - pub attributes: HashMap, -} - -impl CedarSchemaRecord { - // if we want to create entity from attributes it should be record - pub fn is_record(&self) -> bool { - self.entity_type == "Record" - } -} - -/// CedarSchemaRecordAttr defines possible type variants of the entity attribute. -/// RecordAttr ::= STR ': {' Type [',' '"required"' ':' ( true | false )] '}' -#[derive(Debug, Clone, PartialEq, serde::Serialize, Hash)] -pub struct CedarSchemaEntityAttribute { - pub cedar_type: CedarSchemaEntityType, - pub required: bool, -} - -impl CedarSchemaEntityAttribute { - pub fn is_required(&self) -> bool { - self.required - } - - pub fn get_type(&self) -> Result { - self.cedar_type.get_type() - } -} - -impl<'de> serde::Deserialize<'de> for CedarSchemaEntityAttribute { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let value: serde_json::Value = serde::Deserialize::deserialize(deserializer)?; - - // used only for deserialization - #[derive(serde::Deserialize)] - pub struct IsRequired { - required: Option, - } - - let is_required = IsRequired::deserialize(&value).map_err(|err| { - serde::de::Error::custom(format!( - "could not deserialize CedarSchemaEntityAttribute, field 'is_required': {}", - err - )) - })?; - - let cedar_type = CedarSchemaEntityType::deserialize(value).map_err(|err| { - serde::de::Error::custom(format!( - "could not deserialize CedarSchemaEntityType: {}", - err - )) - })?; - - Ok(CedarSchemaEntityAttribute { - cedar_type, - required: is_required.required.unwrap_or(true), - }) - } -} - -#[derive(Debug, Clone, PartialEq, serde::Serialize, Hash)] -pub enum CedarSchemaEntityType { - Set(Box), - Typed(EntityType), - Primitive(PrimitiveType), -} - -impl CedarSchemaEntityType { - pub fn get_type(&self) -> Result { - match self { - Self::Set(v) => Ok(CedarType::Set(Box::new(v.element.get_type()?))), - Self::Typed(v) => v.get_type(), - Self::Primitive(primitive) => Ok(primitive.kind.get_type()), - } - } -} - -impl<'de> serde::Deserialize<'de> for CedarSchemaEntityType { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - // is used only on deserialization. - #[derive(serde::Deserialize)] - struct TypeStruct { - #[serde(rename = "type")] - type_name: String, - } - - let value: serde_json::Value = serde::Deserialize::deserialize(deserializer)?; - - let entity_type = match TypeStruct::deserialize(&value) - .map_err(serde::de::Error::custom)? - .type_name - .as_str() - { - "Set" => { - CedarSchemaEntityType::Set(Box::new(SetEntityType::deserialize(&value).map_err( - |err| serde::de::Error::custom(format!("failed to deserialize Set: {}", err)), - )?)) - }, - "EntityOrCommon" => { - CedarSchemaEntityType::Typed(EntityType::deserialize(&value).map_err(|err| { - serde::de::Error::custom(format!( - "failed to deserialize EntityOrCommon: {}", - err - )) - })?) - }, - _ => CedarSchemaEntityType::Primitive(PrimitiveType::deserialize(&value).map_err( - |err| { - // will newer happen because we know that field "type" is string - serde::de::Error::custom(format!( - "failed to deserialize PrimitiveType: {}", - err - )) - }, - )?), - }; - - Ok(entity_type) - } -} - -/// The Primitive element describes -/// Primitive ::= '"type":' ('"Long"' | '"String"' | '"Boolean"' | TYPENAME) -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq, Hash)] -pub struct PrimitiveType { - #[serde(rename = "type")] - pub kind: PrimitiveTypeKind, -} - -/// Variants of primitive type. -/// Primitive ::= '"type":' ('"Long"' | '"String"' | '"Boolean"' | TYPENAME) -#[derive(Debug, Clone, serde::Serialize, PartialEq, Hash)] -pub enum PrimitiveTypeKind { - Long, - String, - Boolean, - TypeName(String), -} - -impl PrimitiveTypeKind { - pub fn get_type(&self) -> CedarType { - match self { - PrimitiveTypeKind::Long => CedarType::Long, - PrimitiveTypeKind::String => CedarType::String, - PrimitiveTypeKind::Boolean => CedarType::Boolean, - PrimitiveTypeKind::TypeName(name) => CedarType::TypeName(name.to_string()), - } - } -} - -/// impement custom deserialization to deserialize it correctly -impl<'de> serde::Deserialize<'de> for PrimitiveTypeKind { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s: String = serde::Deserialize::deserialize(deserializer)?; - match s.as_str() { - "Long" => Ok(PrimitiveTypeKind::Long), - "String" => Ok(PrimitiveTypeKind::String), - "Boolean" => Ok(PrimitiveTypeKind::Boolean), - _ => Ok(PrimitiveTypeKind::TypeName(s)), - } - } -} - -/// This structure can hold `Extension`, `EntityOrCommon`, `EntityRef` -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq, Hash)] -pub struct EntityType { - // it also can be primitive type - #[serde(rename = "type")] - pub kind: String, - pub name: String, -} - -impl EntityType { - pub fn get_type(&self) -> Result { - if self.kind == "EntityOrCommon" { - match self.name.as_str() { - "Long" => Ok(CedarType::Long), - "String" => Ok(CedarType::String), - "Boolean" => Ok(CedarType::Boolean), - type_name => Ok(CedarType::TypeName(type_name.to_string())), - } - } else { - Err(GetCedarTypeError::TypeNotImplemented(self.kind.to_string())) - } - } -} - -/// Describes the Set element -/// Set ::= '"type": "Set", "element": ' TypeJson -// "type": "Set" checked during deserialization -#[derive(Debug, Clone, serde::Deserialize, PartialEq, serde::Serialize, Hash)] -pub struct SetEntityType { - pub element: CedarSchemaEntityType, -} diff --git a/jans-cedarling/cedarling/src/common/cedar_schema/mod.rs b/jans-cedarling/cedarling/src/common/cedar_schema/mod.rs index dda9cef224f..6d0489ce491 100644 --- a/jans-cedarling/cedarling/src/common/cedar_schema/mod.rs +++ b/jans-cedarling/cedarling/src/common/cedar_schema/mod.rs @@ -3,8 +3,8 @@ // // Copyright (c) 2024, Gluu, Inc. -pub(crate) use cedar_json::CedarSchemaJson; pub(crate) mod cedar_json; +pub(crate) const CEDAR_NAMESPACE_SEPARATOR: &str = "::"; /// cedar_schema value which specifies both encoding and content_type /// diff --git a/jans-cedarling/cedarling/src/common/policy_store.rs b/jans-cedarling/cedarling/src/common/policy_store.rs index 794a00babd8..cba32133696 100644 --- a/jans-cedarling/cedarling/src/common/policy_store.rs +++ b/jans-cedarling/cedarling/src/common/policy_store.rs @@ -202,7 +202,7 @@ pub struct TokensMetadata<'a> { pub tx_tokens: &'a TokenEntityMetadata, } -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] pub enum TokenKind { /// Access token used for granting access to resources. Access, diff --git a/jans-cedarling/cedarling/src/common/policy_store/claim_mapping.rs b/jans-cedarling/cedarling/src/common/policy_store/claim_mapping.rs index d959e724d21..6137dc069ce 100644 --- a/jans-cedarling/cedarling/src/common/policy_store/claim_mapping.rs +++ b/jans-cedarling/cedarling/src/common/policy_store/claim_mapping.rs @@ -17,9 +17,29 @@ use serde_json::Value; pub struct ClaimMappings(HashMap); impl ClaimMappings { - pub fn get_mapping(&self, field: &str, cedar_policy_type: &str) -> Option<&ClaimMapping> { + pub fn get(&self, claim: &str) -> Option<&ClaimMapping> { + self.0.get(claim) + } + + // returns (claim_name, &ClaimMapping) + pub fn get_mapping_for_type(&self, type_name: &str) -> Option<(&String, &ClaimMapping)> { + // PERF: we can probably avoiding iterating through all of this by changing the + // `claim_mapping` in the Token Entity Metadata Schema + self.0 + .iter() + .find_map(|(claim_name, mapping)| match mapping { + ClaimMapping::Regex(regex_mapping) => { + (regex_mapping.cedar_policy_type == type_name).then_some((claim_name, mapping)) + }, + ClaimMapping::Json { r#type } => { + (r#type == type_name).then_some((claim_name, mapping)) + }, + }) + } + + pub fn get_mapping(&self, claim: &str, cedar_policy_type: &str) -> Option<&ClaimMapping> { self.0 - .get(field) + .get(claim) .filter(|claim_mapping| match claim_mapping { ClaimMapping::Regex(regexp_mapping) => { regexp_mapping.cedar_policy_type == cedar_policy_type diff --git a/jans-cedarling/cedarling/src/init/service_factory.rs b/jans-cedarling/cedarling/src/init/service_factory.rs index 9b7ea155cd9..6438248f1a1 100644 --- a/jans-cedarling/cedarling/src/init/service_factory.rs +++ b/jans-cedarling/cedarling/src/init/service_factory.rs @@ -7,16 +7,14 @@ //! Module to lazily initialize internal cedarling services -use std::sync::Arc; - -use crate::bootstrap_config::BootstrapConfig; -use crate::common::policy_store::PolicyStoreWithID; -use crate::jwt::{JwtService, JwtServiceInitError}; - use super::service_config::ServiceConfig; use crate::authz::{Authz, AuthzConfig}; +use crate::bootstrap_config::BootstrapConfig; use crate::common::app_types; +use crate::common::policy_store::PolicyStoreWithID; +use crate::jwt::{JwtService, JwtServiceInitError}; use crate::log; +use std::sync::Arc; #[derive(Clone)] pub(crate) struct ServiceFactory<'a> { diff --git a/jans-cedarling/cedarling/src/jwt/mod.rs b/jans-cedarling/cedarling/src/jwt/mod.rs index 9f671ac871e..98f39586194 100644 --- a/jans-cedarling/cedarling/src/jwt/mod.rs +++ b/jans-cedarling/cedarling/src/jwt/mod.rs @@ -24,7 +24,7 @@ use std::sync::Arc; pub use jsonwebtoken::Algorithm; use key_service::{KeyService, KeyServiceError}; -pub use token::{Token, TokenClaim, TokenClaimTypeError, TokenClaims, TokenStr}; +pub use token::{Token, TokenClaimTypeError, TokenClaims, TokenStr}; use validator::{JwtValidator, JwtValidatorConfig, JwtValidatorError}; use crate::common::policy_store::TrustedIssuer; diff --git a/jans-cedarling/cedarling/src/jwt/token.rs b/jans-cedarling/cedarling/src/jwt/token.rs index 58a28f31b57..47e5ab3b7a0 100644 --- a/jans-cedarling/cedarling/src/jwt/token.rs +++ b/jans-cedarling/cedarling/src/jwt/token.rs @@ -81,13 +81,13 @@ impl<'a> Token<'a> { self.claims.logging_info(claim) } - pub fn claims(&self) -> &TokenClaims { - &self.claims + pub fn claims_value(&self) -> &HashMap { + &self.claims.claims } } /// A struct holding information on a decoded JWT. -#[derive(Debug, PartialEq, Default, Deserialize)] +#[derive(Debug, PartialEq, Default, Deserialize, Clone)] pub struct TokenClaims { #[serde(flatten)] claims: HashMap, @@ -100,14 +100,11 @@ impl From> for TokenClaims { } impl TokenClaims { + #[cfg(test)] pub fn new(claims: HashMap) -> Self { Self { claims } } - pub fn from_json_map(map: serde_json::Map) -> Self { - Self::new(HashMap::from_iter(map)) - } - pub fn get_claim(&self, name: &str) -> Option { self.claims.get(name).map(|value| TokenClaim { key: name.to_string(), @@ -132,22 +129,10 @@ pub struct TokenClaim<'a> { } impl TokenClaim<'_> { - pub fn key(&self) -> &str { - &self.key - } - pub fn value(&self) -> &serde_json::Value { self.value } - pub fn as_i64(&self) -> Result { - self.value - .as_i64() - .ok_or(TokenClaimTypeError::type_mismatch( - &self.key, "i64", self.value, - )) - } - pub fn as_str(&self) -> Result<&str, TokenClaimTypeError> { self.value .as_str() @@ -155,39 +140,10 @@ impl TokenClaim<'_> { &self.key, "String", self.value, )) } - - pub fn as_bool(&self) -> Result { - self.value - .as_bool() - .ok_or(TokenClaimTypeError::type_mismatch( - &self.key, "bool", self.value, - )) - } - - pub fn as_array(&self) -> Result, TokenClaimTypeError> { - self.value - .as_array() - .map(|array| { - array - .iter() - .enumerate() - .map(|(i, v)| { - TokenClaim { - // show current key and index in array - key: format!("{}[{}]", self.key, i), - value: v, - } - }) - .collect() - }) - .ok_or(TokenClaimTypeError::type_mismatch( - &self.key, "Array", self.value, - )) - } } -#[derive(Debug, thiserror::Error)] -#[error("type mismatch for key '{key}'. expected: '{expected_type}', but found: '{actual_type}'")] +#[derive(Debug, thiserror::Error, PartialEq)] +#[error("type mismatch for token claim '{key}'. expected: '{expected_type}', but found: '{actual_type}'")] pub struct TokenClaimTypeError { pub key: String, pub expected_type: String, @@ -208,7 +164,7 @@ impl TokenClaimTypeError { } /// Constructs a `TypeMismatch` error with detailed information about the expected and actual types. - fn type_mismatch(key: &str, expected_type_name: &str, got_value: &Value) -> Self { + pub fn type_mismatch(key: &str, expected_type_name: &str, got_value: &Value) -> Self { let got_value_type_name = Self::json_value_type_name(got_value); Self { diff --git a/jans-cedarling/cedarling/src/lib.rs b/jans-cedarling/cedarling/src/lib.rs index 7f60bcc1d6c..389b4ab177f 100644 --- a/jans-cedarling/cedarling/src/lib.rs +++ b/jans-cedarling/cedarling/src/lib.rs @@ -45,8 +45,6 @@ use log::interface::LogWriter; use log::{LogEntry, LogType}; pub use log::{LogLevel, LogStorage}; -pub use crate::authz::entities::CreateCedarEntityError; - #[doc(hidden)] pub mod bindings { pub use cedar_policy; @@ -124,12 +122,12 @@ impl Cedarling { /// Get entites derived from `cedar-policy` schema and tokens for `authorize` request. #[doc(hidden)] #[cfg(test)] - pub async fn authorize_entities_data( + pub async fn build_entities( &self, request: &Request, ) -> Result { let tokens = self.authz.decode_tokens(request).await?; - self.authz.build_entities(request, &tokens).await + self.authz.build_entities(request, &tokens) } } diff --git a/jans-cedarling/cedarling/src/tests/mapping_entities.rs b/jans-cedarling/cedarling/src/tests/mapping_entities.rs index 28f11b40192..05dde523b33 100644 --- a/jans-cedarling/cedarling/src/tests/mapping_entities.rs +++ b/jans-cedarling/cedarling/src/tests/mapping_entities.rs @@ -10,16 +10,17 @@ //! CEDARLING_MAPPING_ACCESS_TOKEN //! CEDARLING_MAPPING_USERINFO_TOKEN +use super::utils::*; +use crate::authz::entity_builder::{ + BuildCedarlingEntityError, BuildEntityError, BuildTokenEntityError, +}; +use crate::common::policy_store::TokenKind; +use crate::{AuthorizeError, Cedarling, cmp_decision, cmp_policy}; +use cedarling_util::get_raw_config; use std::collections::HashSet; use std::sync::LazyLock; -use tokio::test; - -use cedarling_util::get_raw_config; use test_utils::assert_eq; - -use super::utils::*; -use crate::common::policy_store::TokenKind; -use crate::{AuthorizeError, Cedarling, CreateCedarEntityError, cmp_decision, cmp_policy}; +use tokio::test; static POLICY_STORE_RAW_YAML: &str = include_str!("../../../test_files/policy-store_entity_mapping.yaml"); @@ -112,8 +113,6 @@ async fn test_default_mapping() { /// This function validates the mapping of users and workloads using the defined `cedar` schema. /// For other entities, currently, it is not possible to automatically validate the mapping. /// -/// TODO: Add validation for `IdToken`, `Access_token`, and `Userinfo_token` once they are added to the context. -/// /// Note: Verified that the mapped entity types are present in the logs. #[test] async fn test_custom_mapping() { @@ -189,14 +188,14 @@ async fn test_failed_user_mapping() { .expect_err("request should be parsed with mapping error"); match err { - AuthorizeError::CreateUserEntity(error) => { + AuthorizeError::BuildEntity(BuildCedarlingEntityError::User(error)) => { assert_eq!(error.errors.len(), 2, "there should be 2 errors"); let (token_kind, err) = &error.errors[0]; assert_eq!(token_kind, &TokenKind::Userinfo); assert!( - matches!(err, CreateCedarEntityError::CouldNotFindEntity(ref err) if err == &entity_type), - "expected CouldNotFindEntity({}), got: {:?}", + matches!(err, BuildEntityError::EntityNotInSchema(ref err) if err == &entity_type), + "expected EntityNotInSchema({}), got: {:?}", &entity_type, err, ); @@ -204,13 +203,13 @@ async fn test_failed_user_mapping() { let (token_kind, err) = &error.errors[1]; assert_eq!(token_kind, &TokenKind::Id); assert!( - matches!(err, CreateCedarEntityError::CouldNotFindEntity(ref err) if err == &entity_type), - "expected CouldNotFindEntity({}), got: {:?}", + matches!(err, BuildEntityError::EntityNotInSchema(ref err) if err == &entity_type), + "expected EntityNotInSchema({}), got: {:?}", &entity_type, err, ); }, - _ => panic!("expected error CreateWorkloadEntity"), + _ => panic!("expected error BuildCedarlingEntityError::User"), } } @@ -237,14 +236,14 @@ async fn test_failed_workload_mapping() { .expect_err("request should be parsed with mapping error"); match err { - AuthorizeError::CreateWorkloadEntity(error) => { + AuthorizeError::BuildEntity(BuildCedarlingEntityError::Workload(error)) => { assert_eq!(error.errors.len(), 2, "there should be 2 errors"); // check for access token error let (token_kind, err) = &error.errors[0]; assert_eq!(token_kind, &TokenKind::Access); assert!( - matches!(err, CreateCedarEntityError::CouldNotFindEntity(ref err) if err == &entity_type), + matches!(err, BuildEntityError::EntityNotInSchema(ref err) if err == &entity_type), "expected CouldNotFindEntity(\"{}\"), got: {:?}", &entity_type, err, @@ -254,13 +253,16 @@ async fn test_failed_workload_mapping() { let (token_kind, err) = &error.errors[1]; assert_eq!(token_kind, &TokenKind::Id); assert!( - matches!(err, CreateCedarEntityError::CouldNotFindEntity(ref err) if err == &entity_type), + matches!(err, BuildEntityError::EntityNotInSchema(ref err) if err == &entity_type), "expected CouldNotFindEntity(\"{}\"), got: {:?}", &entity_type, err, ); }, - _ => panic!("expected error CreateWorkloadEntity"), + _ => panic!( + "expected BuildEntity(BuildCedarlingEntityError::Workload(_))) error, got: {:?}", + err + ), } } @@ -285,13 +287,22 @@ async fn test_failed_id_token_mapping() { .await .expect_err("request should be parsed with mapping error"); - assert!( - matches!( - err, - AuthorizeError::CreateIdTokenEntity(CreateCedarEntityError::CouldNotFindEntity(_)) + match err { + AuthorizeError::BuildEntity(BuildCedarlingEntityError::IdToken( + BuildTokenEntityError { token_kind, err }, + )) => { + assert_eq!(token_kind, TokenKind::Id); + assert!( + matches!(err, BuildEntityError::EntityNotInSchema(ref name) if name == "MappedIdTokenNotExist"), + "expected EntityNotInSchema(\"MappedIdTokenNotExist\") got: {:?}", + err + ); + }, + _ => panic!( + "expected BuildEntity(BuildCedarlingEntityError::IdToken(_)) error, got: {:?}", + err ), - "should be error CouldNotFindEntity, got: {err:?}" - ); + } } /// Check if we get error on mapping access_token to undefined entity @@ -315,13 +326,19 @@ async fn test_failed_access_token_mapping() { .await .expect_err("request should be parsed with mapping error"); - assert!( - matches!( - err, - AuthorizeError::CreateAccessTokenEntity(CreateCedarEntityError::CouldNotFindEntity(_)) - ), - "should be error CouldNotFindEntity" - ); + match err { + AuthorizeError::BuildEntity(BuildCedarlingEntityError::AccessToken( + BuildTokenEntityError { token_kind, err }, + )) => { + assert_eq!(token_kind, TokenKind::Access); + assert!( + matches!(err, BuildEntityError::EntityNotInSchema(ref name) if name == "MappedAccess_tokenNotExist"), + "expected EntityNotInSchema(\"MappedAccess_tokenNotExist\") got: {:?}", + err + ); + }, + _ => panic!("expected BuildEntity error, got: {:?}", err), + } } /// Check if we get error on mapping userinfo_token to undefined entity @@ -345,15 +362,19 @@ async fn test_failed_userinfo_token_mapping() { .await .expect_err("request should be parsed with mapping error"); - assert!( - matches!( - err, - AuthorizeError::CreateUserinfoTokenEntity(CreateCedarEntityError::CouldNotFindEntity( - _ - )) - ), - "should be error CouldNotFindEntity" - ); + match err { + AuthorizeError::BuildEntity(BuildCedarlingEntityError::UserinfoToken( + BuildTokenEntityError { token_kind, err }, + )) => { + assert_eq!(token_kind, TokenKind::Userinfo); + assert!( + matches!(err, BuildEntityError::EntityNotInSchema(ref name) if name == "MappedUserinfo_tokenNotExist"), + "expected EntityNotInSchema(\"MappedUserinfo_tokenNotExist\") got: {:?}", + err + ); + }, + _ => panic!("expected BuildEntity error, got: {:?}", err), + } } /// Check if we get roles mapping from all tokens. @@ -416,7 +437,7 @@ async fn test_role_many_tokens_mapping() { // iterate over roles that created and filter expected roles let roles_left = cedarling - .authorize_entities_data(&request) + .build_entities(&request) .await .expect("should get authorize_entities_data without errors") .roles diff --git a/jans-cedarling/cedarling/src/tests/schema_type_mapping.rs b/jans-cedarling/cedarling/src/tests/schema_type_mapping.rs index a90f7d2b949..f1bdb7e9be7 100644 --- a/jans-cedarling/cedarling/src/tests/schema_type_mapping.rs +++ b/jans-cedarling/cedarling/src/tests/schema_type_mapping.rs @@ -110,7 +110,7 @@ async fn check_mapping_tokens_data() { .expect("Request should be deserialized from json"); let entities = cedarling - .authorize_entities_data(&request) + .build_entities(&request) .await // log err to be human readable .inspect_err(|err| println!("Error: {}", err.to_string())) diff --git a/jans-cedarling/flask-sidecar/Dockerfile b/jans-cedarling/flask-sidecar/Dockerfile index 53a6ea677d6..6ee942bd088 100644 --- a/jans-cedarling/flask-sidecar/Dockerfile +++ b/jans-cedarling/flask-sidecar/Dockerfile @@ -31,7 +31,7 @@ RUN pip3 install "poetry==$POETRY_VERSION" gunicorn \ # =============== # Project setup # =============== -ENV JANS_SOURCE_VERSION=2779a7e70e23be1c0afc810abd27910c60fcd9b1 +ENV JANS_SOURCE_VERSION=6c06ade0dcfc8530d85f9949a604acf77fe1e6be COPY docker-entrypoint.sh / RUN chmod +x /docker-entrypoint.sh diff --git a/jans-linux-setup/jans_setup/templates/jans-keycloak-link/jans-keycloak-link-config.json b/jans-linux-setup/jans_setup/templates/jans-keycloak-link/jans-keycloak-link-config.json index ecfd1981a09..b79e5cccf5e 100644 --- a/jans-linux-setup/jans_setup/templates/jans-keycloak-link/jans-keycloak-link-config.json +++ b/jans-linux-setup/jans_setup/templates/jans-keycloak-link/jans-keycloak-link-config.json @@ -73,13 +73,13 @@ "snapshotFolder": "%(snapshots_dir)s", "snapshotMaxCount": 10, "keycloakConfiguration": { - "serverUrl": "http://localhost:8180", - "realm": "master", - "clientId": "clientserviceaccount", - "clientSecret": "cbyFHt3MMU2vNluAmzXopl9SHx9CUmfC", - "grantType": "client_credentials", - "username": "admin", - "password": "keycloak" + "serverUrl": "", + "realm": "", + "clientId": "", + "clientSecret": "", + "grantType": "", + "username": "", + "password": "" }, "baseDN": null, "personObjectClassTypes": null, diff --git a/jans-orm/util/src/main/java/io/jans/orm/util/properties/FileConfiguration.java b/jans-orm/util/src/main/java/io/jans/orm/util/properties/FileConfiguration.java index 0a61b86aa10..5f0cf479081 100644 --- a/jans-orm/util/src/main/java/io/jans/orm/util/properties/FileConfiguration.java +++ b/jans-orm/util/src/main/java/io/jans/orm/util/properties/FileConfiguration.java @@ -6,6 +6,7 @@ package io.jans.orm.util.properties; +import java.io.File; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -77,9 +78,9 @@ protected void loadProperties() { this.loaded = true; } catch (ConfigurationException ex) { - LOG.error(String.format("Failed to load '%s' configuration file from config folder", this.fileName)); + LOG.error(String.format("Failed to load '%s' configuration file from config folder. Current folder: '%s'", this.fileName, (new File(".").getAbsolutePath()))); } catch (Exception e) { - LOG.error(String.format("Failed to load '%s' configuration file from config folder", this.fileName)); + LOG.error(String.format("Failed to load '%s' configuration file from config folder. Current folder: '%s'", this.fileName, (new File(".").getAbsolutePath()))); LOG.error(e.getMessage(), e); } }