Added support for multi-tenancy and running IPAM Engine as an Azure Function
|
@ -0,0 +1 @@
|
|||
local.settings.json
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
- **[<str>]**: 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']
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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 |
|
@ -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 |
|
@ -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}`;
|
||||
|
|