import { HttpMethods } from './../../shared/models/constants' ; import { FunctionAppService } from 'app/shared/services/function-app.service' ; import { Headers } from '@angular/http' ; import { UUID } from 'angular2-uuid' ; import { Observable } from 'rxjs/Observable' ; import { BindingComponent } from '../../binding/binding.component' ; import { FunctionNewComponent } from '../../function/function-new/function-new.component' ; import { CacheService } from '../../shared/services/cache.service' ; import { AiService } from '../../shared/services/ai.service' ; import { ArmObj } from '../../shared/models/arm/arm-obj' ; import { MSGraphConstants , AADPermissions , AADRegistrationInfo } from '../../shared/models/microsoft-graph' ; import { FunctionAppContext } from 'app/shared/function-app-context' ; declare const Buffer ; declare var require ; export class MicrosoftGraphHelper { public binding ? : BindingComponent ; public function ? : FunctionNewComponent ; private _token : string ; private _jwt : any ; private setClientSecret = false ; constructor ( private context , private _functionAppService , private _cacheService , private _aiService ? ) { } getADDAppRegistrationInfo ( necessaryAADPermisisons , graphToken ) { const cloneNecessaryAADPermisisons = JSON . parse ( JSON . stringify ( necessaryAADPermisisons ) ) ; const rootUri = this . getRootUri ( graphToken ) ; const result = { isPermissionConfigured : false , isAADAppCreated : false , permissions : cloneNecessaryAADPermisisons , } ; return this . checkForExistingAAD ( rootUri ) . flatMap ( applicationInformation => { let existingApplication ; if ( applicationInformation ) { existingApplication = applicationInformation . json ( ) . value [ 0 ] ; } if ( existingApplication ) { result . isAADAppCreated = true ; result . isPermissionConfigured = true ; if ( ! existingApplication . requiredResourceAccess ) { existingApplication . requiredResourceAccess = [ ] ; } let appPerm = null ; result . permissions . forEach ( requiredPerm => { const findPerm = existingApplication . requiredResourceAccess . find ( p => { if ( p . resourceAppId . toLocaleLowerCase ( ) === requiredPerm . resourceAppId . toLocaleLowerCase ( ) ) { appPerm = p ; return true ; } else { return false ; } } ) ; if ( findPerm ) { requiredPerm . resourceAccess . forEach ( requiredAccess => { const findAccess = appPerm . resourceAccess . find ( appAccess => { return requiredAccess . id . toLocaleLowerCase ( ) === appAccess . id . toLocaleLowerCase ( ) ; } ) ; requiredAccess . configured = ! ! findAccess ; } ) ; } } ) ; result . permissions . forEach ( p => { p . resourceAccess . forEach ( ra => { if ( ! ra . configured ) { result . isPermissionConfigured = false ; } } ) ; } ) ; return Observable . of ( result ) ; } else { return Observable . of ( result ) ; } } ) ; } configureAAD ( necessaryAADPermisisons , graphToken ) { const rootUri = this . getRootUri ( graphToken ) ; const appUri = this . context . mainSiteUrl ; const name = this . context . site . name ; const app = { displayName : name , homepage : appUri , identifierUris : [ appUri ] , oauth2AllowImplicitFlow : true , replyUrls : [ trimTrailingSlash ( appUri ) + MSGraphConstants . General . AADReplyUrl ] , passwordCredentials : [ GeneratePasswordCredentials ( ) ] , requiredResourceAccess : necessaryAADPermisisons , } ; const pwCreds = app . passwordCredentials [ 0 ] ; const application = JSON . stringify ( app ) ; return this . sendRequest ( rootUri , '/applications' , 'POST' , application ) . do ( null , err => { if ( this . _aiService ) { this . _aiService . trackException ( err , 'Error while creating new AAD application' ) ; } } ) . flatMap ( response => { const newApplication = JSON . parse ( response . _body ) ; return this . setAuthSettings ( newApplication . appId , this . _jwt , pwCreds . value , app . replyUrls ) ; } ) ; } addPermissions ( necessaryAADPermisisons , graphToken ) { this . _token = graphToken ; const jwt = parseToken ( this . _token ) ; if ( ! jwt ) { throw Error ( 'Unable to parse MS Graph JWT; cannot retrieve audience or tenant ID' ) ; } const rootUri = jwt . aud + jwt . tid ; return this . checkForExistingAAD ( rootUri ) . flatMap ( applicationInformation => { let existingApplication = { } ; if ( applicationInformation ) { existingApplication = applicationInformation . json ( ) . value [ 0 ] ; } if ( existingApplication ) { if ( this . setClientSecret ) { this . createClientSecret ( rootUri , existingApplication . objectId ) . subscribe ( ( ) => { this . setClientSecret = false ; } , error => { if ( this . _aiService ) { this . _aiService . trackException ( error , "Could not update AAD manifest's client secret" ) ; } } ) ; } const patch = { } ; patch . value = CompareResources ( existingApplication . requiredResourceAccess , necessaryAADPermisisons ) ; return this . sendRequest ( rootUri , '/applications/' + existingApplication . objectId + '/requiredResourceAccess' , 'PATCH' , JSON . stringify ( patch ) ) ; } else { return Observable . of ( null ) ; } } ) ; } private setAuthSettings ( applicationId , jwt , clientSecret , replyUrls ) { const authSettings = new Map < string , any > ( ) ; authSettings . set ( 'enabled' , true ) ; authSettings . set ( 'unauthenticatedClientAction' , 'AllowAnonymous' ) ; authSettings . set ( 'tokenStoreEnabled' , true ) ; authSettings . set ( 'allowedExternalRedirectUrls' , window . location . hostname ) ; authSettings . set ( 'defaultProvider' , 'AzureActiveDirectory' ) ; authSettings . set ( 'clientId' , applicationId ) ; authSettings . set ( 'clientSecret' , clientSecret ) ; authSettings . set ( 'issuer' , jwt . iss ) ; authSettings . set ( 'allowedAudiences' , replyUrls ) ; authSettings . set ( 'isAadAutoProvisioned' , true ) ; return this . _functionAppService . createAuthSettings ( this . context , authSettings ) . do ( null , error => { if ( this . _aiService ) { this . _aiService . trackException ( error , 'Error occurred while setting necessary authsettings' ) ; } } ) ; } private checkForExistingAAD ( rootUri ) { return this . _cacheService . postArm ( `${ this . context . site . id }/config/authsettings/list` , true ) . flatMap ( r => { const authSettings = r . json ( ) ; const clientId = authSettings . properties [ 'clientId' ] ; this . setClientSecret = ! authSettings . properties [ 'clientSecret' ] ; if ( clientId ) { return this . sendRequest ( rootUri , '/applications' , 'GET' , null , "appId eq '" + clientId + "'" , true ) ; } else { return Observable . of ( null ) ; } } ) ; } private createClientSecret ( rootUri , objectId ) { const pwCreds = GeneratePasswordCredentials ( ) ; const authSettings = new Map < string , any > ( ) ; authSettings . set ( 'clientSecret' , pwCreds . keyId ) ; return this . _functionAppService . createAuthSettings ( this . context , authSettings ) . do ( ( ) => { return this . sendRequest ( rootUri , '/applications/' + objectId + '/passwordCredentials' , 'PATCH' , JSON . stringify ( pwCreds ) ) ; } , error => { if ( this . _aiService ) { this . _aiService . trackException ( error , 'Error while updating authsetting with new client secret' ) ; } } ) ; } private sendRequest ( baseUrl , extension , method , jsonPayload ? , queryParameters ? , force ? ) { let url = trimTrailingSlash ( baseUrl ) + extension + '?api-version=' + MSGraphConstants . General . ApiVersion ; if ( queryParameters ) { url += '&$filter=' + encodeURIComponent ( queryParameters ) ; } const headers = new Headers ( ) ; headers . append ( 'Authorization' , `Bearer ${ this . _token }` ) ; if ( method . toLowerCase ( ) === HttpMethods . POST ) { headers . append ( 'Content-Type' , 'application/json' ) ; return this . _cacheService . post ( url , force , headers , jsonPayload ) ; } else if ( method . toLowerCase ( ) === HttpMethods . PUT ) { headers . append ( 'Content-Type' , 'application/json' ) ; return this . _cacheService . put ( url , headers , jsonPayload ) ; } else if ( method . toLowerCase ( ) === HttpMethods . PATCH ) { headers . append ( 'Content-Type' , 'application/json; charset=utf-8' ) ; return this . _cacheService . patch ( url , headers , jsonPayload ) ; } return this . _cacheService . get ( url , force , headers ) ; } private getRootUri ( graphToken ) { this . _token = graphToken ; this . _jwt = parseToken ( this . _token ) ; if ( ! this . _jwt ) { throw Error ( 'Unable to parse MS Graph JWT; cannot retrieve audience or tenant ID' ) ; } return this . _jwt . aud + this . _jwt . tid ; } } function base64urlUnescape ( str ) { str += new Array ( 5 - ( str . length % 4 ) ) . join ( '=' ) ; return str . replace ( /\-/g , '+' ) . replace ( /_/g , '/' ) ; } function trimTrailingSlash ( url ) { return url . replace ( /\/$/ , '' ) ; } export function parseToken ( token ) { const segments = token . split ( '.' ) ; if ( segments . length >= 2 ) { try { const payload = JSON . parse ( base64urlDecode ( segments [ 1 ] ) ) ; return payload ; } catch ( e ) { return null ; } } } function base64urlDecode ( payloadSegment ) { return new Buffer ( base64urlUnescape ( payloadSegment ) , 'base64' ) . toString ( ) ; } function GeneratePasswordCredentials ( ) { return { endDate : new Date ( Date . now ( ) + 365 * 24 * 60 * 60 * 1000 ) , keyId : UUID . UUID ( ) , startDate : new Date ( ) , value : GeneratePassword ( ) , } ; } function GeneratePassword ( ) { const crypto = require ( 'crypto-browserify' ) ; return crypto . randomBytes ( 32 ) . toString ( 'base64' ) ; } export function CompareResources ( current , necessary ) { const existingMSGraph = current . find ( obj => { return obj . resourceAppId === MSGraphConstants . RequiredResources . MicrosoftGraph ; } ) ; const necessaryMSGraph = necessary . find ( obj => { return obj . resourceAppId === MSGraphConstants . RequiredResources . MicrosoftGraph ; } ) ; const unionMSGraph = { } ; if ( existingMSGraph ) { unionMSGraph . resourceAccess = necessaryMSGraph . resourceAccess . concat ( existingMSGraph . resourceAccess . filter ( item => { return ( necessaryMSGraph . resourceAccess . findIndex ( necessary => { return necessary . type === item . type && necessary . id === item . id ; } ) < 0 ) ; } ) ) ; } else { unionMSGraph . resourceAccess = necessaryMSGraph . resourceAccess ; } unionMSGraph . resourceAppId = MSGraphConstants . RequiredResources . MicrosoftGraph ; const unionAAD = { } ; const existingAAD = current . find ( obj => { return obj . resourceAppId === MSGraphConstants . RequiredResources . WindowsAzureActiveDirectory ; } ) ; const necessaryAAD = necessary . find ( obj => { return obj . resourceAppId === MSGraphConstants . RequiredResources . WindowsAzureActiveDirectory ; } ) ; if ( existingAAD ) { unionAAD . resourceAccess = necessaryAAD . resourceAccess . concat ( existingAAD . resourceAccess . filter ( item => { return ( necessaryAAD . resourceAccess . findIndex ( necessary => { return necessary . type === item . type && necessary . id === item . id ; } ) < 0 ) ; } ) ) ; } else { unionAAD . resourceAccess = necessaryAAD . resourceAccess ; } unionAAD . resourceAppId = MSGraphConstants . RequiredResources . WindowsAzureActiveDirectory ; return [ unionMSGraph , unionAAD ] ; }