Merge branch 'multi-tenant' of github-msft:Azure/ipam into multi-tenant
|
@ -64,6 +64,13 @@ param(
|
|||
[switch]
|
||||
$NoConsent,
|
||||
|
||||
[Parameter(Mandatory = $false,
|
||||
ParameterSetName = 'Full')]
|
||||
[Parameter(Mandatory = $false,
|
||||
ParameterSetName = 'TemplateOnly')]
|
||||
[switch]
|
||||
$AsFunction,
|
||||
|
||||
[Parameter(Mandatory = $false,
|
||||
ParameterSetName = 'Full')]
|
||||
[Parameter(Mandatory = $false,
|
||||
|
@ -193,7 +200,7 @@ Function Deploy-IPAMApplications {
|
|||
Value = "access_as_user"
|
||||
}
|
||||
)
|
||||
# PreAuthorizedApplication = @(
|
||||
# PreAuthorizedApplication = @( # Allow Azure PowerShell/CLI to obtain access tokens
|
||||
# @{
|
||||
# AppId = "1950a258-227b-4e31-a9cf-717495945fc2" # Azure PowerShell
|
||||
# DelegatedPermissionId = @( $engineApiGuid )
|
||||
|
@ -433,6 +440,8 @@ Function Deploy-Bicep {
|
|||
[Parameter(Mandatory=$false)]
|
||||
[string]$NamePrefix,
|
||||
[Parameter(Mandatory=$false)]
|
||||
[boolean]$AsFunction,
|
||||
[Parameter(Mandatory=$false)]
|
||||
[hashtable]$Tags
|
||||
)
|
||||
|
||||
|
@ -450,6 +459,10 @@ Function Deploy-Bicep {
|
|||
$deploymentParameters.Add('namePrefix', $NamePrefix)
|
||||
}
|
||||
|
||||
if($AsFunction) {
|
||||
$deploymentParameters.Add('deployAsFunc', $AsFunction)
|
||||
}
|
||||
|
||||
if($Tags) {
|
||||
$deploymentParameters.Add('tags', $Tags)
|
||||
}
|
||||
|
@ -535,6 +548,7 @@ try {
|
|||
if ($PSCmdlet.ParameterSetName -in ('Full', 'TemplateOnly')) {
|
||||
$deployment = Deploy-Bicep @appDetails `
|
||||
-NamePrefix $NamePrefix `
|
||||
-AsFunction $AsFunction `
|
||||
-Tags $Tags
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
@description('Function App Name')
|
||||
param functionAppName string
|
||||
|
||||
@description('Function App Plan Name')
|
||||
param functionAppPlanName string
|
||||
|
||||
@description('CosmosDB URI')
|
||||
param cosmosDbUri string
|
||||
|
||||
@description('KeyVault URI')
|
||||
param keyVaultUri string
|
||||
|
||||
@description('Deployment Location')
|
||||
param location string = resourceGroup().location
|
||||
|
||||
@description('Managed Identity Id')
|
||||
param managedIdentityId string
|
||||
|
||||
@description('Storage Account Name')
|
||||
param storageAccountName string
|
||||
|
||||
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-06-01' existing = {
|
||||
name: storageAccountName
|
||||
}
|
||||
|
||||
resource functionAppPlan 'Microsoft.Web/serverfarms@2021-02-01' = {
|
||||
name: functionAppPlanName
|
||||
location: location
|
||||
sku: {
|
||||
name: 'EP1'
|
||||
size: 'EP1'
|
||||
tier: 'ElasticPremium'
|
||||
capacity: 1
|
||||
}
|
||||
kind: 'linux'
|
||||
properties: {
|
||||
reserved: true
|
||||
}
|
||||
}
|
||||
|
||||
resource functionApp 'Microsoft.Web/sites@2021-03-01' = {
|
||||
name: functionAppName
|
||||
location: location
|
||||
kind: 'functionapp,linux,container'
|
||||
identity: {
|
||||
type: 'UserAssigned'
|
||||
userAssignedIdentities: {
|
||||
'${managedIdentityId}': {}
|
||||
}
|
||||
}
|
||||
properties: {
|
||||
httpsOnly: true
|
||||
serverFarmId: functionAppPlan.id
|
||||
keyVaultReferenceIdentity: managedIdentityId
|
||||
siteConfig: {
|
||||
alwaysOn: true
|
||||
linuxFxVersion: 'azureipam.azurecr.io/ipam-func:latest'
|
||||
appSettings: [
|
||||
{
|
||||
name: 'COSMOS_URL'
|
||||
value: cosmosDbUri
|
||||
}
|
||||
{
|
||||
name: 'COSMOS_KEY'
|
||||
value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/COSMOS-KEY/)'
|
||||
}
|
||||
{
|
||||
name: 'CLIENT_ID'
|
||||
value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/ENGINE-ID/)'
|
||||
}
|
||||
{
|
||||
name: 'CLIENT_SECRET'
|
||||
value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/ENGINE-SECRET/)'
|
||||
}
|
||||
{
|
||||
name: 'TENANT_ID'
|
||||
value: '@Microsoft.KeyVault(SecretUri=${keyVaultUri}secrets/TENANT-ID/)'
|
||||
}
|
||||
{
|
||||
name: 'KEYVAULT_URL'
|
||||
value: keyVaultUri
|
||||
}
|
||||
{
|
||||
name: 'AzureWebJobsStorage'
|
||||
value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
|
||||
}
|
||||
{
|
||||
name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
|
||||
value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}'
|
||||
}
|
||||
{
|
||||
name: 'WEBSITE_CONTENTSHARE'
|
||||
value: toLower(functionAppName)
|
||||
}
|
||||
{
|
||||
name: 'FUNCTIONS_EXTENSION_VERSION'
|
||||
value: '~4'
|
||||
}
|
||||
{
|
||||
name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
|
||||
value: applicationInsights.properties.InstrumentationKey
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = {
|
||||
name: functionAppName
|
||||
location: location
|
||||
kind: 'web'
|
||||
properties: {
|
||||
Application_Type: 'web'
|
||||
Request_Source: 'rest'
|
||||
}
|
||||
}
|
||||
|
||||
output functionAppHostName string = functionApp.properties.defaultHostName
|
|
@ -10,6 +10,9 @@ param location string = deployment().location
|
|||
@description('Prefix for Resource Naming')
|
||||
param namePrefix string = 'ipam'
|
||||
|
||||
@description('Flag to Deploy IPAM as a Function')
|
||||
param deployAsFunc bool = false
|
||||
|
||||
@description('IPAM-UI App Registration Client/App ID')
|
||||
param uiAppId string
|
||||
|
||||
|
@ -51,7 +54,7 @@ module managedIdentity 'managedIdentity.bicep' = {
|
|||
}
|
||||
|
||||
// KeyVault for Secure Values
|
||||
module keyVault 'keyVault.bicep' ={
|
||||
module keyVault 'keyVault.bicep' = {
|
||||
name: 'keyVaultModule'
|
||||
scope: resourceGroup
|
||||
params: {
|
||||
|
@ -84,11 +87,12 @@ module storageAccount 'storageAccount.bicep' = {
|
|||
storageAccountName: storageName
|
||||
principalId: managedIdentity.outputs.principalId
|
||||
managedIdentityId: managedIdentity.outputs.id
|
||||
deployAsFunc: deployAsFunc
|
||||
}
|
||||
}
|
||||
|
||||
// App Service w/ Docker Compose + CI
|
||||
module appService 'appService.bicep' = {
|
||||
module appService 'appService.bicep' = if (!deployAsFunc) {
|
||||
scope: resourceGroup
|
||||
name: 'appServiceModule'
|
||||
params: {
|
||||
|
@ -103,5 +107,20 @@ module appService 'appService.bicep' = {
|
|||
}
|
||||
}
|
||||
|
||||
// Function App
|
||||
module functionApp 'functionApp.bicep' = if (deployAsFunc) {
|
||||
scope: resourceGroup
|
||||
name: 'functionAppModule'
|
||||
params: {
|
||||
location: location
|
||||
functionAppPlanName: appServicePlanName
|
||||
functionAppName: appServiceName
|
||||
keyVaultUri: keyVault.outputs.keyVaultUri
|
||||
cosmosDbUri: cosmos.outputs.cosmosDocumentEndpoint
|
||||
managedIdentityId: managedIdentity.outputs.id
|
||||
storageAccountName: storageAccount.outputs.name
|
||||
}
|
||||
}
|
||||
|
||||
// Outputs
|
||||
output appServiceHostName string = appService.outputs.appServiceHostName
|
||||
|
|
|
@ -16,6 +16,9 @@ param roleAssignmentName string = newGuid()
|
|||
@description('Storage Account Name')
|
||||
param storageAccountName string
|
||||
|
||||
@description('Flag to Deploy IPAM as a Function')
|
||||
param deployAsFunc bool
|
||||
|
||||
var storageBlobDataContributor = 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'
|
||||
var storageBlobDataContributorId = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', storageBlobDataContributor)
|
||||
|
||||
|
@ -32,11 +35,11 @@ resource storageAccount 'Microsoft.Storage/storageAccounts@2021-06-01' = {
|
|||
}
|
||||
}
|
||||
|
||||
resource blobContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2021-06-01' = {
|
||||
resource blobContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2021-06-01' = if (!deployAsFunc) {
|
||||
name: '${storageAccount.name}/default/${containerName}'
|
||||
}
|
||||
|
||||
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
|
||||
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = if (!deployAsFunc) {
|
||||
name: roleAssignmentName
|
||||
scope: blobContainer
|
||||
properties: {
|
||||
|
@ -46,7 +49,7 @@ resource roleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-prev
|
|||
}
|
||||
}
|
||||
|
||||
resource copyNginxConfig 'Microsoft.Resources/deploymentScripts@2020-10-01' = {
|
||||
resource copyNginxConfig 'Microsoft.Resources/deploymentScripts@2020-10-01' = if (!deployAsFunc) {
|
||||
name: 'copyNginxConfig'
|
||||
location: location
|
||||
kind: 'AzurePowerShell'
|
||||
|
|
|
@ -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-appservice
|
||||
|
||||
ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
|
||||
AzureFunctionsJobHost__Logging__Console__IsEnabled=true
|
||||
|
||||
COPY requirements.txt /
|
||||
RUN pip install -r /requirements.txt
|
||||
|
||||
COPY . /home/site/wwwroot
|
|
@ -1,9 +1,8 @@
|
|||
from fastapi import Request, HTTPException
|
||||
|
||||
from azure.cosmos.aio import CosmosClient
|
||||
|
||||
import jwt
|
||||
import time
|
||||
import copy
|
||||
|
||||
from app.routers.common.helper import (
|
||||
cosmos_query
|
||||
|
@ -26,13 +25,20 @@ async def check_token_expired(request: Request):
|
|||
if(now >= int(decoded['exp'])):
|
||||
raise HTTPException(status_code=401, detail="Token has expired.")
|
||||
|
||||
await check_admin(request, decoded['oid'])
|
||||
request.state.tenant_id = decoded['tid']
|
||||
|
||||
async def check_admin(request: Request, user_oid: str):
|
||||
item = await cosmos_query("admins")
|
||||
await check_admin(request, decoded['oid'], decoded['tid'])
|
||||
|
||||
if item['admins']:
|
||||
is_admin = next((x for x in item['admins'] if user_oid == x['id']), None)
|
||||
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 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
|
||||
|
||||
|
@ -40,3 +46,6 @@ async def check_admin(request: Request, user_oid: str):
|
|||
|
||||
async def get_admin(request: Request):
|
||||
return request.state.admin
|
||||
|
||||
async def get_tenant_id(request: Request):
|
||||
return request.state.tenant_id
|
||||
|
|
|
@ -1,6 +1,40 @@
|
|||
CLIENT_ID=""
|
||||
CLIENT_SECRET=""
|
||||
TENANT_ID=""
|
||||
COSMOS_URL=""
|
||||
COSMOS_KEY=""
|
||||
KEYVAULT_URL=""
|
||||
import os
|
||||
|
||||
class Globals:
|
||||
@property
|
||||
def CLIENT_ID(self):
|
||||
return os.environ.get('CLIENT_ID')
|
||||
|
||||
@property
|
||||
def CLIENT_SECRET(self):
|
||||
return os.environ.get('CLIENT_SECRET')
|
||||
|
||||
@property
|
||||
def TENANT_ID(self):
|
||||
return os.environ.get('TENANT_ID')
|
||||
|
||||
@property
|
||||
def COSMOS_URL(self):
|
||||
return os.environ.get('COSMOS_URL')
|
||||
|
||||
@property
|
||||
def COSMOS_KEY(self):
|
||||
return os.environ.get('COSMOS_KEY')
|
||||
|
||||
@property
|
||||
def KEYVAULT_URL(self):
|
||||
return os.environ.get('KEYVAULT_URL')
|
||||
|
||||
@property
|
||||
def DATABASE_NAME(self):
|
||||
db_name = os.environ.get('DATABASE_NAME')
|
||||
|
||||
return db_name if db_name else 'ipam-db'
|
||||
|
||||
@property
|
||||
def CONTAINER_NAME(self):
|
||||
ctr_name = os.environ.get('CONTAINER_NAME')
|
||||
|
||||
return ctr_name if ctr_name else 'ipam-ctr'
|
||||
|
||||
globals = Globals()
|
||||
|
|
|
@ -1,22 +1,27 @@
|
|||
from audioop import tostereo
|
||||
from fastapi import FastAPI, Request, HTTPException, Header
|
||||
from fastapi.responses import JSONResponse, RedirectResponse, FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.exceptions import HTTPException as StarletteHTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi_restful.tasks import repeat_every
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
|
||||
from azure.cosmos.aio import CosmosClient
|
||||
from azure.cosmos import PartitionKey
|
||||
from azure.cosmos.exceptions import CosmosResourceExistsError
|
||||
from azure.cosmos.exceptions import CosmosResourceExistsError, CosmosResourceNotFoundError
|
||||
|
||||
from app.routers import azure, admin, user, space
|
||||
|
||||
import os
|
||||
import uuid
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import app.globals as globals
|
||||
from app.globals import globals
|
||||
|
||||
from app.routers.common.helper import (
|
||||
cosmos_upsert
|
||||
)
|
||||
|
||||
BUILD_DIR = os.path.join(os.getcwd(), "app", "build")
|
||||
|
||||
|
@ -119,18 +124,85 @@ if os.path.isdir(BUILD_DIR):
|
|||
|
||||
return FileResponse(BUILD_DIR + "/index.html")
|
||||
|
||||
async def db_upgrade():
|
||||
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:
|
||||
spaces_query = await container.read_item("spaces", partition_key="spaces")
|
||||
|
||||
for space in spaces_query['spaces']:
|
||||
if 'vnets' in space:
|
||||
del space['vnets']
|
||||
|
||||
new_space = {
|
||||
"id": uuid.uuid4(),
|
||||
"type": "space",
|
||||
"tenant_id": globals.TENANT_ID,
|
||||
**space
|
||||
}
|
||||
|
||||
await cosmos_upsert(jsonable_encoder(new_space))
|
||||
|
||||
await container.delete_item("spaces", partition_key = "spaces")
|
||||
|
||||
logger.info('Spaces database conversion complete!')
|
||||
except CosmosResourceNotFoundError:
|
||||
logger.info('No existing spaces to convert...')
|
||||
pass
|
||||
|
||||
try:
|
||||
users_query = await container.read_item("users", partition_key="users")
|
||||
|
||||
for user in users_query['users']:
|
||||
new_user = {
|
||||
"id": uuid.uuid4(),
|
||||
"type": "user",
|
||||
"tenant_id": globals.TENANT_ID,
|
||||
"data": user
|
||||
}
|
||||
|
||||
await cosmos_upsert(jsonable_encoder(new_user))
|
||||
|
||||
await container.delete_item("users", partition_key = "users")
|
||||
|
||||
logger.info('Users database conversion complete!')
|
||||
except CosmosResourceNotFoundError:
|
||||
logger.info('No existing users to convert...')
|
||||
pass
|
||||
|
||||
try:
|
||||
admins_query = await container.read_item("admins", partition_key="admins")
|
||||
|
||||
admin_data = {
|
||||
"id": uuid.uuid4(),
|
||||
"type": "admin",
|
||||
"tenant_id": globals.TENANT_ID,
|
||||
"admins": admins_query['admins'],
|
||||
"exclusions": []
|
||||
}
|
||||
|
||||
await cosmos_upsert(jsonable_encoder(admin_data))
|
||||
|
||||
await container.delete_item("admins", partition_key = "admins")
|
||||
|
||||
logger.info('Admins database conversion complete!')
|
||||
except CosmosResourceNotFoundError:
|
||||
logger.info('No existing admins to convert...')
|
||||
pass
|
||||
|
||||
await cosmos_client.close()
|
||||
|
||||
@app.on_event("startup")
|
||||
async def set_globals():
|
||||
globals.CLIENT_ID = os.environ.get('CLIENT_ID')
|
||||
globals.CLIENT_SECRET = os.environ.get('CLIENT_SECRET')
|
||||
globals.TENANT_ID = os.environ.get('TENANT_ID')
|
||||
globals.COSMOS_URL = os.environ.get('COSMOS_URL')
|
||||
globals.COSMOS_KEY = os.environ.get('COSMOS_KEY')
|
||||
globals.KEYVAULT_URL = os.environ.get('KEYVAULT_URL')
|
||||
|
||||
client = CosmosClient(globals.COSMOS_URL, credential=globals.COSMOS_KEY)
|
||||
|
||||
database_name = 'ipam-db'
|
||||
database_name = globals.DATABASE_NAME
|
||||
|
||||
try:
|
||||
logger.info('Creating Database...')
|
||||
|
@ -141,37 +213,13 @@ async def set_globals():
|
|||
logger.warning('Database exists! Using existing database...')
|
||||
database = client.get_database_client(database_name)
|
||||
|
||||
container_name = 'ipam-container'
|
||||
container_name = globals.CONTAINER_NAME
|
||||
|
||||
try:
|
||||
logger.info('Creating Container...')
|
||||
container = await database.create_container(
|
||||
id = container_name,
|
||||
partition_key = PartitionKey(path = "/id")
|
||||
)
|
||||
|
||||
logger.info('Creating Spaces Item...')
|
||||
await container.upsert_item(
|
||||
{
|
||||
'id': 'spaces',
|
||||
'spaces': []
|
||||
}
|
||||
)
|
||||
|
||||
logger.info('Creating Admins Item...')
|
||||
await container.upsert_item(
|
||||
{
|
||||
'id': 'admins',
|
||||
'admins': []
|
||||
}
|
||||
)
|
||||
|
||||
logger.info('Creating Users Item...')
|
||||
await container.upsert_item(
|
||||
{
|
||||
'id': 'users',
|
||||
'users': []
|
||||
}
|
||||
partition_key = PartitionKey(path = "/tenant_id")
|
||||
)
|
||||
except CosmosResourceExistsError:
|
||||
logger.warning('Container exists! Using existing container...')
|
||||
|
@ -179,11 +227,14 @@ async def set_globals():
|
|||
|
||||
await client.close()
|
||||
|
||||
await db_upgrade()
|
||||
|
||||
# https://github.com/yuval9313/FastApi-RESTful/issues/138
|
||||
@app.on_event("startup")
|
||||
@repeat_every(seconds = 60, wait_first = True) # , wait_first=True
|
||||
async def find_reservations() -> None:
|
||||
await azure.match_resv_to_vnets()
|
||||
if not os.environ.get("FUNCTIONS_WORKER_RUNTIME"):
|
||||
await azure.match_resv_to_vnets()
|
||||
|
||||
@app.exception_handler(StarletteHTTPException)
|
||||
async def http_exception_handler(request, exc):
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
from pydantic import BaseModel, ValidationError, EmailStr
|
||||
from pydantic import BaseModel, ValidationError, EmailStr, root_validator
|
||||
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 #
|
||||
######################
|
||||
|
@ -231,6 +166,13 @@ class Reservation(BaseModel):
|
|||
userId: str
|
||||
createdOn: float
|
||||
status: str
|
||||
tag: Optional[dict]
|
||||
|
||||
@root_validator
|
||||
def format_tag(cls, values) -> dict:
|
||||
values["tag"] = { "X-IPAM-RES-ID": values["id"]}
|
||||
|
||||
return values
|
||||
|
||||
class BlockBasic(BaseModel):
|
||||
"""DOCSTRING"""
|
||||
|
@ -333,3 +275,51 @@ class SpaceExpandUtil(BaseModel):
|
|||
blocks: List[BlockExpandUtil]
|
||||
size: int
|
||||
used: int
|
||||
|
||||
####################
|
||||
# ADMIN MODELS #
|
||||
####################
|
||||
|
||||
class Admin(BaseModel):
|
||||
"""DOCSTRING"""
|
||||
|
||||
name: str
|
||||
email: EmailStr
|
||||
id: UUID
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
UUID: lambda v: str(v),
|
||||
}
|
||||
|
||||
class Subscription(UUID):
|
||||
"""DOCSTRING"""
|
||||
|
||||
class Exclusions(List[UUID]):
|
||||
"""DOCSTRING"""
|
||||
|
||||
###################
|
||||
# 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,41 +3,59 @@ 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 . import argquery
|
||||
|
||||
from app.routers.common.helper import (
|
||||
cosmos_query,
|
||||
cosmos_upsert
|
||||
cosmos_upsert,
|
||||
cosmos_replace,
|
||||
cosmos_delete,
|
||||
cosmos_retry,
|
||||
arg_query
|
||||
)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/admins",
|
||||
tags=["admins"],
|
||||
prefix="/admin",
|
||||
tags=["admin"],
|
||||
dependencies=[Depends(check_token_expired)]
|
||||
)
|
||||
|
||||
class Admin(BaseModel):
|
||||
name: str
|
||||
email: EmailStr
|
||||
id: UUID
|
||||
async def new_admin_db(admin_list, exclusion_list, tenant_id):
|
||||
admin_data = {
|
||||
"id": uuid.uuid4(),
|
||||
"type": "admin",
|
||||
"tenant_id": tenant_id,
|
||||
"admins": admin_list,
|
||||
"exclusions": exclusion_list
|
||||
}
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
UUID: lambda v: str(v),
|
||||
}
|
||||
query_results = await cosmos_upsert(jsonable_encoder(admin_data))
|
||||
|
||||
return query_results
|
||||
|
||||
@router.get(
|
||||
"",
|
||||
summary = "Get All Admins"
|
||||
"/admins",
|
||||
summary = "Get All Admins",
|
||||
response_model = List[Admin],
|
||||
status_code = 200
|
||||
)
|
||||
async def get_admins(
|
||||
tenant_id: str = Depends(get_tenant_id),
|
||||
is_admin: str = Depends(get_admin)
|
||||
):
|
||||
"""
|
||||
|
@ -47,17 +65,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(
|
||||
"",
|
||||
"/admins",
|
||||
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,84 +96,39 @@ 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:
|
||||
await new_admin_db([admin], [], tenant_id)
|
||||
else:
|
||||
admin_data = copy.deepcopy(admin_query[0])
|
||||
|
||||
if target_admin:
|
||||
raise HTTPException(status_code=400, detail="User is already an admin.")
|
||||
target_admin = next((x for x in admin_data['admins'] if x['id'] == admin.id), None)
|
||||
|
||||
item['admins'].append(jsonable_encoder(admin))
|
||||
if target_admin:
|
||||
raise HTTPException(status_code=400, detail="User is already an admin.")
|
||||
|
||||
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
|
||||
admin_data['admins'].append(jsonable_encoder(admin))
|
||||
|
||||
await cosmos_replace(admin_query[0], admin_data)
|
||||
|
||||
return Response(status_code=status.HTTP_201_CREATED)
|
||||
|
||||
@router.delete(
|
||||
"/{objectId}",
|
||||
summary = "Delete IPAM Admin",
|
||||
status_code=200
|
||||
)
|
||||
async def delete_admin(
|
||||
objectId: UUID,
|
||||
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_index = next((i for i, admin in enumerate(item['admins']) if admin['id'] == str(objectId)), None)
|
||||
|
||||
if not admin_index:
|
||||
raise HTTPException(status_code=400, detail="Invalid admin objectId.")
|
||||
|
||||
del item['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
|
||||
|
||||
return Response(status_code=status.HTTP_200_OK)
|
||||
|
||||
@router.put(
|
||||
"",
|
||||
"/admins",
|
||||
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 +140,213 @@ 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.")
|
||||
|
||||
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.")
|
||||
|
||||
admin_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'admin'", tenant_id)
|
||||
|
||||
if not admin_query:
|
||||
await new_admin_db(admin_list, [], tenant_id)
|
||||
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)
|
||||
|
||||
@router.delete(
|
||||
"/admins/{objectId}",
|
||||
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
|
||||
"""
|
||||
|
||||
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)
|
||||
|
||||
id_list = [x.id for x in admin_list]
|
||||
unique_admins = len(set(id_list)) == len(admin_list)
|
||||
if admin_query is None:
|
||||
raise HTTPException(status_code=400, detail="Admin not found.")
|
||||
|
||||
if not unique_admins:
|
||||
raise HTTPException(status_code=400, detail="List contains one or more duplicate objectId's.")
|
||||
admin_data = copy.deepcopy(admin_query[0])
|
||||
|
||||
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
|
||||
admin_index = next((i for i, admin in enumerate(admin_data['admins']) if admin['id'] == str(objectId)), None)
|
||||
|
||||
return PlainTextResponse(status_code=status.HTTP_200_OK)
|
||||
if admin_index is None:
|
||||
raise HTTPException(status_code=400, detail="Invalid admin objectId.")
|
||||
|
||||
del admin_data['admins'][admin_index]
|
||||
|
||||
await cosmos_replace(admin_query[0], admin_data)
|
||||
|
||||
return Response(status_code=status.HTTP_200_OK)
|
||||
|
||||
@router.get(
|
||||
"/exclusions",
|
||||
summary = "Get Excluded Subscriptions",
|
||||
response_model = List[Subscription],
|
||||
status_code = 200
|
||||
)
|
||||
async def get_exclusions(
|
||||
tenant_id: str = Depends(get_tenant_id),
|
||||
is_admin: str = Depends(get_admin)
|
||||
):
|
||||
"""
|
||||
Get a list of excluded subscriptions.
|
||||
"""
|
||||
|
||||
if not is_admin:
|
||||
raise HTTPException(status_code=403, detail="API restricted to admins.")
|
||||
|
||||
admin_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'admin'", tenant_id)
|
||||
|
||||
if admin_query:
|
||||
admin_data = copy.deepcopy(admin_query[0])
|
||||
|
||||
return admin_data['exclusions']
|
||||
else:
|
||||
return []
|
||||
|
||||
@router.post(
|
||||
"/exclusions",
|
||||
summary = "Add Excluded Subscription(s)",
|
||||
status_code=200
|
||||
)
|
||||
@cosmos_retry(
|
||||
max_retry = 5,
|
||||
error_msg = "Error adding exclusion(s), please try again."
|
||||
)
|
||||
async def add_exclusions(
|
||||
exclusions: List[Subscription],
|
||||
tenant_id: str = Depends(get_tenant_id),
|
||||
is_admin: str = Depends(get_admin)
|
||||
):
|
||||
"""
|
||||
Add a list of excluded subscriptions:
|
||||
|
||||
- **[<UUID>]**: Array of Subscription ID's
|
||||
"""
|
||||
|
||||
if not is_admin:
|
||||
raise HTTPException(status_code=403, detail="API restricted to admins.")
|
||||
|
||||
subscription_list = await arg_query(None, True, argquery.SUBSCRIPTION)
|
||||
invalid_subscriptions = [str(x) for x in exclusions if str(x) not in [y['subscription_id'] for y in subscription_list]]
|
||||
|
||||
if invalid_subscriptions:
|
||||
print(invalid_subscriptions)
|
||||
raise HTTPException(status_code=400, detail="One or more invalid subscriptions id's provided.")
|
||||
|
||||
admin_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'admin'", tenant_id)
|
||||
|
||||
if not admin_query:
|
||||
await new_admin_db([], list(set(exclusions)), tenant_id)
|
||||
else:
|
||||
admin_data = copy.deepcopy(admin_query[0])
|
||||
|
||||
admin_data['exclusions'] = jsonable_encoder(list(set(admin_data['exclusions'] + exclusions)))
|
||||
|
||||
await cosmos_replace(admin_query[0], admin_data)
|
||||
|
||||
return Response(status_code=status.HTTP_200_OK)
|
||||
|
||||
@router.put(
|
||||
"/exclusions",
|
||||
summary = "Replace Excluded Subscriptions",
|
||||
status_code=200
|
||||
)
|
||||
@cosmos_retry(
|
||||
max_retry = 5,
|
||||
error_msg = "Error updating exclusions, please try again."
|
||||
)
|
||||
async def update_exclusions(
|
||||
exclusions: List[Subscription],
|
||||
tenant_id: str = Depends(get_tenant_id),
|
||||
is_admin: str = Depends(get_admin)
|
||||
):
|
||||
"""
|
||||
Replace the list of excluded subscriptions:
|
||||
|
||||
- **[<UUID>]**: Array of Subscription ID's
|
||||
"""
|
||||
|
||||
if not is_admin:
|
||||
raise HTTPException(status_code=403, detail="API restricted to admins.")
|
||||
|
||||
subscription_list = await arg_query(None, True, argquery.SUBSCRIPTION)
|
||||
invalid_subscriptions = [str(x) for x in exclusions if str(x) not in [y['subscription_id'] for y in subscription_list]]
|
||||
|
||||
if invalid_subscriptions:
|
||||
raise HTTPException(status_code=400, detail="One or more invalid subscriptions id's provided.")
|
||||
|
||||
admin_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'admin'", tenant_id)
|
||||
|
||||
if not admin_query:
|
||||
await new_admin_db([], list(set(exclusions)), tenant_id)
|
||||
else:
|
||||
admin_data = copy.deepcopy(admin_query[0])
|
||||
|
||||
admin_data['exclusions'] = jsonable_encoder(list(set(exclusions)))
|
||||
|
||||
await cosmos_replace(admin_query[0], admin_data)
|
||||
|
||||
return Response(status_code=status.HTTP_200_OK)
|
||||
|
||||
@router.delete(
|
||||
"/exclusions/{subscriptionId}",
|
||||
summary = "Remove Excluded Subscription",
|
||||
status_code=200
|
||||
)
|
||||
@cosmos_retry(
|
||||
max_retry = 5,
|
||||
error_msg = "Error removing exclusion, please try again."
|
||||
)
|
||||
async def remove_exclusion(
|
||||
subscriptionId: Subscription,
|
||||
tenant_id: str = Depends(get_tenant_id),
|
||||
is_admin: str = Depends(get_admin)
|
||||
):
|
||||
"""
|
||||
Remove an excluded subscription id
|
||||
"""
|
||||
|
||||
if not is_admin:
|
||||
raise HTTPException(status_code=403, detail="API restricted to admins.")
|
||||
|
||||
admin_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'admin'", tenant_id)
|
||||
|
||||
if not admin_query:
|
||||
raise HTTPException(status_code=400, detail="Subscription id not found.")
|
||||
|
||||
admin_data = copy.deepcopy(admin_query[0])
|
||||
|
||||
exclusion_index = next((i for i, exclusion in enumerate(admin_data['exclusions']) if exclusion == str(subscriptionId)), None)
|
||||
|
||||
if exclusion_index is None:
|
||||
raise HTTPException(status_code=400, detail="Invalid subscription id.")
|
||||
|
||||
del admin_data['exclusions'][exclusion_index]
|
||||
|
||||
await cosmos_replace(admin_query[0], admin_data)
|
||||
|
||||
return Response(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 = """
|
||||
|
@ -41,7 +47,8 @@ resources
|
|||
VNET="""
|
||||
resources
|
||||
| where type =~ 'Microsoft.Network/virtualNetworks'
|
||||
| project name, id, resource_group = resourceGroup, subscription_id = subscriptionId, tenant_id = tenantId, prefixes = properties.addressSpace.addressPrefixes, resv = tostring(tags["ipam-res-id"])
|
||||
| where subscriptionId !in~ {}
|
||||
| project name, id, resource_group = resourceGroup, subscription_id = subscriptionId, tenant_id = tenantId, prefixes = properties.addressSpace.addressPrefixes, resv = tostring(coalesce(tags['X-IPAM-RES-ID'], tags['ipam-res-id']))
|
||||
| join kind = leftouter(
|
||||
resources
|
||||
| where type =~ 'Microsoft.Network/virtualNetworks'
|
||||
|
@ -57,14 +64,16 @@ resources
|
|||
SUBNET = """
|
||||
resources
|
||||
| where type =~ 'Microsoft.Network/virtualNetworks'
|
||||
| where subscriptionId !in~ {}
|
||||
| mv-expand subnet = todynamic(properties.subnets)
|
||||
| extend subnet_size = array_length(subnet.properties.ipConfigurations)
|
||||
| project name = subnet.name, id = subnet.id, prefix = subnet.properties.addressPrefix,resource_group = resourceGroup, subscription_id = subscriptionId, tenant_id = tenantId,vnet_name = name, vnet_id = id, used = (iif(isnull(subnet_size), 0, subnet_size) + 5), appgw_config = subnet.properties.applicationGatewayIPConfigurations
|
||||
| project name = subnet.name, id = subnet.id, prefix = subnet.properties.addressPrefix, resource_group = resourceGroup, subscription_id = subscriptionId, tenant_id = tenantId,vnet_name = name, vnet_id = id, used = (iif(isnull(subnet_size), 0, subnet_size) + 5), appgw_config = subnet.properties.applicationGatewayIPConfigurations
|
||||
"""
|
||||
|
||||
PRIVATE_ENDPOINT = """
|
||||
resources
|
||||
| where type =~ 'microsoft.network/networkinterfaces'
|
||||
| where subscriptionId !in~ {}
|
||||
| where isnotempty(properties.privateEndpoint)
|
||||
| mv-expand ipconfig = properties.ipConfigurations
|
||||
| project pe_id = tostring(properties.privateEndpoint.id), subnet_id = tolower(tostring(ipconfig.properties.subnet.id)), group_id = ipconfig.properties.privateLinkConnectionProperties.groupId, private_ip = ipconfig.properties.privateIPAddress
|
||||
|
@ -89,6 +98,7 @@ resources
|
|||
VIRTUAL_MACHINE = """
|
||||
resources
|
||||
| where type =~ 'microsoft.compute/virtualmachines'
|
||||
| where subscriptionId !in~ {}
|
||||
| extend nics = array_length(properties.networkProfile.networkInterfaces)
|
||||
| mv-expand nic = properties.networkProfile.networkInterfaces
|
||||
| project tenant_id = tenantId, id = id, name = name, size = properties.hardwareProfile.vmSize, resource_group = resourceGroup, subscription_id = subscriptionId, nic_id = nic.id
|
||||
|
@ -122,6 +132,7 @@ resources
|
|||
VM_SCALE_SET = """
|
||||
ComputeResources
|
||||
| where type =~ "microsoft.compute/virtualmachinescalesets/virtualmachines"
|
||||
| where subscriptionId !in~ {}
|
||||
| project name, tostring(id), resource_group = resourceGroup, subscription_id = subscriptionId, tenant_id = tenantId
|
||||
| join kind = leftouter (
|
||||
ComputeResources
|
||||
|
@ -146,6 +157,7 @@ ComputeResources
|
|||
FIREWALL_VNET = """
|
||||
resources
|
||||
| where type =~ 'Microsoft.Network/azureFirewalls'
|
||||
| where subscriptionId !in~ {}
|
||||
| where properties.sku.name =~ 'AZFW_VNet'
|
||||
| extend ipConfigs = array_length(properties.ipConfigurations)
|
||||
| mv-expand ipConfig = properties.ipConfigurations
|
||||
|
@ -173,6 +185,7 @@ resources
|
|||
FIREWALL_VHUB = """
|
||||
resources
|
||||
| where type =~ 'Microsoft.Network/azureFirewalls'
|
||||
| where subscriptionId !in~ {}
|
||||
| where properties.sku.name =~ 'AZFW_Hub'
|
||||
| project name = name, id = id, size = properties.sku.tier, resource_group = resourceGroup, subscription_id = subscriptionId, private_ip_address = properties.hubIPAddresses.privateIPAddress, virtual_hub_id = properties.virtualHub.id, tenant_id = tenantId
|
||||
| join kind = leftouter (
|
||||
|
@ -188,6 +201,7 @@ resources
|
|||
BASTION = """
|
||||
resources
|
||||
| where type =~ 'Microsoft.Network/bastionHosts'
|
||||
| where subscriptionId !in~ {}
|
||||
| extend ipConfigs = array_length(properties.ipConfigurations)
|
||||
| mv-expand ipConfig = properties.ipConfigurations
|
||||
| project tenant_id = tenantId, id = id, name = name, size = sku.name, resource_group = resourceGroup, subscription_id = subscriptionId, private_ip = dynamic(null), private_ip_alloc_method = ipConfig.properties.privateIPAllocationMethod, subnet_id = ipConfig.properties.subnet.id, public_ip_id = ipConfig.properties.publicIPAddress.id
|
||||
|
@ -214,17 +228,18 @@ resources
|
|||
APP_GATEWAY = """
|
||||
resources
|
||||
| where type =~ 'Microsoft.Network/applicationGateways'
|
||||
| where subscriptionId !in~ {}
|
||||
| mv-expand ipConfig = properties.frontendIPConfigurations
|
||||
| where isnotempty(ipConfig.properties.publicIPAddress)
|
||||
| project tenant_id = tenantId, name, id, size = properties.sku.tier, resource_group = resourceGroup, subscription_id = subscriptionId, public_ip_id = ipConfig.properties.publicIPAddress.id, public_ip_alloc_method = ipConfig.properties.privateIPAllocationMethod
|
||||
| extend public_ip_id = tostring(public_ip_id)
|
||||
| where isnotempty(ipConfig.properties.privateIPAddress)
|
||||
| project name, tenant_id = tenantId, id, size = properties.sku.tier, resource_group = resourceGroup, subscription_id = subscriptionId, private_ip = ipConfig.properties.privateIPAddress, private_ip_alloc_method = ipConfig.properties.privateIPAllocationMethod, subnet_id = ipConfig.properties.subnet.id
|
||||
| extend subnet_id = tolower(tostring(subnet_id))
|
||||
| join kind = leftouter (
|
||||
resources
|
||||
| where type =~ 'Microsoft.Network/applicationGateways'
|
||||
| mv-expand ipConfig = properties.frontendIPConfigurations
|
||||
| where isnotempty(ipConfig.properties.privateIPAddress)
|
||||
| project name, private_ip = ipConfig.properties.privateIPAddress, private_ip_alloc_method = ipConfig.properties.privateIPAllocationMethod, subnet_id = ipConfig.properties.subnet.id
|
||||
| extend subnet_id = tolower(tostring(subnet_id))
|
||||
| where isnotempty(ipConfig.properties.publicIPAddress)
|
||||
| project name, public_ip_id = ipConfig.properties.publicIPAddress.id, public_ip_alloc_method = ipConfig.properties.privateIPAllocationMethod
|
||||
| extend public_ip_id = tostring(public_ip_id)
|
||||
) on name
|
||||
| project-away name1
|
||||
| join kind = leftouter (
|
||||
|
@ -249,6 +264,7 @@ resources
|
|||
APIM = """
|
||||
resources
|
||||
| where type =~ 'Microsoft.ApiManagement/service'
|
||||
| where subscriptionId !in~ {}
|
||||
| where properties.provisioningState =~ 'Succeeded'
|
||||
| mv-expand privateIP = properties.privateIPAddresses, publicIP = properties.publicIPAddresses
|
||||
| project name, id, resource_group = resourceGroup, subscription_id = subscriptionId, private_ip = privateIP, public_ip = publicIP, subnet_id = properties.virtualNetworkConfiguration.subnetResourceId, tenant_id = tenantId, vnet_type = properties.virtualNetworkType
|
||||
|
|
|
@ -11,13 +11,21 @@ from azure.mgmt.resource.subscriptions.aio import SubscriptionClient
|
|||
import azure.cosmos.exceptions as exceptions
|
||||
|
||||
import re
|
||||
import copy
|
||||
import asyncio
|
||||
import logging
|
||||
from ipaddress import IPv4Network
|
||||
from netaddr import IPSet, IPNetwork
|
||||
from uuid import uuid4
|
||||
|
||||
from app.dependencies import check_token_expired, get_admin
|
||||
from sqlalchemy import true
|
||||
|
||||
from app.dependencies import (
|
||||
check_token_expired,
|
||||
get_admin,
|
||||
get_tenant_id
|
||||
)
|
||||
|
||||
from . import argquery
|
||||
|
||||
from app.routers.common.helper import (
|
||||
|
@ -25,6 +33,8 @@ from app.routers.common.helper import (
|
|||
get_obo_credentials,
|
||||
cosmos_query,
|
||||
cosmos_upsert,
|
||||
cosmos_replace,
|
||||
cosmos_retry,
|
||||
arg_query
|
||||
)
|
||||
|
||||
|
@ -32,6 +42,8 @@ from app.routers.space import (
|
|||
get_spaces
|
||||
)
|
||||
|
||||
from app.globals import globals
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
console = logging.StreamHandler()
|
||||
|
@ -181,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.
|
||||
|
@ -197,18 +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)
|
||||
authorization: str = Header(None),
|
||||
tenant_id: str = Depends(get_tenant_id),
|
||||
admin: str = Depends(get_admin)
|
||||
):
|
||||
"""
|
||||
Get a list of Azure Virtual Networks.
|
||||
"""
|
||||
|
||||
item = await cosmos_query("spaces")
|
||||
space_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'space'", tenant_id)
|
||||
|
||||
vnet_list = await arg_query(authorization, admin, argquery.VNET)
|
||||
|
||||
|
@ -230,24 +243,24 @@ async def get_vnet(
|
|||
vnet['used'] = total_used
|
||||
|
||||
# Python 3.9+
|
||||
# ip_blocks = [(block | {'parentSpace': space['name']}) for space in item['spaces'] for block in space['blocks']]
|
||||
ip_blocks = [{**block , **{'parentSpace': space['name']}} for space in item['spaces'] for block in space['blocks']]
|
||||
# ip_blocks = [(block | {'parentSpace': space['name']}) for space in space_query for block in space['blocks']]
|
||||
ip_blocks = [{**block , **{'parentSpace': space['name']}} for space in space_query for block in space['blocks']]
|
||||
ip_block = next((x for x in ip_blocks if vnet['id'] in [v['id'] for v in x['vnets']]), None)
|
||||
|
||||
vnet['parentSpace'] = ip_block['parentSpace'] if ip_block else None
|
||||
vnet['parentBlock'] = ip_block['name'] if ip_block else None
|
||||
|
||||
updated_vnet_list.append(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.
|
||||
|
@ -303,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.
|
||||
|
@ -319,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
|
||||
|
@ -335,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.
|
||||
|
@ -352,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.
|
||||
|
@ -368,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.
|
||||
|
@ -384,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.
|
||||
|
@ -400,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.
|
||||
|
@ -416,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.
|
||||
|
@ -438,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.
|
||||
|
@ -465,12 +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)
|
||||
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.
|
||||
|
@ -482,8 +496,8 @@ async def multi(
|
|||
subnet_list = []
|
||||
endpoint_list = []
|
||||
|
||||
tasks.append(asyncio.create_task(multi_helper(get_spaces, space_list, False, True, authorization, True)))
|
||||
tasks.append(asyncio.create_task(multi_helper(get_vnet, vnet_list, authorization, admin)))
|
||||
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, 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)))
|
||||
|
@ -577,76 +591,79 @@ async def multi(
|
|||
|
||||
return tree
|
||||
|
||||
@cosmos_retry(
|
||||
max_retry = 5,
|
||||
error_msg = "Error updating reservation status!"
|
||||
)
|
||||
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)
|
||||
|
||||
current_try = 0
|
||||
max_retry = 5
|
||||
space_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'space'", globals.TENANT_ID)
|
||||
|
||||
while True:
|
||||
try:
|
||||
query = await cosmos_query("spaces")
|
||||
for space in space_query:
|
||||
original_space = copy.deepcopy(space)
|
||||
|
||||
for space in query['spaces']:
|
||||
for block in space['blocks']:
|
||||
for vnet in block['vnets']:
|
||||
active = next((x for x in vnet_list if x['id'] == vnet['id']), None)
|
||||
for block in space['blocks']:
|
||||
for vnet in block['vnets']:
|
||||
active = next((x for x in vnet_list if x['id'] == vnet['id']), None)
|
||||
|
||||
if active:
|
||||
vnet['active'] = True
|
||||
else:
|
||||
vnet['active'] = False
|
||||
if active:
|
||||
vnet['active'] = True
|
||||
else:
|
||||
vnet['active'] = False
|
||||
|
||||
for index, resv in enumerate(block['resv']):
|
||||
if resv['id'] in stale_resv:
|
||||
vnet = next((x for x in vnet_list if x['resv'] == resv['id']), None)
|
||||
for index, resv in enumerate(block['resv']):
|
||||
if resv['id'] in stale_resv:
|
||||
vnet = next((x for x in vnet_list if x['resv'] == resv['id']), None)
|
||||
|
||||
# print("RESV: {}".format(vnet['resv']))
|
||||
# print("BLOCK {}".format(block['name']))
|
||||
# print("VNET {}".format(vnet['id']))
|
||||
# print("INDEX: {}".format(index))
|
||||
# print("RESV: {}".format(vnet['resv']))
|
||||
# print("BLOCK {}".format(block['name']))
|
||||
# print("VNET {}".format(vnet['id']))
|
||||
# print("INDEX: {}".format(index))
|
||||
|
||||
stale_resv.remove(resv['id'])
|
||||
resv['status'] = "wait"
|
||||
stale_resv.remove(resv['id'])
|
||||
resv['status'] = "wait"
|
||||
|
||||
cidr_match = resv['cidr'] in vnet['prefixes']
|
||||
cidr_match = resv['cidr'] in vnet['prefixes']
|
||||
|
||||
if not cidr_match:
|
||||
# print("Reservation ID assigned to vNET which does not have an address space that matches the reservation.")
|
||||
resv['status'] = "warnCIDRMismatch"
|
||||
if not cidr_match:
|
||||
# print("Reservation ID assigned to vNET which does not have an address space that matches the reservation.")
|
||||
# logging.info("Reservation ID assigned to vNET which does not have an address space that matches the reservation.")
|
||||
resv['status'] = "warnCIDRMismatch"
|
||||
|
||||
existing_block_cidrs = []
|
||||
existing_block_cidrs = []
|
||||
|
||||
for v in block['vnets']:
|
||||
target_vnet = next((x for x in vnet_list if x['id'].lower() == v['id'].lower()), None)
|
||||
for v in block['vnets']:
|
||||
target_vnet = next((x for x in vnet_list if x['id'].lower() == v['id'].lower()), None)
|
||||
|
||||
if target_vnet:
|
||||
target_cidr = next((x for x in target_vnet['prefixes'] if IPNetwork(x) in IPNetwork(block['cidr'])), None)
|
||||
existing_block_cidrs.append(target_cidr)
|
||||
if target_vnet:
|
||||
target_cidr = next((x for x in target_vnet['prefixes'] if IPNetwork(x) in IPNetwork(block['cidr'])), None)
|
||||
existing_block_cidrs.append(target_cidr)
|
||||
|
||||
vnet_cidr = next((x for x in vnet['prefixes'] if IPNetwork(x) in IPNetwork(block['cidr'])), None)
|
||||
vnet_cidr = next((x for x in vnet['prefixes'] if IPNetwork(x) in IPNetwork(block['cidr'])), None)
|
||||
|
||||
if vnet_cidr in existing_block_cidrs:
|
||||
# print("A vNET with the assigned CIDR has already been associated with the target IP Block.")
|
||||
resv['status'] = "errCIDRExists"
|
||||
if vnet_cidr in existing_block_cidrs:
|
||||
# print("A vNET with the assigned CIDR has already been associated with the target IP Block.")
|
||||
# logging.info("A vNET with the assigned CIDR has already been associated with the target IP Block.")
|
||||
resv['status'] = "errCIDRExists"
|
||||
|
||||
if resv['status'] == "wait":
|
||||
# print("vNET association complete, adding vNET to Block.")
|
||||
block['vnets'].append({
|
||||
"id": vnet['id'],
|
||||
"active": True
|
||||
})
|
||||
del block['resv'][index]
|
||||
await cosmos_upsert("spaces", query)
|
||||
except exceptions.CosmosAccessConditionFailedError:
|
||||
if current_try < max_retry:
|
||||
current_try += 1
|
||||
continue
|
||||
else:
|
||||
print("Error updating reservation status!")
|
||||
else:
|
||||
break
|
||||
if resv['status'] == "wait":
|
||||
# print("vNET is being added to IP Block...")
|
||||
# logging.info("vNET is being added to IP Block...")
|
||||
block['vnets'].append(
|
||||
{
|
||||
"id": vnet['id'],
|
||||
"active": True
|
||||
}
|
||||
)
|
||||
del block['resv'][index]
|
||||
else:
|
||||
# print("Resetting status to 'wait'.")
|
||||
# logging.info("Resetting status to 'wait'.")
|
||||
resv['status'] = "wait"
|
||||
|
||||
await cosmos_replace(original_space, space)
|
||||
|
||||
# print("STALE:")
|
||||
# print(stale_resv)
|
||||
|
|
|
@ -12,8 +12,11 @@ import azure.cosmos.exceptions as exceptions
|
|||
|
||||
import os
|
||||
import jwt
|
||||
from functools import wraps
|
||||
|
||||
import app.globals as globals
|
||||
from requests import options
|
||||
|
||||
from app.globals import globals
|
||||
|
||||
SCOPE = "https://management.azure.com/user_impersonation"
|
||||
|
||||
|
@ -61,57 +64,148 @@ async def get_obo_credentials(assertion):
|
|||
|
||||
return credential
|
||||
|
||||
async def cosmos_query(target: str):
|
||||
async def cosmos_query(query: str, tenant_id: str):
|
||||
"""DOCSTRING"""
|
||||
|
||||
cosmos_client = CosmosClient(globals.COSMOS_URL, credential=globals.COSMOS_KEY)
|
||||
|
||||
database_name = "ipam-db"
|
||||
database_name = globals.DATABASE_NAME
|
||||
database = cosmos_client.get_database_client(database_name)
|
||||
|
||||
container_name = "ipam-container"
|
||||
container_name = globals.CONTAINER_NAME
|
||||
container = database.get_container_client(container_name)
|
||||
|
||||
item = await container.read_item(target, partition_key=target)
|
||||
query_results = container.query_items(
|
||||
query = query,
|
||||
# enable_cross_partition_query=True,
|
||||
partition_key = tenant_id
|
||||
)
|
||||
|
||||
result_array = [result async for result in query_results]
|
||||
|
||||
await cosmos_client.close()
|
||||
|
||||
return item
|
||||
return result_array
|
||||
|
||||
async def cosmos_upsert(target: str, data):
|
||||
async def cosmos_upsert(data):
|
||||
"""DOCSTRING"""
|
||||
|
||||
cosmos_client = CosmosClient(globals.COSMOS_URL, credential=globals.COSMOS_KEY)
|
||||
|
||||
database_name = "ipam-db"
|
||||
database_name = globals.DATABASE_NAME
|
||||
database = cosmos_client.get_database_client(database_name)
|
||||
|
||||
container_name = "ipam-container"
|
||||
container_name = globals.CONTAINER_NAME
|
||||
container = database.get_container_client(container_name)
|
||||
|
||||
try:
|
||||
await container.upsert_item(
|
||||
data,
|
||||
match_condition=MatchConditions.IfNotModified,
|
||||
etag=data['_etag']
|
||||
res = await container.upsert_item(data)
|
||||
except:
|
||||
raise
|
||||
finally:
|
||||
await cosmos_client.close()
|
||||
|
||||
await cosmos_client.close()
|
||||
|
||||
return res
|
||||
|
||||
async def cosmos_replace(old, new):
|
||||
"""DOCSTRING"""
|
||||
|
||||
cosmos_client = CosmosClient(globals.COSMOS_URL, credential=globals.COSMOS_KEY)
|
||||
|
||||
database_name = globals.DATABASE_NAME
|
||||
database = cosmos_client.get_database_client(database_name)
|
||||
|
||||
container_name = globals.CONTAINER_NAME
|
||||
container = database.get_container_client(container_name)
|
||||
|
||||
try:
|
||||
await container.replace_item(
|
||||
item = old,
|
||||
body = new,
|
||||
match_condition = MatchConditions.IfNotModified,
|
||||
etag = old['_etag']
|
||||
)
|
||||
except:
|
||||
raise
|
||||
finally:
|
||||
await cosmos_client.close()
|
||||
|
||||
await cosmos_client.close()
|
||||
|
||||
return
|
||||
|
||||
async def cosmos_delete(item, tenant_id: str):
|
||||
"""DOCSTRING"""
|
||||
|
||||
cosmos_client = CosmosClient(globals.COSMOS_URL, credential=globals.COSMOS_KEY)
|
||||
|
||||
database_name = globals.DATABASE_NAME
|
||||
database = cosmos_client.get_database_client(database_name)
|
||||
|
||||
container_name = globals.CONTAINER_NAME
|
||||
container = database.get_container_client(container_name)
|
||||
|
||||
try:
|
||||
await container.delete_item(
|
||||
item = item,
|
||||
partition_key = tenant_id
|
||||
)
|
||||
except:
|
||||
raise
|
||||
finally:
|
||||
await cosmos_client.close()
|
||||
|
||||
await cosmos_client.close()
|
||||
|
||||
return
|
||||
|
||||
def cosmos_retry(error_msg, max_retry = 5):
|
||||
"""DOCSTRING"""
|
||||
|
||||
def cosmos_retry_decorator(func):
|
||||
@wraps(func)
|
||||
async def func_with_retries(*args, **kwargs):
|
||||
_tries = max_retry
|
||||
|
||||
while _tries > 0:
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except exceptions.CosmosAccessConditionFailedError:
|
||||
_tries -= 1
|
||||
|
||||
if _tries == 0:
|
||||
raise HTTPException(status_code=500, detail=error_msg)
|
||||
|
||||
return func_with_retries
|
||||
return cosmos_retry_decorator
|
||||
|
||||
async def arg_query(auth, admin, query):
|
||||
"""DOCSTRING"""
|
||||
|
||||
if admin:
|
||||
creds = await get_client_credentials()
|
||||
tenant_id = globals.TENANT_ID
|
||||
else:
|
||||
user_assertion=auth.split(' ')[1]
|
||||
creds = await get_obo_credentials(user_assertion)
|
||||
tenant_id = get_tenant_from_jwt(user_assertion)
|
||||
|
||||
exclusions_query = await cosmos_query("SELECT * FROM c WHERE c.type = 'admin'", tenant_id)
|
||||
|
||||
if exclusions_query:
|
||||
exclusions_array = exclusions_query[0]['exclusions']
|
||||
|
||||
if exclusions_array:
|
||||
exclusions = "(" + str(exclusions_array)[1:-1] + ")"
|
||||
else:
|
||||
exclusions = "('')"
|
||||
else:
|
||||
exclusions = "('')"
|
||||
|
||||
try:
|
||||
results = await arg_query_helper(creds, query)
|
||||
results = await arg_query_helper(creds, query.format(exclusions))
|
||||
except ClientAuthenticationError:
|
||||
raise HTTPException(status_code=401, detail="Token has expired.")
|
||||
except HttpResponseError as e:
|
||||
|
@ -156,21 +250,34 @@ 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)
|
||||
except ServiceRequestError:
|
||||
skip_token = None
|
||||
|
||||
while True:
|
||||
query_request = 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_request)
|
||||
results = results + poll.data
|
||||
|
||||
if poll.skip_token:
|
||||
skip_token = poll.skip_token
|
||||
else:
|
||||
break
|
||||
except ServiceRequestError as e:
|
||||
print(e)
|
||||
raise HTTPException(status_code=500, detail="Error communicating with Azure.")
|
||||
finally:
|
||||
await resource_graph_client.close()
|
||||
|
||||
return poll.data
|
||||
return results
|
||||
|
|
|
@ -8,17 +8,31 @@ 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.admin import (
|
||||
new_admin_db
|
||||
)
|
||||
|
||||
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 +41,20 @@ router = APIRouter(
|
|||
dependencies=[Depends(check_token_expired)]
|
||||
)
|
||||
|
||||
class User(BaseModel):
|
||||
"""DOCSTRING"""
|
||||
|
||||
id: UUID
|
||||
apiRefresh: int
|
||||
isAdmin: bool
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
UUID: lambda v: str(v),
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
class JSONPatch(BaseModel):
|
||||
"""DOCSTRING"""
|
||||
query_results = await cosmos_upsert(jsonable_encoder(new_user))
|
||||
|
||||
op: str
|
||||
path: str
|
||||
value: Any
|
||||
|
||||
class UserUpdate(List[JSONPatch]):
|
||||
"""DOCSTRING"""
|
||||
return query_results
|
||||
|
||||
async def scrub_patch(patch):
|
||||
scrubbed_patch = []
|
||||
|
@ -79,6 +86,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 +98,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 +121,43 @@ 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)
|
||||
if not admin_query:
|
||||
admin_query = [await new_admin_db([], [], tenant_id)]
|
||||
|
||||
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 +165,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 +188,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,30 @@
|
|||
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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import datetime
|
||||
import logging
|
||||
|
||||
import azure.functions as func
|
||||
|
||||
from app.routers.azure import match_resv_to_vnets
|
||||
async def main(mytimer: func.TimerRequest) -> None:
|
||||
utc_timestamp = datetime.datetime.utcnow().replace(
|
||||
tzinfo=datetime.timezone.utc).isoformat()
|
||||
|
||||
if mytimer.past_due:
|
||||
logging.info('The timer is past due!')
|
||||
|
||||
logging.info('Python timer trigger function ran at %s', utc_timestamp)
|
||||
|
||||
await match_resv_to_vnets()
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"scriptFile": "__init__.py",
|
||||
"bindings": [
|
||||
{
|
||||
"name": "mytimer",
|
||||
"type": "timerTrigger",
|
||||
"direction": "in",
|
||||
"schedule": "0 * * * * *"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -18,3 +18,5 @@ azure-mgmt-resource
|
|||
azure-mgmt-resourcegraph
|
||||
azure-keyvault-secrets
|
||||
azure-cosmos==4.3.0
|
||||
azure-functions
|
||||
nest_asyncio
|
||||
|
|
|
@ -3,11 +3,11 @@ import { styled } from '@mui/material/styles';
|
|||
|
||||
import { useMsal } from "@azure/msal-react";
|
||||
import { InteractionRequiredAuthError, InteractionStatus } from "@azure/msal-browser";
|
||||
import { callMsGraphUsers } from "../../msal/graph";
|
||||
import { callMsGraphUsersFilter } from "../../msal/graph";
|
||||
|
||||
import { useSnackbar } from 'notistack';
|
||||
|
||||
import { isEqual } from 'lodash';
|
||||
import { isEqual, throttle } from 'lodash';
|
||||
|
||||
import {
|
||||
DataGrid,
|
||||
|
@ -42,71 +42,65 @@ import {
|
|||
import { apiRequest } from "../../msal/authConfig";
|
||||
|
||||
function CustomToolbar(props) {
|
||||
const { admins, loadedAdmins, setAdmins, selectionModel, refresh } = 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 selectedAdmin = admins.find(obj => { return obj.id === selectionModel[0] });
|
||||
const changed = isEqual(admins, loadedAdmins);
|
||||
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;
|
||||
|
||||
function SearchUsers() {
|
||||
const request = {
|
||||
scopes: ["Directory.Read.All"],
|
||||
account: accounts[0],
|
||||
};
|
||||
|
||||
if (!options && inProgress === InteractionStatus.None) {
|
||||
(async () => {
|
||||
try {
|
||||
const response = await instance.acquireTokenSilent(request);
|
||||
const userData = await callMsGraphUsers(response.accessToken);
|
||||
setOptions(userData.value);
|
||||
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" });
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
if (active) {
|
||||
fetchUsers(input);
|
||||
}
|
||||
|
||||
if (!loading) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
if (active) {
|
||||
SearchUsers();
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [loading, accounts, instance]);
|
||||
}, [input, fetchUsers]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) {
|
||||
setOptions(null);
|
||||
}
|
||||
}, [open]);
|
||||
}, [input, open]);
|
||||
|
||||
function handleAdd(user) {
|
||||
let newAdmin = {
|
||||
|
@ -191,6 +185,9 @@ function CustomToolbar(props) {
|
|||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
onInputChange={(event, newInput) => {
|
||||
setInput(newInput);
|
||||
}}
|
||||
onChange={(event, newValue) => {
|
||||
newValue ? handleAdd(newValue) : setSelected(null);
|
||||
}}
|
||||
|
@ -228,9 +225,9 @@ function CustomToolbar(props) {
|
|||
aria-label="upload picture"
|
||||
component="span"
|
||||
style={{
|
||||
visibility: changed ? 'hidden' : 'visible',
|
||||
disabled: sending
|
||||
visibility: unchanged ? 'hidden' : 'visible'
|
||||
}}
|
||||
disabled={sending || refreshing}
|
||||
onClick={onSave}
|
||||
>
|
||||
<SaveAlt />
|
||||
|
@ -263,6 +260,7 @@ export default function Administration() {
|
|||
}, []);
|
||||
|
||||
function refreshData() {
|
||||
console.log("REFRESHING...");
|
||||
const request = {
|
||||
scopes: apiRequest.scopes,
|
||||
account: accounts[0],
|
||||
|
@ -291,23 +289,7 @@ export default function Administration() {
|
|||
})();
|
||||
}
|
||||
|
||||
function handleDelete(id) {
|
||||
// const index = admins.findIndex(x => {
|
||||
// return x.id === id;
|
||||
// });
|
||||
|
||||
setAdmins(admins.filter(x => x.id !== id));
|
||||
}
|
||||
|
||||
function renderDelete(params) {
|
||||
const onClick = (e) => {
|
||||
e.stopPropagation();
|
||||
console.log("CLICK: " + params.row.id);
|
||||
// setSelectionModel([params.value]);
|
||||
// setRowData(params.row);
|
||||
// setMenuExpand(true);
|
||||
};
|
||||
|
||||
function renderDelete(params) {
|
||||
const flexCenter = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
|
@ -326,7 +308,7 @@ export default function Administration() {
|
|||
disableFocusRipple
|
||||
disableTouchRipple
|
||||
disableRipple
|
||||
onClick={() => handleDelete(params.row.id)}
|
||||
onClick={() => setAdmins(admins.filter(x => x.id !== params.row.id))}
|
||||
>
|
||||
<HighlightOff />
|
||||
</IconButton>
|
||||
|
@ -373,42 +355,39 @@ export default function Administration() {
|
|||
}
|
||||
|
||||
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 height="100%" width="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
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
"&.MuiDataGrid-root .MuiDataGrid-columnHeader:focus, &.MuiDataGrid-root .MuiDataGrid-cell:focus, &.MuiDataGrid-root .MuiDataGrid-cell:focus-within":
|
||||
{
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -251,6 +251,16 @@ export default function EditVnets(props) {
|
|||
outline: "none",
|
||||
}
|
||||
}}
|
||||
initialState={{
|
||||
sorting: {
|
||||
sortModel: [
|
||||
{
|
||||
field: 'name',
|
||||
sort: 'asc',
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
|
|
|
@ -62,14 +62,16 @@ 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 ConfigureIPAM from "../configure/configure";
|
||||
|
||||
import Refresh from "./refresh";
|
||||
|
@ -149,6 +151,7 @@ export default function NavDrawer() {
|
|||
{
|
||||
title: "Discover",
|
||||
icon: Discover,
|
||||
admin: false,
|
||||
children: [
|
||||
{
|
||||
title: "Spaces",
|
||||
|
@ -185,6 +188,7 @@ export default function NavDrawer() {
|
|||
{
|
||||
title: "Analysis",
|
||||
icon: Analysis,
|
||||
admin: false,
|
||||
children: [
|
||||
{
|
||||
title: "Visualize",
|
||||
|
@ -208,11 +212,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 +305,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 +650,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>
|
||||
|
|
|
@ -15,6 +15,9 @@ import {
|
|||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
Switch,
|
||||
Stack,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
|
||||
import {
|
||||
|
@ -104,9 +107,19 @@ export default function UserSettings(props) {
|
|||
Settings
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
{/*
|
||||
<DialogContentText>
|
||||
IP Usage Format:
|
||||
</DialogContentText>
|
||||
<Box sx={{ display: "flex", flexDirection: "row", alignItems: "center", p: 1 }}>
|
||||
<Typography>Standard</Typography>
|
||||
<Switch inputProps={{ 'aria-label': 'ant design' }} />
|
||||
<Typography>Azure</Typography>
|
||||
</Box>
|
||||
<DialogContentText>
|
||||
Data refresh interval (minutes):
|
||||
</DialogContentText>
|
||||
*/}
|
||||
<Box sx={{ p: 1 }}>
|
||||
<Slider
|
||||
aria-label="Restricted values"
|
||||
|
|
|
@ -0,0 +1,362 @@
|
|||
import * as React from "react";
|
||||
import { useDispatch } from 'react-redux';
|
||||
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,
|
||||
getExclusions,
|
||||
replaceExclusions
|
||||
} from "../ipam/ipamAPI";
|
||||
|
||||
import {
|
||||
refreshAllAsync
|
||||
} from '../ipam/ipamSlice';
|
||||
|
||||
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
|
||||
// disableSelectionOnClick
|
||||
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 dispatch = useDispatch();
|
||||
|
||||
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 stack = [
|
||||
(async () => await fetchSubscriptions(response.accessToken))(),
|
||||
(async () => await getExclusions(response.accessToken))()
|
||||
];
|
||||
|
||||
Promise.all(stack).then((results) => {
|
||||
var includedSubs = results[0];
|
||||
var excludedSubs = [];
|
||||
|
||||
results[1].forEach(exclusion => {
|
||||
includedSubs = includedSubs.filter(object => {
|
||||
return object.subscription_id !== exclusion;
|
||||
});
|
||||
|
||||
const excludeObj = results[0].find(element => element.subscription_id == exclusion);
|
||||
|
||||
excludedSubs = [...excludedSubs, excludeObj];
|
||||
});
|
||||
|
||||
setIncluded(includedSubs);
|
||||
setExcluded(excludedSubs);
|
||||
setLoadedExclusions(excludedSubs);
|
||||
setLoading(false);
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof InteractionRequiredAuthError) {
|
||||
instance.acquireTokenRedirect(request);
|
||||
} else {
|
||||
console.log("ERROR");
|
||||
console.log("------------------");
|
||||
console.log(e);
|
||||
console.log("------------------");
|
||||
enqueueSnackbar("Error fetching subscriptions/exclusions", { variant: "error" });
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
function onSave() {
|
||||
const request = {
|
||||
scopes: apiRequest.scopes,
|
||||
account: accounts[0],
|
||||
};
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
setSending(true);
|
||||
let update = excluded.map(item => item.subscription_id);
|
||||
const response = await instance.acquireTokenSilent(request);
|
||||
const data = await replaceExclusions(response.accessToken, update);
|
||||
enqueueSnackbar("Successfully updated exclusions", { variant: "success" });
|
||||
setLoadedExclusions(excluded);
|
||||
dispatch(refreshAllAsync(response.accessToken))
|
||||
} 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);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
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={onSave}
|
||||
>
|
||||
<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={loading}
|
||||
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`);
|
||||
|
||||
|
@ -341,7 +358,7 @@ export function fetchTreeView(token) {
|
|||
}
|
||||
|
||||
export function getAdmins(token) {
|
||||
const url = new URL(`${ENGINE_URL}/api/admins`);
|
||||
const url = new URL(`${ENGINE_URL}/api/admin/admins`);
|
||||
|
||||
return axios
|
||||
.get(url, {
|
||||
|
@ -358,7 +375,7 @@ export function getAdmins(token) {
|
|||
}
|
||||
|
||||
export function replaceAdmins(token, body) {
|
||||
const url = new URL(`${ENGINE_URL}/api/admins`);
|
||||
const url = new URL(`${ENGINE_URL}/api/admin/admins`);
|
||||
|
||||
return axios
|
||||
.put(url, body, {
|
||||
|
@ -374,6 +391,40 @@ export function replaceAdmins(token, body) {
|
|||
});
|
||||
}
|
||||
|
||||
export function getExclusions(token) {
|
||||
const url = new URL(`${ENGINE_URL}/api/admin/exclusions`);
|
||||
|
||||
return axios
|
||||
.get(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
})
|
||||
.then(response => response.data)
|
||||
.catch(error => {
|
||||
console.log("ERROR FETCHING EXCLUSIONS VIA API");
|
||||
console.log(error);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
export function replaceExclusions(token, body) {
|
||||
const url = new URL(`${ENGINE_URL}/api/admin/exclusions`);
|
||||
|
||||
return axios
|
||||
.put(url, body, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
})
|
||||
.then(response => response.data)
|
||||
.catch(error => {
|
||||
console.log("ERROR UPDATING EXCLUSIONS VIA API");
|
||||
console.log(error);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
export function getMe(token) {
|
||||
const url = new URL(`${ENGINE_URL}/api/users/me`);
|
||||
|
||||
|
|
|
@ -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/admin';
|
||||
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}`;
|
||||
|
|