Added support for multi-tenancy and running IPAM Engine as an Azure Function

This commit is contained in:
Matthew Garrett 2022-07-11 21:37:20 -07:00
Родитель fcc4a1e972
Коммит 0c40986eb8
35 изменённых файлов: 1442 добавлений и 415 удалений

Просмотреть файл

@ -0,0 +1 @@
local.settings.json

50
engine/.gitignore поставляемый
Просмотреть файл

@ -24,3 +24,53 @@ archive.zip
# vim temporary files
*~
.*.sw?
# Azure Functions
bin
obj
csx
.vs
edge
Publish
*.user
*.suo
*.cscfg
*.Cache
project.lock.json
/packages
/TestResults
/tools/NuGet.exe
/App_Data
/secrets
/data
.secrets
appsettings.json
local.settings.json
node_modules
dist
# Local python packages
.python_packages/
# Python Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# Azurite artifacts
__blobstorage__
__queuestorage__
__azurite_db*__.json

11
engine/Dockerfile.func Normal file
Просмотреть файл

@ -0,0 +1,11 @@
# To enable ssh & remote debugging on app service change the base image to the one below
# FROM mcr.microsoft.com/azure-functions/python:3.0-python3.9-appservice
FROM mcr.microsoft.com/azure-functions/python:4-python3.9
ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
AzureFunctionsJobHost__Logging__Console__IsEnabled=true
COPY requirements.txt /
RUN pip install -r /requirements.txt
COPY . /home/site/wwwroot

Просмотреть файл

