Compare commits

...
4 Commits
Author SHA1 Message Date
Givi Khojanashvili e57e5b726d Feat add custom id claim (#129)
* Fix management API endpoint ENV var. Format README.

* Add and use id_current user flag

* Use mix of the new and old methods to detect current user.
2023-02-03 21:48:03 +01:00
Misha Bragin 2c4ada0ad8 Use peerID in the Routes view (#130)
Relates to the netbirdio/netbird PR
Use Peer.ID instead of Peer.Key as peer identifier (#664)
2023-02-03 10:34:43 +01:00
Misha Bragin 8195587c85 Handle additional activity events (#128) 2023-01-31 13:56:46 +01:00
Maycon Santos adf6e1e71f Use ID token payload when oidcUser is nil (#127)
With some IDPs like MS Azure the
oidcUser is not being set, so we can fallback to id token
to validate UI and current user
2023-01-24 09:10:14 +01:00
12 changed files with 108 additions and 56 deletions

No files matched your search

+28 -9
View File
@@ -36,24 +36,43 @@ Disclaimer. We believe that proper user management system is not a trivial task
use Auth0 service that covers all our needs (user management, social login, JTW for the management API).
Auth0 so far is the only 3rd party dependency that can't be really self-hosted.
1. install [Docker](https://docs.docker.com/get-docker/)
2. register [Auth0](https://auth0.com/) account
3. running Wiretrustee UI Dashboard requires the following Auth0 environmental variables to be set (see docker command below):
1. Install [Docker](https://docs.docker.com/get-docker/)
2. Register [Auth0](https://auth0.com/) account
3. Running Wiretrustee UI Dashboard requires the following Auth0 environmental variables to be set (see docker command below):
```AUTH0_DOMAIN``` ```AUTH0_CLIENT_ID``` ```AUTH0_AUDIENCE```
`AUTH0_DOMAIN` `AUTH0_CLIENT_ID` `AUTH0_AUDIENCE`
To obtain these, please use [Auth0 React SDK Guide](https://auth0.com/docs/quickstart/spa/react/01-login#configure-auth0) up until "Configure Allowed Web Origins"
4. Wiretrustee UI Dashboard uses Wiretrustee Management Service HTTP API, so setting ```WIRETRUSTEE_MGMT_API_ENDPOINT``` is required. Most likely it will be ```http://localhost:33071``` if you are hosting Management API on the same server.
4. Wiretrustee UI Dashboard uses Wiretrustee Management Service HTTP API, so setting `NETBIRD_MGMT_API_ENDPOINT` is required. Most likely it will be `http://localhost:33071` if you are hosting Management API on the same server.
5. Run docker container without SSL (Let's Encrypt):
```docker run -d --name wiretrustee-dashboard --rm -p 80:80 -p 443:443 -e AUTH0_DOMAIN=<SET YOUR AUTH DOMAIN> -e AUTH0_CLIENT_ID=<SET YOUR CLIENT ID> -e AUTH0_AUDIENCE=<SET YOUR AUDIENCE> -e WIRETRUSTEE_MGMT_API_ENDPOINT=<SET YOUR MANAGEMETN API URL> wiretrustee/dashboard:main```
```shell
docker run -d --name wiretrustee-dashboard \
--rm -p 80:80 -p 443:443 \
-e AUTH0_DOMAIN=<SET YOUR AUTH DOMAIN> \
-e AUTH0_CLIENT_ID=<SET YOUR CLIENT ID> \
-e AUTH0_AUDIENCE=<SET YOUR AUDIENCE> \
-e NETBIRD_MGMT_API_ENDPOINT=<SET YOUR MANAGEMETN API URL> \
wiretrustee/dashboard:main
```
6. Run docker container with SSL (Let's Encrypt):
```docker run -d --name wiretrustee-dashboard --rm -p 80:80 -p 443:443 -e NGINX_SSL_PORT=443 -e LETSENCRYPT_DOMAIN=<YOUR PUBLIC DOMAIN> -e LETSENCRYPT_EMAIL=<YOUR EMAIL> -e AUTH0_DOMAIN=<SET YOUR AUTH DOMAIN> -e AUTH0_CLIENT_ID=<SET YOUR CLEITN ID> -e AUTH0_AUDIENCE=<SET YOUR AUDIENCE> -e WIRETRUSTEE_MGMT_API_ENDPOINT=<SET YOUR MANAGEMETN API URL> wiretrustee/dashboard:main```
```shell
docker run -d --name wiretrustee-dashboard \
--rm -p 80:80 -p 443:443 \
-e NGINX_SSL_PORT=443 \
-e LETSENCRYPT_DOMAIN=<YOUR PUBLIC DOMAIN> \
-e LETSENCRYPT_EMAIL=<YOUR EMAIL> \
-e AUTH0_DOMAIN=<SET YOUR AUTH DOMAIN> \
-e AUTH0_CLIENT_ID=<SET YOUR CLEITN ID> \
-e AUTH0_AUDIENCE=<SET YOUR AUDIENCE> \
-e NETBIRD_MGMT_API_ENDPOINT=<SET YOUR MANAGEMETN API URL> \
wiretrustee/dashboard:main
```
## How to run local development
1. Install node 16
2. create and update the src/.local-config.json file. This file should contain values to be replaced from src/config.json
2. create and update the `src/.local-config.json` file. This file should contain values to be replaced from `src/config.json`
3. run `npm install`
4. run `npm run start dev`
4. run `npm run start dev`
+8 -3
View File
@@ -5,7 +5,7 @@ import {Avatar, Button, Col, Dropdown, Grid, Menu, Row} from 'antd'
import {ItemType} from "antd/lib/menu/hooks/useItems";
import {AvatarSize} from "antd/es/avatar/SizeContext";
import {UserOutlined} from '@ant-design/icons';
import {useOidc, useOidcUser} from '@axa-fr/react-oidc';
import {useOidc, useOidcIdToken, useOidcUser} from '@axa-fr/react-oidc';
import {getConfig} from "../config";
import {User} from "../store/user/types";
import {useDispatch, useSelector} from "react-redux";
@@ -23,6 +23,7 @@ const Navbar = () => {
const dispatch = useDispatch()
const {oidcUser} = useOidcUser();
const {idTokenPayload} = useOidcIdToken()
const user = oidcUser;
const [currentUser, setCurrentUser] = useState({} as User)
@@ -85,9 +86,13 @@ const Navbar = () => {
if (users.length === 0 && isRefreshingUserState) {
return
}
let runUser = oidcUser
if (!oidcUser) {
runUser = idTokenPayload
}
setIsRefreshingUserState(false)
if (oidcUser && oidcUser.sub) {
const found = users.find(u => u.id == oidcUser.sub)
if (runUser) {
const found = users.find(u => u.is_current ? u.is_current : runUser.sub ? u.id == runUser.sub : false)
if (found) {
setCurrentUser(found)
}
+5 -5
View File
@@ -73,14 +73,14 @@ const RouteUpdate = () => {
const [masqueradeMSG, setMasqueradeMSG] = useState(defaultMasqueradeMSG)
const defaultStatusMSG = "Status"
const [statusMSG, setStatusMSG] = useState(defaultStatusMSG)
const [peerNameToIP, peerIPToName] = initPeerMaps(peers);
const [peerNameToIP, peerIPToName, peerIPToID] = initPeerMaps(peers);
const [newRoute, setNewRoute] = useState(false)
const optionsDisabledEnabled = [{label: 'Enabled', value: true}, {label: 'Disabled', value: false}]
useEffect(() => {
if (!newRoute ) {
setRoutingPeerMSG("Add additional routing peer")
setRoutingPeerMSG(defaultRoutingPeerMSG)
setMasqueradeMSG("Update Masquerade")
setStatusMSG("Update Status")
} else {
@@ -136,9 +136,9 @@ const RouteUpdate = () => {
let peerIDList = inputRoute.peer.split(routePeerSeparator)
let peerID: string
if (peerIDList[1]) {
peerID = peerIDList[1]
peerID = peerIPToID[peerIDList[1]]
} else {
peerID = peerNameToIP[inputRoute.peer]
peerID = peerIPToID[peerNameToIP[inputRoute.peer]]
}
let [ existingGroups, groupsToCreate ] = getExistingAndToCreateGroupsLists(inputRoute.groups)
@@ -167,7 +167,7 @@ const RouteUpdate = () => {
payload: routeToSave
}))
} else {
let groupedDataTable = transformGroupedDataTable(routes, peerIPToName)
let groupedDataTable = transformGroupedDataTable(routes, peers)
groupedDataTable.forEach((group) => {
if (group.key == previousRouteKey) {
group.groupedRoutes.forEach((route) => {
+2 -1
View File
@@ -233,7 +233,8 @@ const UserUpdate = () => {
role: "",
status: "",
auto_groups: [],
name: user.name
name: user.name,
is_current: user.is_current,
} as User));
setFormUser({} as FormUser)
toggleEditName(false)
+1 -1
View File
@@ -59,7 +59,7 @@ export function* deletePeer(action: ReturnType<typeof actions.deletedPeer.reques
} as DeleteResponse<string | null>));
const peers = (yield select(state => state.peer.data)) as Peer[]
yield put(actions.getPeers.success(peers.filter((p:Peer) => p.ip !== action.payload.payload)))
yield put(actions.getPeers.success(peers.filter((p:Peer) => p.id !== action.payload.payload)))
} catch (err) {
yield put(actions.deletedPeer.failure({
loading: false,
+4
View File
@@ -40,6 +40,10 @@ export interface PeerIPToName {
[key: string]: string;
}
export interface PeerIPToID {
[key: string]: string;
}
export interface PeerDataTable extends Peer {
key: string;
groups: Group[];
+5 -5
View File
@@ -3,15 +3,15 @@ export interface User {
email?: string;
name: string;
role: string;
status: string
auto_groups: string[]
status: string;
auto_groups: string[];
is_current?: boolean;
}
export interface FormUser extends User {
autoGroupsNames: string[]
}
export interface UserToSave extends User
{
export interface UserToSave extends User {
groupsToCreate: string[]
}
}
+26 -18
View File
@@ -1,4 +1,4 @@
import {Peer, PeerNameToIP, PeerIPToName} from "../store/peer/types";
import {Peer, PeerIPToID, PeerIPToName, PeerNameToIP} from "../store/peer/types";
import {Route} from "../store/route/types";
export const routePeerSeparator = " - "
@@ -7,22 +7,26 @@ export const masqueradeDisabledMSG = "Enabling this option hides other NetBird n
export const masqueradeEnabledMSG = "Disabling this option stops hiding all traffic coming from other NetBird peers behind the routing peer local address when accessing the target Network CIDR. You will need to configure routes for your NetBird network pointing to your routing peer on your local routers or other devices."
export const peerToPeerIP = (name:string,ip:string):string => {
export const peerToPeerIP = (name: string, ip: string): string => {
return name + routePeerSeparator + ip
}
export const initPeerMaps = (peers:Peer[]): [PeerNameToIP, PeerIPToName] => {
export const initPeerMaps = (peers: Peer[]): [PeerNameToIP, PeerIPToName, PeerIPToID] => {
let peerNameToIP = {} as PeerNameToIP
let peerIPToName = {} as PeerIPToName
peers.forEach((p) =>{
let peerIPToID = {} as PeerIPToID
peers.forEach((p) => {
peerNameToIP[p.name] = p.ip
peerIPToName[p.ip] = p.name
peerIPToID[p.ip] = p.id ? p.id : ""
})
return [ peerNameToIP, peerIPToName]
return [peerNameToIP, peerIPToName, peerIPToID]
}
export interface RouteDataTable extends Route {
key: string;
peer_ip: string;
peer_name: string;
}
export interface GroupedDataTable {
@@ -37,30 +41,34 @@ export interface GroupedDataTable {
routesGroups: string[]
}
export const transformDataTable = (d:Route[],peerIPToName:PeerIPToName):RouteDataTable[] => {
return d.map(p => {
export const transformDataTable = (routes: Route[], peers: Peer[]): RouteDataTable[] => {
let peerMap = Object.fromEntries(peers.map(p => [p.id, p]));
return routes.map(route => {
return {
key: p.id,
...p,
peer: peerIPToName[p.peer] ? peerIPToName[p.peer] : p.peer,
key: route.id,
...route,
peer: route.peer,
peer_ip: peerMap[route.peer] ? peerMap[route.peer].ip : route.peer,
peer_name: peerMap[route.peer] ? peerMap[route.peer].name : route.peer,
} as RouteDataTable
})
}
export const transformGroupedDataTable = (routes:Route[],peerIPToName:PeerIPToName):GroupedDataTable[] => {
export const transformGroupedDataTable = (routes: Route[], peers: Peer[]): GroupedDataTable[] => {
let keySet = new Set(routes.map(r => {
return r.network_id + r.network
}))
let groupedRoutes:GroupedDataTable[] = []
let groupedRoutes: GroupedDataTable[] = []
keySet.forEach((p) => {
let hasEnabled = false
let lastRoute:Route
let listedRoutes:Route[] = []
let groupList:string[] = []
let lastRoute: Route
let listedRoutes: Route[] = []
let groupList: string[] = []
routes.forEach((r) => {
if ( p === r.network_id + r.network ) {
if (p === r.network_id + r.network) {
lastRoute = r
if (r.enabled) {
hasEnabled = true
@@ -69,8 +77,8 @@ export const transformGroupedDataTable = (routes:Route[],peerIPToName:PeerIPToNa
groupList = groupList.concat(r.groups)
}
})
groupList = groupList.filter((value,index,arrary) => arrary.indexOf(value) === index)
let groupDataTableRoutes = transformDataTable(listedRoutes,peerIPToName)
groupList = groupList.filter((value, index, arrary) => arrary.indexOf(value) === index)
let groupDataTableRoutes = transformDataTable(listedRoutes, peers)
groupedRoutes.push({
key: p.toString(),
network_id: lastRoute!.network_id,
+12 -2
View File
@@ -160,10 +160,21 @@ export const Activity = () => {
case "group.add":
case "group.update":
return renderMultiRowSpan(event.meta.name,"Group")
case "nameserver.group.add":
case "nameserver.group.update":
case "nameserver.group.delete":
return renderMultiRowSpan(event.meta.name,"Nameserver group")
case "setupkey.peer.add":
case "user.peer.add":
case "user.peer.delete":
case "peer.ssh.enable":
case "peer.ssh.disable":
case "peer.rename":
return renderMultiRowSpan(event.meta.fqdn,event.meta.ip)
case "route.add":
case "route.delete":
case "route.update":
return renderMultiRowSpan(event.meta.name, "Route for range " + event.meta.network_range)
case "user.group.add":
case "user.group.delete":
if (user) {
@@ -177,9 +188,8 @@ export const Activity = () => {
case "peer.group.delete":
return renderMultiRowSpan(event.meta.peer_fqdn,event.meta.peer_ip)
case "dns.setting.disabled.management.group.add":
return "-"
case "dns.setting.disabled.management.group.delete":
return "-"
return renderMultiRowSpan("","System setting")
case "user.invite":
if (user) {
return renderMultiRowSpan(user.name ? user.name : user.id,user.email ? user.email : "User")
+2 -2
View File
@@ -237,7 +237,7 @@ export const Peers = () => {
const showConfirmDelete = () => {
let peerRoutes: string[] = []
routes.forEach((r) => {
if (r.peer == peerToAction?.ip) {
if (r.peer == peerToAction?.id) {
peerRoutes.push(r.network_id)
}
})
@@ -288,7 +288,7 @@ export const Peers = () => {
onOk() {
dispatch(peerActions.deletedPeer.request({
getAccessTokenSilently: getAccessTokenSilently,
payload: peerToAction ? peerToAction.ip : ''
payload: (peerToAction && peerToAction.id) ? peerToAction.id! : ""
}));
},
onCancel() {
+7 -7
View File
@@ -127,7 +127,7 @@ export const Routes = () => {
}
useEffect(() => {
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peerIPToName)))
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peers)))
}, [dataTable])
useEffect(() => {
@@ -135,12 +135,12 @@ export const Routes = () => {
setShowTutorial(false)
} else {
setShowTutorial(isShowTutorial(routes))
setDataTable(sortBy(transformDataTable(routes, peerIPToName), "network_id"))
setDataTable(sortBy(transformDataTable(routes, peers), "network_id"))
}
}, [routes])
useEffect(() => {
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peerIPToName)))
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peers)))
}, [textToSearch, optionAllEnable])
const styleNotification = {marginTop: 85}
@@ -186,7 +186,7 @@ export const Routes = () => {
if (deletedRoute.loading) {
message.loading({content: 'Deleting...', key: deleteKey, style})
} else if (deletedRoute.success) {
message.success({content: 'Route has been successfully disabled.', key: deleteKey, duration: 2, style})
message.success({content: 'Route has been successfully deleted.', key: deleteKey, duration: 2, style})
dispatch(routeActions.resetDeletedRoute(null))
} else if (deletedRoute.error) {
message.error({
@@ -204,7 +204,7 @@ export const Routes = () => {
};
const searchDataTable = () => {
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peerIPToName)))
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peers)))
}
const onChangeAllEnabled = ({target: {value}}: RadioChangeEvent) => {
@@ -278,7 +278,7 @@ export const Routes = () => {
network: route.network,
network_id: route.network_id,
description: route.description,
peer: route.peer ? peerToPeerIP(route.peer, peerNameToIP[route.peer]) : '',
peer: route.peer ? peerToPeerIP(route.peer_name, route.peer_ip) : '',
metric: route.metric ? route.metric : 9999,
masquerade: route.masquerade,
enabled: route.enabled,
@@ -393,7 +393,7 @@ export const Routes = () => {
size="small"
bordered={true}
>
<Column title="Routing Peer" dataIndex="peer" align="center"
<Column title="Routing Peer" dataIndex="peer_name" align="center"
onFilter={(value: string | number | boolean, record) => (record as any).peer.includes(value)}
sorter={(a, b) => ((a as any).peer.localeCompare((b as any).peer))}
render={(text, record) => {
+8 -3
View File
@@ -28,7 +28,7 @@ import UserUpdate from "../components/UserUpdate";
import {actions as groupActions} from "../store/group";
import {Group} from "../store/group/types";
import {TooltipPlacement} from "antd/es/tooltip";
import {useOidcUser} from "@axa-fr/react-oidc";
import {useOidcIdToken, useOidcUser} from "@axa-fr/react-oidc";
import {Link} from "react-router-dom";
import {actions as setupKeyActions} from "../store/setup-key";
import {SetupKey} from "../store/setup-key/types";
@@ -46,6 +46,7 @@ const styleNotification = {marginTop: 85}
export const Users = () => {
const {getAccessTokenSilently} = useGetAccessTokenSilently()
const {oidcUser} = useOidcUser();
const {idTokenPayload} = useOidcIdToken()
const dispatch = useDispatch()
const groups = useSelector((state: RootState) => state.group.data)
@@ -96,8 +97,12 @@ export const Users = () => {
}, [textToSearch])
useEffect(() => {
if (oidcUser && oidcUser.sub) {
const found = users.find(u => u.id == oidcUser.sub)
let runUser = oidcUser
if (!oidcUser) {
runUser = idTokenPayload
}
if (runUser && runUser.sub) {
const found = users.find(u => u.id == runUser.sub)
if (found) {
setCurrentUser(found)
}