@ -4,6 +4,7 @@ from azure.cosmos.aio import CosmosClient
import jwt
import time
import copy
from app.routers.common.helper import (
cosmos_query
@ -28,13 +29,18 @@ async def check_token_expired(request: Request):
request.state.tenant_id = decoded['tid']
await check_admin(request, decoded['oid'])
await check_admin(request, decoded['oid'], decoded['tid'])
async def check_admin(request: Request, user_oid: str):
item = await cosmos_query("admins")
async def check_admin(request: Request, user_oid: str, user_tid: str):
admin_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'admin'", user_tid)
if item['admins']:
is_admin = next((x for x in item['admins'] if user_oid == x['id']), None)
if admin_query:
admin_data = copy.deepcopy(admin_query[0])
if admin_data['admins']:
is_admin = next((x for x in admin_data['admins'] if user_oid == x['id']), None)
else:
is_admin = True
else:
is_admin = True

Просмотреть файл

@ -1,4 +1,3 @@
from audioop import tostereo
from fastapi import FastAPI, Request, HTTPException, Header
from fastapi.responses import JSONResponse, RedirectResponse, FileResponse
from fastapi.staticfiles import StaticFiles

Просмотреть файл

@ -2,8 +2,8 @@ from pydantic import BaseModel, ValidationError, EmailStr
from typing import Optional, List, Any
from netaddr import IPSet, IPNetwork, IPAddress
# from ipaddress import IPv4Network, IPv4Address
from datetime import datetime
from uuid import UUID
import json
class IPv4Network(str):
@ -68,71 +68,6 @@ class IPv4Address(str):
def __repr__(self):
return f'IPAddress({super().__repr__()})'
# class VNet(BaseModel):
# """DOCSTRING"""
# vnet: str
# class IPReservation(BaseModel):
# """DOCSTRING"""
# cidr: IPv4Address
# userId: EmailStr
# createdOn: datetime
# class Config:
# json_encoders = {
# datetime: lambda v: v.timestamp(),
# IPAddress: lambda v: str(v),
# }
# class Reservation(BaseModel):
# """DOCSTRING"""
# id: str
# cidr: str
# userId: str #EmailStr
# createdOn: float
# status: str
# class BlockReq(BaseModel):
# """DOCSTRING"""
# name: str
# cidr: IPv4Network
# class Config:
# json_encoders = {
# IPNetwork: lambda v: str(v),
# }
# class BlockRes(BaseModel):
# """DOCSTRING"""
# name: str
# cidr: IPv4Network
# vnets: List
# resv: List
# class Config:
# json_encoders = {
# IPNetwork: lambda v: str(v),
# }
# class SpaceReq(BaseModel):
# """DOCSTRING"""
# name: str
# desc: str
# class SpaceRes(BaseModel):
# """DOCSTRING"""
# name: str
# desc: str
# vnets: List[str]
# blocks: List[BlockRes]
######################
# REQUEST MODELS #
######################
@ -333,3 +268,43 @@ class SpaceExpandUtil(BaseModel):
blocks: List[BlockExpandUtil]
size: int
used: int
####################
# ADMIN MODELS #
####################
class Admin(BaseModel):
name: str
email: EmailStr
id: UUID
class Config:
json_encoders = {
UUID: lambda v: str(v),
}
###################
# USER MODELS #
###################
class User(BaseModel):
"""DOCSTRING"""
id: UUID
apiRefresh: int
isAdmin: bool
class Config:
json_encoders = {
UUID: lambda v: str(v),
}
class JSONPatch(BaseModel):
"""DOCSTRING"""
op: str
path: str
value: Any
class UserUpdate(List[JSONPatch]):
"""DOCSTRING"""

Просмотреть файл

@ -3,18 +3,28 @@ from fastapi.responses import JSONResponse, PlainTextResponse
from fastapi.exceptions import HTTPException as StarletteHTTPException
from fastapi.encoders import jsonable_encoder
import azure.cosmos.exceptions as exceptions
from pydantic import BaseModel, EmailStr, constr
from typing import Optional, List
import azure.cosmos.exceptions as exceptions
import copy
import uuid
from app.dependencies import check_token_expired, get_admin
from app.dependencies import (
check_token_expired,
get_admin,
get_tenant_id
)
from uuid import UUID
from app.models import *
from app.routers.common.helper import (
cosmos_query,
cosmos_upsert
cosmos_upsert,
cosmos_replace,
cosmos_delete,
cosmos_retry
)
router = APIRouter(
@ -23,21 +33,12 @@ router = APIRouter(
dependencies=[Depends(check_token_expired)]
)
class Admin(BaseModel):
name: str
email: EmailStr
id: UUID
class Config:
json_encoders = {
UUID: lambda v: str(v),
}
@router.get(
"",
summary = "Get All Admins"
)
async def get_admins(
tenant_id: str = Depends(get_tenant_id),
is_admin: str = Depends(get_admin)
):
"""
@ -47,17 +48,27 @@ async def get_admins(
if not is_admin:
raise HTTPException(status_code=403, detail="API restricted to admins.")
item = await cosmos_query("admins")
admin_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'admin'", tenant_id)
return item['admins']
if admin_query:
admin_data = copy.deepcopy(admin_query[0])
return admin_data['admins']
else:
return []
@router.post(
"",
summary = "Create IPAM Admin",
status_code=201
)
@cosmos_retry(
max_retry = 5,
error_msg = "Error creating admin, please try again."
)
async def create_admin(
admin: Admin,
tenant_id: str = Depends(get_tenant_id),
is_admin: str = Depends(get_admin)
):
"""
@ -68,32 +79,32 @@ async def create_admin(
- **id**: Azure AD ObjectID for the Administrator user
"""
current_try = 0
max_retry = 5
if not is_admin:
raise HTTPException(status_code=403, detail="API restricted to admins.")
while True:
try:
item = await cosmos_query("admins")
admin_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'admin'", tenant_id)
target_admin = next((x for x in item['admins'] if x['id'] == admin.id), None)
if not admin_query:
admin_data = {
"id": uuid.uuid4(),
"type": "admin",
"tenant_id": tenant_id,
"admins": [admin],
"excluded": []
}
if target_admin:
raise HTTPException(status_code=400, detail="User is already an admin.")
await cosmos_upsert(jsonable_encoder(admin_data))
else:
admin_data = copy.deepcopy(admin_query[0])
item['admins'].append(jsonable_encoder(admin))
target_admin = next((x for x in admin_data['admins'] if x['id'] == admin.id), None)
await cosmos_upsert("admins", item)
except exceptions.CosmosAccessConditionFailedError:
if current_try < max_retry:
current_try += 1
continue
else:
raise HTTPException(status_code=500, detail="Error creating admin, please try again.")
else:
break
if target_admin:
raise HTTPException(status_code=400, detail="User is already an admin.")
admin_data['admins'].append(jsonable_encoder(admin))
await cosmos_replace(admin_query[0], admin_data)
return Response(status_code=status.HTTP_201_CREATED)
@ -102,40 +113,33 @@ async def create_admin(
summary = "Delete IPAM Admin",
status_code=200
)
@cosmos_retry(
max_retry = 5,
error_msg = "Error removing admin, please try again."
)
async def delete_admin(
objectId: UUID,
tenant_id: str = Depends(get_tenant_id),
is_admin: str = Depends(get_admin)
):
"""
Remove a specific IPAM Administrator
"""
current_try = 0
max_retry = 5
if not is_admin:
raise HTTPException(status_code=403, detail="API restricted to admins.")
while True:
try:
item = await cosmos_query("admins")
admin_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'admin'", tenant_id)
admin_data = copy.deepcopy(admin_query[0])
admin_index = next((i for i, admin in enumerate(item['admins']) if admin['id'] == str(objectId)), None)
admin_index = next((i for i, admin in enumerate(admin_data['admins']) if admin['id'] == str(objectId)), None)
if not admin_index:
raise HTTPException(status_code=400, detail="Invalid admin objectId.")
if admin_index is None:
raise HTTPException(status_code=400, detail="Invalid admin objectId.")
del item['admins'][admin_index]
del admin_data['admins'][admin_index]
await cosmos_upsert("admins", item)
except exceptions.CosmosAccessConditionFailedError:
if current_try < max_retry:
current_try += 1
continue
else:
raise HTTPException(status_code=500, detail="Error removing admin, please try again.")
else:
break
await cosmos_replace(admin_query[0], admin_data)
return Response(status_code=status.HTTP_200_OK)
@ -144,8 +148,13 @@ async def delete_admin(
summary = "Replace IPAM Admins",
status_code=200
)
@cosmos_retry(
max_retry = 5,
error_msg = "Error updating admins, please try again."
)
async def update_admins(
admin_list: List[Admin],
tenant_id: str = Depends(get_tenant_id),
is_admin: str = Depends(get_admin)
):
"""
@ -157,31 +166,32 @@ async def update_admins(
- **id**: Azure AD ObjectID for the Administrator user
"""
current_try = 0
max_retry = 5
if not is_admin:
raise HTTPException(status_code=403, detail="API restricted to admins.")
while True:
try:
item = await cosmos_query("admins")
id_list = [x.id for x in admin_list]
unique_admins = len(set(id_list)) == len(admin_list)
id_list = [x.id for x in admin_list]
unique_admins = len(set(id_list)) == len(admin_list)
if not unique_admins:
raise HTTPException(status_code=400, detail="List contains one or more duplicate objectId's.")
if not unique_admins:
raise HTTPException(status_code=400, detail="List contains one or more duplicate objectId's.")
admin_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'admin'", tenant_id)
item['admins'] = jsonable_encoder(admin_list)
await cosmos_upsert("admins", item)
except exceptions.CosmosAccessConditionFailedError:
if current_try < max_retry:
current_try += 1
continue
else:
raise HTTPException(status_code=500, detail="Error updating admins, please try again.")
else:
break
if not admin_query:
admin_data = {
"id": uuid.uuid4(),
"type": "admin",
"tenant_id": tenant_id,
"admins": admin_list,
"excluded": []
}
await cosmos_upsert(jsonable_encoder(admin_data))
else:
admin_data = copy.deepcopy(admin_query[0])
admin_data['admins'] = jsonable_encoder(admin_list)
await cosmos_replace(admin_query[0], admin_data)
return PlainTextResponse(status_code=status.HTTP_200_OK)

Просмотреть файл

@ -9,7 +9,13 @@ resources
SUBSCRIPTION = """
ResourceContainers
| where type =~ 'microsoft.resources/subscriptions'
| project name, id, tenant_id = tenantId, subscription_id = subscriptionId
| extend quotaId = properties.subscriptionPolicies.quotaId
| extend type = case(
quotaId startswith "EnterpriseAgreement", "Enterprise Agreement",
quotaId startswith "MSDNDevTest", "Dev/Test",
"Unknown"
)
| project name, id, type, subscription_id = subscriptionId, tenant_id = tenantId
"""
SPACE = """

Просмотреть файл

@ -21,9 +21,9 @@ from uuid import uuid4
from sqlalchemy import true
from app.dependencies import (
check_token_expired,
get_admin,
get_tenant_id
check_token_expired,
get_admin,
get_tenant_id
)
from . import argquery
@ -31,9 +31,9 @@ from . import argquery
from app.routers.common.helper import (
get_client_credentials,
get_obo_credentials,
cosmos_query_x,
cosmos_upsert_x,
cosmos_replace_x,
cosmos_query,
cosmos_upsert,
cosmos_replace,
cosmos_retry,
arg_query
)
@ -193,12 +193,12 @@ async def get_vmss_interfaces_sdk_helper(credentials, vmss, list):
await network_client.close()
@router.get(
"/subscription",
summary = "Get All Subscriptions"
"/subscription",
summary = "Get All Subscriptions"
)
async def subscription(
authorization: str = Header(None),
admin: str = Depends(get_admin)
authorization: str = Header(None),
admin: str = Depends(get_admin)
):
"""
Get a list of Azure subscriptions.
@ -209,19 +209,19 @@ async def subscription(
return subscription_list
@router.get(
"/vnet",
summary = "Get All Virtual Networks"
"/vnet",
summary = "Get All Virtual Networks"
)
async def get_vnet(
authorization: str = Header(None),
admin: str = Depends(get_admin),
tenant_id: str = Depends(get_tenant_id)
authorization: str = Header(None),
tenant_id: str = Depends(get_tenant_id),
admin: str = Depends(get_admin)
):
"""
Get a list of Azure Virtual Networks.
"""
space_query = await cosmos_query_x("SELECT * FROM c WHERE c.type = 'space'", tenant_id)
space_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'space'", tenant_id)
vnet_list = await arg_query(authorization, admin, argquery.VNET)
@ -255,12 +255,12 @@ async def get_vnet(
return updated_vnet_list
@router.get(
"/subnet",
summary = "Get All Subnets"
"/subnet",
summary = "Get All Subnets"
)
async def get_subnet(
authorization: str = Header(None),
admin: str = Depends(get_admin)
authorization: str = Header(None),
admin: str = Depends(get_admin)
):
"""
Get a list of Azure Subnets.
@ -316,12 +316,12 @@ async def get_subnet(
return updated_subnet_list
@router.get(
"/pe",
summary = "Get All Private Endpoints"
"/pe",
summary = "Get All Private Endpoints"
)
async def pe(
authorization: str = Header(None),
admin: str = Depends(get_admin)
authorization: str = Header(None),
admin: str = Depends(get_admin)
):
"""
Get a list of Azure Private Endpoints.
@ -332,12 +332,12 @@ async def pe(
return pe_list
@router.get(
"/vm",
summary = "Get All Virtual Machines"
"/vm",
summary = "Get All Virtual Machines"
)
async def vm(
authorization: str = Header(None),
admin: str = Depends(get_admin)
authorization: str = Header(None),
admin: str = Depends(get_admin)
):
"""
Get a list of Azure Virtual Machines
@ -348,12 +348,12 @@ async def vm(
return vm_list
@router.get(
"/vmss",
summary = "Get All VM Scale Sets"
"/vmss",
summary = "Get All VM Scale Sets"
)
async def vmss(
authorization: str = Header(None),
admin: str = Depends(get_admin)
authorization: str = Header(None),
admin: str = Depends(get_admin)
):
"""
Get a list of Azure VM Scale Sets.
@ -365,12 +365,12 @@ async def vmss(
return vmss_list
@router.get(
"/fwvnet",
summary = "Get All vNet Firewalls"
"/fwvnet",
summary = "Get All vNet Firewalls"
)
async def fwvnet(
authorization: str = Header(None),
admin: str = Depends(get_admin)
authorization: str = Header(None),
admin: str = Depends(get_admin)
):
"""
Get a list of vNet integrated Azure Firewalls.
@ -381,12 +381,12 @@ async def fwvnet(
return vm_list
@router.get(
"/fwvhub",
summary = "Get all vWAN Hub Firewalls"
"/fwvhub",
summary = "Get all vWAN Hub Firewalls"
)
async def fwvhub(
authorization: str = Header(None),
admin: str = Depends(get_admin)
authorization: str = Header(None),
admin: str = Depends(get_admin)
):
"""
Get a list of all vWAN Hub integrated Azure Firewalls.
@ -397,12 +397,12 @@ async def fwvhub(
return vm_list
@router.get(
"/bastion",
summary = "Get All Bastion Hosts"
"/bastion",
summary = "Get All Bastion Hosts"
)
async def bastion(
authorization: str = Header(None),
admin: str = Depends(get_admin)
authorization: str = Header(None),
admin: str = Depends(get_admin)
):
"""
Get a list of all Azure Bastions hosts.
@ -413,12 +413,12 @@ async def bastion(
return vm_list
@router.get(
"/appgw",
summary = "Get All Application Gateways"
"/appgw",
summary = "Get All Application Gateways"
)
async def appgw(
authorization: str = Header(None),
admin: str = Depends(get_admin)
authorization: str = Header(None),
admin: str = Depends(get_admin)
):
"""
Get a list of all Azure Application Gateways.
@ -429,12 +429,12 @@ async def appgw(
return vm_list
@router.get(
"/apim",
summary = "Get All API Management Instances"
"/apim",
summary = "Get All API Management Instances"
)
async def apim(
authorization: str = Header(None),
admin: str = Depends(get_admin)
authorization: str = Header(None),
admin: str = Depends(get_admin)
):
"""
Get a list of all Azure API Management instances.
@ -451,12 +451,12 @@ async def multi_helper(func, list, *args):
list.append(results)
@router.get(
"/multi",
summary = "Get All Endpoints"
"/multi",
summary = "Get All Endpoints"
)
async def multi(
authorization: str = Header(None),
admin: str = Depends(get_admin)
authorization: str = Header(None),
admin: str = Depends(get_admin)
):
"""
Get a consolidated list of all Azure endpoints.
@ -478,13 +478,13 @@ async def multi(
return [item for sublist in result_list for item in sublist]
@router.get(
"/tree",
summary = "Get Space Tree View"
"/tree",
summary = "Get Space Tree View"
)
async def multi(
authorization: str = Header(None),
admin: str = Depends(get_admin),
tenant_id: str = Depends(get_tenant_id)
authorization: str = Header(None),
tenant_id: str = Depends(get_tenant_id),
admin: str = Depends(get_admin)
):
"""
Get a hierarchical tree view of Spaces, Blocks, Virtual Networks, Subnets, and Endpoints.
@ -497,7 +497,7 @@ async def multi(
endpoint_list = []
tasks.append(asyncio.create_task(multi_helper(get_spaces, space_list, False, True, authorization, tenant_id, True)))
tasks.append(asyncio.create_task(multi_helper(get_vnet, vnet_list, authorization, admin)))
tasks.append(asyncio.create_task(multi_helper(get_vnet, vnet_list, authorization, tenant_id, admin)))
tasks.append(asyncio.create_task(multi_helper(get_subnet, subnet_list, authorization, admin)))
tasks.append(asyncio.create_task(multi_helper(pe, endpoint_list, authorization, admin)))
tasks.append(asyncio.create_task(multi_helper(vm, endpoint_list, authorization, admin)))
@ -599,7 +599,7 @@ async def match_resv_to_vnets():
vnet_list = await arg_query(None, True, argquery.VNET)
stale_resv = list(x['resv'] for x in vnet_list if x['resv'] != None)
space_query = await cosmos_query_x("SELECT * FROM c WHERE c.type = 'space'", globals.TENANT_ID)
space_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'space'", globals.TENANT_ID)
for space in space_query:
original_space = copy.deepcopy(space)
@ -623,6 +623,7 @@ async def match_resv_to_vnets():
# print("INDEX: {}".format(index))
stale_resv.remove(resv['id'])
resv['status'] = "wait"
cidr_match = resv['cidr'] in vnet['prefixes']
@ -646,6 +647,7 @@ async def match_resv_to_vnets():
resv['status'] = "errCIDRExists"
if resv['status'] == "wait":
# print("vNET is being added to IP Block...")
block['vnets'].append(
{
"id": vnet['id'],
@ -653,10 +655,8 @@ async def match_resv_to_vnets():
}
)
del block['resv'][index]
else:
resv['status'] = "wait"
await cosmos_replace_x(original_space, space)
await cosmos_replace(original_space, space)
# print("STALE:")
# print(stale_resv)

Просмотреть файл

@ -14,6 +14,8 @@ import os
import jwt
from functools import wraps
from requests import options
import app.globals as globals
SCOPE = "https://management.azure.com/user_impersonation"
@ -62,23 +64,47 @@ async def get_obo_credentials(assertion):
return credential
async def cosmos_query(target: str):
"""DOCSTRING"""
cosmos_client = CosmosClient(globals.COSMOS_URL, credential=globals.COSMOS_KEY)
# async def cosmos_query(target: str):
# """DOCSTRING"""
# cosmos_client = CosmosClient(globals.COSMOS_URL, credential=globals.COSMOS_KEY)
database_name = "ipam-db"
database = cosmos_client.get_database_client(database_name)
# database_name = "ipam-db"
# database = cosmos_client.get_database_client(database_name)
container_name = "ipam-container"
container = database.get_container_client(container_name)
# container_name = "ipam-container"
# container = database.get_container_client(container_name)
item = await container.read_item(target, partition_key=target)
# item = await container.read_item(target, partition_key=target)
await cosmos_client.close()
# await cosmos_client.close()
return item
# return item
async def cosmos_query_x(query: str, tenant_id: str):
# async def cosmos_upsert(target: str, data):
# """DOCSTRING"""
# cosmos_client = CosmosClient(globals.COSMOS_URL, credential=globals.COSMOS_KEY)
# database_name = "ipam-db"
# database = cosmos_client.get_database_client(database_name)
# container_name = "ipam-container"
# container = database.get_container_client(container_name)
# try:
# await container.upsert_item(
# data,
# match_condition=MatchConditions.IfNotModified,
# etag=data['_etag']
# )
# except:
# raise
# finally:
# await cosmos_client.close()
# return
async def cosmos_query(query: str, tenant_id: str):
"""DOCSTRING"""
result_array = []
@ -98,13 +124,13 @@ async def cosmos_query_x(query: str, tenant_id: str):
)
async for result in query_results:
result_array.append(result)
result_array.append(result)
await cosmos_client.close()
return result_array
async def cosmos_upsert_x(data):
async def cosmos_upsert(data):
"""DOCSTRING"""
cosmos_client = CosmosClient(globals.COSMOS_URL, credential=globals.COSMOS_KEY)
@ -116,7 +142,7 @@ async def cosmos_upsert_x(data):
container = database.get_container_client(container_name)
try:
await container.upsert_item(data)
res = await container.upsert_item(data)
except:
raise
finally:
@ -124,33 +150,9 @@ async def cosmos_upsert_x(data):
await cosmos_client.close()
return
return res
async def cosmos_upsert(target: str, data):
"""DOCSTRING"""
cosmos_client = CosmosClient(globals.COSMOS_URL, credential=globals.COSMOS_KEY)
database_name = "ipam-db"
database = cosmos_client.get_database_client(database_name)
container_name = "ipam-container"
container = database.get_container_client(container_name)
try:
await container.upsert_item(
data,
match_condition=MatchConditions.IfNotModified,
etag=data['_etag']
)
except:
raise
finally:
await cosmos_client.close()
return
async def cosmos_replace_x(old, new):
async def cosmos_replace(old, new):
"""DOCSTRING"""
cosmos_client = CosmosClient(globals.COSMOS_URL, credential=globals.COSMOS_KEY)
@ -177,7 +179,7 @@ async def cosmos_replace_x(old, new):
return
async def cosmos_delete_x(item, tenant_id: str):
async def cosmos_delete(item, tenant_id: str):
"""DOCSTRING"""
cosmos_client = CosmosClient(globals.COSMOS_URL, credential=globals.COSMOS_KEY)
@ -277,21 +279,33 @@ async def arg_query_obo(auth, query):
async def arg_query_helper(credentials, query):
"""DOCSTRING"""
results = []
resource_graph_client = ResourceGraphClient(credentials)
query = QueryRequest(
query=query,
management_groups=[globals.TENANT_ID],
options=QueryRequestOptions(
result_format=ResultFormat.object_array
)
)
try:
poll = await resource_graph_client.resources(query)
skip_token = None
while True:
query = QueryRequest(
query=query,
management_groups=[globals.TENANT_ID],
options=QueryRequestOptions(
result_format=ResultFormat.object_array,
skip_token=skip_token
)
)
poll = await resource_graph_client.resources(query)
results = results + poll.data
if poll.skip_token:
skip_token = poll.skip_token
else:
break
except ServiceRequestError:
raise HTTPException(status_code=500, detail="Error communicating with Azure.")
finally:
await resource_graph_client.close()
return poll.data
return results

Просмотреть файл

@ -30,11 +30,9 @@ from . import argquery
from app.routers.common.helper import (
get_username_from_jwt,
cosmos_query,
cosmos_query_x,
cosmos_upsert_x,
cosmos_replace_x,
cosmos_delete_x,
cosmos_upsert,
cosmos_replace,
cosmos_delete,
cosmos_retry,
arg_query
)
@ -111,7 +109,7 @@ async def get_spaces(
if expand or utilization:
vnets = await arg_query(authorization, True, argquery.VNET)
space_query = await cosmos_query_x("SELECT * FROM c WHERE c.type = 'space'", tenant_id)
space_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'space'", tenant_id)
for space in space_query:
if utilization:
@ -192,7 +190,7 @@ async def create_space(
if not is_admin:
raise HTTPException(status_code=403, detail="This API is admin restricted.")
space_query = await cosmos_query_x("SELECT * FROM c WHERE c.type = 'space'", tenant_id)
space_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'space'", tenant_id)
duplicate = next((x for x in space_query if x['name'].lower() == space.name.lower()), None)
@ -200,14 +198,14 @@ async def create_space(
raise HTTPException(status_code=400, detail="Space name must be unique.")
new_space = {
# "id": uuid.uuid4(),
"id": uuid.uuid4(),
"type": "space",
"tenant_id": tenant_id,
**space.dict(),
"blocks": []
}
await cosmos_upsert_x(jsonable_encoder(new_space))
await cosmos_upsert(jsonable_encoder(new_space))
return new_space
@ -241,7 +239,7 @@ async def get_space(
if expand and not is_admin:
raise HTTPException(status_code=403, detail="Expand parameter can only be used by admins.")
space_query = await cosmos_query_x("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
space_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
try:
target_space = copy.deepcopy(space_query[0])
@ -337,7 +335,7 @@ async def update_space(
if not is_admin:
raise HTTPException(status_code=403, detail="This API is admin restricted.")
space_query = await cosmos_query_x("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
space_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
try:
target_space = copy.deepcopy(space_query[0])
@ -352,7 +350,7 @@ async def update_space(
scrubbed_patch = jsonpatch.JsonPatch(await scrub_space_patch(patch))
update_space = scrubbed_patch.apply(target_space)
await cosmos_replace_x(target_space, update_space)
await cosmos_replace(target_space, update_space)
return update_space
@ -378,7 +376,7 @@ async def delete_space(
if not is_admin:
raise HTTPException(status_code=403, detail="This API is admin restricted.")
space_query = await cosmos_query_x("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
space_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
try:
target_space = copy.deepcopy(space_query[0])
@ -389,7 +387,7 @@ async def delete_space(
if len(target_space['blocks']) > 0:
raise HTTPException(status_code=400, detail="Cannot delete space while it contains blocks.")
await cosmos_delete_x(target_space, tenant_id)
await cosmos_delete(target_space, tenant_id)
return PlainTextResponse(status_code=status.HTTP_200_OK)
@ -423,7 +421,7 @@ async def get_blocks(
if expand and not is_admin:
raise HTTPException(status_code=403, detail="Expand parameter can only be used by admins.")
space_query = await cosmos_query_x("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
space_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
try:
target_space = copy.deepcopy(space_query[0])
@ -507,7 +505,7 @@ async def create_block(
if not is_admin:
raise HTTPException(status_code=403, detail="This API is admin restricted.")
space_query = await cosmos_query_x("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
space_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
try:
target_space = copy.deepcopy(space_query[0])
@ -529,7 +527,7 @@ async def create_block(
target_space['blocks'].append(jsonable_encoder(new_block))
await cosmos_replace_x(space_query[0], target_space)
await cosmos_replace(space_query[0], target_space)
return new_block
@ -564,7 +562,7 @@ async def get_block(
if expand and not is_admin:
raise HTTPException(status_code=403, detail="Expand parameter can only be used by admins.")
space_query = await cosmos_query_x("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
space_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
try:
target_space = copy.deepcopy(space_query[0])
@ -647,7 +645,7 @@ async def delete_block(
if not is_admin:
raise HTTPException(status_code=403, detail="This API is admin restricted.")
space_query = await cosmos_query_x("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
space_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
try:
target_space = copy.deepcopy(space_query[0])
@ -666,7 +664,7 @@ async def delete_block(
index = next((i for i, item in enumerate(target_space['blocks']) if item['name'] == block), None)
del target_space['blocks'][index]
await cosmos_replace_x(space_query[0], target_space)
await cosmos_replace(space_query[0], target_space)
return PlainTextResponse(status_code=status.HTTP_200_OK)
@ -696,7 +694,7 @@ async def available_block_vnets(
if not is_admin:
raise HTTPException(status_code=403, detail="API restricted to admins.")
space_query = await cosmos_query_x("SELECT * FROM c WHERE c.type = 'space'", tenant_id)
space_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'space'", tenant_id)
target_space = next((x for x in space_query if x['name'].lower() == space.lower()), None)
@ -761,7 +759,7 @@ async def available_block_vnets(
if not is_admin:
raise HTTPException(status_code=403, detail="API restricted to admins.")
space_query = await cosmos_query_x("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
space_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
try:
target_space = copy.deepcopy(space_query[0])
@ -811,7 +809,7 @@ async def create_block_vnet(
if not is_admin:
raise HTTPException(status_code=403, detail="API restricted to admins.")
space_query = await cosmos_query_x("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
space_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
try:
target_space = copy.deepcopy(space_query[0])
@ -853,9 +851,9 @@ async def create_block_vnet(
raise HTTPException(status_code=400, detail="Block already contains vNet(s) within the CIDR range of target vNet.")
vnet.active = True
target_block['vnets'].append(vnet)
target_block['vnets'].append(jsonable_encoder(vnet))
await cosmos_replace_x(space_query[0], target_space)
await cosmos_replace(space_query[0], target_space)
return target_block
@ -888,7 +886,7 @@ async def update_block_vnets(
if not is_admin:
raise HTTPException(status_code=403, detail="API restricted to admins.")
space_query = await cosmos_query_x("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
space_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
try:
target_space = copy.deepcopy(space_query[0])
@ -949,7 +947,7 @@ async def update_block_vnets(
target_block['vnets'] = new_vnet_list
await cosmos_replace_x(space_query[0], target_space)
await cosmos_replace(space_query[0], target_space)
return target_block['vnets']
@ -966,7 +964,7 @@ async def update_block_vnets(
async def delete_block_vnets(
space: str,
block: str,
req: VNets,
req: VNetsUpdate,
tenant_id: str = Depends(get_tenant_id),
is_admin: str = Depends(get_admin)
):
@ -979,7 +977,7 @@ async def delete_block_vnets(
if not is_admin:
raise HTTPException(status_code=403, detail="API restricted to admins.")
space_query = await cosmos_query_x("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
space_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
try:
target_space = copy.deepcopy(space_query[0])
@ -991,23 +989,23 @@ async def delete_block_vnets(
if not target_block:
raise HTTPException(status_code=400, detail="Invalid block name.")
unique_vnets = len(set(req.ids)) == len(req.ids)
unique_vnets = len(set(req)) == len(req)
if not unique_vnets:
raise HTTPException(status_code=400, detail="List contains one or more duplicate vNet id's.")
current_vnets = list(x['id'] for x in target_block['vnets'])
ids_exist = all(elem in current_vnets for elem in req.ids)
ids_exist = all(elem in current_vnets for elem in req)
if not ids_exist:
raise HTTPException(status_code=400, detail="List contains one or more invalid vNet id's.")
# OR VNET IDS THAT DON'T BELONG TO THE CURRENT BLOCK
for id in req.ids:
for id in req:
index = next((i for i, item in enumerate(target_block['vnets']) if item['id'] == id), None)
del target_block['vnets'][index]
await cosmos_replace_x(space_query[0], target_space)
await cosmos_replace(space_query[0], target_space)
return PlainTextResponse(status_code=status.HTTP_200_OK)
@ -1030,7 +1028,7 @@ async def get_block_reservations(
user_assertion = authorization.split(' ')[1]
space_query = await cosmos_query_x("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
space_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
try:
target_space = copy.deepcopy(space_query[0])
@ -1074,7 +1072,7 @@ async def create_block_reservation(
user_assertion = authorization.split(' ')[1]
decoded = jwt.decode(user_assertion, options={"verify_signature": False})
space_query = await cosmos_query_x("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
space_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
try:
target_space = copy.deepcopy(space_query[0])
@ -1118,7 +1116,7 @@ async def create_block_reservation(
"id": shortuuid.uuid(),
"cidr": str(next_cidr),
"userId": creator_id,
"createdOn": (time.time() * 1000),
"createdOn": time.time(),
"status": "wait"
}
@ -1126,7 +1124,7 @@ async def create_block_reservation(
# NEED TO RETURN GUID FOR USER TO APPEND TO AZURE TAG ON VNET
await cosmos_replace_x(space_query[0], target_space)
await cosmos_replace(space_query[0], target_space)
return new_cidr
@ -1148,13 +1146,15 @@ async def delete_block_reservations(
is_admin: str = Depends(get_admin)
):
"""
Remove a CIDR Reservation for the target Block.
Remove one or more CIDR Reservations for the target Block.
- **[&lt;str&gt;]**: Array of CIDR Reservation ID's
"""
user_assertion = authorization.split(' ')[1]
user_name = get_username_from_jwt(user_assertion)
space_query = await cosmos_query_x("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
space_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'space' AND LOWER(c.name) = LOWER('{}')".format(space), tenant_id)
try:
target_space = copy.deepcopy(space_query[0])
@ -1187,6 +1187,6 @@ async def delete_block_reservations(
index = next((i for i, item in enumerate(target_block['resv']) if item['id'] == id), None)
del target_block['resv'][index]
await cosmos_replace_x(space_query[0], target_space)
await cosmos_replace(space_query[0], target_space)
return PlainTextResponse(status_code=status.HTTP_200_OK)

Просмотреть файл

@ -8,17 +8,27 @@ from typing import Optional, List, Any
import azure.cosmos.exceptions as exceptions
from app.dependencies import check_token_expired, get_admin
from app.dependencies import (
check_token_expired,
get_admin,
get_tenant_id
)
import re
import jsonpatch
from uuid import UUID
import uuid
import copy
from app.models import *
from app.routers.common.helper import (
get_username_from_jwt,
get_user_id_from_jwt,
cosmos_query,
cosmos_upsert
cosmos_upsert,
cosmos_replace,
cosmos_delete,
cosmos_retry
)
router = APIRouter(
@ -27,27 +37,20 @@ router = APIRouter(
dependencies=[Depends(check_token_expired)]
)
class User(BaseModel):
"""DOCSTRING"""
async def new_user(user_id, tenant_id):
new_user = {
"id": uuid.uuid4(),
"type": "user",
"tenant_id": tenant_id,
"data": {
"id": user_id,
"apiRefresh": 5
}
}
id: UUID
apiRefresh: int
isAdmin: bool
query_results = await cosmos_upsert(jsonable_encoder(new_user))
class Config:
json_encoders = {
UUID: lambda v: str(v),
}
class JSONPatch(BaseModel):
"""DOCSTRING"""
op: str
path: str
value: Any
class UserUpdate(List[JSONPatch]):
"""DOCSTRING"""
return query_results
async def scrub_patch(patch):
scrubbed_patch = []
@ -79,6 +82,7 @@ async def scrub_patch(patch):
status_code = 200
)
async def get_users(
tenant_id: str = Depends(get_tenant_id),
is_admin: str = Depends(get_admin)
):
"""
@ -90,10 +94,12 @@ async def get_users(
if not is_admin:
raise HTTPException(status_code=403, detail="API restricted to admins.")
users = await cosmos_query("users")
admins = await cosmos_query("admins")
users = await cosmos_query("SELECT VALUE c.data FROM c WHERE c.type = 'user'", tenant_id)
admin_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'admin'", tenant_id)
for user in users['users']:
admins = admin_query[0]
for user in users:
is_admin = next((x for x in admins['admins'] if x['id'] == user['id']), None)
current_user = {
@ -111,53 +117,40 @@ async def get_users(
response_model = User,
status_code = 200
)
@cosmos_retry(
max_retry = 5,
error_msg = "Error creating user, please try again."
)
async def get_user(
authorization: str = Header(None)
authorization: str = Header(None),
tenant_id: str = Depends(get_tenant_id)
):
"""
Get your IPAM user details.
"""
user_assertion = authorization.split(' ')[1]
userId = get_user_id_from_jwt(user_assertion)
user_id = get_user_id_from_jwt(user_assertion)
current_try = 0
max_retry = 5
user_query = await cosmos_query("SELECT * FROM c WHERE (c.type = 'user' AND c['data']['id'] = '{}')".format(user_id), tenant_id)
while True:
try:
item = await cosmos_query("users")
if not user_query:
user_query = [await new_user(user_id, tenant_id)]
target_user = next((x for x in item['users'] if x['id'] == userId), None)
user_data = copy.deepcopy(user_query[0])
if not target_user:
target_user = {
"id": userId,
"apiRefresh": 5
}
admin_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'admin'", tenant_id)
item['users'].append(target_user)
await cosmos_upsert("users", item)
except exceptions.CosmosAccessConditionFailedError as e:
if current_try < max_retry:
current_try += 1
continue
else:
raise HTTPException(status_code=500, detail="Error creating user, please try again.")
else:
break
admins = await cosmos_query("admins")
admins = admin_query[0]
if admins['admins']:
is_admin = next((x for x in admins['admins'] if x['id'] == target_user['id']), None)
is_admin = next((x for x in admins['admins'] if x['id'] == user_id), None)
else:
is_admin = True
is_admin = True
target_user['isAdmin'] = True if is_admin else False
user_data['data']['isAdmin'] = True if is_admin else False
return target_user
return user_data['data']
@router.patch(
"/me",
@ -165,9 +158,14 @@ async def get_user(
response_model = User,
status_code=200
)
@cosmos_retry(
max_retry = 5,
error_msg = "Error updating user, please try again."
)
async def update_user(
updates: UserUpdate,
authorization: str = Header(None)
authorization: str = Header(None),
tenant_id: str = Depends(get_tenant_id)
):
"""
Update a User with a JSON patch:
@ -183,40 +181,32 @@ async def update_user(
- **/apiRefresh**
"""
current_try = 0
max_retry = 5
user_assertion = authorization.split(' ')[1]
userId = get_user_id_from_jwt(user_assertion)
user_id = get_user_id_from_jwt(user_assertion)
while True:
try:
item = await cosmos_query("users")
user_query = await cosmos_query("SELECT * FROM c WHERE (c.type = 'user' AND c['data']['id'] = '{}')".format(user_id), tenant_id)
target_user = next((x for x in item['users'] if x['id'] == userId), None)
if not user_query:
user_query = [await new_user(user_id, tenant_id)]
try:
patch = jsonpatch.JsonPatch(updates)
except jsonpatch.InvalidJsonPatch:
raise HTTPException(status_code=500, detail="Invalid JSON patch, please review and try again.")
user_data = copy.deepcopy(user_query[0])
scrubbed_patch = jsonpatch.JsonPatch(await scrub_patch(patch))
scrubbed_patch.apply(target_user, in_place = True)
try:
patch = jsonpatch.JsonPatch(updates)
except jsonpatch.InvalidJsonPatch:
raise HTTPException(status_code=500, detail="Invalid JSON patch, please review and try again.")
await cosmos_upsert("users", item)
except exceptions.CosmosAccessConditionFailedError:
if current_try < max_retry:
current_try += 1
continue
else:
raise HTTPException(status_code=500, detail="Error updating user, please try again.")
else:
break
scrubbed_patch = jsonpatch.JsonPatch(await scrub_patch(patch))
user_data['data'] = scrubbed_patch.apply(user_data['data'], in_place = True)
admins = await cosmos_query("admins")
await cosmos_replace(user_query[0], user_data)
is_admin = next((x for x in admins['admins'] if x['id'] == target_user['id']), None)
admin_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'admin'", tenant_id)
target_user['isAdmin'] = True if is_admin else False
admins = admin_query[0]
return target_user
is_admin = next((x for x in admins['admins'] if x['id'] == user_id), None)
user_data['data']['isAdmin'] = True if is_admin else False
return user_data['data']

20
engine/host.json Normal file
Просмотреть файл

@ -0,0 +1,20 @@
{
"version": "2.0",
"extensions": {
"http": {
"routePrefix": ""
}
},
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
}
}
},
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[2.*, 3.0.0)"
}
}

Просмотреть файл

@ -0,0 +1,32 @@
import azure.functions as func
from azure.functions._http_asgi import AsgiResponse, AsgiRequest
import nest_asyncio
from app.main import app as ipam
IS_INITED = False
nest_asyncio.apply()
async def run_setup(app):
"""Workaround to run Starlette startup events on Azure Function Workers."""
global IS_INITED
if not IS_INITED:
await app.router.startup()
IS_INITED = True
async def handle_asgi_request(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
asgi_request = AsgiRequest(req, context)
scope = asgi_request.to_asgi_http_scope()
asgi_response = await AsgiResponse.from_app(ipam, scope, req.get_body())
return asgi_response.to_func_response()
async def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
await run_setup(ipam)
return await handle_asgi_request(req, context)

Просмотреть файл

@ -0,0 +1,24 @@
{
"scriptFile": "__init__.py",
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"get",
"post",
"patch",
"put",
"delete"
],
"route": "/{*route}"
},
{
"type": "http",
"direction": "out",
"name": "$return"
}
]
}

Просмотреть файл

@ -18,3 +18,5 @@ azure-mgmt-resource
azure-mgmt-resourcegraph
azure-keyvault-secrets
azure-cosmos==4.3.0
azure-functions
nest_asyncio

Просмотреть файл

@ -0,0 +1,397 @@
import * as React from "react";
import { styled } from '@mui/material/styles';
import { useMsal } from "@azure/msal-react";
import { InteractionRequiredAuthError, InteractionStatus } from "@azure/msal-browser";
import { callMsGraphUsersFilter } from "../../msal/graph";
import { useSnackbar } from 'notistack';
import { isEqual, throttle } from 'lodash';
import {
DataGrid,
GridOverlay,
GridToolbarContainer
} from "@mui/x-data-grid";
import {
Box,
Typography,
Tooltip,
IconButton,
Autocomplete,
TextField,
LinearProgress,
CircularProgress,
Popper,
} from "@mui/material";
import {
SaveAlt,
HighlightOff
} from "@mui/icons-material";
import Shrug from "../../img/pam/Shrug";
import {
getAdmins,
replaceAdmins
} from "../ipam/ipamAPI";
import { apiRequest } from "../../msal/authConfig";
function CustomToolbar(props) {
const { admins, loadedAdmins, setAdmins, selectionModel, refresh, refreshing } = props;
const { instance, inProgress, accounts } = useMsal();
const { enqueueSnackbar } = useSnackbar();
const [open, setOpen] = React.useState(false);
const [options, setOptions] = React.useState(null);
const [input, setInput] = React.useState("");
const [selected, setSelected] = React.useState(null);
const [sending, setSending] = React.useState(false);
const loading = open && !options
const unchanged = isEqual(admins, loadedAdmins);
function SearchUsers(nameFilter) {
const request = {
scopes: ["Directory.Read.All"],
account: accounts[0],
};
(async () => {
try {
setOptions(null);
const response = await instance.acquireTokenSilent(request);
const userData = await callMsGraphUsersFilter(response.accessToken, nameFilter);
setOptions(userData.value);
} catch (e) {
if (e instanceof InteractionRequiredAuthError) {
instance.acquireTokenRedirect(request);
} else {
console.log("ERROR");
console.log("------------------");
console.log(e);
console.log("------------------");
enqueueSnackbar(e.response.data.error, { variant: "error" });
}
}
})();
}
const fetchUsers = React.useMemo(() => throttle((input) => SearchUsers(input), 500), []);
React.useEffect(() => {
let active = true;
if (active) {
fetchUsers(input);
}
return () => {
active = false;
};
}, [input, fetchUsers]);
React.useEffect(() => {
if (!open) {
setOptions(null);
}
}, [input, open]);
function handleAdd(user) {
let newAdmin = {
name: user.displayName,
id: user.id,
email: user.userPrincipalName,
};
if(!admins.find(obj => { return obj.id === user.id })) {
setAdmins((admins) => [...admins, newAdmin]);
} else {
console.log("Admin already added!");
enqueueSnackbar('Admin already added!', { variant: 'error' });
}
setSelected(null);
}
function onSave() {
const request = {
scopes: apiRequest.scopes,
account: accounts[0],
};
(async () => {
try {
setSending(true);
const response = await instance.acquireTokenSilent(request);
const data = await replaceAdmins(response.accessToken, admins);
enqueueSnackbar("Successfully updated admins", { variant: "success" });
refresh();
} catch (e) {
if (e instanceof InteractionRequiredAuthError) {
instance.acquireTokenRedirect(request);
} else {
console.log("ERROR");
console.log("------------------");
console.log(e);
console.log("------------------");
enqueueSnackbar(e.response.data.error, { variant: "error" });
}
} finally {
setSending(false);
}
})();
}
const popperStyle = {
popper: {
width: "fit-content"
}
};
const MyPopper = function (props) {
return <Popper {...props} style={{ popperStyle }} placement="bottom-start" />;
};
return (
<GridToolbarContainer>
<Box
height="65px"
width="100%"
display="flex"
flexDirection="row"
justifyContent="flex-start"
style={{ borderBottom: "1px solid rgba(224, 224, 224, 1)" }}
>
<Box display="flex" justifyContent="flex-start" sx={{ flexBasis: "300px", flexGrow: 0, flexShrink: 0, ml: 2, mr: 2 }}>
<Autocomplete
PopperComponent={MyPopper}
key="12345"
id="asynchronous-demo"
autoHighlight
blurOnSelect={true}
forcePopupIcon={false}
sx={{ width: 300 }}
open={open}
value={selected}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
onInputChange={(event, newInput) => {
setInput(newInput);
}}
onChange={(event, newValue) => {
newValue ? handleAdd(newValue) : setSelected(null);
}}
isOptionEqualToValue={(option, value) => option.displayName === value.displayName}
getOptionLabel={(option) => `${option.displayName} (${option.userPrincipalName})`}
options={options || []}
loading={loading}
renderInput={(params) => (
<TextField
{...params}
label="User Search"
variant="standard"
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{loading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</React.Fragment>
),
}}
/>
)}
/>
</Box>
<Box width="100%" display="flex" alignSelf="center" textAlign="center">
<Typography sx={{ flex: "1 1 100%" }} variant="h6" component="div">
IPAM Admins
</Typography>
</Box>
<Box display="flex" justifyContent="flex-end" alignItems="center" sx={{ flexBasis: "300px", flexGrow: 0, flexShrink: 0, ml: 2, mr: 2 }}>
<Tooltip title="Save" >
<IconButton
color="primary"
aria-label="upload picture"
component="span"
style={{
visibility: unchanged ? 'hidden' : 'visible'
}}
disabled={sending || refreshing}
onClick={onSave}
>
<SaveAlt />
</IconButton>
</Tooltip>
</Box>
</Box>
</GridToolbarContainer>
);
}
export default function Administration() {
const { instance, accounts } = useMsal();
const { enqueueSnackbar } = useSnackbar();
const [admins, setAdmins] = React.useState([]);
const [loadedAdmins, setLoadedAdmins] = React.useState([]);
const [selectionModel, setSelectionModel] = React.useState([]);
const [loading, setLoading] = React.useState(false);
const columns = [
{ field: "name", headerName: "Name", flex: 0.5 },
{ field: "email", headerName: "Email", flex: 1 },
{ field: "id", headerName: "Object ID", flex: 0.75 },
{ field: "", headerName: "", headerAlign: "center", align: "center", width: 25, sortable: false, renderCell: renderDelete }
];
React.useEffect(() => {
refreshData();
}, []);
function refreshData() {
console.log("REFRESHING...");
const request = {
scopes: apiRequest.scopes,
account: accounts[0],
};
(async () => {
try {
setLoading(true);
const response = await instance.acquireTokenSilent(request);
const data = await getAdmins(response.accessToken);
setAdmins(data);
setLoadedAdmins(data);
} catch (e) {
if (e instanceof InteractionRequiredAuthError) {
instance.acquireTokenRedirect(request);
} else {
console.log("ERROR");
console.log("------------------");
console.log(e);
console.log("------------------");
enqueueSnackbar("Error fetching admins", { variant: "error" });
}
} finally {
setLoading(false);
}
})();
}
function renderDelete(params) {
const flexCenter = {
display: "flex",
alignItems: "center",
justifyContent: "center"
}
return (
<Tooltip title="Delete">
<span style={{...flexCenter}}>
<IconButton
color="error"
sx={{
padding: 0,
display: (JSON.stringify([params.row.id]) === JSON.stringify(selectionModel)) ? "flex" : "none"
}}
disableFocusRipple
disableTouchRipple
disableRipple
onClick={() => setAdmins(admins.filter(x => x.id !== params.row.id))}
>
<HighlightOff />
</IconButton>
</span>
</Tooltip>
);
}
function onModelChange(newModel) {
if(JSON.stringify(newModel) === JSON.stringify(selectionModel)) {
setSelectionModel([]);
} else {
setSelectionModel(newModel);
}
}
const StyledGridOverlay = styled('div')({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
});
function CustomNoRowsOverlay() {
return (
<StyledGridOverlay>
<Shrug />
<Typography variant="overline" display="block" sx={{ mt: 1 }}>
Nothing yet...
</Typography>
</StyledGridOverlay>
);
}
function CustomLoadingOverlay() {
return (
<GridOverlay>
<div style={{ position: "absolute", top: 0, width: "100%" }}>
<LinearProgress />
</div>
</GridOverlay>
);
}
return (
// <Box sx={{ height: "calc(100vh - 64px)", width: "100%" }}>
// <Box sx={{ p: 3, height: "calc(100vh - 112px)", display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
<Box sx={{ flexGrow: 1, height: "100%" }}>
<DataGrid
disableColumnMenu
hideFooter
hideFooterPagination
hideFooterSelectedRowCount
rows={admins}
columns={columns}
loading={loading}
onSelectionModelChange={(newSelectionModel) => onModelChange(newSelectionModel)}
setSelectionModel={selectionModel}
components={{
Toolbar: CustomToolbar,
NoRowsOverlay: CustomNoRowsOverlay,
LoadingOverlay: CustomLoadingOverlay,
}}
componentsProps={{
toolbar: {
admins: admins,
loadedAdmins: loadedAdmins,
setAdmins, setAdmins,
selectionModel: selectionModel,
refresh: refreshData,
refreshing: loading
}
}}
sx={{
"&.MuiDataGrid-root .MuiDataGrid-columnHeader:focus, &.MuiDataGrid-root .MuiDataGrid-cell:focus, &.MuiDataGrid-root .MuiDataGrid-cell:focus-within":
{
outline: "none",
},
}}
/>
</Box>
// </Box>
// </Box>
);
}

Просмотреть файл

@ -62,14 +62,17 @@ import Configure from "../../img/Configure";
import Admin from "../../img/Admin";
import Visualize from "../../img/Visualize";
import Conflict from "../../img/Conflict";
import Person from "../../img/Person";
import Rule from "../../img/Rule";
import UserSettings from "./userSettings";
import Welcome from "../welcome/Welcome";
import DiscoverTabs from "../tabs/discoverTabs";
import AnalyzeTabs from "../tabs/analyzeTabs";
import AdminTabs from "../tabs/adminTabs";
// import AnalysisTool from "../analysis/analysis";
import Administration from "../admin/admin";
import Administration from "../admin/admin2";
import ConfigureIPAM from "../configure/configure";
import Refresh from "./refresh";
@ -149,6 +152,7 @@ export default function NavDrawer() {
{
title: "Discover",
icon: Discover,
admin: false,
children: [
{
title: "Spaces",
@ -185,6 +189,7 @@ export default function NavDrawer() {
{
title: "Analysis",
icon: Analysis,
admin: false,
children: [
{
title: "Visualize",
@ -208,11 +213,30 @@ export default function NavDrawer() {
link: "configure",
admin: false
},
// {
// title: "Admin",
// icon: Admin,
// link: "admin",
// admin: true
// },
{
title: "Admin",
icon: Admin,
link: "admin",
admin: true
admin: true,
children: [
{
title: "Admins",
icon: Person,
link: "admin/admins",
admin: true
},
{
title: "Subscriptions",
icon: Rule,
link: "admin/subscriptions",
admin: true
}
]
},
]
];
@ -282,7 +306,8 @@ export default function NavDrawer() {
<List>
{navItem.map((item, itemIndex) => {
return item.hasOwnProperty('children')
? <React.Fragment key={`item-${item.title}`}>
? ((item.admin && isAdmin) || !item.admin) &&
<React.Fragment key={`item-${item.title}`}>
<ListItem
key={item.title}
component="div"
@ -626,7 +651,9 @@ export default function NavDrawer() {
<Route path="analyze/visualize" element={<AnalyzeTabs />} />
<Route path="analyze/conflict" element={<AnalyzeTabs />} />
<Route path="configure" element={<ConfigureIPAM />} />
<Route path="admin" element={<Administration />} />
{/* <Route path="admin" element={<Administration />} /> */}
<Route path="admin/admins" element={<AdminTabs />} />
<Route path="admin/subscriptions" element={<AdminTabs />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Box>

Просмотреть файл

@ -0,0 +1,297 @@
import * as React from "react";
import { styled } from "@mui/material/styles";
import { useSnackbar } from "notistack";
import { useMsal } from "@azure/msal-react";
import { InteractionRequiredAuthError } from "@azure/msal-browser";
import { isEqual } from 'lodash';
import { DataGrid, GridOverlay } from "@mui/x-data-grid";
import {
Box,
Typography,
LinearProgress,
Tooltip,
IconButton
} from "@mui/material";
import {
SaveAlt
} from "@mui/icons-material";
import { fetchSubscriptions } from "../ipam/ipamAPI";
import { apiRequest } from "../../msal/authConfig";
// Page Styles
const Wrapper = styled("div")(({ theme }) => ({
display: "flex",
flexGrow: 1,
height: "calc(100vh - 160px)"
}));
const MainBody = styled("div")({
display: "flex",
height: "100%",
width: "100%",
flexDirection: "column",
});
const FloatingHeader = styled("div")(({ theme }) => ({
...theme.typography.h6,
display: "flex",
flexDirection: "row",
height: "7%",
width: "100%",
border: "1px solid rgba(224, 224, 224, 1)",
borderRadius: "4px",
marginBottom: theme.spacing(3)
}));
const HeaderTitle = styled("div")(({ theme }) => ({
...theme.typography.h6,
width: "80%",
textAlign: "center",
alignSelf: "center",
}));
const TopSection = styled("div")(({ theme }) => ({
display: "flex",
flexDirection: "column",
height: "50%",
width: "100%",
border: "1px solid rgba(224, 224, 224, 1)",
borderRadius: "4px",
marginBottom: theme.spacing(1.5)
}));
const BottomSection = styled("div")(({ theme }) => ({
display: "flex",
flexDirection: "column",
height: "50%",
width: "100%",
border: "1px solid rgba(224, 224, 224, 1)",
borderRadius: "4px",
marginTop: theme.spacing(1.5)
}));
// Grid Styles
const GridHeader = styled("div")({
height: "50px",
width: "100%",
display: "flex",
borderBottom: "1px solid rgba(224, 224, 224, 1)",
});
const GridTitle = styled("div")(({ theme }) => ({
...theme.typography.subtitle1,
width: "80%",
textAlign: "center",
alignSelf: "center",
}));
const GridBody = styled("div")({
height: "100%",
width: "100%",
});
const StyledGridOverlay = styled("div")({
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
height: "100%",
});
function GridSection(props) {
const { title, action, columns, rows, loading, onClick } = props;
function CustomLoadingOverlay() {
return (
<GridOverlay>
<div style={{ position: "absolute", top: 0, width: "100%" }}>
<LinearProgress />
</div>
</GridOverlay>
);
}
function CustomNoRowsOverlay() {
return (
<StyledGridOverlay>
<Typography variant="overline" display="block" sx={{ mt: 1 }}>
No Subscriptions Selected
</Typography>
</StyledGridOverlay>
);
}
const message = `Click to ${action}`;
return (
<React.Fragment>
<GridHeader
style={{
borderBottom: "1px solid rgba(224, 224, 224, 1)",
}}
>
<Box sx={{ width: "20%" }} />
<GridTitle>{title}</GridTitle>
<Box sx={{ width: "20%" }} />
</GridHeader>
<Tooltip title={message} followCursor>
<GridBody>
<DataGrid
disableColumnMenu
hideFooter
hideFooterPagination
hideFooterSelectedRowCount
density="compact"
rows={rows}
columns={columns}
onRowClick={(rowData) => onClick(rowData.row)}
loading={loading}
components={{
LoadingOverlay: CustomLoadingOverlay,
NoRowsOverlay: CustomNoRowsOverlay,
}}
initialState={{
sorting: {
sortModel: [{ field: 'name', sort: 'asc' }],
},
}}
sx={{
"&.MuiDataGrid-root .MuiDataGrid-columnHeader:focus, &.MuiDataGrid-root .MuiDataGrid-cell:focus":
{
outline: "none",
},
border: "none",
}}
/>
</GridBody>
</Tooltip>
</React.Fragment>
);
}
const columns = [
{ field: "subscription_id", headerName: "Subscription ID", headerAlign: "left", align: "left", flex: 1 },
{ field: "name", headerName: "Subscription Name", headerAlign: "left", align: "left", flex: 2 },
{ field: "type", headerName: "Subscription Type", headerAlign: "left", align: "left", flex: 0.75 },
];
export default function ManageExclusions() {
const { instance, inProgress, accounts } = useMsal();
const { enqueueSnackbar } = useSnackbar();
const [loading, setLoading] = React.useState(false);
const [included, setIncluded] = React.useState([]);
const [excluded, setExcluded] = React.useState([]);
const [loadedExclusions, setLoadedExclusions] = React.useState([]);
const [sending, setSending] = React.useState(false);
const unchanged = isEqual(excluded, loadedExclusions);
React.useEffect(() => {
const request = {
scopes: apiRequest.scopes,
account: accounts[0],
};
(async () => {
try {
setLoading(true);
const response = await instance.acquireTokenSilent(request);
const data = await fetchSubscriptions(response.accessToken);
setIncluded(data);
} catch (e) {
if (e instanceof InteractionRequiredAuthError) {
instance.acquireTokenRedirect(request);
} else {
console.log("ERROR");
console.log("------------------");
console.log(e);
console.log("------------------");
enqueueSnackbar("Error fetching Subscriptions", { variant: "error" });
}
} finally {
setLoading(false);
}
})();
}, []);
function subscriptionExclude(elem) {
const newArr = included.filter(object => {
return object.id !== elem.id;
});
setIncluded(newArr);
setExcluded(excluded => [...excluded, elem]);
}
function subscriptionInclude(elem) {
const newArr = excluded.filter(object => {
return object.id !== elem.id;
});
setExcluded(newArr);
setIncluded(included => [...included, elem]);
}
return (
<Wrapper>
<MainBody>
<FloatingHeader>
<Box sx={{ width: "20%" }}></Box>
<HeaderTitle>Subscription Management</HeaderTitle>
<Box display="flex" justifyContent="flex-end" alignItems="center" sx={{ width: "20%", ml: 2, mr: 2 }}>
<Tooltip title="Save" >
<IconButton
color="primary"
aria-label="upload picture"
component="span"
style={{
visibility: unchanged ? 'hidden' : 'visible',
disabled: sending
}}
onClick={() => console.log("CLICK")}
>
<SaveAlt />
</IconButton>
</Tooltip>
</Box>
</FloatingHeader>
<TopSection>
<GridSection
title="Included Subscriptions"
action="exclude"
// columns={columns.map((x) => ({...x, renderCell: renderExclude}))}
columns={columns}
rows={included}
loading={loading}
onClick={subscriptionExclude}
/>
</TopSection>
<BottomSection>
<GridSection
title="Excluded Subscriptions"
action="include"
// columns={columns.map((x) => ({...x, renderCell: renderInclude}))}
columns={columns}
rows={excluded}
loading={false}
onClick={subscriptionInclude}
/>
</BottomSection>
</MainBody>
</Wrapper>
);
}

Просмотреть файл

@ -235,6 +235,23 @@ export function deleteBlockResvs(token, space, block, body) {
});
}
export function fetchSubscriptions(token) {
var url = new URL(`${ENGINE_URL}/api/azure/subscription`);
return axios
.get(url, {
headers: {
Authorization: `Bearer ${token}`
}
})
.then(response => response.data)
.catch(error => {
console.log("ERROR FETCHING SUBSCRIPTIONS FROM API");
console.log(error);
throw error;
});
}
export function fetchVNets(token) {
var url = new URL(`${ENGINE_URL}/api/azure/vnet`);

Просмотреть файл

@ -0,0 +1,64 @@
import * as React from 'react';
import { Link, useLocation } from "react-router-dom";
import PropTypes from 'prop-types';
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';
import Box from '@mui/material/Box';
import Administration from '../admin/admin2';
import ManageExclusions from '../exclusions/exclusions';
function TabPanel(props) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ p: 3, height: 'calc(100vh - 161px)' }}>
{children}
</Box>
)}
</div>
);
}
TabPanel.propTypes = {
children: PropTypes.node,
index: PropTypes.number.isRequired,
value: PropTypes.number.isRequired,
};
function a11yProps(index) {
return {
id: `simple-tab-${index}`,
'aria-controls': `simple-tabpanel-${index}`,
};
}
export default function AdminTabs() {
const allTabs = ['/admin/admins', '/admin/subscriptions'];
let location = useLocation();
return (
<Box sx={{ width: '100%', height: 'calc(100vh - 137px)'}}>
<React.Fragment>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={allTabs.indexOf(location.pathname)}>
<Tab label="Admins" component={Link} to={allTabs[0]} {...a11yProps(0)} />
<Tab label="Subscriptions" component={Link} to={allTabs[1]} {...a11yProps(1)} />
</Tabs>
</Box>
<TabPanel value={allTabs.indexOf(location.pathname)} index={0}><Administration /></TabPanel>
<TabPanel value={allTabs.indexOf(location.pathname)} index={1}><ManageExclusions /></TabPanel>
</React.Fragment>
</Box>
);
}

Просмотреть файл

@ -8,8 +8,9 @@ function Account() {
height="24"
viewBox="0 0 24 24"
>
<path fill="none" d="M0 0h24v24H0z"></path>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2a7.2 7.2 0 01-6-3.22c.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08a7.2 7.2 0 01-6 3.22z"></path>
<path fill="none" d="M0 0H24V24H0z"></path>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zM7.35 18.5C8.66 17.56 10.26 17 12 17s3.34.56 4.65 1.5c-1.31.94-2.91 1.5-4.65 1.5s-3.34-.56-4.65-1.5zm10.79-1.38a9.947 9.947 0 00-12.28 0A7.957 7.957 0 014 12c0-4.42 3.58-8 8-8s8 3.58 8 8c0 1.95-.7 3.73-1.86 5.12z"></path>
<path d="M12 6c-1.93 0-3.5 1.57-3.5 3.5S10.07 13 12 13s3.5-1.57 3.5-3.5S13.93 6 12 6zm0 5c-.83 0-1.5-.67-1.5-1.5S11.17 8 12 8s1.5.67 1.5 1.5S12.83 11 12 11z"></path>
</svg>
);
}

Просмотреть файл

@ -9,8 +9,11 @@ function Admin() {
viewBox="0 0 24 24"
>
<path fill="none" d="M0 0H24V24H0z"></path>
<path d="M17 11c.34 0 .67.04 1 .09V6.27L10.5 3 3 6.27v4.91c0 4.54 3.2 8.79 7.5 9.82.55-.13 1.08-.32 1.6-.55-.69-.98-1.1-2.17-1.1-3.45 0-3.31 2.69-6 6-6z"></path>
<path d="M17 13c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 1.38c.62 0 1.12.51 1.12 1.12s-.51 1.12-1.12 1.12-1.12-.51-1.12-1.12.5-1.12 1.12-1.12zm0 5.37c-.93 0-1.74-.46-2.24-1.17.05-.72 1.51-1.08 2.24-1.08s2.19.36 2.24 1.08c-.5.71-1.31 1.17-2.24 1.17z"></path>
<g fillRule="evenodd">
<circle cx="17" cy="15.5" r="1.12"></circle>
<path d="M17 17.5c-.73 0-2.19.36-2.24 1.08.5.71 1.32 1.17 2.24 1.17s1.74-.46 2.24-1.17c-.05-.72-1.51-1.08-2.24-1.08z"></path>
<path d="M18 11.09V6.27L10.5 3 3 6.27v4.91c0 4.54 3.2 8.79 7.5 9.82.55-.13 1.08-.32 1.6-.55A5.973 5.973 0 0017 23c3.31 0 6-2.69 6-6 0-2.97-2.16-5.43-5-5.91zM11 17c0 .56.08 1.11.23 1.62-.24.11-.48.22-.73.3-3.17-1-5.5-4.24-5.5-7.74v-3.6l5.5-2.4 5.5 2.4v3.51c-2.84.48-5 2.94-5 5.91zm6 4c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4z"></path>
</g>
</svg>
);
}

Просмотреть файл

@ -8,8 +8,8 @@ function Configure() {
height="24"
viewBox="0 0 24 24"
>
<path fill="none" d="M0 0h24v24H0z" clipRule="evenodd"></path>
<path d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9-2-2-5-2.4-7.4-1.3L9 6 6 9 1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4z"></path>
<path fill="none" d="M0 0h24v24H0V0z"></path>
<path d="M22.61 18.99l-9.08-9.08c.93-2.34.45-5.1-1.44-7C9.79.61 6.21.4 3.66 2.26L7.5 6.11 6.08 7.52 2.25 3.69C.39 6.23.6 9.82 2.9 12.11c1.86 1.86 4.57 2.35 6.89 1.48l9.11 9.11c.39.39 1.02.39 1.41 0l2.3-2.3c.4-.38.4-1.01 0-1.41zm-3 1.6l-9.46-9.46c-.61.45-1.29.72-2 .82-1.36.2-2.79-.21-3.83-1.25C3.37 9.76 2.93 8.5 3 7.26l3.09 3.09 4.24-4.24-3.09-3.09c1.24-.07 2.49.37 3.44 1.31a4.469 4.469 0 011.24 3.96 4.35 4.35 0 01-.88 1.96l9.45 9.45-.88.89z"></path>
</svg>
);
}

17
ui/src/img/Exclude.js Normal file
Просмотреть файл

@ -0,0 +1,17 @@
import React from "react";
function Exclude() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path fill="none" d="M0 0H24V24H0z"></path>
<path d="M14 10H3v2h11v-2zm0-4H3v2h11V6zM3 16h7v-2H3v2zm11.41 6L17 19.41 19.59 22 21 20.59 18.41 18 21 15.41 19.59 14 17 16.59 14.41 14 13 15.41 15.59 18 13 20.59 14.41 22z"></path>
</svg>
);
}
export default Exclude;

Просмотреть файл

@ -8,8 +8,8 @@ function Home() {
height="24"
viewBox="0 0 24 24"
>
<path fill="none" d="M0 0h24v24H0z"></path>
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"></path>
<path fill="none" d="M0 0h24v24H0V0z"></path>
<path d="M12 5.69l5 4.5V18h-2v-6H9v6H7v-7.81l5-4.5M12 3L2 12h3v8h6v-6h2v6h6v-8h3L12 3z"></path>
</svg>
);
}

17
ui/src/img/Person.js Normal file
Просмотреть файл

@ -0,0 +1,17 @@
import React from "react";
function Person() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path fill="none" d="M0 0h24v24H0V0z"></path>
<path d="M4 18v-.65c0-.34.16-.66.41-.81C6.1 15.53 8.03 15 10 15c.03 0 .05 0 .08.01.1-.7.3-1.37.59-1.98-.22-.02-.44-.03-.67-.03-2.42 0-4.68.67-6.61 1.82-.88.52-1.39 1.5-1.39 2.53V20h9.26c-.42-.6-.75-1.28-.97-2H4zM10 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0-6c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2zM20.75 16c0-.22-.03-.42-.06-.63l1.14-1.01-1-1.73-1.45.49c-.32-.27-.68-.48-1.08-.63L18 11h-2l-.3 1.49c-.4.15-.76.36-1.08.63l-1.45-.49-1 1.73 1.14 1.01c-.03.21-.06.41-.06.63s.03.42.06.63l-1.14 1.01 1 1.73 1.45-.49c.32.27.68.48 1.08.63L16 21h2l.3-1.49c.4-.15.76-.36 1.08-.63l1.45.49 1-1.73-1.14-1.01c.03-.21.06-.41.06-.63zM17 18c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"></path>
</svg>
);
}
export default Person;

17
ui/src/img/Rule.js Normal file
Просмотреть файл

@ -0,0 +1,17 @@
import React from "react";
function Rule() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path fill="none" d="M0 0H24V24H0z"></path>
<path d="M16.54 11L13 7.46l1.41-1.41 2.12 2.12 4.24-4.24 1.41 1.41L16.54 11zM11 7H2v2h9V7zm10 6.41L19.59 12 17 14.59 14.41 12 13 13.41 15.59 16 13 18.59 14.41 20 17 17.41 19.59 20 21 18.59 18.41 16 21 13.41zM11 15H2v2h9v-2z"></path>
</svg>
);
}
export default Rule;

Просмотреть файл

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><rect fill="none" height="24" width="24"/></g><g><g><path d="M12,2C6.48,2,2,6.48,2,12s4.48,10,10,10s10-4.48,10-10S17.52,2,12,2z M7.35,18.5C8.66,17.56,10.26,17,12,17 s3.34,0.56,4.65,1.5C15.34,19.44,13.74,20,12,20S8.66,19.44,7.35,18.5z M18.14,17.12L18.14,17.12C16.45,15.8,14.32,15,12,15 s-4.45,0.8-6.14,2.12l0,0C4.7,15.73,4,13.95,4,12c0-4.42,3.58-8,8-8s8,3.58,8,8C20,13.95,19.3,15.73,18.14,17.12z"/><path d="M12,6c-1.93,0-3.5,1.57-3.5,3.5S10.07,13,12,13s3.5-1.57,3.5-3.5S13.93,6,12,6z M12,11c-0.83,0-1.5-0.67-1.5-1.5 S11.17,8,12,8s1.5,0.67,1.5,1.5S12.83,11,12,11z"/></g></g></svg>

До

Ширина:  |  Высота:  |  Размер: 365 B

После

Ширина:  |  Высота:  |  Размер: 698 B

Просмотреть файл

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><rect fill="none" height="24" width="24"/></g><g><g><path d="M17,11c0.34,0,0.67,0.04,1,0.09V6.27L10.5,3L3,6.27v4.91c0,4.54,3.2,8.79,7.5,9.82c0.55-0.13,1.08-0.32,1.6-0.55 C11.41,19.47,11,18.28,11,17C11,13.69,13.69,11,17,11z"/><path d="M17,13c-2.21,0-4,1.79-4,4c0,2.21,1.79,4,4,4s4-1.79,4-4C21,14.79,19.21,13,17,13z M17,14.38c0.62,0,1.12,0.51,1.12,1.12 s-0.51,1.12-1.12,1.12s-1.12-0.51-1.12-1.12S16.38,14.38,17,14.38z M17,19.75c-0.93,0-1.74-0.46-2.24-1.17 c0.05-0.72,1.51-1.08,2.24-1.08s2.19,0.36,2.24,1.08C18.74,19.29,17.93,19.75,17,19.75z"/></g></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><rect fill="none" height="24" width="24"/></g><g><g><circle cx="17" cy="15.5" fill-rule="evenodd" r="1.12"/><path d="M17,17.5c-0.73,0-2.19,0.36-2.24,1.08c0.5,0.71,1.32,1.17,2.24,1.17 s1.74-0.46,2.24-1.17C19.19,17.86,17.73,17.5,17,17.5z" fill-rule="evenodd"/><path d="M18,11.09V6.27L10.5,3L3,6.27v4.91c0,4.54,3.2,8.79,7.5,9.82 c0.55-0.13,1.08-0.32,1.6-0.55C13.18,21.99,14.97,23,17,23c3.31,0,6-2.69,6-6C23,14.03,20.84,11.57,18,11.09z M11,17 c0,0.56,0.08,1.11,0.23,1.62c-0.24,0.11-0.48,0.22-0.73,0.3c-3.17-1-5.5-4.24-5.5-7.74v-3.6l5.5-2.4l5.5,2.4v3.51 C13.16,11.57,11,14.03,11,17z M17,21c-2.21,0-4-1.79-4-4c0-2.21,1.79-4,4-4s4,1.79,4,4C21,19.21,19.21,21,17,21z" fill-rule="evenodd"/></g></g></svg>

До

Ширина:  |  Высота:  |  Размер: 675 B

После

Ширина:  |  Высота:  |  Размер: 814 B

Просмотреть файл

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path clip-rule="evenodd" d="M0 0h24v24H0z" fill="none"/><path d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9-2-2-5-2.4-7.4-1.3L9 6 6 9 1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M22.61 18.99l-9.08-9.08c.93-2.34.45-5.1-1.44-7C9.79.61 6.21.4 3.66 2.26L7.5 6.11 6.08 7.52 2.25 3.69C.39 6.23.6 9.82 2.9 12.11c1.86 1.86 4.57 2.35 6.89 1.48l9.11 9.11c.39.39 1.02.39 1.41 0l2.3-2.3c.4-.38.4-1.01 0-1.41zm-3 1.6l-9.46-9.46c-.61.45-1.29.72-2 .82-1.36.2-2.79-.21-3.83-1.25C3.37 9.76 2.93 8.5 3 7.26l3.09 3.09 4.24-4.24-3.09-3.09c1.24-.07 2.49.37 3.44 1.31 1.08 1.08 1.49 2.57 1.24 3.96-.12.71-.42 1.37-.88 1.96l9.45 9.45-.88.89z"/></svg>

До

Ширина:  |  Высота:  |  Размер: 326 B

После

Ширина:  |  Высота:  |  Размер: 580 B

1
ui/src/img/exclude.svg Normal file
Просмотреть файл

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><rect fill="none" height="24" width="24"/></g><g><path d="M14,10H3v2h11V10z M14,6H3v2h11V6z M3,16h7v-2H3V16z M14.41,22L17,19.41L19.59,22L21,20.59L18.41,18L21,15.41L19.59,14 L17,16.59L14.41,14L13,15.41L15.59,18L13,20.59L14.41,22z"/></g></svg>

После

Ширина:  |  Высота:  |  Размер: 361 B

Просмотреть файл

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 5.69l5 4.5V18h-2v-6H9v6H7v-7.81l5-4.5M12 3L2 12h3v8h6v-6h2v6h6v-8h3L12 3z"/></svg>

До

Ширина:  |  Высота:  |  Размер: 173 B

После

Ширина:  |  Высота:  |  Размер: 217 B

1
ui/src/img/rule.svg Normal file
Просмотреть файл

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><rect fill="none" height="24" width="24"/><path d="M16.54,11L13,7.46l1.41-1.41l2.12,2.12l4.24-4.24l1.41,1.41L16.54,11z M11,7H2v2h9V7z M21,13.41L19.59,12L17,14.59 L14.41,12L13,13.41L15.59,16L13,18.59L14.41,20L17,17.41L19.59,20L21,18.59L18.41,16L21,13.41z M11,15H2v2h9V15z"/></g></svg>

После

Ширина:  |  Высота:  |  Размер: 403 B

Просмотреть файл

@ -35,24 +35,52 @@ export async function callMsGraphUsers(accessToken) {
.catch((error) => console.log(error));
}
export async function callMsGraphUsersFilter(accessToken, search) {
const headers = new Headers();
const bearer = `Bearer ${accessToken}`;
export async function callMsGraphUsersFilter(accessToken, nameFilter = "") {
const headers = new Headers();
const bearer = `Bearer ${accessToken}`;
headers.append("Authorization", bearer);
var endpoint = graphConfig.graphUsersEndpoint + "?";
const options = {
method: "GET",
headers: headers,
};
headers.append("Authorization", bearer);
headers.append("ConsistencyLevel", "eventual");
let filter = `?$filter=startsWith(userPrincipalName,'${search}') OR startsWith(displayName, '${search}')`
const options = {
method: "GET",
headers: headers,
};
return fetch((graphConfig.graphUsersEndpoint + filter), options)
.then((response) => response.json())
.catch((error) => console.log(error));
if (nameFilter != "") {
endpoint += `$filter=startsWith(userPrincipalName,'${nameFilter}') OR startsWith(displayName, '${nameFilter}')&`;
}
endpoint += "$orderby=displayName&$count=true";
return fetch(endpoint, options)
.then((response) => response.json())
.catch((error) => console.log(error));
}
// export async function callMsGraphUsersFilter(accessToken, search) {
// const headers = new Headers();
// const bearer = `Bearer ${accessToken}`;
// headers.append("Authorization", bearer);
// headers.append("ConsistencyLevel", "eventual");
// const options = {
// method: "GET",
// headers: headers,
// };
// let filter = `?$filter=startsWith(userPrincipalName,'${search}') OR startsWith(displayName, '${search}')`
// let sort = "&$orderby=displayName&$count=true";
// return fetch((graphConfig.graphUsersEndpoint + filter + sort), options)
// .then((response) => response.json())
// .catch((error) => console.log(error));
// }
export async function callMsGraphPhoto(accessToken) {
const headers = new Headers();
const bearer = `Bearer ${accessToken}`;