├── .Rbuildignore ├── .gitattributes ├── .github └── workflows │ └── check-standard.yaml ├── .gitignore ├── CONTRIBUTING.md ├── DESCRIPTION ├── LICENSE ├── LICENSE.md ├── NAMESPACE ├── NEWS.md ├── R ├── AzureGraph.R ├── az_app.r ├── az_device.R ├── az_dir_role.R ├── az_group.R ├── az_object.R ├── az_svc_principal.R ├── az_user.R ├── batch.R ├── call_graph.R ├── format.R ├── graph_login.R ├── is.R ├── ms_graph.R ├── ms_graph_pager.R ├── ms_object.R ├── read_cert.R ├── utils.R └── zzz_class_directory.R ├── README.md ├── SECURITY.md ├── man ├── az_app.Rd ├── az_device.Rd ├── az_directory_role.Rd ├── az_group.Rd ├── az_object.Rd ├── az_service_principal.Rd ├── az_user.Rd ├── call_batch_endpoint.Rd ├── call_graph.Rd ├── extract_list_values.Rd ├── figures │ └── logo.png ├── find_class_generator.Rd ├── format.Rd ├── graph_login.Rd ├── graph_request.Rd ├── info.Rd ├── ms_graph.Rd ├── ms_graph_pager.Rd ├── ms_object.Rd ├── register_graph_class.Rd └── utils.Rd ├── tests ├── testthat.R └── testthat │ ├── test00_class.R │ ├── test01_auth.R │ ├── test02_app_sp.R │ ├── test03_usergrp.R │ ├── test04_batch.R │ ├── test05_pager.R │ └── test06_filter.R └── vignettes ├── auth.Rmd ├── batching_paging.Rmd ├── extend.Rmd └── intro.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^misc$ 2 | ^\.vs$ 3 | \.sln$ 4 | \.Rproj$ 5 | \.Rxproj$ 6 | ^\.Rproj\.user$ 7 | CONTRIBUTING.md 8 | ^LICENSE\.md$ 9 | ^\.github$ 10 | ^SECURITY.md$ 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/check-standard.yaml: -------------------------------------------------------------------------------- 1 | # For help debugging build failures open an issue on the RStudio community with the 'github-actions' tag. 2 | # https://community.rstudio.com/new-topic?category=Package%20development&tags=github-actions 3 | on: [push, pull_request] 4 | 5 | name: R-CMD-check 6 | 7 | jobs: 8 | R-CMD-check: 9 | if: github.repository_owner != 'cloudyr' 10 | runs-on: ${{ matrix.config.os }} 11 | 12 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 13 | 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | config: 18 | - {os: windows-latest, r: 'release'} 19 | - {os: macOS-latest, r: 'release'} 20 | - {os: ubuntu-20.04, r: 'release', rspm: "https://packagemanager.rstudio.com/cran/__linux__/focal/latest"} 21 | 22 | env: 23 | R_REMOTES_NO_ERRORS_FROM_WARNINGS: true 24 | RSPM: ${{ matrix.config.rspm }} 25 | 26 | steps: 27 | - uses: actions/checkout@v2 28 | with: 29 | fetch-depth: 0 # required for mirroring, see https://stackoverflow.com/a/64272409/474349 30 | 31 | - name: Copy to Cloudyr 32 | if: github.repository_owner == 'Azure' && runner.os == 'Linux' && github.ref == 'refs/heads/master' 33 | env: 34 | token: "${{ secrets.ghPat }}" 35 | # git config hack required, see https://stackoverflow.com/q/64270867/474349 36 | run: | 37 | export CLOUDYR_REPO=$(echo $GITHUB_REPOSITORY | sed "s/Azure/cloudyr/") 38 | git config -l | grep 'http\..*\.extraheader' | cut -d= -f1 | \ 39 | xargs -L1 git config --unset-all 40 | git push --prune https://token:$token@github.com/${CLOUDYR_REPO}.git +refs/remotes/origin/*:refs/heads/* +refs/tags/*:refs/tags/* 41 | 42 | - uses: r-lib/actions/setup-r@v2 43 | with: 44 | r-version: ${{ matrix.config.r }} 45 | 46 | - uses: r-lib/actions/setup-pandoc@v2 47 | 48 | - name: Query dependencies 49 | run: | 50 | install.packages('remotes') 51 | saveRDS(remotes::dev_package_deps(dependencies = TRUE), ".github/depends.Rds", version = 2) 52 | writeLines(sprintf("R-%i.%i", getRversion()$major, getRversion()$minor), ".github/R-version") 53 | shell: Rscript {0} 54 | 55 | - name: Cache R packages 56 | if: runner.os != 'Windows' 57 | uses: actions/cache@v2 58 | with: 59 | path: ${{ env.R_LIBS_USER }} 60 | key: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-${{ hashFiles('.github/depends.Rds') }} 61 | restore-keys: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1- 62 | 63 | - name: Install system dependencies 64 | if: runner.os == 'Linux' 65 | run: | 66 | while read -r cmd 67 | do 68 | eval sudo $cmd 69 | done < <(Rscript -e 'writeLines(remotes::system_requirements("ubuntu", "20.04"))') 70 | 71 | - name: Install dependencies 72 | run: | 73 | remotes::install_deps(dependencies = TRUE) 74 | remotes::install_cran(c("pkgbuild", "rcmdcheck", "drat")) 75 | shell: Rscript {0} 76 | 77 | - name: Check 78 | env: 79 | _R_CHECK_CRAN_INCOMING_REMOTE_: false 80 | _R_CHECK_FORCE_SUGGESTS_: false 81 | run: | 82 | pkg <- pkgbuild::build() 83 | rcmdcheck::rcmdcheck(pkg, args = c("--no-manual", "--as-cran"), error_on = "warning", check_dir = "check") 84 | shell: Rscript {0} 85 | 86 | - name: Upload check results 87 | if: failure() 88 | uses: actions/upload-artifact@main 89 | with: 90 | name: ${{ runner.os }}-r${{ matrix.config.r }}-results 91 | path: check 92 | 93 | - name: Update Cloudyr drat 94 | if: success() && github.repository_owner == 'Azure' && runner.os == 'Linux' && github.ref == 'refs/heads/master' 95 | env: 96 | token: "${{ secrets.ghPat }}" 97 | run: | 98 | cd .. 99 | export PKGBUILD_GZ=$(ls *.gz) 100 | mkdir drat 101 | cd drat 102 | git init 103 | git config user.email "dummy@example.com" 104 | git config user.name "Github Actions" 105 | git remote add upstream "https://token:$token@github.com/cloudyr/cloudyr.github.io.git" 106 | git fetch upstream 107 | git checkout master 108 | Rscript -e "drat::insertPackage('../$PKGBUILD_GZ', repodir='./drat')" 109 | git add --all 110 | git commit -m "add $PKGBUILD_GZ (build $GITHUB_RUN_NUMBER)" 111 | git push 112 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc 262 | 263 | .RHistory 264 | misc/ 265 | .Rproj.user 266 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 6 | 7 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: AzureGraph 2 | Title: Simple Interface to 'Microsoft Graph' 3 | Version: 1.3.4 4 | Authors@R: c( 5 | person("Hong", "Ooi", , "hongooi73@gmail.com", role = c("aut", "cre")), 6 | person("Microsoft", role="cph") 7 | ) 8 | Description: A simple interface to the 'Microsoft Graph' API . 'Graph' is a comprehensive framework for accessing data in various online Microsoft services. This package was originally intended to provide an R interface only to the 'Azure Active Directory' part, with a view to supporting interoperability of R and 'Azure': users, groups, registered apps and service principals. However it has since been expanded into a more general tool for interacting with Graph. Part of the 'AzureR' family of packages. 9 | URL: https://github.com/Azure/AzureGraph https://github.com/Azure/AzureR 10 | BugReports: https://github.com/Azure/AzureGraph/issues 11 | License: MIT + file LICENSE 12 | VignetteBuilder: knitr 13 | Depends: 14 | R (>= 3.3) 15 | Imports: 16 | AzureAuth (>= 1.0.1), 17 | utils, 18 | httr (>= 1.3), 19 | jsonlite, 20 | openssl, 21 | curl, 22 | R6 23 | Suggests: 24 | AzureRMR, 25 | vctrs, 26 | knitr, 27 | rmarkdown, 28 | testthat 29 | Roxygen: list(markdown=TRUE, r6=FALSE, old_usage=TRUE) 30 | RoxygenNote: 7.1.1 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2019-2021 2 | COPYRIGHT HOLDER: Microsoft 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2019 Microsoft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(az_app) 4 | export(az_device) 5 | export(az_directory_role) 6 | export(az_group) 7 | export(az_object) 8 | export(az_service_principal) 9 | export(az_user) 10 | export(call_batch_endpoint) 11 | export(call_graph_endpoint) 12 | export(call_graph_url) 13 | export(create_graph_login) 14 | export(delete_graph_login) 15 | export(extract_list_values) 16 | export(find_class_generator) 17 | export(format_public_fields) 18 | export(format_public_methods) 19 | export(get_graph_login) 20 | export(graph_request) 21 | export(is_aad_object) 22 | export(is_app) 23 | export(is_directory_role) 24 | export(is_empty) 25 | export(is_group) 26 | export(is_msgraph_object) 27 | export(is_service_principal) 28 | export(is_user) 29 | export(list_graph_logins) 30 | export(ms_graph) 31 | export(ms_graph_pager) 32 | export(ms_object) 33 | export(named_list) 34 | export(register_graph_class) 35 | import(AzureAuth) 36 | importFrom(utils,modifyList) 37 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # AzureGraph 1.3.4 2 | 3 | - Fix handling of OneDrive/SharePoint paths and other URLs that contain "#". Thanks to Jonathan Carroll (@jonocarroll). 4 | 5 | # AzureGraph 1.3.3 6 | 7 | - Paths containing a hash (#) are encoded in `call_graph_endpoint()` and no longer fail with 404 8 | 9 | # AzureGraph 1.3.2 10 | 11 | - Minor backend fixes. 12 | 13 | # AzureGraph 1.3.1 14 | 15 | - Fix a bug in `ms_object$get_list_pager()` where the `default_generator` argument wasn't being used. 16 | - Add basic print methods for the `ms_graph_pager` and `graph_request` R6 classes. 17 | - Add "Authentication basics" vignette providing more information on this topic. 18 | 19 | # AzureGraph 1.3.0 20 | 21 | - New API for working with paged result sets: 22 | - New `ms_graph_pager` R6 class, which is an _iterator_ for the pages in the result. 23 | - The `ms_object` base class now has a `get_list_pager()` method which returns an object of class `ms_graph_pager`. 24 | - New `extract_list_values()` function to get all or part of the results from a paged result set. 25 | - The current (private) `ms_object$get_paged_list()` and `ms_object$init_list_objects()` methods are retained for backward compatibility, but are otherwise deprecated. 26 | - The `ms_graph$get_user()` method can now get a user by email or display name. Similarly, the `get_group()` method can get a group by display name. 27 | - Fix a bug in retrieving a paged list of values as a data frame, when `n` (the maximum number of rows) is supplied. 28 | - New `ms_graph$get_aad_object()` method to retrieve an Azure Active Directory object by ID. Mostly intended for use with the `list_object_memberships()` and `list_group_memberships()` methods, which return only IDs and not full object information. 29 | - All `list_*` R6 methods now have `filter` and `n` arguments to filter the result set and cap the number of results. The default values are `filter=NULL` and `n=Inf`. If `n=NULL`, the `ms_graph_pager` iterator object is returned instead to allow manual iteration over the results. 30 | - Export the `find_class_generator()` function. 31 | - New "Batching and paging" vignette describing these APIs. 32 | - Add `list_users()`, `list_groups()`, `list_apps()` and `list_service_principals()` methods to the main `ms_graph` client class. 33 | 34 | # AzureGraph 1.2.2 35 | 36 | - Add support for batch requests: 37 | - Each individual request is stored in an object of R6 class `graph_request`. 38 | - Add `call_batch_endpoint()` function and `ms_graph$call_batch_endpoint()` method for calling the batch endpoint with a list of requests. 39 | - Handle throttling (HTTP 429 errors) gracefully. 40 | 41 | # AzureGraph 1.2.1 42 | 43 | - Allow setting an optional limit to the number of objects returned by the private `ms_object$get_paged_list()` method. 44 | - The private `ms_object$init_list_objects()` method now has a `...` argument to allow passing extra parameters to class constructors. 45 | - Add documentation on how to use `get_paged_list` and `init_list_objects`. 46 | 47 | # AzureGraph 1.2.0 48 | 49 | - Internal refactoring to support future extensibility, including transferring some utility functions from AzureRMR to here. 50 | - New "Extending AzureGraph" vignette, showing how to extend this package to represent other object types in Microsoft Graph. 51 | - Switch to AAD v2.0 as the default for authenticating. 52 | - Enhance `get_graph_login` to allow specifying scopes. 53 | 54 | # AzureGraph 1.1.2 55 | 56 | - Change maintainer email address. 57 | 58 | # AzureGraph 1.1.1 59 | 60 | - Switch to the v1.0 REST endpoint. 61 | 62 | # AzureGraph 1.1.0 63 | 64 | - Updated to use the new Graph API calls for managing app passwords. Call the `az_app$add_password()` method to add a password to an app, and `az_app$remove_password()` to remove it. As a security measure, app passwords can no longer be manually specified; instead all passwords are now auto-generated on the server with a cryptographically secure PRNG. 65 | - The `az_app$update_password()` method is defunct. 66 | - Better handling of app creation with certificates: 67 | - The `certificate` argument to `ms_graph$create_app()` can be the name of a .pfx or .pem file, an `openssl::cert` object, an `AzureKeyVault::stored_cert` object, or a raw or character vector containing the certificate. 68 | - New `az_app$add_certificate()` and `az_app$remove_certificate()` methods, matching `add_password` and `remove_password`. 69 | - Treat the access token as opaque; this prevents errors when logging in without an AAD tenant. 70 | 71 | # AzureGraph 1.0.5 72 | 73 | - Fix a bug in user methods for listing objects when the result is empty. 74 | - Fix a bug in retrieving users added to an Azure Active Directory (AAD) tenant from an external directory. 75 | 76 | # AzureGraph 1.0.4 77 | 78 | - Allow AAD v2.0 tokens to be used for authenticating. Note that AAD v1.0 is still the default and recommended version. 79 | - Use `utils::askYesNo` for confirmation prompts on R >= 3.5, eg when deleting objects; this fixes a bug in reading the input. As a side-effect, Windows users who are using RGUI.exe will see a popup dialog box instead of a message in the terminal. 80 | - Various other bug fixes. 81 | 82 | # AzureGraph 1.0.3 83 | 84 | - Improved handling of null object properties. 85 | 86 | # AzureGraph 1.0.2 87 | 88 | - Changes to login functionality to better accommodate AzureAuth options. As part of this, the `config_file` argument for `az_graph$new` has been removed; to use a configuration file, call the (recommended) `create_graph_login` function. 89 | 90 | # AzureGraph 1.0.1 91 | 92 | - Fix some bugs in the login functionality. 93 | - Add direct support for creating apps with certificate credentials. 94 | 95 | # AzureGraph 1.0.0 96 | 97 | - Submitted to CRAN 98 | -------------------------------------------------------------------------------- /R/AzureGraph.R: -------------------------------------------------------------------------------- 1 | #' @import AzureAuth 2 | #' @importFrom utils modifyList 3 | NULL 4 | 5 | utils::globalVariables(c("self", "private")) 6 | 7 | .onLoad <- function(libname, pkgname) 8 | { 9 | options(azure_graph_api_version="v1.0") 10 | invisible(NULL) 11 | } 12 | 13 | 14 | # default authentication app ID: leverage the az CLI 15 | .az_cli_app_id <- "04b07795-8ddb-461a-bbee-02f9e1bf7b46" 16 | 17 | # authentication app ID for personal accounts 18 | .azurer_graph_app_id <- "5bb21e8a-06bf-4ac4-b613-110ac0e582c1" 19 | -------------------------------------------------------------------------------- /R/az_app.r: -------------------------------------------------------------------------------- 1 | #' Registered app in Azure Active Directory 2 | #' 3 | #' Base class representing an AAD app. 4 | #' 5 | #' @docType class 6 | #' @section Fields: 7 | #' - `token`: The token used to authenticate with the Graph host. 8 | #' - `tenant`: The Azure Active Directory tenant for this app. 9 | #' - `type`: always "application" for an app object. 10 | #' - `properties`: The app properties. 11 | #' - `password`: The app password. Note that the Graph API does not return previously-generated passwords. This field will only be populated for an app object created with `ms_graph$create_app()`, or after a call to the `add_password()` method below. 12 | #' @section Methods: 13 | #' - `new(...)`: Initialize a new app object. Do not call this directly; see 'Initialization' below. 14 | #' - `delete(confirm=TRUE)`: Delete an app. By default, ask for confirmation first. 15 | #' - `update(...)`: Update the app data in Azure Active Directory. For what properties can be updated, consult the REST API documentation link below. 16 | #' - `do_operation(...)`: Carry out an arbitrary operation on the app. 17 | #' - `sync_fields()`: Synchronise the R object with the app data in Azure Active Directory. 18 | #' - `list_owners(type=c("user", "group", "application", "servicePrincipal"), filter=NULL, n=Inf)`: Return a list of all owners of this app. Specify the `type` argument to limit the result to specific object type(s). 19 | #' - `create_service_principal(...)`: Create a service principal for this app, by default in the current tenant. 20 | #' - `get_service_principal()`: Get the service principal for this app. 21 | #' - `delete_service_principal(confirm=TRUE)`: Delete the service principal for this app. By default, ask for confirmation first. 22 | #' - `add_password(password_name=NULL, password_duration=NULL)`: Adds a strong password. `password_duration` is the length of time in years that the password remains valid, with default duration 2 years. Returns the ID of the generated password. 23 | #' - `remove_password(password_id, confirm=TRUE)`: Removes the password with the given ID. By default, ask for confirmation first. 24 | #' - `add_certificate(certificate)`: Adds a certificate for authentication. This can be specified as the name of a .pfx or .pem file, an `openssl::cert` object, an `AzureKeyVault::stored_cert` object, or a raw or character vector. 25 | #' - `remove_certificate(certificate_id, confirm=TRUE`): Removes the certificate with the given ID. By default, ask for confirmation first. 26 | #' 27 | #' @section Initialization: 28 | #' Creating new objects of this class should be done via the `create_app` and `get_app` methods of the [ms_graph] class. Calling the `new()` method for this class only constructs the R object; it does not call the Microsoft Graph API to create the actual app. 29 | #' 30 | #' [Microsoft Graph overview](https://learn.microsoft.com/en-us/graph/overview), 31 | #' [REST API reference](https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-beta) 32 | #' 33 | #' @section List methods: 34 | #' All `list_*` methods have `filter` and `n` arguments to limit the number of results. The former should be an [OData expression](https://learn.microsoft.com/en-us/graph/query-parameters#filter-parameter) as a string to filter the result set on. The latter should be a number setting the maximum number of (filtered) results to return. The default values are `filter=NULL` and `n=Inf`. If `n=NULL`, the `ms_graph_pager` iterator object is returned instead to allow manual iteration over the results. 35 | #' 36 | #' Support in the underlying Graph API for OData queries is patchy. Not all endpoints that return lists of objects support filtering, and if they do, they may not allow all of the defined operators. If your filtering expression results in an error, you can carry out the operation without filtering and then filter the results on the client side. 37 | #' @seealso 38 | #' [ms_graph], [az_service_principal], [az_user], [az_group], [az_object] 39 | #' 40 | #' @examples 41 | #' \dontrun{ 42 | #' 43 | #' gr <- get_graph_login() 44 | #' app <- gr$create_app("MyNewApp") 45 | #' 46 | #' # password resetting: remove the old password, add a new one 47 | #' pwd_id <- app$properties$passwordCredentials[[1]]$keyId 48 | #' app$add_password() 49 | #' app$remove_password(pwd_id) 50 | #' 51 | #' # set a redirect URI 52 | #' app$update(publicClient=list(redirectUris=I("http://localhost:1410"))) 53 | #' 54 | #' # add API permission (access Azure Storage as user) 55 | #' app$update(requiredResourceAccess=list( 56 | #' list( 57 | #' resourceAppId="e406a681-f3d4-42a8-90b6-c2b029497af1", 58 | #' resourceAccess=list( 59 | #' list( 60 | #' id="03e0da56-190b-40ad-a80c-ea378c433f7f", 61 | #' type="Scope" 62 | #' ) 63 | #' ) 64 | #' ) 65 | #' )) 66 | #' 67 | #' # add a certificate from a .pem file 68 | #' app$add_certificate("cert.pem") 69 | #' 70 | #' # can also read the file into an openssl object, and then add the cert 71 | #' cert <- openssl::read_cert("cert.pem") 72 | #' app$add_certificate(cert) 73 | #' 74 | #' # add a certificate stored in Azure Key Vault 75 | #' vault <- AzureKeyVault::key_vault("mytenant") 76 | #' cert2 <- vault$certificates$get("certname") 77 | #' app$add_certificate(cert2) 78 | #' 79 | #' # change the app name 80 | #' app$update(displayName="MyRenamedApp") 81 | #' 82 | #' } 83 | #' @format An R6 object of class `az_app`, inheriting from `az_object`. 84 | #' @export 85 | az_app <- R6::R6Class("az_app", inherit=az_object, 86 | 87 | public=list( 88 | 89 | password=NULL, 90 | 91 | initialize=function(token, tenant=NULL, properties=NULL, password=NULL) 92 | { 93 | self$type <- "application" 94 | private$api_type <- "applications" 95 | self$password <- password 96 | super$initialize(token, tenant, properties) 97 | }, 98 | 99 | add_password=function(password_name=NULL, password_duration=NULL) 100 | { 101 | creds <- list() 102 | if(!is.null(password_name)) 103 | creds$displayName <- password_name 104 | if(!is.null(password_duration)) 105 | { 106 | now <- as.POSIXlt(Sys.time()) 107 | now$year <- now$year + password_duration 108 | creds$endDateTime <- format(as.POSIXct(now), "%Y-%m-%dT%H:%M:%SZ", tz="GMT") 109 | } 110 | 111 | properties <- if(!is_empty(creds)) 112 | list(passwordCredential=creds) 113 | else NULL 114 | 115 | res <- self$do_operation("addPassword", body=properties, http_verb="POST") 116 | self$properties <- self$do_operation() 117 | self$password <- res$secretText 118 | invisible(res$keyId) 119 | }, 120 | 121 | remove_password=function(password_id, confirm=TRUE) 122 | { 123 | if(confirm && interactive()) 124 | { 125 | msg <- sprintf("Do you really want to remove the password '%s'?", password_id) 126 | if(!get_confirmation(msg, FALSE)) 127 | return(invisible(NULL)) 128 | } 129 | 130 | self$do_operation("removePassword", body=list(keyId=password_id), http_verb="POST") 131 | self$sync_fields() 132 | invisible(NULL) 133 | }, 134 | 135 | add_certificate=function(certificate) 136 | { 137 | key <- read_cert(certificate) 138 | creds <- c(self$properties$keyCredentials, list(list( 139 | key=key, 140 | type="AsymmetricX509Cert", 141 | usage="verify" 142 | ))) 143 | 144 | self$update(keyCredentials=creds) 145 | }, 146 | 147 | remove_certificate=function(certificate_id, confirm=TRUE) 148 | { 149 | if(confirm && interactive()) 150 | { 151 | msg <- sprintf("Do you really want to remove the certificate '%s'?", certificate_id) 152 | if(!get_confirmation(msg, FALSE)) 153 | return(invisible(NULL)) 154 | } 155 | 156 | creds <- self$properties$keyCredentials 157 | idx <- vapply(creds, function(keycred) keycred$keyId == certificate_id, logical(1)) 158 | if(!any(idx)) 159 | stop("Certificate not found", call.=FALSE) 160 | 161 | self$update(keyCredentials=creds[-idx]) 162 | }, 163 | 164 | list_owners=function(type=c("user", "group", "application", "servicePrincipal"), filter=NULL, n=Inf) 165 | { 166 | opts <- list(`$filter`=filter, `$count`=if(!is.null(filter)) "true") 167 | hdrs <- if(!is.null(filter)) httr::add_headers(consistencyLevel="eventual") 168 | pager <- self$get_list_pager(self$do_operation("owners", options=opts, hdrs, type_filter=type)) 169 | extract_list_values(pager, n) 170 | }, 171 | 172 | create_service_principal=function(...) 173 | { 174 | properties <- modifyList(list(...), list(appId=self$properties$appId)) 175 | az_service_principal$new( 176 | self$token, 177 | self$tenant, 178 | call_graph_endpoint(self$token, "servicePrincipals", body=properties, encode="json", http_verb="POST") 179 | ) 180 | }, 181 | 182 | get_service_principal=function() 183 | { 184 | op <- sprintf("servicePrincipals?$filter=appId+eq+'%s'", self$properties$appId) 185 | az_service_principal$new( 186 | self$token, 187 | self$tenant, 188 | call_graph_endpoint(self$token, op)$value[[1]] 189 | ) 190 | }, 191 | 192 | delete_service_principal=function(confirm=TRUE) 193 | { 194 | self$get_service_principal()$delete(confirm=confirm) 195 | }, 196 | 197 | print=function(...) 198 | { 199 | cat("\n", sep="") 200 | cat(" app id:", self$properties$appId, "\n") 201 | cat(" directory id:", self$properties$id, "\n") 202 | cat(" domain:", self$properties$publisherDomain, "\n") 203 | cat("---\n") 204 | cat(format_public_methods(self)) 205 | invisible(self) 206 | } 207 | )) 208 | -------------------------------------------------------------------------------- /R/az_device.R: -------------------------------------------------------------------------------- 1 | #' Device in Azure Active Directory 2 | #' 3 | #' Class representing a registered device. 4 | #' 5 | #' @docType class 6 | #' @section Fields: 7 | #' - `token`: The token used to authenticate with the Graph host. 8 | #' - `tenant`: The Azure Active Directory tenant for this group. 9 | #' - `type`: always "device" for a device object. 10 | #' - `properties`: The device properties. 11 | #' @section Methods: 12 | #' - `new(...)`: Initialize a new device object. Do not call this directly; see 'Initialization' below. 13 | #' - `delete(confirm=TRUE)`: Delete a device. By default, ask for confirmation first. 14 | #' - `update(...)`: Update the device information in Azure Active Directory. 15 | #' - `do_operation(...)`: Carry out an arbitrary operation on the device. 16 | #' - `sync_fields()`: Synchronise the R object with the app data in Azure Active Directory. 17 | #' 18 | #' @section Initialization: 19 | #' Create objects of this class via the `list_registered_devices()` and `list_owned_devices()` methods of the `az_user` class. 20 | #' 21 | #' @seealso 22 | #' [ms_graph], [az_user], [az_object] 23 | #' 24 | #' [Microsoft Graph overview](https://learn.microsoft.com/en-us/graph/overview), 25 | #' [REST API reference](https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0) 26 | #' 27 | #' @format An R6 object of class `az_device`, inheriting from `az_object`. 28 | #' @export 29 | az_device <- R6::R6Class("az_device", inherit=az_object, 30 | 31 | public=list( 32 | 33 | initialize=function(token, tenant=NULL, properties=NULL) 34 | { 35 | self$type <- "device" 36 | super$initialize(token, tenant, properties) 37 | }, 38 | 39 | print=function(...) 40 | { 41 | cat("\n", sep="") 42 | cat(" directory id:", self$properties$id, "\n") 43 | cat(" device id:", self$properties$deviceId, "\n") 44 | invisible(self) 45 | } 46 | )) 47 | -------------------------------------------------------------------------------- /R/az_dir_role.R: -------------------------------------------------------------------------------- 1 | #' Directory role 2 | #' 3 | #' Class representing a role in Azure Active Directory. 4 | #' 5 | #' @docType class 6 | #' @section Fields: 7 | #' - `token`: The token used to authenticate with the Graph host. 8 | #' - `tenant`: The Azure Active Directory tenant for this role. 9 | #' - `type`: always "directory role" for a directory role object. 10 | #' - `properties`: The item properties. 11 | #' @section Methods: 12 | #' - `new(...)`: Initialize a new object. Do not call this directly; see 'Initialization' below. 13 | #' - `delete(confirm=TRUE)`: Delete this item. By default, ask for confirmation first. 14 | #' - `update(...)`: Update the item's properties in Microsoft Graph. 15 | #' - `do_operation(...)`: Carry out an arbitrary operation on the item. 16 | #' - `sync_fields()`: Synchronise the R object with the item metadata in Microsoft Graph. 17 | #' - `list_members(filter=NULL, n=Inf)`: Return a list of all members of this group. 18 | #' 19 | #' @section Initialization: 20 | #' Currently support for directory roles is limited. Objects of this class should not be initialized directly. 21 | #' 22 | #' @section List methods: 23 | #' All `list_*` methods have `filter` and `n` arguments to limit the number of results. The former should be an [OData expression](https://learn.microsoft.com/en-us/graph/query-parameters#filter-parameter) as a string to filter the result set on. The latter should be a number setting the maximum number of (filtered) results to return. The default values are `filter=NULL` and `n=Inf`. If `n=NULL`, the `ms_graph_pager` iterator object is returned instead to allow manual iteration over the results. 24 | #' 25 | #' Support in the underlying Graph API for OData queries is patchy. Not all endpoints that return lists of objects support filtering, and if they do, they may not allow all of the defined operators. If your filtering expression results in an error, you can carry out the operation without filtering and then filter the results on the client side. 26 | #' @seealso 27 | #' [ms_graph], [az_user] 28 | #' 29 | #' [Microsoft Graph overview](https://learn.microsoft.com/en-us/graph/overview), 30 | #' [REST API reference](https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0) 31 | #' 32 | #' @format An R6 object of class `az_directory_role`, inheriting from `az_object`. 33 | #' @export 34 | az_directory_role <- R6::R6Class("az_directory_role", inherit=az_object, 35 | 36 | public=list( 37 | 38 | initialize=function(token, tenant=NULL, properties=NULL) 39 | { 40 | self$type <- "directory role" 41 | private$api_type <- "directoryRoles" 42 | super$initialize(token, tenant, properties) 43 | }, 44 | 45 | list_members=function(filter=NULL, n=Inf) 46 | { 47 | opts <- list(`$filter`=filter, `$count`=if(!is.null(filter)) "true") 48 | hdrs <- if(!is.null(filter)) httr::add_headers(consistencyLevel="eventual") 49 | pager <- self$get_list_pager(self$do_operation("members", options=opts, hdrs)) 50 | extract_list_values(pager, n) 51 | }, 52 | 53 | print=function(...) 54 | { 55 | cat("\n", sep="") 56 | cat(" directory id:", self$properties$id, "\n") 57 | cat(" description:", self$properties$description, "\n") 58 | cat("---\n") 59 | cat(format_public_methods(self)) 60 | invisible(self) 61 | } 62 | )) 63 | -------------------------------------------------------------------------------- /R/az_group.R: -------------------------------------------------------------------------------- 1 | #' Group in Azure Active Directory 2 | #' 3 | #' Class representing an AAD group. 4 | #' 5 | #' @docType class 6 | #' @section Fields: 7 | #' - `token`: The token used to authenticate with the Graph host. 8 | #' - `tenant`: The Azure Active Directory tenant for this group. 9 | #' - `type`: always "group" for a group object. 10 | #' - `properties`: The group properties. 11 | #' @section Methods: 12 | #' - `new(...)`: Initialize a new group object. Do not call this directly; see 'Initialization' below. 13 | #' - `delete(confirm=TRUE)`: Delete a group. By default, ask for confirmation first. 14 | #' - `update(...)`: Update the group information in Azure Active Directory. 15 | #' - `do_operation(...)`: Carry out an arbitrary operation on the group. 16 | #' - `sync_fields()`: Synchronise the R object with the app data in Azure Active Directory. 17 | #' - `list_members(type=c("user", "group", "application", "servicePrincipal"), filter=NULL, n=Inf)`: Return a list of all members of this group. Specify the `type` argument to limit the result to specific object type(s). 18 | #' - `list_owners(type=c("user", "group", "application", "servicePrincipal"), filter=NULL, n=Inf)`: Return a list of all owners of this group. Specify the `type` argument to limit the result to specific object type(s). 19 | #' 20 | #' @section Initialization: 21 | #' Creating new objects of this class should be done via the `create_group` and `get_group` methods of the [ms_graph] and [az_app] classes. Calling the `new()` method for this class only constructs the R object; it does not call the Microsoft Graph API to create the actual group. 22 | #' 23 | #' @section List methods: 24 | #' All `list_*` methods have `filter` and `n` arguments to limit the number of results. The former should be an [OData expression](https://learn.microsoft.com/en-us/graph/query-parameters#filter-parameter) as a string to filter the result set on. The latter should be a number setting the maximum number of (filtered) results to return. The default values are `filter=NULL` and `n=Inf`. If `n=NULL`, the `ms_graph_pager` iterator object is returned instead to allow manual iteration over the results. 25 | #' 26 | #' Support in the underlying Graph API for OData queries is patchy. Not all endpoints that return lists of objects support filtering, and if they do, they may not allow all of the defined operators. If your filtering expression results in an error, you can carry out the operation without filtering and then filter the results on the client side. 27 | #' @seealso 28 | #' [ms_graph], [az_app], [az_user], [az_object] 29 | #' 30 | #' [Microsoft Graph overview](https://learn.microsoft.com/en-us/graph/overview), 31 | #' [REST API reference](https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0) 32 | #' 33 | #' @examples 34 | #' \dontrun{ 35 | #' 36 | #' gr <- get_graph_login() 37 | #' usr <- gr$get_user("myname@aadtenant.com") 38 | #' 39 | #' grps <- usr$list_group_memberships() 40 | #' grp <- gr$get_group(grps[1]) 41 | #' 42 | #' grp$list_members() 43 | #' grp$list_owners() 44 | #' 45 | #' # capping the number of results 46 | #' grp$list_members(n=10) 47 | #' 48 | #' # get the pager object for a listing method 49 | #' pager <- grp$list_members(n=NULL) 50 | #' pager$value 51 | #' 52 | #' } 53 | #' @format An R6 object of class `az_group`, inheriting from `az_object`. 54 | #' @export 55 | az_group <- R6::R6Class("az_group", inherit=az_object, 56 | 57 | public=list( 58 | 59 | initialize=function(token, tenant=NULL, properties=NULL) 60 | { 61 | self$type <- "group" 62 | private$api_type <- "groups" 63 | super$initialize(token, tenant, properties) 64 | }, 65 | 66 | list_members=function(type=c("user", "group", "application", "servicePrincipal"), filter=NULL, n=Inf) 67 | { 68 | opts <- list(`$filter`=filter, `$count`=if(!is.null(filter)) "true") 69 | hdrs <- if(!is.null(filter)) httr::add_headers(consistencyLevel="eventual") 70 | pager <- self$get_list_pager(self$do_operation("members", options=opts, hdrs), type_filter=type) 71 | extract_list_values(pager, n) 72 | }, 73 | 74 | list_owners=function(type=c("user", "group", "application", "servicePrincipal"), filter=NULL, n=Inf) 75 | { 76 | opts <- list(`$filter`=filter, `$count`=if(!is.null(filter)) "true") 77 | hdrs <- if(!is.null(filter)) httr::add_headers(consistencyLevel="eventual") 78 | pager <- self$get_list_pager(self$do_operation("owners", options=opts, hdrs), type_filter=type) 79 | extract_list_values(pager, n) 80 | }, 81 | 82 | print=function(...) 83 | { 84 | group_type <- if("Unified" %in% self$properties$groupTypes) 85 | "Microsoft 365" 86 | else if(!self$properties$mailEnabled) 87 | "Security" 88 | else if(self$properties$securityEnabled) 89 | "Mail-enabled security" 90 | else "Distribution" 91 | cat("<", group_type, " group '", self$properties$displayName, "'>\n", sep="") 92 | cat(" directory id:", self$properties$id, "\n") 93 | cat(" description:", self$properties$description, "\n") 94 | cat("---\n") 95 | cat(format_public_methods(self)) 96 | invisible(self) 97 | } 98 | )) 99 | -------------------------------------------------------------------------------- /R/az_object.R: -------------------------------------------------------------------------------- 1 | #' Azure Active Directory object 2 | #' 3 | #' Base class representing an Azure Active Directory object in Microsoft Graph. 4 | #' 5 | #' @docType class 6 | #' @section Fields: 7 | #' - `token`: The token used to authenticate with the Graph host. 8 | #' - `tenant`: The Azure Active Directory tenant for this object. 9 | #' - `type`: The type of object: user, group, application or service principal. 10 | #' - `properties`: The object properties. 11 | #' @section Methods: 12 | #' - `new(...)`: Initialize a new directory object. Do not call this directly; see 'Initialization' below. 13 | #' - `delete(confirm=TRUE)`: Delete an object. By default, ask for confirmation first. 14 | #' - `update(...)`: Update the object information in Azure Active Directory. 15 | #' - `do_operation(...)`: Carry out an arbitrary operation on the object. 16 | #' - `sync_fields()`: Synchronise the R object with the data in Azure Active Directory. 17 | #' - `list_group_memberships(security_only=FALSE, filter=NULL, n=Inf)`: Return the IDs of all groups this object is a member of. If `security_only` is TRUE, only security group IDs are returned. 18 | #' - `list_object_memberships(security_only=FALSE, filter=NULL, n=Inf)`: Return the IDs of all groups, administrative units and directory roles this object is a member of. 19 | #' 20 | #' @section Initialization: 21 | #' Objects of this class should not be created directly. Instead, create an object of the appropriate subclass: [az_app], [az_service_principal], [az_user], [az_group]. 22 | #' 23 | #' @section List methods: 24 | #' All `list_*` methods have `filter` and `n` arguments to limit the number of results. The former should be an [OData expression](https://learn.microsoft.com/en-us/graph/query-parameters#filter-parameter) as a string to filter the result set on. The latter should be a number setting the maximum number of (filtered) results to return. The default values are `filter=NULL` and `n=Inf`. If `n=NULL`, the `ms_graph_pager` iterator object is returned instead to allow manual iteration over the results. 25 | #' 26 | #' Support in the underlying Graph API for OData queries is patchy. Not all endpoints that return lists of objects support filtering, and if they do, they may not allow all of the defined operators. If your filtering expression results in an error, you can carry out the operation without filtering and then filter the results on the client side. 27 | #' @seealso 28 | #' [ms_graph], [az_app], [az_service_principal], [az_user], [az_group] 29 | #' 30 | #' [Microsoft Graph overview](https://learn.microsoft.com/en-us/graph/overview), 31 | #' [REST API reference](https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0) 32 | #' 33 | #' @format An R6 object of class `az_object`, inheriting from `ms_object`. 34 | #' @export 35 | az_object <- R6::R6Class("az_object", inherit=ms_object, 36 | 37 | public=list( 38 | 39 | list_object_memberships=function(security_only=FALSE, filter=NULL, n=Inf) 40 | { 41 | if(!is.null(filter)) 42 | stop("This method doesn't support filtering", call.=FALSE) 43 | body <- list(securityEnabledOnly=security_only) 44 | pager <- self$get_list_pager(self$do_operation("getMemberObjects", body=body, http_verb="POST"), 45 | generate_objects=FALSE) 46 | unlist(extract_list_values(pager, n)) 47 | }, 48 | 49 | list_group_memberships=function(security_only=FALSE, filter=NULL, n=Inf) 50 | { 51 | if(!is.null(filter)) 52 | stop("This method doesn't support filtering", call.=FALSE) 53 | body <- list(securityEnabledOnly=security_only) 54 | pager <- self$get_list_pager(self$do_operation("getMemberGroups", body=body, http_verb="POST"), 55 | generate_objects=FALSE) 56 | unlist(extract_list_values(pager, n)) 57 | }, 58 | 59 | print=function(...) 60 | { 61 | cat("\n", sep="") 62 | cat(" directory id:", self$properties$id, "\n") 63 | cat("---\n") 64 | cat(format_public_methods(self)) 65 | invisible(self) 66 | } 67 | )) 68 | 69 | -------------------------------------------------------------------------------- /R/az_svc_principal.R: -------------------------------------------------------------------------------- 1 | #' Service principal in Azure Active Directory 2 | #' 3 | #' Class representing an AAD service principal. 4 | #' 5 | #' @docType class 6 | #' @section Fields: 7 | #' - `token`: The token used to authenticate with the Graph host. 8 | #' - `tenant`: The Azure Active Directory tenant for this service principal. 9 | #' - `type`: always "service principal" for a service principal object. 10 | #' - `properties`: The service principal properties. 11 | #' @section Methods: 12 | #' - `new(...)`: Initialize a new service principal object. Do not call this directly; see 'Initialization' below. 13 | #' - `delete(confirm=TRUE)`: Delete a service principal. By default, ask for confirmation first. 14 | #' - `update(...)`: Update the service principal information in Azure Active Directory. 15 | #' - `do_operation(...)`: Carry out an arbitrary operation on the service principal. 16 | #' - `sync_fields()`: Synchronise the R object with the service principal data in Azure Active Directory. 17 | #' 18 | #' @section Initialization: 19 | #' Creating new objects of this class should be done via the `create_service_principal` and `get_service_principal` methods of the [ms_graph] and [az_app] classes. Calling the `new()` method for this class only constructs the R object; it does not call the Microsoft Graph API to create the actual service principal. 20 | #' 21 | #' @seealso 22 | #' [ms_graph], [az_app], [az_object] 23 | #' 24 | #' [Azure Microsoft Graph overview](https://learn.microsoft.com/en-us/graph/overview), 25 | #' [REST API reference](https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0) 26 | #' 27 | #' @format An R6 object of class `az_service_principal`, inheriting from `az_object`. 28 | #' @export 29 | az_service_principal <- R6::R6Class("az_service_principal", inherit=az_object, 30 | 31 | public=list( 32 | 33 | initialize=function(token, tenant=NULL, properties=NULL) 34 | { 35 | self$type <- "service principal" 36 | private$api_type <- "servicePrincipals" 37 | super$initialize(token, tenant, properties) 38 | }, 39 | 40 | print=function(...) 41 | { 42 | cat("\n", sep="") 43 | cat(" app id:", self$properties$appId, "\n") 44 | cat(" directory id:", self$properties$id, "\n") 45 | cat(" app tenant:", self$properties$appOwnerOrganizationId, "\n") 46 | cat("---\n") 47 | cat(format_public_methods(self)) 48 | invisible(self) 49 | } 50 | )) 51 | -------------------------------------------------------------------------------- /R/az_user.R: -------------------------------------------------------------------------------- 1 | #' User in Azure Active Directory 2 | #' 3 | #' Class representing an AAD user account. 4 | #' 5 | #' @docType class 6 | #' @section Fields: 7 | #' - `token`: The token used to authenticate with the Graph host. 8 | #' - `tenant`: The Azure Active Directory tenant for this user. 9 | #' - `type`: always "user" for a user object. 10 | #' - `properties`: The user properties. 11 | #' @section Methods: 12 | #' - `new(...)`: Initialize a new user object. Do not call this directly; see 'Initialization' below. 13 | #' - `delete(confirm=TRUE)`: Delete a user account. By default, ask for confirmation first. 14 | #' - `update(...)`: Update the user information in Azure Active Directory. 15 | #' - `do_operation(...)`: Carry out an arbitrary operation on the user account. 16 | #' - `sync_fields()`: Synchronise the R object with the app data in Azure Active Directory. 17 | #' - `list_direct_memberships(filter=NULL, n=Inf)`: List the groups and directory roles this user is a direct member of. 18 | #' - `list_owned_objects(type=c("user", "group", "application", "servicePrincipal"), filter=NULL, n=Inf)`: List directory objects (groups/apps/service principals) owned by this user. Specify the `type` argument to limit the result to specific object type(s). 19 | #' - `list_created_objects(type=c("user", "group", "application", "servicePrincipal"), filter=NULL, n=Inf)`: List directory objects (groups/apps/service principals) created by this user. Specify the `type` argument to limit the result to specific object type(s). 20 | #' - `list_owned_devices(filter=NULL, n=Inf)`: List the devices owned by this user. 21 | #' - `list_registered_devices(filter=NULL, n=Inf)`: List the devices registered by this user. 22 | #' - `reset_password(password=NULL, force_password_change=TRUE)`: Resets a user password. By default the new password will be randomly generated, and must be changed at next login. 23 | #' 24 | #' @section Initialization: 25 | #' Creating new objects of this class should be done via the `create_user` and `get_user` methods of the [ms_graph] and [az_app] classes. Calling the `new()` method for this class only constructs the R object; it does not call the Microsoft Graph API to create the actual user account. 26 | #' 27 | #' @section List methods: 28 | #' All `list_*` methods have `filter` and `n` arguments to limit the number of results. The former should be an [OData expression](https://learn.microsoft.com/en-us/graph/query-parameters#filter-parameter) as a string to filter the result set on. The latter should be a number setting the maximum number of (filtered) results to return. The default values are `filter=NULL` and `n=Inf`. If `n=NULL`, the `ms_graph_pager` iterator object is returned instead to allow manual iteration over the results. 29 | #' 30 | #' Support in the underlying Graph API for OData queries is patchy. Not all endpoints that return lists of objects support filtering, and if they do, they may not allow all of the defined operators. If your filtering expression results in an error, you can carry out the operation without filtering and then filter the results on the client side. 31 | #' @seealso 32 | #' [ms_graph], [az_app], [az_group], [az_device], [az_object] 33 | #' 34 | #' [Microsoft Graph overview](https://learn.microsoft.com/en-us/graph/overview), 35 | #' [REST API reference](https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0) 36 | #' 37 | #' @examples 38 | #' \dontrun{ 39 | #' 40 | #' gr <- get_graph_login() 41 | #' 42 | #' # my user account 43 | #' gr$get_user() 44 | #' 45 | #' # another user account 46 | #' usr <- gr$get_user("myname@aadtenant.com") 47 | #' 48 | #' grps <- usr$list_direct_memberships() 49 | #' head(grps) 50 | #' 51 | #' # owned objects 52 | #' usr$list_owned_objects() 53 | #' 54 | #' # owned apps and service principals 55 | #' usr$list_owned_objects(type=c("application", "servicePrincipal")) 56 | #' 57 | #' # first 5 objects 58 | #' usr$list_owned_objects(n=5) 59 | #' 60 | #' # get the pager object 61 | #' pager <- usr$list_owned_objects(n=NULL) 62 | #' pager$value 63 | #' 64 | #' } 65 | #' @format An R6 object of class `az_user`, inheriting from `az_object`. 66 | #' @export 67 | az_user <- R6::R6Class("az_user", inherit=az_object, 68 | 69 | public=list( 70 | 71 | password=NULL, 72 | 73 | initialize=function(token, tenant=NULL, properties=NULL, password=NULL) 74 | { 75 | self$type <- "user" 76 | private$api_type <- "users" 77 | self$password <- password 78 | super$initialize(token, tenant, properties) 79 | }, 80 | 81 | reset_password=function(password=NULL, force_password_change=TRUE) 82 | { 83 | if(is.null(password)) 84 | password <- openssl::base64_encode(openssl::rand_bytes(40)) 85 | 86 | properties <- modifyList(properties, list( 87 | passwordProfile=list( 88 | password=password, 89 | forceChangePasswordNextSignIn=force_password_change, 90 | forceChangePasswordNextSignInWithMfa=FALSE 91 | ) 92 | )) 93 | 94 | self$do_operation(body=properties, encode="json", http_verb="PATCH") 95 | self$properties <- self$do_operation() 96 | self$password <- password 97 | password 98 | }, 99 | 100 | list_owned_objects=function(type=c("user", "group", "application", "servicePrincipal"), filter=NULL, n=Inf) 101 | { 102 | opts <- list(`$filter`=filter, `$count`=if(!is.null(filter)) "true") 103 | hdrs <- if(!is.null(filter)) httr::add_headers(consistencyLevel="eventual") 104 | pager <- self$get_list_pager(self$do_operation("ownedObjects", options=opts, hdrs), type_filter=type) 105 | extract_list_values(pager, n) 106 | }, 107 | 108 | list_created_objects=function(type=c("user", "group", "application", "servicePrincipal"), filter=NULL, n=Inf) 109 | { 110 | opts <- list(`$filter`=filter, `$count`=if(!is.null(filter)) "true") 111 | hdrs <- if(!is.null(filter)) httr::add_headers(consistencyLevel="eventual") 112 | pager <- self$get_list_pager(self$do_operation("createdObjects", options=opts, hdrs), type_filter=type) 113 | extract_list_values(pager, n) 114 | }, 115 | 116 | list_owned_devices=function(filter=NULL, n=Inf) 117 | { 118 | opts <- list(`$filter`=filter, `$count`=if(!is.null(filter)) "true") 119 | hdrs <- if(!is.null(filter)) httr::add_headers(consistencyLevel="eventual") 120 | pager <- self$get_list_pager(self$do_operation("ownedDevices", options=opts, hdrs)) 121 | extract_list_values(pager, n) 122 | }, 123 | 124 | list_direct_memberships=function(filter=NULL, n=Inf) 125 | { 126 | opts <- list(`$filter`=filter, `$count`=if(!is.null(filter)) "true") 127 | hdrs <- if(!is.null(filter)) httr::add_headers(consistencyLevel="eventual") 128 | pager <- self$get_list_pager(self$do_operation("memberOf", options=opts, hdrs)) 129 | extract_list_values(pager, n) 130 | }, 131 | 132 | print=function(...) 133 | { 134 | cat("\n", sep="") 135 | cat(" principal name:", self$properties$userPrincipalName, "\n") 136 | cat(" email:", self$properties$mail, "\n") 137 | cat(" directory id:", self$properties$id, "\n") 138 | cat("---\n") 139 | cat(format_public_methods(self)) 140 | invisible(self) 141 | } 142 | )) 143 | -------------------------------------------------------------------------------- /R/batch.R: -------------------------------------------------------------------------------- 1 | #' Microsoft Graph request 2 | #' 3 | #' Class representing a request to the Microsoft Graph API. Currently this is used only in building a batch call. 4 | #' 5 | #' @docType class 6 | #' @section Methods: 7 | #' - `new(...)`: Initialize a new request object with the given parameters. See 'Details' below. 8 | #' - `batchify()`: Generate a list object suitable for incorporating into a call to the batch endpoint. 9 | #' @section Details: 10 | #' The `initialize()` method takes the following arguments, representing the components of a HTTPS request: 11 | #' - `op`: The path of the HTTPS URL, eg `/me/drives`. 12 | #' - `body`: The body of the HTTPS request, if it is a PUT, POST or PATCH. 13 | #' - `options`: A list containing the query parameters for the URL. 14 | #' - `headers`: Any optional HTTP headers for the request. 15 | #' - `encode`: If a request body is present, how it should be encoded when sending it to the endpoint. The default is `json`, meaning it will be sent as JSON text; an alternative is `raw`, for binary data. 16 | #' - `http_verb`: One of "GET" (the default), "DELETE", "PUT", "POST", "HEAD", or "PATCH". 17 | #' 18 | #' This class is currently used only for building batch calls. Future versions of AzureGraph may be refactored to use it in general API calls as well. 19 | #' @seealso 20 | #' [call_batch_endpoint] 21 | #' 22 | #' [Microsoft Graph overview](https://learn.microsoft.com/en-us/graph/overview), 23 | #' [Batch endpoint documentation](https://learn.microsoft.com/en-us/graph/json-batching) 24 | #' 25 | #' @examples 26 | #' graph_request$new("me") 27 | #' 28 | #' # a new email message in Outlook 29 | #' graph_request$new("me/messages", 30 | #' body=list( 31 | #' body=list( 32 | #' content="Hello from R", 33 | #' content_type="text" 34 | #' ), 35 | #' subject="Hello", 36 | #' toRecipients="bob@example.com" 37 | #' ), 38 | #' http_verb="POST" 39 | #' ) 40 | #' @format An R6 object of class `graph_request`. 41 | #' @export 42 | graph_request <- R6::R6Class("graph_request", 43 | 44 | public=list( 45 | method=NULL, 46 | op=NULL, 47 | options=list(), 48 | headers=list(), 49 | body=NULL, 50 | encode=NULL, 51 | 52 | initialize=function(op, body=NULL, options=list(), headers=list(), encode="json", 53 | http_verb=c("GET", "DELETE", "PUT", "POST", "HEAD", "PATCH")) 54 | { 55 | self$op <- op 56 | self$method <- match.arg(http_verb) 57 | self$options <- options 58 | self$headers <- headers 59 | self$body <- body 60 | self$encode <- encode 61 | }, 62 | 63 | batchify=function() 64 | { 65 | url <- httr::parse_url("foo://bar") # dummy scheme and host 66 | url$path <- self$op 67 | url$query <- self$options 68 | url <- httr::build_url(url) 69 | req <- list( 70 | id=NULL, 71 | method=self$method, 72 | url=substr(url, 10, nchar(url)) 73 | ) 74 | hdrs <- self$headers 75 | if(!is_empty(self$body)) 76 | { 77 | hdrs$`Content-Type` <- if(self$encode == "json") 78 | "application/json" 79 | else if(self$encode == "raw") 80 | "application/octet-stream" 81 | else self$encode 82 | } 83 | if(!is_empty(hdrs)) 84 | req$headers <- hdrs 85 | if(!is_empty(self$body)) 86 | req$body <- self$body 87 | req 88 | }, 89 | 90 | print=function(...) 91 | { 92 | path <- httr::parse_url(self$op) 93 | path$query <- self$options 94 | path <- sub("^://", "", httr::build_url(path)) 95 | cat("\n") 96 | cat(" path:", self$method, path, "\n") 97 | invisible(self) 98 | } 99 | )) 100 | 101 | 102 | #' Call the Graph API batch endpoint 103 | #' 104 | #' @param token An Azure OAuth token, of class [AzureToken]. 105 | #' @param requests A list of [graph_request] objects, representing individual requests to the Graph API. 106 | #' @param depends_on An optional named vector, or TRUE. See below. 107 | #' @param api_version The API version to use, which will form part of the URL sent to the host. 108 | #' 109 | #' @details 110 | #' Use this function to combine multiple requests into a single HTTPS call. This can save significant network latency. 111 | #' 112 | #' The `depends_on` argument specifies the dependencies that may exist between requests. The default is to treat the requests as independent, which allows them to be executed in parallel. If `depends_on` is TRUE, each request is specified as depending on the immediately preceding request. Otherwise, this should be a named vector or list that gives the dependency or dependencies for each request. 113 | #' 114 | #' There are 2 restrictions on `depends_on`: 115 | #' - If one request has a dependency, then all requests must have dependencies specified 116 | #' - A request can only depend on previous requests in the list, not on later ones. 117 | #' 118 | #' A request list that has dependencies will be executed serially. 119 | #' 120 | #' @return 121 | #' A list containing the responses to each request. Each item has components `id` and `status` at a minimum. It may also contain `headers` and `body`, depending on the specifics of the request. 122 | #' @seealso 123 | #' [graph_request], [call_graph_endpoint] 124 | #' 125 | #' [Microsoft Graph overview](https://learn.microsoft.com/en-us/graph/overview), 126 | #' [Batch endpoint documentation](https://learn.microsoft.com/en-us/graph/json-batching) 127 | #' 128 | #' [OData documentation on batch requests](https://docs.oasis-open.org/odata/odata-json-format/v4.01/odata-json-format-v4.01.html#sec_BatchRequestsandResponses) 129 | #' 130 | #' @examples 131 | #' \dontrun{ 132 | #' 133 | #' req1 <- graph_request$new("me") 134 | #' 135 | #' # a new email message in Outlook 136 | #' req_create <- graph_request$new("me/messages", 137 | #' body=list( 138 | #' body=list( 139 | #' content="Hello from R", 140 | #' content_type="text" 141 | #' ), 142 | #' subject="Hello", 143 | #' toRecipients="bob@example.com" 144 | #' ), 145 | #' http_verb="POST" 146 | #' ) 147 | #' 148 | #' # messages in drafts 149 | #' req_get <- graph_request$new("me/mailFolders/drafts/messages") 150 | #' 151 | #' # requests are dependent: 2nd list of drafts will include just-created message 152 | #' call_batch_endpoint(token, list(req_get, req_create, req_get), depends_on=TRUE) 153 | #' 154 | #' # alternate way: enumerate all requests 155 | #' call_batch_endpoint(token, list(req_get, req_create, req_get), depends_on=c("2"=1, "3"=2)) 156 | #' 157 | #' } 158 | #' @export 159 | call_batch_endpoint <- function(token, requests=list(), depends_on=NULL, 160 | api_version=getOption("azure_graph_api_version")) 161 | { 162 | if(is_empty(requests)) 163 | return(invisible(NULL)) 164 | 165 | for(req in requests) 166 | if(!inherits(req, "graph_request")) 167 | stop("Must supply a list of request objects", call.=FALSE) 168 | 169 | if(length(requests) > 20) 170 | stop("Maximum of 20 requests per batch job", call.=FALSE) 171 | 172 | ids <- as.character(seq_along(requests)) 173 | 174 | # populate the batch request body 175 | reqlst <- lapply(requests, function(req) req$batchify()) 176 | for(i in seq_along(reqlst)) 177 | reqlst[[i]]$id <- as.character(i) 178 | 179 | # insert depends_on if required 180 | if(isTRUE(depends_on)) 181 | { 182 | for(i in seq_along(requests)[-1]) 183 | reqlst[[i]]$dependsOn <- I(as.character(i - 1)) 184 | } 185 | else if(!is_empty(depends_on)) 186 | { 187 | names_depends <- names(depends_on) 188 | if(is.null(names_depends) || any(names_depends == "")) 189 | stop("'depends_on' should be TRUE or a named vector identifying dependencies") 190 | 191 | for(i in seq_along(depends_on)) 192 | { 193 | id <- as.numeric(names_depends)[i] 194 | reqlst[[id]]$dependsOn <- I(as.character(depends_on[[i]])) 195 | } 196 | } 197 | 198 | reslst <- call_graph_endpoint(token, "$batch", body=list(requests=reqlst), 199 | http_verb="POST", api_version=api_version)$responses 200 | 201 | reslst <- reslst[order(sapply(reslst, `[[`, "id"))] 202 | err_msgs <- lapply(reslst, function(res) 203 | { 204 | if(res$status >= 300) 205 | error_message(res$body) 206 | else NULL 207 | }) 208 | errs <- !sapply(err_msgs, is.null) 209 | if(any(errs)) 210 | stop("Graph batch job encountered errors on requests ", paste(which(errs), collapse=", "), 211 | "\nMessages:\n", 212 | paste(unlist(err_msgs[errs]), collapse="\n"), 213 | call.=FALSE) 214 | reslst 215 | } 216 | 217 | -------------------------------------------------------------------------------- /R/call_graph.R: -------------------------------------------------------------------------------- 1 | #' Call the Microsoft Graph REST API 2 | #' 3 | #' @param token An Azure OAuth token, of class [AzureToken]. 4 | #' @param operation The operation to perform, which will form part of the URL path. 5 | #' @param options A named list giving the URL query parameters. 6 | #' @param api_version The API version to use, which will form part of the URL sent to the host. 7 | #' @param url A complete URL to send to the host. 8 | #' @param http_verb The HTTP verb as a string, one of `GET`, `PUT`, `POST`, `DELETE`, `HEAD` or `PATCH`. 9 | #' @param http_status_handler How to handle in R the HTTP status code of a response. `"stop"`, `"warn"` or `"message"` will call the appropriate handlers in httr, while `"pass"` ignores the status code. 10 | #' @param simplify Whether to turn arrays of objects in the JSON response into data frames. Set this to `TRUE` if you are expecting the endpoint to return tabular data and you want a tabular result, as opposed to a list of objects. 11 | #' @param auto_refresh Whether to refresh/renew the OAuth token if it is no longer valid. 12 | #' @param body The body of the request, for `PUT`/`POST`/`PATCH`. 13 | #' @param encode The encoding (really content-type) for the request body. The default value "json" means to serialize a list body into a JSON object. If you pass an already-serialized JSON object as the body, set `encode` to "raw". 14 | #' @param ... Other arguments passed to lower-level code, ultimately to the appropriate functions in httr. 15 | #' 16 | #' @details 17 | #' These functions form the low-level interface between R and Microsoft Graph. `call_graph_endpoint` forms a URL from its arguments and passes it to `call_graph_url`. 18 | #' 19 | #' If `simplify` is `TRUE`, `call_graph_url` will exploit the ability of `jsonlite::fromJSON` to convert arrays of objects into R data frames. This can be useful for REST calls that return tabular data. However, it can also cause problems for _paged_ lists, where each page will be turned into a separate data frame; as the individual objects may not have the same fields, the resulting data frames will also have differing columns. This will cause base R's `rbind` to fail when binding the pages together. When processing paged lists, AzureGraph will use `vctrs::vec_rbind` instead of `rbind` when the vctrs package is available; `vec_rbind` does not have this problem. For safety, you should only set `simplify=TRUE` when vctrs is installed. 20 | #' 21 | #' @return 22 | #' If `http_status_handler` is one of `"stop"`, `"warn"` or `"message"`, the status code of the response is checked. If an error is not thrown, the parsed content of the response is returned with the status code attached as the "status" attribute. 23 | #' 24 | #' If `http_status_handler` is `"pass"`, the entire response is returned without modification. 25 | #' 26 | #' @seealso 27 | #' [httr::GET], [httr::PUT], [httr::POST], [httr::DELETE], [httr::stop_for_status], [httr::content] 28 | #' @rdname call_graph 29 | #' @export 30 | call_graph_endpoint <- function(token, operation, ..., options=list(), 31 | api_version=getOption("azure_graph_api_version")) 32 | { 33 | url <- find_resource_host(token) 34 | url$path <- construct_path(api_version, operation) 35 | url$path <- encode_hash(url$path) 36 | url$query <- options 37 | 38 | call_graph_url(token, url, ...) 39 | } 40 | 41 | #' @rdname call_graph 42 | #' @export 43 | call_graph_url <- function(token, url, ..., body=NULL, encode="json", 44 | http_verb=c("GET", "DELETE", "PUT", "POST", "HEAD", "PATCH"), 45 | http_status_handler=c("stop", "warn", "message", "pass"), 46 | simplify=FALSE, auto_refresh=TRUE) 47 | { 48 | # if content-type is json, serialize it manually to ensure proper handling of nulls 49 | if(encode == "json" && !is_empty(body)) 50 | { 51 | null <- vapply(body, is.null, logical(1)) 52 | body <- jsonlite::toJSON(body[!null], auto_unbox=TRUE, digits=22, null="null") 53 | encode <- "raw" 54 | } 55 | 56 | # do actual API call, checking for throttling (max 10 retries) 57 | for(i in 1:10) 58 | { 59 | headers <- process_headers(token, url, auto_refresh) 60 | res <- httr::VERB(match.arg(http_verb), url, headers, ..., body=body, encode=encode) 61 | if(httr::status_code(res) == 429) 62 | { 63 | delay <- httr::headers(res)$`Retry-After` 64 | Sys.sleep(if(!is.null(delay)) as.numeric(delay) else i^1.5) 65 | } 66 | else break 67 | } 68 | 69 | process_response(res, match.arg(http_status_handler), simplify) 70 | } 71 | 72 | 73 | process_headers <- function(token, host, auto_refresh) 74 | { 75 | # if token has expired, renew it 76 | if(auto_refresh && !token$validate()) 77 | { 78 | message("Access token has expired or is no longer valid; refreshing") 79 | token$refresh() 80 | } 81 | 82 | creds <- token$credentials 83 | host <- httr::parse_url(host)$hostname 84 | headers <- c( 85 | Host=host, 86 | Authorization=paste(creds$token_type, creds$access_token), 87 | `Content-Type`="application/json" 88 | ) 89 | 90 | httr::add_headers(.headers=headers) 91 | } 92 | 93 | 94 | process_response <- function(response, handler, simplify) 95 | { 96 | if(handler != "pass") 97 | { 98 | cont <- httr::content(response, simplifyVector=simplify) 99 | handler <- get(paste0(handler, "_for_status"), getNamespace("httr")) 100 | handler(response, paste0("complete operation. Message:\n", 101 | sub("\\.$", "", error_message(cont)))) 102 | 103 | if(is.null(cont)) 104 | cont <- list() 105 | 106 | attr(cont, "status") <- httr::status_code(response) 107 | cont 108 | } 109 | else response 110 | } 111 | 112 | 113 | # provide complete error messages from Resource Manager/Microsoft Graph/etc 114 | error_message <- function(cont) 115 | { 116 | # kiboze through possible message locations 117 | msg <- if(is.character(cont)) 118 | cont 119 | # else if(inherits(cont, "xml_node")) # Graph 120 | # paste(xml2::xml_text(xml2::xml_children(cont)), collapse=": ") 121 | else if(is.list(cont)) 122 | { 123 | if(is.character(cont$message)) 124 | cont$message 125 | else if(is.list(cont$error) && is.character(cont$error$message)) 126 | cont$error$message 127 | else if(is.list(cont$odata.error)) # Graph OData 128 | cont$odata.error$message$value 129 | } 130 | else "" 131 | 132 | paste0(strwrap(msg), collapse="\n") 133 | } 134 | 135 | 136 | # handle different behaviour of file_path on Windows/Linux wrt trailing / 137 | construct_path <- function(...) 138 | { 139 | sub("/$", "", file.path(..., fsep="/")) 140 | } 141 | 142 | # paths containing hash need to be encoded 143 | encode_hash <- function(x) 144 | { 145 | gsub("#", "%23", x, fixed = TRUE) 146 | } 147 | 148 | # display confirmation prompt, return TRUE/FALSE (no NA) 149 | get_confirmation <- function(msg, default=TRUE) 150 | { 151 | ok <- if(getRversion() < numeric_version("3.5.0")) 152 | { 153 | msg <- paste(msg, if(default) "(Yes/no/cancel) " else "(yes/No/cancel) ") 154 | yn <- readline(msg) 155 | if(nchar(yn) == 0) 156 | default 157 | else tolower(substr(yn, 1, 1)) == "y" 158 | } 159 | else utils::askYesNo(msg, default) 160 | isTRUE(ok) 161 | } 162 | 163 | 164 | find_resource_host <- function(token) 165 | { 166 | if(is_azure_v2_token(token)) 167 | { 168 | # search the vector of scopes for the actual resource URL 169 | url <- list() 170 | i <- 1 171 | while(is.null(url$scheme) && i <= length(token$scope)) 172 | { 173 | url <- httr::parse_url(token$scope[i]) 174 | i <- i + 1 175 | } 176 | } 177 | else url <- httr::parse_url(token$resource) # v1 token is the easy case 178 | 179 | if(is.null(url$scheme)) 180 | stop("Could not find Graph host URL", call.=FALSE) 181 | url$path <- NULL 182 | url 183 | } 184 | 185 | -------------------------------------------------------------------------------- /R/format.R: -------------------------------------------------------------------------------- 1 | #' Format a Microsoft Graph or Azure object 2 | #' 3 | #' Miscellaneous functions for printing Microsoft Graph and Azure R6 objects 4 | #' 5 | #' @param env An R6 object's environment for printing. 6 | #' @param exclude Objects in `env` to exclude from the printout. 7 | #' 8 | #' @details 9 | #' These are utilities to aid in printing R6 objects created by this package or its descendants. They are not meant to be called by the user. 10 | #' 11 | #' @rdname format 12 | #' @export 13 | format_public_fields <- function(env, exclude=character(0)) 14 | { 15 | objnames <- ls(env) 16 | std_fields <- c("token") 17 | objnames <- setdiff(objnames, c(exclude, std_fields)) 18 | 19 | maxwidth <- as.integer(0.8 * getOption("width")) 20 | 21 | objconts <- sapply(objnames, function(n) 22 | { 23 | x <- get(n, env) 24 | deparsed <- if(is_empty(x) || is.function(x)) # don't print empty fields 25 | return(NULL) 26 | else if(is.list(x)) 27 | paste0("list(", paste(names(x), collapse=", "), ")") 28 | else if(is.vector(x)) 29 | { 30 | x <- paste0(x, collapse=", ") 31 | if(nchar(x) > maxwidth - nchar(n) - 10) 32 | x <- paste0(substr(x, 1, maxwidth - nchar(n) - 10), " ...") 33 | x 34 | } 35 | else deparse(x)[[1]] 36 | 37 | paste0(strwrap(paste0(n, ": ", deparsed), width=maxwidth, indent=2, exdent=4), 38 | collapse="\n") 39 | }, simplify=FALSE) 40 | 41 | empty <- sapply(objconts, is.null) 42 | objconts <- objconts[!empty] 43 | 44 | # print etag at the bottom, not the top 45 | if("etag" %in% names(objconts)) 46 | objconts <- c(objconts[-which(names(objconts) == "etag")], objconts["etag"]) 47 | 48 | paste0(paste0(objconts, collapse="\n"), "\n---\n") 49 | } 50 | 51 | 52 | #' @rdname format 53 | #' @export 54 | format_public_methods <- function(env) 55 | { 56 | objnames <- ls(env) 57 | std_methods <- c("clone", "print", "initialize") 58 | objnames <- setdiff(objnames, std_methods) 59 | is_method <- sapply(objnames, function(obj) is.function(.subset2(env, obj))) 60 | 61 | maxwidth <- as.integer(0.8 * getOption("width")) 62 | 63 | objnames <- strwrap(paste(objnames[is_method], collapse=", "), width=maxwidth, indent=4, exdent=4) 64 | paste0(" Methods:\n", paste0(objnames, collapse="\n"), "\n") 65 | } 66 | -------------------------------------------------------------------------------- /R/is.R: -------------------------------------------------------------------------------- 1 | #' Informational functions 2 | #' 3 | #' These functions return whether the object is of the corresponding class. 4 | #' 5 | #' @param object An R object. 6 | #' 7 | #' @return 8 | #' A boolean. 9 | #' @rdname info 10 | #' @export 11 | is_app <- function(object) 12 | { 13 | R6::is.R6(object) && inherits(object, "az_app") 14 | } 15 | 16 | 17 | #' @rdname info 18 | #' @export 19 | is_service_principal <- function(object) 20 | { 21 | R6::is.R6(object) && inherits(object, "az_service_principal") 22 | } 23 | 24 | 25 | #' @rdname info 26 | #' @export 27 | is_user <- function(object) 28 | { 29 | R6::is.R6(object) && inherits(object, "az_user") 30 | } 31 | 32 | 33 | #' @rdname info 34 | #' @export 35 | is_group <- function(object) 36 | { 37 | R6::is.R6(object) && inherits(object, "az_group") 38 | } 39 | 40 | 41 | #' @rdname info 42 | #' @export 43 | is_directory_role <- function(object) 44 | { 45 | R6::is.R6(object) && inherits(object, "az_directory_role") 46 | } 47 | 48 | 49 | #' @rdname info 50 | #' @export 51 | is_aad_object <- function(object) 52 | { 53 | R6::is.R6(object) && inherits(object, "az_object") 54 | } 55 | 56 | 57 | #' @rdname info 58 | #' @export 59 | is_msgraph_object <- function(object) 60 | { 61 | R6::is.R6(object) && inherits(object, "ms_object") 62 | } 63 | 64 | -------------------------------------------------------------------------------- /R/ms_graph_pager.R: -------------------------------------------------------------------------------- 1 | #' Pager object for Graph list results 2 | #' 3 | #' Class representing an _iterator_ for a set of paged query results. 4 | #' 5 | #' @docType class 6 | #' @section Fields: 7 | #' - `token`: The token used to authenticate with the Graph host. 8 | #' - `output`: What the pager should yield on each iteration, either "data.frame","list" or "object". See 'Value' below. 9 | #' @section Methods: 10 | #' - `new(...)`: Initialize a new user object. See 'Initialization' below. 11 | #' - `has_data()`: Returns TRUE if there are pages remaining in the iterator, or FALSE otherwise. 12 | #' @section Active bindings: 13 | #' - `value`: The returned value on each iteration of the pager. 14 | #' 15 | #' @section Initialization: 16 | #' The recommended way to create objects of this class is via the `ms_object$get_list_pager()` method, but it can also be initialized directly. The arguments to the `new()` method are: 17 | #' - `token`: The token used to authenticate with the Graph host. 18 | #' - `first_page`: A list containing the first page of results, generally from a call to `call_graph_endpoint()` or the `do_operation()` method of an AzureGraph R6 object. 19 | #' - `next_link_name,value_name`: The names of the components of `first_page` containing the link to the next page, and the set of values for the page respectively. The default values are `@odata.nextLink` and `value`. 20 | #' - `generate_objects`: Whether the iterator should return a list containing the parsed JSON for the page values, or convert it into a list of R6 objects. See 'Value' below. 21 | #' - `type_filter`: Any extra arguments required to initialise the returned objects. Only used if `generate_objects` is TRUE. 22 | #' - `default_generator`: The default generator object to use when converting a list of properties into an R6 object, if the class can't be detected. Defaults to `ms_object`. Only used if `generate_objects` is TRUE. 23 | #' - `...`: Any extra arguments required to initialise the returned objects. Only used if `generate_objects` is TRUE. 24 | #' 25 | #' @section Value: 26 | #' The `value` active binding returns the page values for each iteration of the pager. This can take one of 3 forms, based on the initial format of the first page and the `generate_objects` argument. 27 | #' 28 | #' If the first page of results is a data frame (each item has been converted into a row), then the pager will return results as data frames. In this case, the `output` field is automatically set to "data.frame" and the `generate_objects` initialization argument is ignored. Usually this will be the case when the results are meant to represent external data, eg items in a SharePoint list. 29 | #' 30 | #' If the first page of results is a list, the `generate_objects` argument sets whether to convert the items in each page into R6 objects defined by the AzureGraph class framework. If `generate_objects` is TRUE, the `output` field is set to "object", and if `generate_objects` is FALSE, the `output` field is set to "list". 31 | #' 32 | #' @seealso 33 | #' [ms_object], [extract_list_values] 34 | #' 35 | #' [Microsoft Graph overview](https://learn.microsoft.com/en-us/graph/overview), 36 | #' [Paging documentation](https://learn.microsoft.com/en-us/graph/paging) 37 | #' 38 | #' @examples 39 | #' \dontrun{ 40 | #' 41 | #' # list direct memberships 42 | #' firstpage <- call_graph_endpoint(token, "me/memberOf") 43 | #' 44 | #' pager <- ms_graph_pager$new(token, firstpage) 45 | #' pager$has_data() 46 | #' pager$value 47 | #' 48 | #' # once all the pages have been returned 49 | #' isFALSE(pager$has_data()) 50 | #' is.null(pager$value) 51 | #' 52 | #' # returning items, 1 per page, as raw lists of properties 53 | #' firstpage <- call_graph_endpoint(token, "me/memberOf", options=list(`$top`=1)) 54 | #' pager <- ms_graph_pager$new(token, firstpage, generate_objects=FALSE) 55 | #' lst <- NULL 56 | #' while(pager$has_data()) 57 | #' lst <- c(lst, pager$value) 58 | #' 59 | #' # returning items as a data frame 60 | #' firstdf <- call_graph_endpoint(token, "me/memberOf", options=list(`$top`=1), 61 | #' simplify=TRUE) 62 | #' pager <- ms_graph_pager$new(token, firstdf) 63 | #' df <- NULL 64 | #' while(pager$has_data()) 65 | #' df <- vctrs::vec_rbin(df, pager$value) 66 | #' 67 | #' } 68 | #' @format An R6 object of class `ms_graph_pager`. 69 | #' @export 70 | ms_graph_pager <- R6::R6Class("ms_graph_pager", 71 | 72 | public=list( 73 | 74 | token=NULL, 75 | output=NULL, 76 | 77 | initialize=function(token, first_page, next_link_name="@odata.nextLink", value_name="value", 78 | generate_objects=TRUE, type_filter=NULL, default_generator=ms_object, ...) 79 | { 80 | self$token <- token 81 | private$value_name <- value_name 82 | private$next_link_name <- next_link_name 83 | private$type_filter <- type_filter 84 | private$default_generator <- default_generator 85 | private$init_args <- list(...) 86 | self$output <- if(is.data.frame(first_page$value)) 87 | "data.frame" 88 | else if(generate_objects) 89 | "object" 90 | else "list" 91 | private$next_link <- first_page[[next_link_name]] 92 | private$next_value <- first_page[[value_name]] 93 | }, 94 | 95 | has_data=function() 96 | { 97 | !is.null(private$next_value) 98 | }, 99 | 100 | print=function(...) 101 | { 102 | cat("\n", sep="") 103 | cat(" output:", self$output, "\n") 104 | cat(" has data:", self$has_data(), "\n") 105 | invisible(self) 106 | } 107 | ), 108 | 109 | active=list( 110 | 111 | value=function() 112 | { 113 | val <- private$next_value 114 | private$next_value <- if(!is.null(private$next_link)) 115 | { 116 | page <- call_graph_url(self$token, private$next_link, simplify=(self$output == "data.frame")) 117 | private$next_link <- page[[private$next_link_name]] 118 | page[[private$value_name]] 119 | } 120 | else NULL 121 | 122 | if(self$output == "object") 123 | private$make_objects(val) 124 | else val 125 | } 126 | ), 127 | 128 | private=list( 129 | 130 | next_link_name=NULL, 131 | value_name=NULL, 132 | next_link=NULL, 133 | next_value=NULL, 134 | type_filter=NULL, 135 | default_generator=NULL, 136 | init_args=NULL, 137 | 138 | make_objects=function(page) 139 | { 140 | if(is_empty(page)) 141 | return(list()) 142 | 143 | page <- lapply(page, function(obj) 144 | { 145 | class_gen <- find_class_generator(obj, private$type_filter, private$default_generator) 146 | if(is.null(class_gen)) 147 | NULL 148 | else do.call(class_gen$new, c(list(self$token, self$tenant, obj), private$init_args)) 149 | }) 150 | page[!sapply(page, is.null)] 151 | } 152 | )) 153 | 154 | 155 | #' Get the list of values from a Graph pager object 156 | #' 157 | #' @param pager An object of class `ms_graph_pager`, which is an iterator for a list of paged query results. 158 | #' @param n The number of items from the list to return. Note this is _not_ the number of _pages_ (each page will usually contain multiple items). The default value of `Inf` extracts all the values from the list, leaving the pager empty. If this is NULL, the pager itself is returned. 159 | #' 160 | #' @details 161 | #' This is a convenience function to perform the common task of extracting all or some of the items from a paged response. 162 | #' 163 | #' @return 164 | #' If `n` is `Inf` or a number, the items from the paged query results. The format of the returned value depends on the pager settings. This will either be a nested list containing the properties for each of the items; a list of R6 objects; or a data frame. If the pager is empty, an error is thrown. 165 | #' 166 | #' If `n` is NULL, the pager itself is returned. 167 | #' 168 | #' @seealso 169 | #' [ms_graph_pager], [ms_object], [call_graph_endpoint] 170 | #' 171 | #' @examples 172 | #' \dontrun{ 173 | #' 174 | #' firstpage <- call_graph_endpoint(token, "me/memberOf") 175 | #' pager <- ms_graph_pager$new(token, firstpage) 176 | #' extract_list_values(pager) 177 | #' 178 | #' # trying to extract values a 2nd time will fail 179 | #' try(extract_list_values(pager)) 180 | #' 181 | #' } 182 | #' @export 183 | extract_list_values <- function(pager, n=Inf) 184 | { 185 | if(is.null(n)) 186 | return(pager) 187 | 188 | if(!pager$has_data()) 189 | stop("Pager is empty", call.=FALSE) 190 | 191 | bind_fn <- if(pager$output != "data.frame") 192 | base::c 193 | else if(requireNamespace("vctrs", quietly=TRUE)) 194 | vctrs::vec_rbind 195 | else base::rbind 196 | 197 | res <- NULL 198 | while(pager$has_data() && NROW(res) < n) # not nrow() 199 | res <- bind_fn(res, pager$value) 200 | 201 | if(NROW(res) > n) 202 | utils::head(res, n) 203 | else res 204 | } 205 | -------------------------------------------------------------------------------- /R/ms_object.R: -------------------------------------------------------------------------------- 1 | #' Microsoft Graph object 2 | #' 3 | #' Base class representing a object in Microsoft Graph. All other Graph object classes ultimately inherit from this class. 4 | #' 5 | #' @docType class 6 | #' @section Fields: 7 | #' - `token`: The token used to authenticate with the Graph host. 8 | #' - `tenant`: The Azure Active Directory tenant for this object. 9 | #' - `type`: The type of object, in a human-readable format. 10 | #' - `properties`: The object properties, as obtained from the Graph host. 11 | #' @section Methods: 12 | #' - `new(...)`: Initialize a new directory object. Do not call this directly; see 'Initialization' below. 13 | #' - `delete(confirm=TRUE)`: Delete an object. By default, ask for confirmation first. 14 | #' - `update(...)`: Update the object information in Azure Active Directory. 15 | #' - `do_operation(...)`: Carry out an arbitrary operation on the object. 16 | #' - `sync_fields()`: Synchronise the R object with the data in Azure Active Directory. 17 | #' - `get_list_pager(...)`: Returns a pager object, which is an _iterator_ for a set of paged query results. See 'Paged results' below. 18 | #' 19 | #' @section Initialization: 20 | #' Objects of this class should not be created directly. Instead, create an object of the appropriate subclass. 21 | #' 22 | #' @section List methods: 23 | #' All `list_*` methods have `filter` and `n` arguments to limit the number of results. The former should be an [OData expression](https://learn.microsoft.com/en-us/graph/query-parameters#filter-parameter) as a string to filter the result set on. The latter should be a number setting the maximum number of (filtered) results to return. The default values are `filter=NULL` and `n=Inf`. If `n=NULL`, the `ms_graph_pager` iterator object is returned instead to allow manual iteration over the results. 24 | #' 25 | #' Support in the underlying Graph API for OData queries is patchy. Not all endpoints that return lists of objects support filtering, and if they do, they may not allow all of the defined operators. If your filtering expression results in an error, you can carry out the operation without filtering and then filter the results on the client side. 26 | #' 27 | #' @section Paged results: 28 | #' Microsoft Graph returns lists in pages, with each page containing a subset of objects and a link to the next page. AzureGraph provides an iterator-based API that lets you access each page individually, or collect them all into a single object. 29 | #' 30 | #' To create a new pager object, call the `get_list_pager()` method with the following arguments: 31 | #' - `lst`: A list containing the first page of results, generally from a call to the `do_operation()` method. 32 | #' - `next_link_name,value_name`: The names of the components of `first_page` containing the link to the next page, and the set of values for the page respectively. The default values are `@odata.nextLink` and `value`. 33 | #' - `generate_objects`: Whether the iterator should return a list containing the parsed JSON for the page values, or convert it into a list of R6 objects. 34 | #' - `type_filter`: Any extra arguments required to initialise the returned objects. Only used if `generate_objects` is TRUE. 35 | #' - `default_generator`: The default generator object to use when converting a list of properties into an R6 object, if the class can't be detected. Defaults to `ms_object`. Only used if `generate_objects` is TRUE. 36 | #' - `...`: Any extra arguments required to initialise the returned objects. Only used if `generate_objects` is TRUE. 37 | #' 38 | #' This returns an object of class [ms_graph_pager], which is an _iterator_ for the set of paged results. Each call to the object's `value` active binding yields the next page. When all pages have been returned, `value` contains NULL. 39 | #' 40 | #' The format of the returned values can take one of 3 forms, based on the initial format of the first page and the `generate_objects` argument. 41 | #' 42 | #' If the first page of results is a data frame (each item has been converted into a row), then the pager will return results as data frames. In this case, the `output` field is automatically set to "data.frame" and the `generate_objects` initialization argument is ignored. Usually this will be the case when the results are meant to represent external data, eg items in a SharePoint list. 43 | #' 44 | #' If the first page of results is a list, the `generate_objects` argument sets whether to convert the items in each page into R6 objects defined by the AzureGraph class framework. If `generate_objects` is TRUE, the `output` field is set to "object", and if `generate_objects` is FALSE, the `output` field is set to "list". 45 | #' 46 | #' You can also call the `extract_list_values()` function to get all or some of the values from a pager, without having to manually combine the pages together. 47 | #' 48 | #' @section Deprecated methods: 49 | #' The following methods are private and **deprecated**, and form the older AzureGraph API for accessing paged results. They will eventually be removed. 50 | #' - `get_paged_list(lst, next_link_name, value_name, simplify, n)`: This method reconstructs the list, given the first page. 51 | #' - `init_list_objects(lst, type_filter, default_generator, ...)`: `get_paged_list` returns a raw list, the result of parsing the JSON response from the Graph host. This method converts the list into actual R6 objects. 52 | #' 53 | #' @seealso 54 | #' [ms_graph], [az_object], [ms_graph_pager], [extract_list_values] 55 | #' 56 | #' [Microsoft Graph overview](https://learn.microsoft.com/en-us/graph/overview), 57 | #' [REST API reference](https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0) 58 | #' 59 | #' @format An R6 object of class `ms_object`. 60 | #' @export 61 | ms_object <- R6::R6Class("ms_object", 62 | 63 | public=list( 64 | 65 | token=NULL, 66 | tenant=NULL, 67 | 68 | # user-readable object type 69 | type=NULL, 70 | 71 | # object data from server 72 | properties=NULL, 73 | 74 | initialize=function(token, tenant=NULL, properties=NULL) 75 | { 76 | self$token <- token 77 | self$tenant <- tenant 78 | self$properties <- properties 79 | }, 80 | 81 | update=function(...) 82 | { 83 | self$do_operation(body=list(...), encode="json", http_verb="PATCH") 84 | self$properties <- self$do_operation() 85 | self 86 | }, 87 | 88 | sync_fields=function() 89 | { 90 | self$properties <- self$do_operation() 91 | invisible(self) 92 | }, 93 | 94 | delete=function(confirm=TRUE) 95 | { 96 | if(confirm && interactive()) 97 | { 98 | name <- self$properties$displayName 99 | if(is.null(name)) 100 | name <- self$properties$name 101 | if(is.null(name)) 102 | name <- self$properties$id 103 | msg <- sprintf("Do you really want to delete the %s '%s'?", 104 | self$type, name) 105 | if(!get_confirmation(msg, FALSE)) 106 | return(invisible(NULL)) 107 | } 108 | 109 | self$do_operation(http_verb="DELETE") 110 | invisible(NULL) 111 | }, 112 | 113 | do_operation=function(op="", ...) 114 | { 115 | op <- construct_path(private$api_type, self$properties$id, op) 116 | call_graph_endpoint(self$token, op, ...) 117 | }, 118 | 119 | get_list_pager=function(lst, next_link_name="@odata.nextLink", value_name="value", generate_objects=TRUE, 120 | type_filter=NULL, default_generator=ms_object, ...) 121 | { 122 | ms_graph_pager$new(self$token, lst, next_link_name, value_name, 123 | generate_objects, type_filter, default_generator, ...) 124 | }, 125 | 126 | print=function(...) 127 | { 128 | cat("\n", sep="") 129 | cat(" directory id:", self$properties$id, "\n") 130 | cat("---\n") 131 | cat(format_public_methods(self)) 132 | invisible(self) 133 | } 134 | ), 135 | 136 | private=list( 137 | 138 | # object type as it appears in REST API path 139 | api_type=NULL, 140 | 141 | get_paged_list=function(lst, next_link_name="@odata.nextLink", value_name="value", simplify=FALSE, n=Inf) 142 | { 143 | bind_fn <- if(requireNamespace("vctrs", quietly=TRUE)) 144 | vctrs::vec_rbind 145 | else base::rbind 146 | res <- lst[[value_name]] 147 | if(n <= 0) n <- Inf 148 | while(!is_empty(lst[[next_link_name]]) && NROW(res) < n) 149 | { 150 | lst <- call_graph_url(self$token, lst[[next_link_name]], simplify=simplify) 151 | res <- if(simplify) 152 | bind_fn(res, lst[[value_name]]) # base::rbind assumes all objects have the exact same fields 153 | else c(res, lst[[value_name]]) 154 | } 155 | if(n < NROW(res)) 156 | { 157 | if(inherits(res, "data.frame")) 158 | res[seq_len(n), ] 159 | else res[seq_len(n)] 160 | } 161 | else res 162 | }, 163 | 164 | init_list_objects=function(lst, type_filter=NULL, default_generator=ms_object, ...) 165 | { 166 | lst <- lapply(lst, function(obj) 167 | { 168 | class_gen <- find_class_generator(obj, type_filter, default_generator) 169 | if(is.null(class_gen)) 170 | NULL 171 | else class_gen$new(self$token, self$tenant, obj, ...) 172 | }) 173 | lst[!sapply(lst, is.null)] 174 | } 175 | )) 176 | 177 | -------------------------------------------------------------------------------- /R/read_cert.R: -------------------------------------------------------------------------------- 1 | # get certificate details to add to an app 2 | read_cert <- function(cert) 3 | { 4 | UseMethod("read_cert") 5 | } 6 | 7 | 8 | read_cert.character <- function(cert) 9 | { 10 | if(file.exists(cert)) 11 | read_cert_file(cert) 12 | else paste0(cert, collapse="") 13 | } 14 | 15 | 16 | read_cert.raw <- function(cert) 17 | { 18 | openssl::base64_encode(cert) 19 | } 20 | 21 | 22 | read_cert.rawConnection <- function(cert) 23 | { 24 | openssl::base64_encode(rawConnectionValue(cert)) 25 | } 26 | 27 | 28 | # AzureKeyVault::stored_cert 29 | read_cert.stored_cert <- function(cert) 30 | { 31 | cert$cer 32 | } 33 | 34 | 35 | # openssl::cert 36 | read_cert.cert <- function(cert) 37 | { 38 | openssl::base64_encode(cert) 39 | } 40 | 41 | 42 | read_cert_file <- function(file) 43 | { 44 | ext <- tolower(tools::file_ext(file)) 45 | if(ext == "pem") 46 | { 47 | pem <- openssl::read_cert(file) 48 | openssl::base64_encode(pem) 49 | } 50 | else if(ext %in% c("p12", "pfx")) 51 | { 52 | pfx <- openssl::read_p12(file) 53 | pfx$cert 54 | } 55 | else stop("Unsupported file extension: ", ext, call.=FALSE) 56 | } 57 | 58 | -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | #' Miscellaneous utility functions 2 | #' 3 | #' @param lst A named list of objects. 4 | #' @param name_fields The components of the objects in `lst`, to be used as names. 5 | #' @param x For `is_empty`, An R object. 6 | #' @details 7 | #' `named_list` extracts from each object in `lst`, the components named by `name_fields`. It then constructs names for `lst` from these components, separated by a `"/"`. 8 | #' 9 | #' @return 10 | #' For `named_list`, the list that was passed in but with names. An empty input results in a _named list_ output: a list of length 0, with a `names` attribute. 11 | #' 12 | #' For `is_empty`, whether the length of the object is zero (this includes the special case of `NULL`). 13 | #' 14 | #' @rdname utils 15 | #' @export 16 | named_list <- function(lst=NULL, name_fields="name") 17 | { 18 | if(is_empty(lst)) 19 | return(structure(list(), names=character(0))) 20 | 21 | lst_names <- sapply(name_fields, function(n) sapply(lst, `[[`, n)) 22 | if(length(name_fields) > 1) 23 | { 24 | dim(lst_names) <- c(length(lst_names) / length(name_fields), length(name_fields)) 25 | lst_names <- apply(lst_names, 1, function(nn) paste(nn, collapse="/")) 26 | } 27 | names(lst) <- lst_names 28 | dups <- duplicated(tolower(names(lst))) 29 | if(any(dups)) 30 | { 31 | duped_names <- names(lst)[dups] 32 | warning("Some names are duplicated: ", paste(unique(duped_names), collapse=" "), call.=FALSE) 33 | } 34 | lst 35 | } 36 | 37 | 38 | #' @rdname utils 39 | #' @export 40 | is_empty <- function(x) 41 | { 42 | length(x) == 0 43 | } 44 | -------------------------------------------------------------------------------- /R/zzz_class_directory.R: -------------------------------------------------------------------------------- 1 | #' Extensible registry of Microsoft Graph classes that AzureGraph supports 2 | #' 3 | #' @param name The name of the Graph class, eg "user", "servicePrincipal", etc. 4 | #' @param R6_generator An R6 class generator corresponding to this Graph class. 5 | #' @param check_function A boolean function that checks if a list of properties is for an object of this class. 6 | #' @details 7 | #' As written, AzureGraph knows about a subset of all the object classes contained in Microsoft Graph. These are mostly the classes originating from Azure Active Directory: users, groups, app registrations, service principals and registered devices. 8 | #' 9 | #' You can extend AzureGraph by writing your own R6 class that inherits from `ms_object`. If so, you should also _register_ your class by calling `register_graph_class` and providing the generator object, along with a check function. The latter should accept a list of object properties (as obtained from the Graph REST API), and return TRUE/FALSE based on whether the object is of your class. 10 | #' 11 | #' @return 12 | #' An invisible vector of registered class names. 13 | #' @examples 14 | #' \dontrun{ 15 | #' 16 | #' # built-in 'az_user' class, for an AAD user object 17 | #' register_graph_class("user", az_user, 18 | #' function(props) !is.null(props$userPrincipalName)) 19 | #' 20 | #' } 21 | #' @export 22 | register_graph_class <- function(name, R6_generator, check_function) 23 | { 24 | if(!R6::is.R6Class(R6_generator)) 25 | stop("R6_generator should be an R6 class generator object", call.=FALSE) 26 | if(!is.function(check_function)) 27 | stop("check_function should be a function") 28 | 29 | .graph_classes[[name]] <- list( 30 | generator=R6_generator, 31 | check=check_function 32 | ) 33 | invisible(ls(.graph_classes)) 34 | } 35 | 36 | 37 | .graph_classes <- new.env() 38 | 39 | # classes supplied by AzureGraph 40 | register_graph_class("user", az_user, 41 | function(props) !is.null(props$userPrincipalName)) 42 | 43 | register_graph_class("group", az_group, 44 | function(props) !is.null(props$groupTypes)) 45 | 46 | register_graph_class("application", az_app, 47 | function(props) !is.null(props$appId) && is.null(props$servicePrincipalType)) 48 | 49 | register_graph_class("servicePrincipal", az_service_principal, 50 | function(props) !is.null(props$appId) && !is.null(props$servicePrincipalType)) 51 | 52 | register_graph_class("device", az_device, 53 | function(props) !is.null(props$publishingState)) 54 | 55 | register_graph_class("directoryRole", az_directory_role, 56 | function(props) !is.null(props$roleTemplateId)) 57 | 58 | 59 | #' Find the R6 class for a Graph object 60 | #' 61 | #' @param props A list of object properties, generally the result of a Graph API call. 62 | #' @param type_filter An optional vector of types by which to filter the result. 63 | #' @param default_generator The default class generator to use, if a match couldn't be found. 64 | #' @details 65 | #' This function maps Graph objects to AzureGraph classes. 66 | #' @return 67 | #' An R6 class generator for the appropriate AzureGraph class. If no matching R6 class could be found, the default generator is returned. If `type_filter` is provided, but the matching R6 class isn't in the filter, NULL is returned. 68 | #' @export 69 | find_class_generator <- function(props, type_filter=NULL, default_generator=ms_object) 70 | { 71 | # use ODATA metadata if available 72 | if(!is.null(props$`@odata.type`)) 73 | { 74 | type <- sub("^#microsoft.graph.", "", props$`@odata.type`) 75 | if(!(type %in% ls(.graph_classes))) 76 | type <- NA 77 | } 78 | else # check for each known type in turn 79 | { 80 | type <- NA 81 | for(n in ls(.graph_classes)) 82 | { 83 | if(.graph_classes[[n]]$check(props)) 84 | { 85 | type <- n 86 | break 87 | } 88 | } 89 | } 90 | 91 | # here, 'type' will be one of the known types or NA for unknown 92 | # always return the default class if unknown, even if a type filter is provided 93 | if(is.na(type)) 94 | return(default_generator) 95 | 96 | if(is.null(type_filter) || type %in% type_filter) 97 | .graph_classes[[type]]$generator 98 | else NULL 99 | } 100 | 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AzureGraph 2 | 3 | [![CRAN](https://www.r-pkg.org/badges/version/AzureGraph)](https://cran.r-project.org/package=AzureGraph) 4 | ![Downloads](https://cranlogs.r-pkg.org/badges/AzureGraph) 5 | ![R-CMD-check](https://github.com/Azure/AzureGraph/workflows/R-CMD-check/badge.svg) 6 | 7 | A simple interface to the [Microsoft Graph API](https://learn.microsoft.com/en-us/graph/overview). The companion package to [AzureRMR](https://github.com/Azure/AzureRMR) and [AzureAuth](https://github.com/Azure/AzureAuth). 8 | 9 | Microsoft Graph is a comprehensive framework for accessing data in various online Microsoft services. Currently, this package aims to provide an R interface only to the Azure Active Directory part, with a view to supporting interoperability of R and Azure: users, groups, registered apps and service principals. Like AzureRMR, it could potentially be extended to cover other services. 10 | 11 | The primary repo for this package is at https://github.com/Azure/AzureGraph; please submit issues and PRs there. It is also mirrored at the Cloudyr org at https://github.com/cloudyr/AzureGraph. You can install the development version of the package with `devtools::install_github("Azure/AzureGraph")`. 12 | 13 | 14 | ## Authentication 15 | 16 | The first time you authenticate with a given Azure Active Directory tenant, you call `create_graph_login()` and supply your credentials. R will prompt you for permission to create a special data directory in which to save the obtained authentication token and AD Graph login. Once this information is saved on your machine, it can be retrieved in subsequent R sessions with `get_graph_login()`. Your credentials will be automatically refreshed so you don't have to reauthenticate. 17 | 18 | See the "Authentication basics" vignette for more details on how to authenticate with AzureGraph. 19 | 20 | ## Sample workflow 21 | 22 | AzureGraph currently includes methods for working with registered apps, service principals, users and groups. A `call_graph_endpoint()` method is also supplied for making arbitrary REST calls. 23 | 24 | ```r 25 | library(AzureGraph) 26 | 27 | # authenticate with AAD 28 | # - on first login, call create_graph_login() 29 | # - on subsequent logins, call get_graph_login() 30 | gr <- create_graph_login() 31 | 32 | # list all users in this tenant 33 | gr$list_users() 34 | 35 | # list all app registrations 36 | gr$list_apps() 37 | 38 | # my user information 39 | me <- gr$get_user("me") 40 | 41 | # my groups 42 | head(me$list_group_memberships()) 43 | 44 | # my registered apps 45 | me$list_owned_objects(type="application") 46 | 47 | # register a new app 48 | # by default, this will have a randomly generated strong password with duration 2 years 49 | app <- gr$create_app("AzureR_newapp") 50 | 51 | # get the associated service principal 52 | app$get_service_principal() 53 | 54 | # using it in conjunction with AzureRMR RBAC 55 | AzureRMR::get_azure_login()$ 56 | get_subscription("sub_id")$ 57 | get_resource_group("rgname")$ 58 | add_role_assignment(app, "Contributor") 59 | ``` 60 | 61 | --- 62 |

63 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /man/az_app.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/az_app.r 3 | \docType{class} 4 | \name{az_app} 5 | \alias{az_app} 6 | \title{Registered app in Azure Active Directory} 7 | \format{ 8 | An R6 object of class \code{az_app}, inheriting from \code{az_object}. 9 | } 10 | \description{ 11 | Base class representing an AAD app. 12 | } 13 | \section{Fields}{ 14 | 15 | \itemize{ 16 | \item \code{token}: The token used to authenticate with the Graph host. 17 | \item \code{tenant}: The Azure Active Directory tenant for this app. 18 | \item \code{type}: always "application" for an app object. 19 | \item \code{properties}: The app properties. 20 | \item \code{password}: The app password. Note that the Graph API does not return previously-generated passwords. This field will only be populated for an app object created with \code{ms_graph$create_app()}, or after a call to the \code{add_password()} method below. 21 | } 22 | } 23 | 24 | \section{Methods}{ 25 | 26 | \itemize{ 27 | \item \code{new(...)}: Initialize a new app object. Do not call this directly; see 'Initialization' below. 28 | \item \code{delete(confirm=TRUE)}: Delete an app. By default, ask for confirmation first. 29 | \item \code{update(...)}: Update the app data in Azure Active Directory. For what properties can be updated, consult the REST API documentation link below. 30 | \item \code{do_operation(...)}: Carry out an arbitrary operation on the app. 31 | \item \code{sync_fields()}: Synchronise the R object with the app data in Azure Active Directory. 32 | \item \code{list_owners(type=c("user", "group", "application", "servicePrincipal"), filter=NULL, n=Inf)}: Return a list of all owners of this app. Specify the \code{type} argument to limit the result to specific object type(s). 33 | \item \code{create_service_principal(...)}: Create a service principal for this app, by default in the current tenant. 34 | \item \code{get_service_principal()}: Get the service principal for this app. 35 | \item \code{delete_service_principal(confirm=TRUE)}: Delete the service principal for this app. By default, ask for confirmation first. 36 | \item \code{add_password(password_name=NULL, password_duration=NULL)}: Adds a strong password. \code{password_duration} is the length of time in years that the password remains valid, with default duration 2 years. Returns the ID of the generated password. 37 | \item \code{remove_password(password_id, confirm=TRUE)}: Removes the password with the given ID. By default, ask for confirmation first. 38 | \item \code{add_certificate(certificate)}: Adds a certificate for authentication. This can be specified as the name of a .pfx or .pem file, an \code{openssl::cert} object, an \code{AzureKeyVault::stored_cert} object, or a raw or character vector. 39 | \item \verb{remove_certificate(certificate_id, confirm=TRUE}): Removes the certificate with the given ID. By default, ask for confirmation first. 40 | } 41 | } 42 | 43 | \section{Initialization}{ 44 | 45 | Creating new objects of this class should be done via the \code{create_app} and \code{get_app} methods of the \link{ms_graph} class. Calling the \code{new()} method for this class only constructs the R object; it does not call the Microsoft Graph API to create the actual app. 46 | 47 | \href{https://learn.microsoft.com/en-us/graph/overview}{Microsoft Graph overview}, 48 | \href{https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-beta}{REST API reference} 49 | } 50 | 51 | \section{List methods}{ 52 | 53 | All \verb{list_*} methods have \code{filter} and \code{n} arguments to limit the number of results. The former should be an \href{https://learn.microsoft.com/en-us/graph/query-parameters#filter-parameter}{OData expression} as a string to filter the result set on. The latter should be a number setting the maximum number of (filtered) results to return. The default values are \code{filter=NULL} and \code{n=Inf}. If \code{n=NULL}, the \code{ms_graph_pager} iterator object is returned instead to allow manual iteration over the results. 54 | 55 | Support in the underlying Graph API for OData queries is patchy. Not all endpoints that return lists of objects support filtering, and if they do, they may not allow all of the defined operators. If your filtering expression results in an error, you can carry out the operation without filtering and then filter the results on the client side. 56 | } 57 | 58 | \examples{ 59 | \dontrun{ 60 | 61 | gr <- get_graph_login() 62 | app <- gr$create_app("MyNewApp") 63 | 64 | # password resetting: remove the old password, add a new one 65 | pwd_id <- app$properties$passwordCredentials[[1]]$keyId 66 | app$add_password() 67 | app$remove_password(pwd_id) 68 | 69 | # set a redirect URI 70 | app$update(publicClient=list(redirectUris=I("http://localhost:1410"))) 71 | 72 | # add API permission (access Azure Storage as user) 73 | app$update(requiredResourceAccess=list( 74 | list( 75 | resourceAppId="e406a681-f3d4-42a8-90b6-c2b029497af1", 76 | resourceAccess=list( 77 | list( 78 | id="03e0da56-190b-40ad-a80c-ea378c433f7f", 79 | type="Scope" 80 | ) 81 | ) 82 | ) 83 | )) 84 | 85 | # add a certificate from a .pem file 86 | app$add_certificate("cert.pem") 87 | 88 | # can also read the file into an openssl object, and then add the cert 89 | cert <- openssl::read_cert("cert.pem") 90 | app$add_certificate(cert) 91 | 92 | # add a certificate stored in Azure Key Vault 93 | vault <- AzureKeyVault::key_vault("mytenant") 94 | cert2 <- vault$certificates$get("certname") 95 | app$add_certificate(cert2) 96 | 97 | # change the app name 98 | app$update(displayName="MyRenamedApp") 99 | 100 | } 101 | } 102 | \seealso{ 103 | \link{ms_graph}, \link{az_service_principal}, \link{az_user}, \link{az_group}, \link{az_object} 104 | } 105 | -------------------------------------------------------------------------------- /man/az_device.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/az_device.R 3 | \docType{class} 4 | \name{az_device} 5 | \alias{az_device} 6 | \title{Device in Azure Active Directory} 7 | \format{ 8 | An R6 object of class \code{az_device}, inheriting from \code{az_object}. 9 | } 10 | \description{ 11 | Class representing a registered device. 12 | } 13 | \section{Fields}{ 14 | 15 | \itemize{ 16 | \item \code{token}: The token used to authenticate with the Graph host. 17 | \item \code{tenant}: The Azure Active Directory tenant for this group. 18 | \item \code{type}: always "device" for a device object. 19 | \item \code{properties}: The device properties. 20 | } 21 | } 22 | 23 | \section{Methods}{ 24 | 25 | \itemize{ 26 | \item \code{new(...)}: Initialize a new device object. Do not call this directly; see 'Initialization' below. 27 | \item \code{delete(confirm=TRUE)}: Delete a device. By default, ask for confirmation first. 28 | \item \code{update(...)}: Update the device information in Azure Active Directory. 29 | \item \code{do_operation(...)}: Carry out an arbitrary operation on the device. 30 | \item \code{sync_fields()}: Synchronise the R object with the app data in Azure Active Directory. 31 | } 32 | } 33 | 34 | \section{Initialization}{ 35 | 36 | Create objects of this class via the \code{list_registered_devices()} and \code{list_owned_devices()} methods of the \code{az_user} class. 37 | } 38 | 39 | \seealso{ 40 | \link{ms_graph}, \link{az_user}, \link{az_object} 41 | 42 | \href{https://learn.microsoft.com/en-us/graph/overview}{Microsoft Graph overview}, 43 | \href{https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0}{REST API reference} 44 | } 45 | -------------------------------------------------------------------------------- /man/az_directory_role.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/az_dir_role.R 3 | \docType{class} 4 | \name{az_directory_role} 5 | \alias{az_directory_role} 6 | \title{Directory role} 7 | \format{ 8 | An R6 object of class \code{az_directory_role}, inheriting from \code{az_object}. 9 | } 10 | \description{ 11 | Class representing a role in Azure Active Directory. 12 | } 13 | \section{Fields}{ 14 | 15 | \itemize{ 16 | \item \code{token}: The token used to authenticate with the Graph host. 17 | \item \code{tenant}: The Azure Active Directory tenant for this role. 18 | \item \code{type}: always "directory role" for a directory role object. 19 | \item \code{properties}: The item properties. 20 | } 21 | } 22 | 23 | \section{Methods}{ 24 | 25 | \itemize{ 26 | \item \code{new(...)}: Initialize a new object. Do not call this directly; see 'Initialization' below. 27 | \item \code{delete(confirm=TRUE)}: Delete this item. By default, ask for confirmation first. 28 | \item \code{update(...)}: Update the item's properties in Microsoft Graph. 29 | \item \code{do_operation(...)}: Carry out an arbitrary operation on the item. 30 | \item \code{sync_fields()}: Synchronise the R object with the item metadata in Microsoft Graph. 31 | \item \code{list_members(filter=NULL, n=Inf)}: Return a list of all members of this group. 32 | } 33 | } 34 | 35 | \section{Initialization}{ 36 | 37 | Currently support for directory roles is limited. Objects of this class should not be initialized directly. 38 | } 39 | 40 | \section{List methods}{ 41 | 42 | All \verb{list_*} methods have \code{filter} and \code{n} arguments to limit the number of results. The former should be an \href{https://learn.microsoft.com/en-us/graph/query-parameters#filter-parameter}{OData expression} as a string to filter the result set on. The latter should be a number setting the maximum number of (filtered) results to return. The default values are \code{filter=NULL} and \code{n=Inf}. If \code{n=NULL}, the \code{ms_graph_pager} iterator object is returned instead to allow manual iteration over the results. 43 | 44 | Support in the underlying Graph API for OData queries is patchy. Not all endpoints that return lists of objects support filtering, and if they do, they may not allow all of the defined operators. If your filtering expression results in an error, you can carry out the operation without filtering and then filter the results on the client side. 45 | } 46 | 47 | \seealso{ 48 | \link{ms_graph}, \link{az_user} 49 | 50 | \href{https://learn.microsoft.com/en-us/graph/overview}{Microsoft Graph overview}, 51 | \href{https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0}{REST API reference} 52 | } 53 | -------------------------------------------------------------------------------- /man/az_group.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/az_group.R 3 | \docType{class} 4 | \name{az_group} 5 | \alias{az_group} 6 | \title{Group in Azure Active Directory} 7 | \format{ 8 | An R6 object of class \code{az_group}, inheriting from \code{az_object}. 9 | } 10 | \description{ 11 | Class representing an AAD group. 12 | } 13 | \section{Fields}{ 14 | 15 | \itemize{ 16 | \item \code{token}: The token used to authenticate with the Graph host. 17 | \item \code{tenant}: The Azure Active Directory tenant for this group. 18 | \item \code{type}: always "group" for a group object. 19 | \item \code{properties}: The group properties. 20 | } 21 | } 22 | 23 | \section{Methods}{ 24 | 25 | \itemize{ 26 | \item \code{new(...)}: Initialize a new group object. Do not call this directly; see 'Initialization' below. 27 | \item \code{delete(confirm=TRUE)}: Delete a group. By default, ask for confirmation first. 28 | \item \code{update(...)}: Update the group information in Azure Active Directory. 29 | \item \code{do_operation(...)}: Carry out an arbitrary operation on the group. 30 | \item \code{sync_fields()}: Synchronise the R object with the app data in Azure Active Directory. 31 | \item \code{list_members(type=c("user", "group", "application", "servicePrincipal"), filter=NULL, n=Inf)}: Return a list of all members of this group. Specify the \code{type} argument to limit the result to specific object type(s). 32 | \item \code{list_owners(type=c("user", "group", "application", "servicePrincipal"), filter=NULL, n=Inf)}: Return a list of all owners of this group. Specify the \code{type} argument to limit the result to specific object type(s). 33 | } 34 | } 35 | 36 | \section{Initialization}{ 37 | 38 | Creating new objects of this class should be done via the \code{create_group} and \code{get_group} methods of the \link{ms_graph} and \link{az_app} classes. Calling the \code{new()} method for this class only constructs the R object; it does not call the Microsoft Graph API to create the actual group. 39 | } 40 | 41 | \section{List methods}{ 42 | 43 | All \verb{list_*} methods have \code{filter} and \code{n} arguments to limit the number of results. The former should be an \href{https://learn.microsoft.com/en-us/graph/query-parameters#filter-parameter}{OData expression} as a string to filter the result set on. The latter should be a number setting the maximum number of (filtered) results to return. The default values are \code{filter=NULL} and \code{n=Inf}. If \code{n=NULL}, the \code{ms_graph_pager} iterator object is returned instead to allow manual iteration over the results. 44 | 45 | Support in the underlying Graph API for OData queries is patchy. Not all endpoints that return lists of objects support filtering, and if they do, they may not allow all of the defined operators. If your filtering expression results in an error, you can carry out the operation without filtering and then filter the results on the client side. 46 | } 47 | 48 | \examples{ 49 | \dontrun{ 50 | 51 | gr <- get_graph_login() 52 | usr <- gr$get_user("myname@aadtenant.com") 53 | 54 | grps <- usr$list_group_memberships() 55 | grp <- gr$get_group(grps[1]) 56 | 57 | grp$list_members() 58 | grp$list_owners() 59 | 60 | # capping the number of results 61 | grp$list_members(n=10) 62 | 63 | # get the pager object for a listing method 64 | pager <- grp$list_members(n=NULL) 65 | pager$value 66 | 67 | } 68 | } 69 | \seealso{ 70 | \link{ms_graph}, \link{az_app}, \link{az_user}, \link{az_object} 71 | 72 | \href{https://learn.microsoft.com/en-us/graph/overview}{Microsoft Graph overview}, 73 | \href{https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0}{REST API reference} 74 | } 75 | -------------------------------------------------------------------------------- /man/az_object.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/az_object.R 3 | \docType{class} 4 | \name{az_object} 5 | \alias{az_object} 6 | \title{Azure Active Directory object} 7 | \format{ 8 | An R6 object of class \code{az_object}, inheriting from \code{ms_object}. 9 | } 10 | \description{ 11 | Base class representing an Azure Active Directory object in Microsoft Graph. 12 | } 13 | \section{Fields}{ 14 | 15 | \itemize{ 16 | \item \code{token}: The token used to authenticate with the Graph host. 17 | \item \code{tenant}: The Azure Active Directory tenant for this object. 18 | \item \code{type}: The type of object: user, group, application or service principal. 19 | \item \code{properties}: The object properties. 20 | } 21 | } 22 | 23 | \section{Methods}{ 24 | 25 | \itemize{ 26 | \item \code{new(...)}: Initialize a new directory object. Do not call this directly; see 'Initialization' below. 27 | \item \code{delete(confirm=TRUE)}: Delete an object. By default, ask for confirmation first. 28 | \item \code{update(...)}: Update the object information in Azure Active Directory. 29 | \item \code{do_operation(...)}: Carry out an arbitrary operation on the object. 30 | \item \code{sync_fields()}: Synchronise the R object with the data in Azure Active Directory. 31 | \item \code{list_group_memberships(security_only=FALSE, filter=NULL, n=Inf)}: Return the IDs of all groups this object is a member of. If \code{security_only} is TRUE, only security group IDs are returned. 32 | \item \code{list_object_memberships(security_only=FALSE, filter=NULL, n=Inf)}: Return the IDs of all groups, administrative units and directory roles this object is a member of. 33 | } 34 | } 35 | 36 | \section{Initialization}{ 37 | 38 | Objects of this class should not be created directly. Instead, create an object of the appropriate subclass: \link{az_app}, \link{az_service_principal}, \link{az_user}, \link{az_group}. 39 | } 40 | 41 | \section{List methods}{ 42 | 43 | All \verb{list_*} methods have \code{filter} and \code{n} arguments to limit the number of results. The former should be an \href{https://learn.microsoft.com/en-us/graph/query-parameters#filter-parameter}{OData expression} as a string to filter the result set on. The latter should be a number setting the maximum number of (filtered) results to return. The default values are \code{filter=NULL} and \code{n=Inf}. If \code{n=NULL}, the \code{ms_graph_pager} iterator object is returned instead to allow manual iteration over the results. 44 | 45 | Support in the underlying Graph API for OData queries is patchy. Not all endpoints that return lists of objects support filtering, and if they do, they may not allow all of the defined operators. If your filtering expression results in an error, you can carry out the operation without filtering and then filter the results on the client side. 46 | } 47 | 48 | \seealso{ 49 | \link{ms_graph}, \link{az_app}, \link{az_service_principal}, \link{az_user}, \link{az_group} 50 | 51 | \href{https://learn.microsoft.com/en-us/graph/overview}{Microsoft Graph overview}, 52 | \href{https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0}{REST API reference} 53 | } 54 | -------------------------------------------------------------------------------- /man/az_service_principal.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/az_svc_principal.R 3 | \docType{class} 4 | \name{az_service_principal} 5 | \alias{az_service_principal} 6 | \title{Service principal in Azure Active Directory} 7 | \format{ 8 | An R6 object of class \code{az_service_principal}, inheriting from \code{az_object}. 9 | } 10 | \description{ 11 | Class representing an AAD service principal. 12 | } 13 | \section{Fields}{ 14 | 15 | \itemize{ 16 | \item \code{token}: The token used to authenticate with the Graph host. 17 | \item \code{tenant}: The Azure Active Directory tenant for this service principal. 18 | \item \code{type}: always "service principal" for a service principal object. 19 | \item \code{properties}: The service principal properties. 20 | } 21 | } 22 | 23 | \section{Methods}{ 24 | 25 | \itemize{ 26 | \item \code{new(...)}: Initialize a new service principal object. Do not call this directly; see 'Initialization' below. 27 | \item \code{delete(confirm=TRUE)}: Delete a service principal. By default, ask for confirmation first. 28 | \item \code{update(...)}: Update the service principal information in Azure Active Directory. 29 | \item \code{do_operation(...)}: Carry out an arbitrary operation on the service principal. 30 | \item \code{sync_fields()}: Synchronise the R object with the service principal data in Azure Active Directory. 31 | } 32 | } 33 | 34 | \section{Initialization}{ 35 | 36 | Creating new objects of this class should be done via the \code{create_service_principal} and \code{get_service_principal} methods of the \link{ms_graph} and \link{az_app} classes. Calling the \code{new()} method for this class only constructs the R object; it does not call the Microsoft Graph API to create the actual service principal. 37 | } 38 | 39 | \seealso{ 40 | \link{ms_graph}, \link{az_app}, \link{az_object} 41 | 42 | \href{https://learn.microsoft.com/en-us/graph/overview}{Azure Microsoft Graph overview}, 43 | \href{https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0}{REST API reference} 44 | } 45 | -------------------------------------------------------------------------------- /man/az_user.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/az_user.R 3 | \docType{class} 4 | \name{az_user} 5 | \alias{az_user} 6 | \title{User in Azure Active Directory} 7 | \format{ 8 | An R6 object of class \code{az_user}, inheriting from \code{az_object}. 9 | } 10 | \description{ 11 | Class representing an AAD user account. 12 | } 13 | \section{Fields}{ 14 | 15 | \itemize{ 16 | \item \code{token}: The token used to authenticate with the Graph host. 17 | \item \code{tenant}: The Azure Active Directory tenant for this user. 18 | \item \code{type}: always "user" for a user object. 19 | \item \code{properties}: The user properties. 20 | } 21 | } 22 | 23 | \section{Methods}{ 24 | 25 | \itemize{ 26 | \item \code{new(...)}: Initialize a new user object. Do not call this directly; see 'Initialization' below. 27 | \item \code{delete(confirm=TRUE)}: Delete a user account. By default, ask for confirmation first. 28 | \item \code{update(...)}: Update the user information in Azure Active Directory. 29 | \item \code{do_operation(...)}: Carry out an arbitrary operation on the user account. 30 | \item \code{sync_fields()}: Synchronise the R object with the app data in Azure Active Directory. 31 | \item \code{list_direct_memberships(filter=NULL, n=Inf)}: List the groups and directory roles this user is a direct member of. 32 | \item \code{list_owned_objects(type=c("user", "group", "application", "servicePrincipal"), filter=NULL, n=Inf)}: List directory objects (groups/apps/service principals) owned by this user. Specify the \code{type} argument to limit the result to specific object type(s). 33 | \item \code{list_created_objects(type=c("user", "group", "application", "servicePrincipal"), filter=NULL, n=Inf)}: List directory objects (groups/apps/service principals) created by this user. Specify the \code{type} argument to limit the result to specific object type(s). 34 | \item \code{list_owned_devices(filter=NULL, n=Inf)}: List the devices owned by this user. 35 | \item \code{list_registered_devices(filter=NULL, n=Inf)}: List the devices registered by this user. 36 | \item \code{reset_password(password=NULL, force_password_change=TRUE)}: Resets a user password. By default the new password will be randomly generated, and must be changed at next login. 37 | } 38 | } 39 | 40 | \section{Initialization}{ 41 | 42 | Creating new objects of this class should be done via the \code{create_user} and \code{get_user} methods of the \link{ms_graph} and \link{az_app} classes. Calling the \code{new()} method for this class only constructs the R object; it does not call the Microsoft Graph API to create the actual user account. 43 | } 44 | 45 | \section{List methods}{ 46 | 47 | All \verb{list_*} methods have \code{filter} and \code{n} arguments to limit the number of results. The former should be an \href{https://learn.microsoft.com/en-us/graph/query-parameters#filter-parameter}{OData expression} as a string to filter the result set on. The latter should be a number setting the maximum number of (filtered) results to return. The default values are \code{filter=NULL} and \code{n=Inf}. If \code{n=NULL}, the \code{ms_graph_pager} iterator object is returned instead to allow manual iteration over the results. 48 | 49 | Support in the underlying Graph API for OData queries is patchy. Not all endpoints that return lists of objects support filtering, and if they do, they may not allow all of the defined operators. If your filtering expression results in an error, you can carry out the operation without filtering and then filter the results on the client side. 50 | } 51 | 52 | \examples{ 53 | \dontrun{ 54 | 55 | gr <- get_graph_login() 56 | 57 | # my user account 58 | gr$get_user() 59 | 60 | # another user account 61 | usr <- gr$get_user("myname@aadtenant.com") 62 | 63 | grps <- usr$list_direct_memberships() 64 | head(grps) 65 | 66 | # owned objects 67 | usr$list_owned_objects() 68 | 69 | # owned apps and service principals 70 | usr$list_owned_objects(type=c("application", "servicePrincipal")) 71 | 72 | # first 5 objects 73 | usr$list_owned_objects(n=5) 74 | 75 | # get the pager object 76 | pager <- usr$list_owned_objects(n=NULL) 77 | pager$value 78 | 79 | } 80 | } 81 | \seealso{ 82 | \link{ms_graph}, \link{az_app}, \link{az_group}, \link{az_device}, \link{az_object} 83 | 84 | \href{https://learn.microsoft.com/en-us/graph/overview}{Microsoft Graph overview}, 85 | \href{https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0}{REST API reference} 86 | } 87 | -------------------------------------------------------------------------------- /man/call_batch_endpoint.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/batch.R 3 | \name{call_batch_endpoint} 4 | \alias{call_batch_endpoint} 5 | \title{Call the Graph API batch endpoint} 6 | \usage{ 7 | call_batch_endpoint(token, requests = list(), depends_on = NULL, 8 | api_version = getOption("azure_graph_api_version")) 9 | } 10 | \arguments{ 11 | \item{token}{An Azure OAuth token, of class \link{AzureToken}.} 12 | 13 | \item{requests}{A list of \link{graph_request} objects, representing individual requests to the Graph API.} 14 | 15 | \item{depends_on}{An optional named vector, or TRUE. See below.} 16 | 17 | \item{api_version}{The API version to use, which will form part of the URL sent to the host.} 18 | } 19 | \value{ 20 | A list containing the responses to each request. Each item has components \code{id} and \code{status} at a minimum. It may also contain \code{headers} and \code{body}, depending on the specifics of the request. 21 | } 22 | \description{ 23 | Call the Graph API batch endpoint 24 | } 25 | \details{ 26 | Use this function to combine multiple requests into a single HTTPS call. This can save significant network latency. 27 | 28 | The \code{depends_on} argument specifies the dependencies that may exist between requests. The default is to treat the requests as independent, which allows them to be executed in parallel. If \code{depends_on} is TRUE, each request is specified as depending on the immediately preceding request. Otherwise, this should be a named vector or list that gives the dependency or dependencies for each request. 29 | 30 | There are 2 restrictions on \code{depends_on}: 31 | \itemize{ 32 | \item If one request has a dependency, then all requests must have dependencies specified 33 | \item A request can only depend on previous requests in the list, not on later ones. 34 | } 35 | 36 | A request list that has dependencies will be executed serially. 37 | } 38 | \examples{ 39 | \dontrun{ 40 | 41 | req1 <- graph_request$new("me") 42 | 43 | # a new email message in Outlook 44 | req_create <- graph_request$new("me/messages", 45 | body=list( 46 | body=list( 47 | content="Hello from R", 48 | content_type="text" 49 | ), 50 | subject="Hello", 51 | toRecipients="bob@example.com" 52 | ), 53 | http_verb="POST" 54 | ) 55 | 56 | # messages in drafts 57 | req_get <- graph_request$new("me/mailFolders/drafts/messages") 58 | 59 | # requests are dependent: 2nd list of drafts will include just-created message 60 | call_batch_endpoint(token, list(req_get, req_create, req_get), depends_on=TRUE) 61 | 62 | # alternate way: enumerate all requests 63 | call_batch_endpoint(token, list(req_get, req_create, req_get), depends_on=c("2"=1, "3"=2)) 64 | 65 | } 66 | } 67 | \seealso{ 68 | \link{graph_request}, \link{call_graph_endpoint} 69 | 70 | \href{https://learn.microsoft.com/en-us/graph/overview}{Microsoft Graph overview}, 71 | \href{https://learn.microsoft.com/en-us/graph/json-batching}{Batch endpoint documentation} 72 | 73 | \href{https://docs.oasis-open.org/odata/odata-json-format/v4.01/odata-json-format-v4.01.html#sec_BatchRequestsandResponses}{OData documentation on batch requests} 74 | } 75 | -------------------------------------------------------------------------------- /man/call_graph.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/call_graph.R 3 | \name{call_graph_endpoint} 4 | \alias{call_graph_endpoint} 5 | \alias{call_graph_url} 6 | \title{Call the Microsoft Graph REST API} 7 | \usage{ 8 | call_graph_endpoint(token, operation, ..., options = list(), 9 | api_version = getOption("azure_graph_api_version")) 10 | 11 | call_graph_url(token, url, ..., body = NULL, encode = "json", 12 | http_verb = c("GET", "DELETE", "PUT", "POST", "HEAD", "PATCH"), 13 | http_status_handler = c("stop", "warn", "message", "pass"), 14 | simplify = FALSE, auto_refresh = TRUE) 15 | } 16 | \arguments{ 17 | \item{token}{An Azure OAuth token, of class \link{AzureToken}.} 18 | 19 | \item{operation}{The operation to perform, which will form part of the URL path.} 20 | 21 | \item{...}{Other arguments passed to lower-level code, ultimately to the appropriate functions in httr.} 22 | 23 | \item{options}{A named list giving the URL query parameters.} 24 | 25 | \item{api_version}{The API version to use, which will form part of the URL sent to the host.} 26 | 27 | \item{url}{A complete URL to send to the host.} 28 | 29 | \item{body}{The body of the request, for \code{PUT}/\code{POST}/\code{PATCH}.} 30 | 31 | \item{encode}{The encoding (really content-type) for the request body. The default value "json" means to serialize a list body into a JSON object. If you pass an already-serialized JSON object as the body, set \code{encode} to "raw".} 32 | 33 | \item{http_verb}{The HTTP verb as a string, one of \code{GET}, \code{PUT}, \code{POST}, \code{DELETE}, \code{HEAD} or \code{PATCH}.} 34 | 35 | \item{http_status_handler}{How to handle in R the HTTP status code of a response. \code{"stop"}, \code{"warn"} or \code{"message"} will call the appropriate handlers in httr, while \code{"pass"} ignores the status code.} 36 | 37 | \item{simplify}{Whether to turn arrays of objects in the JSON response into data frames. Set this to \code{TRUE} if you are expecting the endpoint to return tabular data and you want a tabular result, as opposed to a list of objects.} 38 | 39 | \item{auto_refresh}{Whether to refresh/renew the OAuth token if it is no longer valid.} 40 | } 41 | \value{ 42 | If \code{http_status_handler} is one of \code{"stop"}, \code{"warn"} or \code{"message"}, the status code of the response is checked. If an error is not thrown, the parsed content of the response is returned with the status code attached as the "status" attribute. 43 | 44 | If \code{http_status_handler} is \code{"pass"}, the entire response is returned without modification. 45 | } 46 | \description{ 47 | Call the Microsoft Graph REST API 48 | } 49 | \details{ 50 | These functions form the low-level interface between R and Microsoft Graph. \code{call_graph_endpoint} forms a URL from its arguments and passes it to \code{call_graph_url}. 51 | 52 | If \code{simplify} is \code{TRUE}, \code{call_graph_url} will exploit the ability of \code{jsonlite::fromJSON} to convert arrays of objects into R data frames. This can be useful for REST calls that return tabular data. However, it can also cause problems for \emph{paged} lists, where each page will be turned into a separate data frame; as the individual objects may not have the same fields, the resulting data frames will also have differing columns. This will cause base R's \code{rbind} to fail when binding the pages together. When processing paged lists, AzureGraph will use \code{vctrs::vec_rbind} instead of \code{rbind} when the vctrs package is available; \code{vec_rbind} does not have this problem. For safety, you should only set \code{simplify=TRUE} when vctrs is installed. 53 | } 54 | \seealso{ 55 | \link[httr:GET]{httr::GET}, \link[httr:PUT]{httr::PUT}, \link[httr:POST]{httr::POST}, \link[httr:DELETE]{httr::DELETE}, \link[httr:stop_for_status]{httr::stop_for_status}, \link[httr:content]{httr::content} 56 | } 57 | -------------------------------------------------------------------------------- /man/extract_list_values.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ms_graph_pager.R 3 | \name{extract_list_values} 4 | \alias{extract_list_values} 5 | \title{Get the list of values from a Graph pager object} 6 | \usage{ 7 | extract_list_values(pager, n = Inf) 8 | } 9 | \arguments{ 10 | \item{pager}{An object of class \code{ms_graph_pager}, which is an iterator for a list of paged query results.} 11 | 12 | \item{n}{The number of items from the list to return. Note this is \emph{not} the number of \emph{pages} (each page will usually contain multiple items). The default value of \code{Inf} extracts all the values from the list, leaving the pager empty. If this is NULL, the pager itself is returned.} 13 | } 14 | \value{ 15 | If \code{n} is \code{Inf} or a number, the items from the paged query results. The format of the returned value depends on the pager settings. This will either be a nested list containing the properties for each of the items; a list of R6 objects; or a data frame. If the pager is empty, an error is thrown. 16 | 17 | If \code{n} is NULL, the pager itself is returned. 18 | } 19 | \description{ 20 | Get the list of values from a Graph pager object 21 | } 22 | \details{ 23 | This is a convenience function to perform the common task of extracting all or some of the items from a paged response. 24 | } 25 | \examples{ 26 | \dontrun{ 27 | 28 | firstpage <- call_graph_endpoint(token, "me/memberOf") 29 | pager <- ms_graph_pager$new(token, firstpage) 30 | extract_list_values(pager) 31 | 32 | # trying to extract values a 2nd time will fail 33 | try(extract_list_values(pager)) 34 | 35 | } 36 | } 37 | \seealso{ 38 | \link{ms_graph_pager}, \link{ms_object}, \link{call_graph_endpoint} 39 | } 40 | -------------------------------------------------------------------------------- /man/figures/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/AzureGraph/983aaaaaa778d9e1a6de440841fced178f4ae1a4/man/figures/logo.png -------------------------------------------------------------------------------- /man/find_class_generator.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/zzz_class_directory.R 3 | \name{find_class_generator} 4 | \alias{find_class_generator} 5 | \title{Find the R6 class for a Graph object} 6 | \usage{ 7 | find_class_generator(props, type_filter = NULL, default_generator = ms_object) 8 | } 9 | \arguments{ 10 | \item{props}{A list of object properties, generally the result of a Graph API call.} 11 | 12 | \item{type_filter}{An optional vector of types by which to filter the result.} 13 | 14 | \item{default_generator}{The default class generator to use, if a match couldn't be found.} 15 | } 16 | \value{ 17 | An R6 class generator for the appropriate AzureGraph class. If no matching R6 class could be found, the default generator is returned. If \code{type_filter} is provided, but the matching R6 class isn't in the filter, NULL is returned. 18 | } 19 | \description{ 20 | Find the R6 class for a Graph object 21 | } 22 | \details{ 23 | This function maps Graph objects to AzureGraph classes. 24 | } 25 | -------------------------------------------------------------------------------- /man/format.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/format.R 3 | \name{format_public_fields} 4 | \alias{format_public_fields} 5 | \alias{format_public_methods} 6 | \title{Format a Microsoft Graph or Azure object} 7 | \usage{ 8 | format_public_fields(env, exclude = character(0)) 9 | 10 | format_public_methods(env) 11 | } 12 | \arguments{ 13 | \item{env}{An R6 object's environment for printing.} 14 | 15 | \item{exclude}{Objects in \code{env} to exclude from the printout.} 16 | } 17 | \description{ 18 | Miscellaneous functions for printing Microsoft Graph and Azure R6 objects 19 | } 20 | \details{ 21 | These are utilities to aid in printing R6 objects created by this package or its descendants. They are not meant to be called by the user. 22 | } 23 | -------------------------------------------------------------------------------- /man/graph_login.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/graph_login.R 3 | \name{create_graph_login} 4 | \alias{create_graph_login} 5 | \alias{get_graph_login} 6 | \alias{delete_graph_login} 7 | \alias{list_graph_logins} 8 | \title{Login to Azure Active Directory Graph} 9 | \usage{ 10 | create_graph_login(tenant = "common", app = NULL, password = NULL, 11 | username = NULL, certificate = NULL, auth_type = NULL, version = 2, 12 | host = "https://graph.microsoft.com/", 13 | aad_host = "https://login.microsoftonline.com/", scopes = ".default", 14 | config_file = NULL, token = NULL, ...) 15 | 16 | get_graph_login(tenant = "common", selection = NULL, app = NULL, 17 | scopes = NULL, auth_type = NULL, refresh = TRUE) 18 | 19 | delete_graph_login(tenant = "common", confirm = TRUE) 20 | 21 | list_graph_logins() 22 | } 23 | \arguments{ 24 | \item{tenant}{The Azure Active Directory tenant for which to obtain a login client. Can be a name ("myaadtenant"), a fully qualified domain name ("myaadtenant.onmicrosoft.com" or "mycompanyname.com"), or a GUID. The default is to login via the "common" tenant, which will infer your actual tenant from your credentials.} 25 | 26 | \item{app}{The client/app ID to use to authenticate with Azure Active Directory. The default is to login interactively using the Azure CLI cross-platform app, but you can supply your own app credentials as well.} 27 | 28 | \item{password}{If \code{auth_type == "client_credentials"}, the app secret; if \code{auth_type == "resource_owner"}, your account password.} 29 | 30 | \item{username}{If \code{auth_type == "resource_owner"}, your username.} 31 | 32 | \item{certificate}{If `auth_type == "client_credentials", a certificate to authenticate with. This is a more secure alternative to using an app secret.} 33 | 34 | \item{auth_type}{The OAuth authentication method to use, one of "client_credentials", "authorization_code", "device_code" or "resource_owner". If \code{NULL}, this is chosen based on the presence of the \code{username} and \code{password} arguments.} 35 | 36 | \item{version}{The Azure Active Directory version to use for authenticating.} 37 | 38 | \item{host}{Your Microsoft Graph host. Defaults to \verb{https://graph.microsoft.com/}. Change this if you are using a government or private cloud.} 39 | 40 | \item{aad_host}{Azure Active Directory host for authentication. Defaults to \verb{https://login.microsoftonline.com/}. Change this if you are using a government or private cloud.} 41 | 42 | \item{scopes}{The Microsoft Graph scopes (permissions) to obtain for this Graph login. For \code{create_graph_login}, this is used only for \code{version=2}. For \code{get_graph_login}, set this to NA to require an AAD v1.0 token.} 43 | 44 | \item{config_file}{Optionally, a JSON file containing any of the arguments listed above. Arguments supplied in this file take priority over those supplied on the command line. You can also use the output from the Azure CLI \verb{az ad sp create-for-rbac} command.} 45 | 46 | \item{token}{Optionally, an OAuth 2.0 token, of class \link[AzureAuth:AzureToken]{AzureAuth::AzureToken}. This allows you to reuse the authentication details for an existing session. If supplied, all other arguments to \code{create_graph_login} will be ignored.} 47 | 48 | \item{...}{Other arguments passed to \code{ms_graph$new()}.} 49 | 50 | \item{selection}{For \code{get_graph_login}, if you have multiple logins for a given tenant, which one to use. This can be a number, or the input MD5 hash of the token used for the login. If not supplied, \code{get_graph_login} will print a menu and ask you to choose a login.} 51 | 52 | \item{refresh}{For \code{get_graph_login}, whether to refresh the authentication token on loading the client.} 53 | 54 | \item{confirm}{For \code{delete_graph_login}, whether to ask for confirmation before deleting.} 55 | } 56 | \value{ 57 | For \code{get_graph_login} and \code{create_graph_login}, an object of class \code{ms_graph}, representing the login client. For \code{list_graph_logins}, a (possibly nested) list of such objects. 58 | 59 | If the AzureR data directory for saving credentials does not exist, \code{get_graph_login} will throw an error. 60 | } 61 | \description{ 62 | Login to Azure Active Directory Graph 63 | } 64 | \details{ 65 | \code{create_graph_login} creates a login client to authenticate with Microsoft Graph, using the supplied arguments. The authentication token is obtained using \link{get_azure_token}, which automatically caches and reuses tokens for subsequent sessions. 66 | 67 | For interactive use, you would normally \emph{not} supply the \code{username} and \code{password} arguments. Omitting them will prompt \code{create_graph_login} to authenticate you with AAD using your browser, which is the recommended method. If you don't have a browser available to your R session, for example if you're using RStudio Server or Azure Databricks, you can specify \verb{auth_type="device_code}". 68 | 69 | For non-interactive use, for example if you're calling AzureGraph in a deployment pipeline, the recommended authentication method is via client credentials. For this method, you supply \emph{only} the \code{password} argument, which should contain the client secret for your app registration. You must also specify your own app registration ID, in the \code{app} argument. 70 | 71 | The AzureAuth package has a \href{https://cran.r-project.org/package=AzureAuth/vignettes/scenarios.html}{vignette} that goes into more detail on these authentication scenarios. 72 | 73 | \code{get_graph_login} returns a previously created login client. If there are multiple existing clients, you can specify which client to return via the \code{selection}, \code{app}, \code{scopes} and \code{auth_type} arguments. If you don't specify which one to return, it will pop up a menu and ask you to choose one. 74 | 75 | One difference between \code{create_graph_login} and \code{get_graph_login} is the former will delete any previously saved credentials that match the arguments it was given. You can use this to force AzureGraph to remove obsolete tokens that may be lying around. 76 | } 77 | \examples{ 78 | \dontrun{ 79 | 80 | # without any arguments, this will create a client using your AAD organisational account 81 | az <- create_graph_login() 82 | 83 | # retrieve the login in subsequent sessions 84 | az <- get_graph_login() 85 | 86 | # this will create an Microsoft Graph client for the tenant 'mytenant.onmicrosoft.com', 87 | # using the client_credentials method 88 | az <- create_graph_login("mytenant", app="{app_id}", password="{password}") 89 | 90 | # you can also login using credentials in a json file 91 | az <- create_graph_login(config_file="~/creds.json") 92 | 93 | # creating and obtaining a login with specific scopes 94 | create_graph_login("mytenant", scopes=c("User.Read", "Files.ReadWrite.All")) 95 | get_graph_login("mytenant", scopes=c("User.Read", "Files.ReadWrite.All")) 96 | 97 | # to use your personal account, set the tenant to one of the following 98 | create_graph_login("9188040d-6c67-4c5b-b112-36a304b66dad") 99 | create_graph_login("consumers") # requires AzureAuth 1.3.0 100 | 101 | } 102 | } 103 | \seealso{ 104 | \link{ms_graph}, \link[AzureAuth:get_azure_token]{AzureAuth::get_azure_token} for more details on authentication methods 105 | 106 | \href{https://cran.r-project.org/package=AzureAuth/vignettes/scenarios.html}{AzureAuth vignette on authentication scenarios} 107 | 108 | \href{https://learn.microsoft.com/en-us/graph/overview}{Microsoft Graph overview}, 109 | \href{https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0}{REST API reference} 110 | } 111 | -------------------------------------------------------------------------------- /man/graph_request.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/batch.R 3 | \docType{class} 4 | \name{graph_request} 5 | \alias{graph_request} 6 | \title{Microsoft Graph request} 7 | \format{ 8 | An R6 object of class \code{graph_request}. 9 | } 10 | \description{ 11 | Class representing a request to the Microsoft Graph API. Currently this is used only in building a batch call. 12 | } 13 | \section{Methods}{ 14 | 15 | \itemize{ 16 | \item \code{new(...)}: Initialize a new request object with the given parameters. See 'Details' below. 17 | \item \code{batchify()}: Generate a list object suitable for incorporating into a call to the batch endpoint. 18 | } 19 | } 20 | 21 | \section{Details}{ 22 | 23 | The \code{initialize()} method takes the following arguments, representing the components of a HTTPS request: 24 | \itemize{ 25 | \item \code{op}: The path of the HTTPS URL, eg \verb{/me/drives}. 26 | \item \code{body}: The body of the HTTPS request, if it is a PUT, POST or PATCH. 27 | \item \code{options}: A list containing the query parameters for the URL. 28 | \item \code{headers}: Any optional HTTP headers for the request. 29 | \item \code{encode}: If a request body is present, how it should be encoded when sending it to the endpoint. The default is \code{json}, meaning it will be sent as JSON text; an alternative is \code{raw}, for binary data. 30 | \item \code{http_verb}: One of "GET" (the default), "DELETE", "PUT", "POST", "HEAD", or "PATCH". 31 | } 32 | 33 | This class is currently used only for building batch calls. Future versions of AzureGraph may be refactored to use it in general API calls as well. 34 | } 35 | 36 | \examples{ 37 | graph_request$new("me") 38 | 39 | # a new email message in Outlook 40 | graph_request$new("me/messages", 41 | body=list( 42 | body=list( 43 | content="Hello from R", 44 | content_type="text" 45 | ), 46 | subject="Hello", 47 | toRecipients="bob@example.com" 48 | ), 49 | http_verb="POST" 50 | ) 51 | } 52 | \seealso{ 53 | \link{call_batch_endpoint} 54 | 55 | \href{https://learn.microsoft.com/en-us/graph/overview}{Microsoft Graph overview}, 56 | \href{https://learn.microsoft.com/en-us/graph/json-batching}{Batch endpoint documentation} 57 | } 58 | -------------------------------------------------------------------------------- /man/info.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/is.R 3 | \name{is_app} 4 | \alias{is_app} 5 | \alias{is_service_principal} 6 | \alias{is_user} 7 | \alias{is_group} 8 | \alias{is_directory_role} 9 | \alias{is_aad_object} 10 | \alias{is_msgraph_object} 11 | \title{Informational functions} 12 | \usage{ 13 | is_app(object) 14 | 15 | is_service_principal(object) 16 | 17 | is_user(object) 18 | 19 | is_group(object) 20 | 21 | is_directory_role(object) 22 | 23 | is_aad_object(object) 24 | 25 | is_msgraph_object(object) 26 | } 27 | \arguments{ 28 | \item{object}{An R object.} 29 | } 30 | \value{ 31 | A boolean. 32 | } 33 | \description{ 34 | These functions return whether the object is of the corresponding class. 35 | } 36 | -------------------------------------------------------------------------------- /man/ms_graph.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ms_graph.R 3 | \docType{class} 4 | \name{ms_graph} 5 | \alias{ms_graph} 6 | \title{Microsoft Graph} 7 | \format{ 8 | An R6 object of class \code{ms_graph}. 9 | } 10 | \description{ 11 | Base class for interacting with Microsoft Graph API. 12 | } 13 | \section{Methods}{ 14 | 15 | \itemize{ 16 | \item \code{new(tenant, app, ...)}: Initialize a new Microsoft Graph connection with the given credentials. See 'Authentication' for more details. 17 | \item \code{create_app(name, ..., add_password=TRUE, password_name=NULL, password_duration=2, certificate=NULL, create_service_principal=TRUE)}: Creates a new app registration in Azure Active Directory. See 'App creation' below. 18 | \item \code{get_app(app_id, object_id)}: Retrieves an existing app registration, via either its app ID or object ID. 19 | \item \code{list_apps(filter=NULL, n=Inf)}: Lists the app registrations in the current tenant. 20 | \item \code{delete_app(app_id, object_id, confirm=TRUE)}: Deletes an existing app registration. Any associated service principal will also be deleted. 21 | \item \code{create_service_principal(app_id, ...)}: Creates a service principal for a app registration. 22 | \item \code{get_service_principal()}: Retrieves an existing service principal. 23 | \item \code{list_service_principals(filter=NULL, n=Inf)}: Lists the service principals in the current tenant. 24 | \item \code{delete_service_principal()}: Deletes an existing service principal. 25 | \item \code{create_user(name, email, enabled=TRUE, ..., password=NULL, force_password_change=TRUE)}: Creates a new user account. By default this will be a work account (not social or local) in the current tenant, and will have a randomly generated password that must be changed at next login. 26 | \item \code{get_user(user_id, email, name)}: Retrieves an existing user account. You can supply either the user ID, email address, or display name. The default is to return the logged-in user. 27 | \item \code{list_users(filter=NULL, n=Inf)}: Lists the users in the current tenant. 28 | \item \code{delete_user(user_id, email, name, confirm=TRUE)}: Deletes a user account. 29 | \item \code{create_group(name, email, ...)}: Creates a new group. Note that only security groups can be created via the Microsoft Graph API. 30 | \item \code{get_group(group_id, name)}: Retrieves an existing group. 31 | \item \code{list_groups(filter=NULL, n=Inf)}: Lists the groups in the current tenant. 32 | \item \code{delete_group(group_id, name, confirm=TRUE)}: Deletes a group. 33 | \item \code{call_graph_endpoint(op="", ...)}: Calls the Microsoft Graph API using this object's token and tenant as authentication arguments. See \link{call_graph_endpoint}. 34 | \item \code{call_batch_endpoint(requests=list(), ...)}: Calls the batch endpoint with a list of individual requests. See \link{call_batch_endpoint}. 35 | \item \code{get_aad_object(id)}: Retrieves an arbitrary Azure Active Directory object by ID. 36 | } 37 | } 38 | 39 | \section{Authentication}{ 40 | 41 | The recommended way to authenticate with Microsoft Graph is via the \link{create_graph_login} function, which creates a new instance of this class. 42 | 43 | To authenticate with the \code{ms_graph} class directly, provide the following arguments to the \code{new} method: 44 | \itemize{ 45 | \item \code{tenant}: Your tenant ID. This can be a name ("myaadtenant"), a fully qualified domain name ("myaadtenant.onmicrosoft.com" or "mycompanyname.com"), or a GUID. 46 | \item \code{app}: The client/app ID to use to authenticate with Azure Active Directory. The default is to login interactively using the Azure CLI cross-platform app, but it's recommended to supply your own app credentials if possible. 47 | \item \code{password}: if \code{auth_type == "client_credentials"}, the app secret; if \code{auth_type == "resource_owner"}, your account password. 48 | \item \code{username}: if \code{auth_type == "resource_owner"}, your username. 49 | \item \code{certificate}: If `auth_type == "client_credentials", a certificate to authenticate with. This is a more secure alternative to using an app secret. 50 | \item \code{auth_type}: The OAuth authentication method to use, one of "client_credentials", "authorization_code", "device_code" or "resource_owner". See \link{get_azure_token} for how the default method is chosen, along with some caveats. 51 | \item \code{version}: The Azure Active Directory (AAD) version to use for authenticating. 52 | \item \code{host}: your Microsoft Graph host. Defaults to \verb{https://graph.microsoft.com/}. 53 | \item \code{aad_host}: Azure Active Directory host for authentication. Defaults to \verb{https://login.microsoftonline.com/}. Change this if you are using a government or private cloud. 54 | \item \code{scopes}: The Microsoft Graph scopes (permissions) to obtain for this Graph login. Only for \code{version=2}. 55 | \item \code{token}: Optionally, an OAuth 2.0 token, of class \link[AzureAuth:AzureToken]{AzureAuth::AzureToken}. This allows you to reuse the authentication details for an existing session. If supplied, all other arguments will be ignored. 56 | } 57 | } 58 | 59 | \section{App creation}{ 60 | 61 | The \code{create_app} method creates a new app registration. By default, a new app will have a randomly generated strong password with duration of 2 years. To skip assigning a password, set the \code{add_password} argument to FALSE. 62 | 63 | The \code{certificate} argument allows authenticating via a certificate instead of a password. This should be a character string containing the certificate public key (aka the CER file). Alternatively it can be an list, or an object of class \code{AzureKeyVault::stored_cert} representing a certificate stored in an Azure Key Vault. See the examples below. 64 | 65 | A new app will also have a service principal created for it by default. To disable this, set \code{create_service_principal=FALSE}. 66 | } 67 | 68 | \section{List methods}{ 69 | 70 | All \verb{list_*} methods have \code{filter} and \code{n} arguments to limit the number of results. The former should be an \href{https://learn.microsoft.com/en-us/graph/query-parameters#filter-parameter}{OData expression} as a string to filter the result set on. The latter should be a number setting the maximum number of (filtered) results to return. The default values are \code{filter=NULL} and \code{n=Inf}. If \code{n=NULL}, the \code{ms_graph_pager} iterator object is returned instead to allow manual iteration over the results. 71 | 72 | Support in the underlying Graph API for OData queries is patchy. Not all endpoints that return lists of objects support filtering, and if they do, they may not allow all of the defined operators. If your filtering expression results in an error, you can carry out the operation without filtering and then filter the results on the client side. 73 | } 74 | 75 | \examples{ 76 | \dontrun{ 77 | 78 | # start a new Graph session 79 | gr <- ms_graph$new(tenant="myaadtenant.onmicrosoft.com") 80 | 81 | # authenticate with credentials in a file 82 | gr <- ms_graph$new(config_file="creds.json") 83 | 84 | # authenticate with device code 85 | gr <- ms_graph$new(tenant="myaadtenant.onmicrosoft.com", app="app_id", auth_type="device_code") 86 | 87 | # retrieve an app registration 88 | gr$get_app(app_id="myappid") 89 | 90 | # create a new app and associated service principal, set password duration to 10 years 91 | app <- gr$create_app("mynewapp", password_duration=10) 92 | 93 | # delete the app 94 | gr$delete_app(app_id=app$properties$appId) 95 | # ... but better to call the object's delete method directly 96 | app$delete() 97 | 98 | # create an app with authentication via a certificate 99 | cert <- readLines("mycert.cer") 100 | gr$create_app("mycertapp", password=FALSE, certificate=cert) 101 | 102 | # retrieving your own user details (assuming interactive authentication) 103 | gr$get_user() 104 | 105 | # retrieving another user's details 106 | gr$get_user("username@myaadtenant.onmicrosoft.com") 107 | gr$get_user(email="firstname.lastname@mycompany.com") 108 | gr$get_user(name="Hong Ooi") 109 | 110 | # get an AAD object (a group) 111 | id <- gr$get_user()$list_group_memberships()[1] 112 | gr$get_aad_object(id) 113 | 114 | # list the users in the tenant 115 | gr$list_users() 116 | 117 | # list (guest) users with a 'gmail.com' email address 118 | gr$list_users(filter="endsWith(mail,'gmail.com')") 119 | 120 | # list Microsoft 365 groups 121 | gr$list_groups(filter="groupTypes/any(c:c eq 'Unified')") 122 | 123 | } 124 | } 125 | \seealso{ 126 | \link{create_graph_login}, \link{get_graph_login} 127 | 128 | \href{https://learn.microsoft.com/en-us/graph/overview}{Microsoft Graph overview}, 129 | \href{https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0}{REST API reference} 130 | } 131 | -------------------------------------------------------------------------------- /man/ms_graph_pager.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ms_graph_pager.R 3 | \docType{class} 4 | \name{ms_graph_pager} 5 | \alias{ms_graph_pager} 6 | \title{Pager object for Graph list results} 7 | \format{ 8 | An R6 object of class \code{ms_graph_pager}. 9 | } 10 | \description{ 11 | Class representing an \emph{iterator} for a set of paged query results. 12 | } 13 | \section{Fields}{ 14 | 15 | \itemize{ 16 | \item \code{token}: The token used to authenticate with the Graph host. 17 | \item \code{output}: What the pager should yield on each iteration, either "data.frame","list" or "object". See 'Value' below. 18 | } 19 | } 20 | 21 | \section{Methods}{ 22 | 23 | \itemize{ 24 | \item \code{new(...)}: Initialize a new user object. See 'Initialization' below. 25 | \item \code{has_data()}: Returns TRUE if there are pages remaining in the iterator, or FALSE otherwise. 26 | } 27 | } 28 | 29 | \section{Active bindings}{ 30 | 31 | \itemize{ 32 | \item \code{value}: The returned value on each iteration of the pager. 33 | } 34 | } 35 | 36 | \section{Initialization}{ 37 | 38 | The recommended way to create objects of this class is via the \code{ms_object$get_list_pager()} method, but it can also be initialized directly. The arguments to the \code{new()} method are: 39 | \itemize{ 40 | \item \code{token}: The token used to authenticate with the Graph host. 41 | \item \code{first_page}: A list containing the first page of results, generally from a call to \code{call_graph_endpoint()} or the \code{do_operation()} method of an AzureGraph R6 object. 42 | \item \verb{next_link_name,value_name}: The names of the components of \code{first_page} containing the link to the next page, and the set of values for the page respectively. The default values are \verb{@odata.nextLink} and \code{value}. 43 | \item \code{generate_objects}: Whether the iterator should return a list containing the parsed JSON for the page values, or convert it into a list of R6 objects. See 'Value' below. 44 | \item \code{type_filter}: Any extra arguments required to initialise the returned objects. Only used if \code{generate_objects} is TRUE. 45 | \item \code{default_generator}: The default generator object to use when converting a list of properties into an R6 object, if the class can't be detected. Defaults to \code{ms_object}. Only used if \code{generate_objects} is TRUE. 46 | \item \code{...}: Any extra arguments required to initialise the returned objects. Only used if \code{generate_objects} is TRUE. 47 | } 48 | } 49 | 50 | \section{Value}{ 51 | 52 | The \code{value} active binding returns the page values for each iteration of the pager. This can take one of 3 forms, based on the initial format of the first page and the \code{generate_objects} argument. 53 | 54 | If the first page of results is a data frame (each item has been converted into a row), then the pager will return results as data frames. In this case, the \code{output} field is automatically set to "data.frame" and the \code{generate_objects} initialization argument is ignored. Usually this will be the case when the results are meant to represent external data, eg items in a SharePoint list. 55 | 56 | If the first page of results is a list, the \code{generate_objects} argument sets whether to convert the items in each page into R6 objects defined by the AzureGraph class framework. If \code{generate_objects} is TRUE, the \code{output} field is set to "object", and if \code{generate_objects} is FALSE, the \code{output} field is set to "list". 57 | } 58 | 59 | \examples{ 60 | \dontrun{ 61 | 62 | # list direct memberships 63 | firstpage <- call_graph_endpoint(token, "me/memberOf") 64 | 65 | pager <- ms_graph_pager$new(token, firstpage) 66 | pager$has_data() 67 | pager$value 68 | 69 | # once all the pages have been returned 70 | isFALSE(pager$has_data()) 71 | is.null(pager$value) 72 | 73 | # returning items, 1 per page, as raw lists of properties 74 | firstpage <- call_graph_endpoint(token, "me/memberOf", options=list(`$top`=1)) 75 | pager <- ms_graph_pager$new(token, firstpage, generate_objects=FALSE) 76 | lst <- NULL 77 | while(pager$has_data()) 78 | lst <- c(lst, pager$value) 79 | 80 | # returning items as a data frame 81 | firstdf <- call_graph_endpoint(token, "me/memberOf", options=list(`$top`=1), 82 | simplify=TRUE) 83 | pager <- ms_graph_pager$new(token, firstdf) 84 | df <- NULL 85 | while(pager$has_data()) 86 | df <- vctrs::vec_rbin(df, pager$value) 87 | 88 | } 89 | } 90 | \seealso{ 91 | \link{ms_object}, \link{extract_list_values} 92 | 93 | \href{https://learn.microsoft.com/en-us/graph/overview}{Microsoft Graph overview}, 94 | \href{https://learn.microsoft.com/en-us/graph/paging}{Paging documentation} 95 | } 96 | -------------------------------------------------------------------------------- /man/ms_object.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ms_object.R 3 | \docType{class} 4 | \name{ms_object} 5 | \alias{ms_object} 6 | \title{Microsoft Graph object} 7 | \format{ 8 | An R6 object of class \code{ms_object}. 9 | } 10 | \description{ 11 | Base class representing a object in Microsoft Graph. All other Graph object classes ultimately inherit from this class. 12 | } 13 | \section{Fields}{ 14 | 15 | \itemize{ 16 | \item \code{token}: The token used to authenticate with the Graph host. 17 | \item \code{tenant}: The Azure Active Directory tenant for this object. 18 | \item \code{type}: The type of object, in a human-readable format. 19 | \item \code{properties}: The object properties, as obtained from the Graph host. 20 | } 21 | } 22 | 23 | \section{Methods}{ 24 | 25 | \itemize{ 26 | \item \code{new(...)}: Initialize a new directory object. Do not call this directly; see 'Initialization' below. 27 | \item \code{delete(confirm=TRUE)}: Delete an object. By default, ask for confirmation first. 28 | \item \code{update(...)}: Update the object information in Azure Active Directory. 29 | \item \code{do_operation(...)}: Carry out an arbitrary operation on the object. 30 | \item \code{sync_fields()}: Synchronise the R object with the data in Azure Active Directory. 31 | \item \code{get_list_pager(...)}: Returns a pager object, which is an \emph{iterator} for a set of paged query results. See 'Paged results' below. 32 | } 33 | } 34 | 35 | \section{Initialization}{ 36 | 37 | Objects of this class should not be created directly. Instead, create an object of the appropriate subclass. 38 | } 39 | 40 | \section{List methods}{ 41 | 42 | All \verb{list_*} methods have \code{filter} and \code{n} arguments to limit the number of results. The former should be an \href{https://learn.microsoft.com/en-us/graph/query-parameters#filter-parameter}{OData expression} as a string to filter the result set on. The latter should be a number setting the maximum number of (filtered) results to return. The default values are \code{filter=NULL} and \code{n=Inf}. If \code{n=NULL}, the \code{ms_graph_pager} iterator object is returned instead to allow manual iteration over the results. 43 | 44 | Support in the underlying Graph API for OData queries is patchy. Not all endpoints that return lists of objects support filtering, and if they do, they may not allow all of the defined operators. If your filtering expression results in an error, you can carry out the operation without filtering and then filter the results on the client side. 45 | } 46 | 47 | \section{Paged results}{ 48 | 49 | Microsoft Graph returns lists in pages, with each page containing a subset of objects and a link to the next page. AzureGraph provides an iterator-based API that lets you access each page individually, or collect them all into a single object. 50 | 51 | To create a new pager object, call the \code{get_list_pager()} method with the following arguments: 52 | \itemize{ 53 | \item \code{lst}: A list containing the first page of results, generally from a call to the \code{do_operation()} method. 54 | \item \verb{next_link_name,value_name}: The names of the components of \code{first_page} containing the link to the next page, and the set of values for the page respectively. The default values are \verb{@odata.nextLink} and \code{value}. 55 | \item \code{generate_objects}: Whether the iterator should return a list containing the parsed JSON for the page values, or convert it into a list of R6 objects. 56 | \item \code{type_filter}: Any extra arguments required to initialise the returned objects. Only used if \code{generate_objects} is TRUE. 57 | \item \code{default_generator}: The default generator object to use when converting a list of properties into an R6 object, if the class can't be detected. Defaults to \code{ms_object}. Only used if \code{generate_objects} is TRUE. 58 | \item \code{...}: Any extra arguments required to initialise the returned objects. Only used if \code{generate_objects} is TRUE. 59 | } 60 | 61 | This returns an object of class \link{ms_graph_pager}, which is an \emph{iterator} for the set of paged results. Each call to the object's \code{value} active binding yields the next page. When all pages have been returned, \code{value} contains NULL. 62 | 63 | The format of the returned values can take one of 3 forms, based on the initial format of the first page and the \code{generate_objects} argument. 64 | 65 | If the first page of results is a data frame (each item has been converted into a row), then the pager will return results as data frames. In this case, the \code{output} field is automatically set to "data.frame" and the \code{generate_objects} initialization argument is ignored. Usually this will be the case when the results are meant to represent external data, eg items in a SharePoint list. 66 | 67 | If the first page of results is a list, the \code{generate_objects} argument sets whether to convert the items in each page into R6 objects defined by the AzureGraph class framework. If \code{generate_objects} is TRUE, the \code{output} field is set to "object", and if \code{generate_objects} is FALSE, the \code{output} field is set to "list". 68 | 69 | You can also call the \code{extract_list_values()} function to get all or some of the values from a pager, without having to manually combine the pages together. 70 | } 71 | 72 | \section{Deprecated methods}{ 73 | 74 | The following methods are private and \strong{deprecated}, and form the older AzureGraph API for accessing paged results. They will eventually be removed. 75 | \itemize{ 76 | \item \code{get_paged_list(lst, next_link_name, value_name, simplify, n)}: This method reconstructs the list, given the first page. 77 | \item \code{init_list_objects(lst, type_filter, default_generator, ...)}: \code{get_paged_list} returns a raw list, the result of parsing the JSON response from the Graph host. This method converts the list into actual R6 objects. 78 | } 79 | } 80 | 81 | \seealso{ 82 | \link{ms_graph}, \link{az_object}, \link{ms_graph_pager}, \link{extract_list_values} 83 | 84 | \href{https://learn.microsoft.com/en-us/graph/overview}{Microsoft Graph overview}, 85 | \href{https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0}{REST API reference} 86 | } 87 | -------------------------------------------------------------------------------- /man/register_graph_class.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/zzz_class_directory.R 3 | \name{register_graph_class} 4 | \alias{register_graph_class} 5 | \title{Extensible registry of Microsoft Graph classes that AzureGraph supports} 6 | \usage{ 7 | register_graph_class(name, R6_generator, check_function) 8 | } 9 | \arguments{ 10 | \item{name}{The name of the Graph class, eg "user", "servicePrincipal", etc.} 11 | 12 | \item{R6_generator}{An R6 class generator corresponding to this Graph class.} 13 | 14 | \item{check_function}{A boolean function that checks if a list of properties is for an object of this class.} 15 | } 16 | \value{ 17 | An invisible vector of registered class names. 18 | } 19 | \description{ 20 | Extensible registry of Microsoft Graph classes that AzureGraph supports 21 | } 22 | \details{ 23 | As written, AzureGraph knows about a subset of all the object classes contained in Microsoft Graph. These are mostly the classes originating from Azure Active Directory: users, groups, app registrations, service principals and registered devices. 24 | 25 | You can extend AzureGraph by writing your own R6 class that inherits from \code{ms_object}. If so, you should also \emph{register} your class by calling \code{register_graph_class} and providing the generator object, along with a check function. The latter should accept a list of object properties (as obtained from the Graph REST API), and return TRUE/FALSE based on whether the object is of your class. 26 | } 27 | \examples{ 28 | \dontrun{ 29 | 30 | # built-in 'az_user' class, for an AAD user object 31 | register_graph_class("user", az_user, 32 | function(props) !is.null(props$userPrincipalName)) 33 | 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /man/utils.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{named_list} 4 | \alias{named_list} 5 | \alias{is_empty} 6 | \title{Miscellaneous utility functions} 7 | \usage{ 8 | named_list(lst = NULL, name_fields = "name") 9 | 10 | is_empty(x) 11 | } 12 | \arguments{ 13 | \item{lst}{A named list of objects.} 14 | 15 | \item{name_fields}{The components of the objects in \code{lst}, to be used as names.} 16 | 17 | \item{x}{For \code{is_empty}, An R object.} 18 | } 19 | \value{ 20 | For \code{named_list}, the list that was passed in but with names. An empty input results in a \emph{named list} output: a list of length 0, with a \code{names} attribute. 21 | 22 | For \code{is_empty}, whether the length of the object is zero (this includes the special case of \code{NULL}). 23 | } 24 | \description{ 25 | Miscellaneous utility functions 26 | } 27 | \details{ 28 | \code{named_list} extracts from each object in \code{lst}, the components named by \code{name_fields}. It then constructs names for \code{lst} from these components, separated by a \code{"/"}. 29 | } 30 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(AzureGraph) 3 | 4 | test_check("AzureGraph") 5 | -------------------------------------------------------------------------------- /tests/testthat/test00_class.R: -------------------------------------------------------------------------------- 1 | context("Class registration") 2 | 3 | test_that("Class registration works", 4 | { 5 | newclass <- R6::R6Class("newclass", inherit=ms_object) 6 | expect_false("newclass" %in% ls(.graph_classes)) 7 | expect_silent(register_graph_class("newclass", newclass, function(x) FALSE)) 8 | expect_true("newclass" %in% ls(.graph_classes)) 9 | expect_error(register_graph_class("badclass", "badclassname", FALSE)) 10 | }) 11 | -------------------------------------------------------------------------------- /tests/testthat/test01_auth.R: -------------------------------------------------------------------------------- 1 | context("Authentication") 2 | 3 | tenant <- Sys.getenv("AZ_TEST_TENANT_ID") 4 | app <- Sys.getenv("AZ_TEST_APP_ID") 5 | password <- Sys.getenv("AZ_TEST_PASSWORD") 6 | 7 | if(tenant == "" || app == "" || password == "") 8 | skip("Authentication tests skipped: Microsoft Graph credentials not set") 9 | 10 | if(!interactive()) 11 | skip("Authentication tests skipped: must be in interactive session") 12 | 13 | scopes <- c("https://graph.microsoft.com/.default", "openid", "offline_access") 14 | 15 | suppressWarnings(dir.create(AzureR_dir(), recursive=TRUE)) 16 | clean_token_directory(confirm=FALSE) 17 | suppressWarnings(file.remove(file.path(AzureR_dir(), "graph_logins.json"))) 18 | 19 | test_that("Graph authentication works", 20 | { 21 | gr <- ms_graph$new(tenant=tenant, app=app, password=password) 22 | expect_is(gr, "ms_graph") 23 | expect_true(is_azure_token(gr$token)) 24 | 25 | token <- get_azure_token(scopes, tenant, app=app, password=password, version=2) 26 | 27 | gr2 <- ms_graph$new(token=token) 28 | expect_is(gr2, "ms_graph") 29 | expect_true(is_azure_token(gr2$token)) 30 | }) 31 | 32 | test_that("Login interface works", 33 | { 34 | delete_graph_login(tenant, confirm=FALSE) 35 | 36 | lst <- list_graph_logins() 37 | expect_true(is.list(lst)) 38 | 39 | gr0 <- create_graph_login(tenant=tenant, app=app, password=password) 40 | expect_is(gr0, "ms_graph") 41 | expect_true(is_azure_v2_token(gr0$token)) 42 | 43 | creds <- tempfile(fileext=".json") 44 | writeLines(jsonlite::toJSON(list(tenant=tenant, app=app, password=password)), creds) 45 | 46 | gr1 <- create_graph_login(config_file=creds) 47 | expect_identical(normalize_tenant(tenant), gr1$tenant) 48 | 49 | expect_length(list_graph_logins()[[normalize_tenant(tenant)]], 1) 50 | 51 | gr2 <- create_graph_login(tenant=tenant) 52 | expect_identical(gr2$token$client$client_id, .az_cli_app_id) 53 | 54 | expect_length(list_graph_logins()[[normalize_tenant(tenant)]], 2) 55 | 56 | gr3 <- create_graph_login(tenant=tenant, version=1) 57 | expect_identical(gr2$token$client$client_id, .az_cli_app_id) 58 | expect_true(is_azure_v1_token(gr3$token)) 59 | 60 | expect_length(list_graph_logins()[[normalize_tenant(tenant)]], 3) 61 | 62 | gr4 <- create_graph_login(tenant=tenant, app=.az_cli_app_id, scopes="user.readwrite.all") 63 | 64 | expect_length(list_graph_logins()[[normalize_tenant(tenant)]], 4) 65 | 66 | gr5 <- get_graph_login(tenant, app=app) 67 | expect_identical(gr5$token$client$client_id, app) 68 | 69 | gr6 <- get_graph_login(tenant, scopes="user.readwrite.all") 70 | expect_identical(gr6$token$client$client_id, .az_cli_app_id) 71 | 72 | gr7 <- get_graph_login(tenant, scopes=NA) 73 | expect_true(is_azure_v1_token(gr7$token)) 74 | 75 | gr8 <- get_graph_login(tenant, auth_type="client_credentials") 76 | expect_identical(gr8$token$client$client_id, app) 77 | }) 78 | 79 | -------------------------------------------------------------------------------- /tests/testthat/test02_app_sp.R: -------------------------------------------------------------------------------- 1 | context("App creation/deletion") 2 | 3 | tenant <- Sys.getenv("AZ_TEST_TENANT_ID") 4 | pemfile <- Sys.getenv("AZ_TEST_CERT_FILE") 5 | 6 | if(tenant == "" || pemfile == "") 7 | skip("App method tests skipped: login credentials not set") 8 | 9 | if(!interactive()) 10 | skip("App method tests skipped: must be in interactive session") 11 | 12 | scopes <- c("https://graph.microsoft.com/.default", "openid", "offline_access") 13 | token <- AzureAuth::get_azure_token(scopes, tenant, .az_cli_app_id, version=2) 14 | gr <- ms_graph$new(token=token) 15 | 16 | 17 | test_that("App creation works", 18 | { 19 | newapp_name <- paste0("AzureRtest_", paste0(sample(letters, 5, TRUE), collapse="")) 20 | newapp <- gr$create_app(name=newapp_name, create_service_principal=FALSE) 21 | expect_true(is_app(newapp)) 22 | expect_true(is.character(newapp$password)) 23 | newapp_id <- newapp$properties$appId 24 | 25 | newsvc <- newapp$create_service_principal() 26 | expect_true(is_service_principal(newsvc)) 27 | expect_true(newsvc$properties$appId == newapp_id) 28 | 29 | newapp2 <- gr$get_app(app_id=newapp_id) 30 | expect_true(is_app(newapp2) && newapp2$properties$appId == newapp_id) 31 | 32 | newsvc2 <- gr$get_service_principal(app_id=newapp_id) 33 | expect_true(is_service_principal(newsvc2) && newsvc2$properties$appId == newapp_id) 34 | 35 | newsvc3 <- newapp2$get_service_principal() 36 | expect_true(is_service_principal(newsvc3) && newsvc3$properties$appId == newapp_id) 37 | 38 | expect_type(newapp$add_password(), "character") 39 | expect_true(is_app(newapp$update(displayName=paste0(newapp_name, "_update")))) 40 | 41 | Sys.setenv(AZ_TEST_NEWAPP_ID=newapp_id) 42 | }) 43 | 44 | test_that("App deletion works", 45 | { 46 | newapp_id <- Sys.getenv("AZ_TEST_NEWAPP_ID") 47 | 48 | expect_silent(gr$delete_service_principal(app_id=newapp_id, confirm=FALSE)) 49 | expect_silent(gr$delete_app(app_id=newapp_id, confirm=FALSE)) 50 | }) 51 | 52 | test_that("App with cert works", 53 | { 54 | newapp_name <- paste0("AzureRtest_", paste0(sample(letters, 5, TRUE), collapse="")) 55 | newapp <- gr$create_app(name=newapp_name, create_service_principal=FALSE, certificate=pemfile) 56 | expect_true(is_app(newapp)) 57 | expect_false(is_empty(newapp$properties$keyCredentials)) 58 | 59 | id <- newapp$properties$keyCredentials[[1]]$keyId 60 | expect_type(id, "character") 61 | expect_silent(newapp$remove_certificate(id, confirm=FALSE)) 62 | expect_true(is_empty(newapp$properties$keyCredentials)) 63 | 64 | expect_silent(newapp$add_certificate(pemfile)) 65 | expect_false(is_empty(newapp$properties$keyCredentials)) 66 | 67 | id <- newapp$properties$keyCredentials[[1]]$keyId 68 | expect_type(id, "character") 69 | expect_silent(newapp$remove_certificate(id, confirm=FALSE)) 70 | expect_true(is_empty(newapp$properties$keyCredentials)) 71 | 72 | cert <- openssl::read_cert(pemfile) 73 | expect_silent(newapp$add_certificate(cert)) 74 | expect_false(is_empty(newapp$properties$keyCredentials)) 75 | 76 | expect_silent(newapp$delete(confirm=FALSE)) 77 | }) 78 | 79 | -------------------------------------------------------------------------------- /tests/testthat/test03_usergrp.R: -------------------------------------------------------------------------------- 1 | context("Users/groups") 2 | 3 | tenant <- Sys.getenv("AZ_TEST_TENANT_ID") 4 | user <- Sys.getenv("AZ_TEST_USERPRINCIPALNAME") 5 | admin_user <- Sys.getenv("AZ_TEST_ADMINUSERPRINCIPALNAME") 6 | 7 | if(tenant == "" || user == "") 8 | skip("User method tests skipped: login credentials not set") 9 | 10 | if(!interactive()) 11 | skip("User method tests skipped: must be in interactive session") 12 | 13 | scopes <- c("https://graph.microsoft.com/.default", "openid", "offline_access") 14 | token <- AzureAuth::get_azure_token(scopes, tenant, .az_cli_app_id, version=2) 15 | gr <- ms_graph$new(token=token) 16 | 17 | 18 | test_that("User/group read functionality works", 19 | { 20 | me <- gr$get_user() 21 | expect_equal(me$properties$userPrincipalName, admin_user) 22 | 23 | me2 <- gr$get_user(user) 24 | expect_equal(me2$properties$userPrincipalName, user) 25 | 26 | email <- me2$properties$mail 27 | me3 <- gr$get_user(email=email) 28 | expect_equal(me3$properties$userPrincipalName, user) 29 | 30 | name <- me2$properties$displayName 31 | me4 <- gr$get_user(name=name) 32 | expect_equal(me4$properties$userPrincipalName, user) 33 | 34 | users <- gr$list_users() 35 | expect_true(is.list(users) && all(sapply(users, is_user))) 36 | 37 | objs <- me$list_object_memberships() 38 | expect_true(is.character(objs)) 39 | 40 | grps1 <- me$list_group_memberships() 41 | expect_true(is.character(grps1)) 42 | 43 | grps3 <- me$list_direct_memberships() 44 | expect_true(all(sapply(grps3, function(x) is_group(x) || is_directory_role(x)))) 45 | expect_true(all(sapply(grps3, function(g) !is.null(g$properties$id)))) 46 | 47 | grp <- gr$get_group(grps1[1]) 48 | expect_true(is_group(grp) && !is.null(grp$properties$id)) 49 | 50 | grps <- gr$list_groups() 51 | expect_true(is.list(grps) && all(sapply(grps, is_group))) 52 | 53 | owned <- me$list_owned_objects() 54 | expect_true(is.list(owned) && all(sapply(owned, inherits, "az_object"))) 55 | 56 | owned_apps <- me$list_owned_objects(type="application") 57 | expect_true(is.list(owned_apps) && all(sapply(owned_apps, is_app))) 58 | 59 | created <- me$list_created_objects() 60 | expect_true(is.list(created) && all(sapply(owned, inherits, "az_object"))) 61 | 62 | created_apps <- me$list_created_objects(type="application") 63 | expect_true(is.list(created_apps) && all(sapply(created_apps, is_app))) 64 | }) 65 | 66 | 67 | -------------------------------------------------------------------------------- /tests/testthat/test04_batch.R: -------------------------------------------------------------------------------- 1 | context("Batch request") 2 | 3 | tenant <- Sys.getenv("AZ_TEST_TENANT_ID") 4 | user <- Sys.getenv("AZ_TEST_USERPRINCIPALNAME") 5 | 6 | if(tenant == "" || user == "") 7 | skip("Batch tests skipped: login credentials not set") 8 | 9 | if(!interactive()) 10 | skip("Batch tests skipped: must be in interactive session") 11 | 12 | scopes <- c("https://graph.microsoft.com/.default", "openid", "offline_access") 13 | token <- AzureAuth::get_azure_token(scopes, tenant, .az_cli_app_id, version=2) 14 | gr <- ms_graph$new(token=token) 15 | 16 | 17 | test_that("Simple batch request works", 18 | { 19 | req1 <- graph_request$new("me") 20 | expect_is(req1, "graph_request") 21 | expect_identical(req1$op, "me") 22 | expect_identical(req1$method, "GET") 23 | 24 | bat1 <- req1$batchify() 25 | expect_identical(bat1, list( 26 | id=NULL, 27 | method="GET", 28 | url="/me")) 29 | 30 | req2 <- graph_request$new("me/ownedObjects") 31 | 32 | res <- gr$call_batch_endpoint(list(req1, req2)) 33 | expect_is(res, "list") 34 | expect_identical(res[[1]]$id, "1") 35 | expect_identical(res[[2]]$id, "2") 36 | 37 | expect_identical(res[[1]]$body$`@odata.context`, "https://graph.microsoft.com/v1.0/$metadata#users/$entity") 38 | expect_identical(res[[2]]$body$`@odata.context`, "https://graph.microsoft.com/v1.0/$metadata#directoryObjects") 39 | }) 40 | 41 | 42 | test_that("Batch request with dependency works", 43 | { 44 | newname <- paste0(sample(letters, 20, TRUE), collapse="") 45 | req_get <- graph_request$new(file.path("users", user)) 46 | req_update <- graph_request$new(file.path("users", user), 47 | body=list(givenName=newname), http_verb="PATCH") 48 | 49 | res <- gr$call_batch_endpoint(list(req_get, req_update, req_get), depends_on=c("2"=1, "3"=2)) 50 | expect_is(res, "list") 51 | expect_identical(res[[1]]$id, "1") 52 | expect_identical(res[[2]]$id, "2") 53 | expect_identical(res[[3]]$id, "3") 54 | 55 | expect_false(identical(res[[1]]$body$givenName, newname)) 56 | expect_identical(res[[3]]$body$givenName, newname) 57 | 58 | # auto-generated depends_on 59 | newname2 <- paste0(sample(letters, 20, TRUE), collapse="") 60 | req_update2 <- graph_request$new(file.path("users", user), 61 | body=list(givenName=newname2), http_verb="PATCH") 62 | res2 <- gr$call_batch_endpoint(list(req_get, req_update2, req_get), depends_on=TRUE) 63 | expect_false(identical(res2[[1]]$body$givenName, newname2)) 64 | expect_identical(res2[[3]]$body$givenName, newname2) 65 | }) 66 | 67 | 68 | test_that("Batch request errors handled gracefully", 69 | { 70 | req1 <- graph_request$new("me") 71 | req2 <- graph_request$new("me/drive", body=list(foo="bar"), http_verb="POST") 72 | 73 | expect_error(gr$call_batch_endpoint(list(req1, req2)), "Graph batch job encountered errors") 74 | }) 75 | -------------------------------------------------------------------------------- /tests/testthat/test05_pager.R: -------------------------------------------------------------------------------- 1 | context("List paging") 2 | 3 | tenant <- Sys.getenv("AZ_TEST_TENANT_ID") 4 | user <- Sys.getenv("AZ_TEST_USERPRINCIPALNAME") 5 | 6 | if(tenant == "" || user == "") 7 | skip("Paging tests skipped: login credentials not set") 8 | 9 | if(!interactive()) 10 | skip("Paging tests skipped: must be in interactive session") 11 | 12 | scopes <- c("https://graph.microsoft.com/.default", "openid", "offline_access") 13 | token <- AzureAuth::get_azure_token(scopes, tenant, .az_cli_app_id, version=2) 14 | gr <- ms_graph$new(token=token) 15 | me <- gr$get_user(user) 16 | 17 | 18 | test_that("Paging works", 19 | { 20 | lst <- me$do_operation("memberOf") 21 | expect_silent(p <- me$get_list_pager(lst, generate_objects=FALSE)) 22 | expect_is(p, "ms_graph_pager") 23 | 24 | # assume result fits on 1 page 25 | expect_true(p$has_data()) 26 | out <- p$value 27 | expect_is(out, "list") 28 | expect_null(p$value) 29 | expect_false(p$has_data()) 30 | 31 | # return lists 32 | lst1 <- me$do_operation("memberOf", options=list(`$top`=1)) 33 | p1 <- me$get_list_pager(lst1, generate_objects=FALSE) 34 | expect_is(p1, "ms_graph_pager") 35 | expect_true(p1$has_data()) 36 | 37 | for(i in seq_along(out)) 38 | expect_identical(out[i], p1$value) 39 | expect_null(p1$value) 40 | expect_false(p1$has_data()) 41 | }) 42 | 43 | 44 | test_that("Page result as data frame works", 45 | { 46 | lstdf <- me$do_operation("memberOf", simplify=TRUE) 47 | expect_silent(pdf <- me$get_list_pager(lstdf)) 48 | expect_is(pdf, "ms_graph_pager") 49 | 50 | expect_true(pdf$has_data()) 51 | outdf <- pdf$value 52 | expect_is(outdf, "data.frame") 53 | expect_null(pdf$value) 54 | expect_false(pdf$has_data()) 55 | 56 | # return data frames 57 | lstdf1 <- me$do_operation("memberOf", options=list(`$top`=1), simplify=TRUE) 58 | pdf1 <- me$get_list_pager(lstdf1) 59 | expect_is(pdf1, "ms_graph_pager") 60 | expect_true(pdf1$has_data()) 61 | 62 | outdf1 <- list() 63 | for(i in seq_len(nrow(outdf))) 64 | outdf1 <- c(outdf1, list(pdf1$value)) 65 | outdf1 <- do.call(vctrs::vec_rbind, outdf1) 66 | expect_identical(outdf, outdf1) 67 | expect_null(pdf1$value) 68 | expect_false(pdf1$has_data()) 69 | }) 70 | 71 | 72 | test_that("Page result as object works", 73 | { 74 | lstobj <- me$do_operation("memberOf") 75 | expect_silent(pobj <- me$get_list_pager(lstobj, generate_objects=TRUE)) 76 | expect_is(pobj, "ms_graph_pager") 77 | 78 | expect_true(pobj$has_data()) 79 | outobj <- pobj$value 80 | expect_is(outobj, "list") 81 | expect_true(all(sapply(outobj, inherits, "ms_object"))) 82 | expect_false(pobj$has_data()) 83 | 84 | lstobj1 <- me$do_operation("memberOf", options=list(`$top`=1)) 85 | pobj1 <- me$get_list_pager(lstobj1, generate_objects=TRUE) 86 | expect_is(pobj1, "ms_graph_pager") 87 | expect_true(pobj1$has_data()) 88 | 89 | outobj1 <- list() 90 | for(i in seq_along(outobj)) 91 | expect_equal(outobj[i], pobj1$value) 92 | expect_true(is_empty(pobj1$value)) 93 | expect_false(pobj1$has_data()) 94 | }) 95 | 96 | 97 | test_that("extract_list_values works", 98 | { 99 | # assume result fits on 1 page 100 | lst <- me$do_operation("memberOf") 101 | p <- me$get_list_pager(lst, generate_objects=FALSE) 102 | out <- p$value 103 | 104 | lst1 <- me$do_operation("memberOf", options=list(`$top`=1)) 105 | p1 <- me$get_list_pager(lst1, generate_objects=FALSE) 106 | out1 <- extract_list_values(p1) 107 | 108 | expect_identical(out, out1) 109 | expect_error(extract_list_values(p1)) 110 | }) 111 | 112 | 113 | test_that("extract_list_values works for data frame", 114 | { 115 | lst <- me$do_operation("memberOf", simplify=TRUE) 116 | p <- me$get_list_pager(lst, generate_objects=FALSE) 117 | out <- p$value 118 | 119 | lst1 <- me$do_operation("memberOf", options=list(`$top`=1), simplify=TRUE) 120 | p1 <- me$get_list_pager(lst1, generate_objects=FALSE) 121 | out1 <- extract_list_values(p1) 122 | 123 | expect_identical(out, out1) 124 | expect_error(extract_list_values(p1)) 125 | }) 126 | 127 | 128 | test_that("extract_list_values works for objects", 129 | { 130 | lst <- me$do_operation("memberOf", simplify=FALSE) 131 | p <- me$get_list_pager(lst, generate_objects=TRUE) 132 | out <- p$value 133 | 134 | lst1 <- me$do_operation("memberOf", options=list(`$top`=1), simplify=FALSE) 135 | p1 <- me$get_list_pager(lst1, generate_objects=TRUE) 136 | out1 <- extract_list_values(p1) 137 | 138 | expect_equal(out, out1) 139 | expect_error(extract_list_values(p1)) 140 | }) 141 | 142 | 143 | test_that("Extra arguments work", 144 | { 145 | testclass <- R6::R6Class("testclass", 146 | public=list( 147 | initialize=function(token, tenant, properties, arg1=NULL) 148 | { 149 | if(is.null(arg1)) stop("arg1 must not be NULL", call.=FALSE) 150 | } 151 | )) 152 | 153 | lst <- list( 154 | nextlink=NULL, 155 | valuelist=list( 156 | list(x=1), 157 | list(x=2), 158 | list(x=3) 159 | ) 160 | ) 161 | 162 | pager <- me$get_list_pager(lst, next_link_name="nextlink", value_name="valuelist", generate_objects=TRUE, 163 | default_generator=testclass, arg1=42) 164 | 165 | expect_is(pager, "ms_graph_pager") 166 | expect_true(pager$has_data()) 167 | vals <- pager$value 168 | expect_is(vals, "list") 169 | expect_true(all(sapply(vals, inherits, "testclass"))) 170 | 171 | register_graph_class("testclass", testclass, function(props) !is.null(props$x)) 172 | pager2 <- me$get_list_pager(lst, next_link_name="nextlink", value_name="valuelist", generate_objects=TRUE, 173 | type_filter="testclass", arg1=42) 174 | 175 | expect_is(pager2, "ms_graph_pager") 176 | expect_true(pager2$has_data()) 177 | vals2 <- pager2$value 178 | expect_is(vals2, "list") 179 | expect_true(all(sapply(vals2, inherits, "testclass"))) 180 | }) 181 | -------------------------------------------------------------------------------- /tests/testthat/test06_filter.R: -------------------------------------------------------------------------------- 1 | context("List filtering") 2 | 3 | tenant <- Sys.getenv("AZ_TEST_TENANT_ID") 4 | user <- Sys.getenv("AZ_TEST_USERPRINCIPALNAME") 5 | 6 | if(tenant == "" || user == "") 7 | skip("Paging tests skipped: login credentials not set") 8 | 9 | if(!interactive()) 10 | skip("Paging tests skipped: must be in interactive session") 11 | 12 | scopes <- c("https://graph.microsoft.com/.default", "openid", "offline_access") 13 | token <- AzureAuth::get_azure_token(scopes, tenant, .az_cli_app_id, version=2) 14 | gr <- ms_graph$new(token=token) 15 | me <- gr$get_user(user) 16 | 17 | 18 | test_that("Filtering works", 19 | { 20 | id <- me$list_group_memberships()[1] 21 | grp <- gr$get_aad_object(id) 22 | expect_true(inherits(grp, "az_group") && !is.null(grp$properties$displayName)) 23 | filtexpr1 <- sprintf("displayName eq '%s'", grp$properties$displayName) 24 | 25 | expect_error(me$list_group_memberships(filter=filtexpr1)) 26 | 27 | lst1 <- me$list_direct_memberships(filter=filtexpr1) 28 | expect_is(lst1, "list") 29 | expect_true(length(lst1) == 1 && 30 | inherits(lst1[[1]], "az_group") && 31 | lst1[[1]]$properties$displayName == grp$properties$displayName) 32 | 33 | filtexpr2 <- sprintf("userPrincipalName eq '%s'", user) 34 | lst2 <- grp$list_members(filter=filtexpr2) 35 | expect_is(lst2, "list") 36 | expect_true(length(lst2) == 1 && 37 | inherits(lst2[[1]], "az_user") && 38 | lst2[[1]]$properties$userPrincipalName == user) 39 | }) 40 | 41 | -------------------------------------------------------------------------------- /vignettes/auth.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Authentication basics" 3 | author: Hong Ooi 4 | output: rmarkdown::html_vignette 5 | vignette: > 6 | %\VignetteIndexEntry{Authentication} 7 | %\VignetteEngine{knitr::rmarkdown} 8 | %\VignetteEncoding{utf8} 9 | --- 10 | 11 | There are a number of ways to authenticate to the Microsoft Graph API with AzureGraph. This vignette goes through the most common scenarios. 12 | 13 | ## Interactive authentication 14 | 15 | This is the scenario where you're using R interactively, such as in your local desktop or laptop, or in a hosted RStudio Server, Jupyter notebook or ssh session. The first time you authenticate with AzureGraph, you run `create_graph_login()`: 16 | 17 | ```r 18 | # on first use 19 | library(AzureGraph) 20 | gr <- create_graph_login() 21 | ``` 22 | 23 | Notice that you _don't_ enter your username and password. 24 | 25 | AzureGraph will attempt to detect which authentication flow to use, based on your session details. In most cases, it will bring up the Azure Active Directory (AAD) login page in your browser, which is where you enter your user credentials. This is also known as the "authorization code" flow. 26 | 27 | There are some complications to be aware of: 28 | 29 | - If you are running R in a hosted session, trying to start a browser will usually fail. In this case, specify the device code authentication flow, with the `auth_type` argument: 30 | 31 | ```r 32 | gr <- create_graph_login(auth_type="device_code") 33 | ``` 34 | 35 | - If you have a personal account that is also a guest in an organisational tenant, you may have to specify your tenant explicitly: 36 | 37 | ```r 38 | gr <- create_graph_login(tenant="yourtenant") 39 | ``` 40 | 41 | - By default, AzureGraph identifies itself using the Azure CLI app registration ID. This is meant for working with the AAD part of the Graph API, so it has permissions which are relevant for this purpose. If you are using Graph for other purposes (eg to interact with Microsoft 365 services), you'll need to supply your own app ID that has the correct permissions. On the client side, you supply the app ID via the `app` argument; see later for creating the app registration on the server side. 42 | 43 | ```r 44 | gr <- create_graph_login(app="yourappid") 45 | ``` 46 | 47 | All of the above arguments can be combined, eg this will authenticate using the device code flow, with an explicit tenant name, and a custom app ID: 48 | 49 | ```r 50 | gr <- create_graph_login(tenant="yourtenant", app="yourappid", auth_type="device_code") 51 | ``` 52 | 53 | If needed, you can also supply other arguments that will be passed to `AzureAuth::get_azure_token()`. 54 | 55 | Having created the login, in subsequent sessions you run `get_graph_login()`. This will load your previous authentication details, saving you from having to login again. If you specified the tenant in the `create_graph_login()` call, you'll also need to specify it for `get_graph_login()`; the other arguments don't have to be repeated. 56 | 57 | ```r 58 | gr <- get_graph_login() 59 | 60 | # if you specified the tenant in create_graph_login 61 | gr <- get_graph_login(tenant="yourtenant") 62 | ``` 63 | 64 | ## Non-interactive authentication 65 | 66 | This is the scenario where you want to use AzureGraph as part of an automated script or unattended session, for example in a deployment pipeline. The appropriate authentication flow in this case is the client credentials flow. 67 | 68 | For this scenario, you must have a custom app ID and client secret. On the client side, these are supplied in the `app` and `password` arguments; see later for creating the app registration on the server side. You must also specify your tenant as AAD won't be able to detect it from a user's credentials. 69 | 70 | ```r 71 | gr <- create_graph_login(tenant="yourtenant", app="yourccappid", password="client_secret") 72 | ``` 73 | 74 | In the non-interactive scenario, you don't use `get_graph_login()`; instead, you simply call `create_graph_login()` as part of your script. 75 | 76 | ## Creating a custom app registration 77 | 78 | This part is meant mostly for Azure tenant administrators, or users who have the appropriate rights to create AAD app registrations. 79 | 80 | You can create a new app registration using any of the usual methods. For example to create an app registration in the Azure Portal (`https://portal.azure.com/`), click on "Azure Active Directory" in the menu bar down the left, go to "App registrations" and click on "New registration". Name the app something suitable, eg "AzureGraph custom app". 81 | 82 | - If you want your users to be able to login with the authorization code flow, you must add a **public client/native redirect URI** of `http://localhost:1410`. This is appropriate if your users will be running R on their local PCs, with an Internet browser available. 83 | - If you want your users to be able to login with the device code flow, you must **enable the "Allow public client flows" setting** for your app. In the Portal, you can find this setting in the "Authentication" pane once the app registration is complete. This is appropriate if your users are running R in a remote session. 84 | - If the app is meant for non-interactive use, you must give the app a **client secret**, which is much the same as a password (and should similarly be kept secure). In the Portal, you can set this in the "Certificates and Secrets" pane for your app registration. 85 | 86 | Once the app registration has been created, note the app ID and, if applicable, the client secret. The latter can't be viewed after app creation, so make sure you note its value now. 87 | 88 | It's also possible to authenticate with a **client certificate (public key)**, but this is more complex and we won't go into it here. For more details, see the [Azure Active Directory documentation](https://learn.microsoft.com/en-au/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) and the [AzureAuth intro vignette](https://cran.r-project.org/package=AzureAuth/vignettes/token.html). 89 | 90 | ### Set the app permissions 91 | 92 | For your app to be useful, you must give it the appropriate permisssions for the Microsoft Graph API. You can set this by going to the "API permissions" pane for your app registration, then clicking on "Add a permission". Choose the Microsoft Graph API, and then enable the permissions that you need. 93 | 94 | - For interactive use, make sure that you enable the _delegated_ permissions. These apply when a logged-in user is present. [See the documentation](https://learn.microsoft.com/en-us/graph/auth/auth-concepts#microsoft-graph-permissions) for how permissions and user roles interact; essentially, if a user wants to use AzureGraph to do an action, they must have the correct role _and_ the app registration must have the correct permission. 95 | - It's highly recommended to enable the "offline_access" permission for an interactive app, as this is necessary to obtain refresh tokens. Without these, a user must reauthenticate each time their access token expires, which by default is after one hour. 96 | - For non-interactive use, enable the _application_ permissions. These are more powerful since there is no user role that can moderate what AzureGraph can do, so assign application permissions with caution. 97 | 98 | 99 | -------------------------------------------------------------------------------- /vignettes/batching_paging.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Batching and paging" 3 | author: Hong Ooi 4 | output: rmarkdown::html_vignette 5 | vignette: > 6 | %\VignetteIndexEntry{Batching and Paging} 7 | %\VignetteEngine{knitr::rmarkdown} 8 | %\VignetteEncoding{utf8} 9 | --- 10 | 11 | This vignette describes a couple of special interfaces that are available in Microsoft Graph, and how to use them in AzureGraph. 12 | 13 | ## Batching 14 | 15 | The batch API allows you to combine multiple requests into a single batch call, potentially resulting in significant performance improvements. If all the requests are independent, they can be executed in parallel, so that they only take the same time as a single request. If the requests depend on each other, they must be executed serially, but even in this case you benefit by not having to make multiple HTTP requests with the associated network overhead. 16 | 17 | For example, let's say you want to get the object information for all the Azure Active Directory groups, directory roles and admin units you're a member of. The `az_object$list_object_memberships()` method returns the IDs for these objects, but to get the remaining object properties, you have to call the `directoryObjects` API endpoint for each individual ID. Rather than making separate calls for every ID, let's combine them into a single batch request. 18 | 19 | ```r 20 | gr <- get_graph_login() 21 | me <- gr$get_user() 22 | 23 | # get the AAD object IDs 24 | obj_ids <- me$list_object_memberships(security_only=FALSE) 25 | ``` 26 | 27 | AzureGraph represents a single request with the `graph_request` R6 class. To create a new request object, call `graph_request$new()` with the following arguments: 28 | 29 | - `op`: The operation to carry out, eg `/me/drives`. 30 | - `body`: The body of the HTTPS request, if it is a PUT, POST or PATCH. 31 | - `options`: A list containing the query parameters for the URL, for example OData parameters. 32 | - `headers`: Any optional HTTP headers for the request. 33 | - `encode`: If a request body is present, how it should be encoded when sending it to the endpoint. The default is `json`, meaning it will be sent as JSON text; an alternative is `raw`, for binary data. 34 | - `http_verb`: One of "GET" (the default), "DELETE", "PUT", "POST", "HEAD", or "PATCH". 35 | 36 | For this example, only `op` is required. 37 | 38 | ```r 39 | obj_reqs <- lapply(obj_ids, function(id) 40 | { 41 | op <- file.path("directoryObjects", id) 42 | graph_request$new(op) 43 | }) 44 | ``` 45 | 46 | To make a request to the batch endpoint, use the `call_batch_endpoint()` function, or the `ms_graph$call_batch_endpoint()` method. This takes as arguments a list of individual requests, as well as an optional named vector of dependencies. The result of the call is a list of the responses from the requests; each response will have components named `id` and `status`, and usually `body` as well. 47 | 48 | In this case, there are no dependencies between the individual requests, so the code is very simple. Simply use the `call_batch_endpoint()` method with the request list; then run the `find_class_generator()` function to get the appropriate class generator for each list of object properties, and instantiate a new object. 49 | 50 | ```r 51 | objs <- gr$call_batch_endpoint(obj_reqs) 52 | lapply(objs, function(obj) 53 | { 54 | cls <- find_class_generator(obj) 55 | cls$new(gr$token, gr$tenant, obj$body) 56 | }) 57 | ``` 58 | 59 | ## Paging 60 | 61 | Some Microsoft Graph calls return multiple results. In AzureGraph, these calls are generally those represented by methods starting with `list_*`, eg the `list_object_memberships` method used previously. Graph handles result sets by breaking them into _pages_, with each page containing several results. 62 | 63 | The built-in AzureGraph methods will generally handle paging automatically, without you needing to be aware of the details. If necessary however, you can also carry out a paged request and handle the results manually. 64 | 65 | The starting point for a paged request is a regular Graph call to an endpoint that returns a paged response. For example, let's take the `memberOf` endpoint, which returns the groups of which a user is a member. Calling this endpoint returns the first page of results, along with a link to the next page. 66 | 67 | ```r 68 | res <- me$do_operation("memberOf") 69 | ``` 70 | 71 | Once you have the first page, you can use that to instantiate a new object of class `ms_graph_pager`. This is an _iterator_ object: each time you access data from it, you retrieve a new page of results. If you have used other programming languages such as Python, Java or C#, the concept of iterators should be familiar. If you're a user of the foreach package, you'll also have used iterators: foreach depends on the iterators package, which implements the same concept using S3 classes. 72 | 73 | The easiest way to instantiate a new pager object is via the `get_list_pager()` method. Once instantiated, you access the `value` active binding to retrieve each page of results, starting from the first page. 74 | 75 | ```r 76 | pager <- me$get_list_pager(res) 77 | pager$value 78 | # [[1]] 79 | # 80 | # directory id: cd806a5f-9d19-426c-b34b-3a3ec662ecf2 81 | # description: test security group 82 | # --- 83 | # Methods: 84 | # delete, do_operation, get_list_pager, list_group_memberships, 85 | # list_members, list_object_memberships, list_owners, sync_fields, 86 | # update 87 | 88 | # [[2]] 89 | # 90 | # directory id: df643f93-3d9d-497f-8f2e-9cfd4c275e41 91 | # description: Can manage all aspects of Azure AD and Microsoft services that use Azure 92 | # AD identities. 93 | # --- 94 | # Methods: 95 | # delete, do_operation, get_list_pager, list_group_memberships, 96 | # list_members, list_object_memberships, sync_fields, update 97 | ``` 98 | Once there are no more pages, calling `value` returns an empty result. 99 | 100 | ```r 101 | pager$value 102 | # list() 103 | ``` 104 | 105 | By default, as shown above, the pager object will create new AzureGraph R6 objects from the properties for each item in the results. You can customise the output in the following ways: 106 | 107 | - If the first page of results consists of a data frame, rather than a list of items, the pager will continue to output data frames. This is most useful when the results are meant to represent external, tabular data, eg items in a SharePoint list or files in a OneDrive folder. Some AzureGraph methods will automatically request data frame output; if you want this from a manual REST API call, specify `simplify=TRUE` in the `do_operation()` call. 108 | 109 | - You can suppress the conversion of the item properties into an R6 object by specifying `generate_objects=FALSE` in the `get_list_pager()` call. In this case, the pager will return raw lists. 110 | 111 | AzureGraph also provides the `extract_list_values()` function to perform the common task of getting all or some of the values from a paged result set. Rather than reading `pager$value` repeatedly until there is no data left, you can simply call: 112 | 113 | ```r 114 | extract_list_values(pager) 115 | ``` 116 | 117 | To restrict the output to only the first N items, call `extract_list_values(pager, n=N)`. However, note that the result set from a paged query usually isn't ordered in any way, so the items you get will be arbitrary. 118 | 119 | -------------------------------------------------------------------------------- /vignettes/extend.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Extending AzureGraph" 3 | author: Hong Ooi 4 | output: rmarkdown::html_vignette 5 | vignette: > 6 | %\VignetteIndexEntry{Extending} 7 | %\VignetteEngine{knitr::rmarkdown} 8 | %\VignetteEncoding{utf8} 9 | --- 10 | 11 | As written, AzureGraph provides support for Microsoft Graph objects derived from Azure Active Directory (AAD): users, groups, app registrations and service principals. This vignette describes how to extend it to support other services. 12 | 13 | ## Extend the `ms_object` base class 14 | 15 | AzureGraph provides the `ms_object` class to represent a generic object in Graph. You can extend this to support specific services by adding custom methods and fields. 16 | 17 | For example, the [Microsoft365R](https://github.com/Azure/Microsoft365R) package extends AzureGraph to support SharePoint Online sites and OneDrive filesystems (both personal and business). This is the `ms_site` class from that package, which represents a SharePoint site. To save space, the actual code in the new methods has been elided. 18 | 19 | ```r 20 | ms_site <- R6::R6Class("ms_site", inherit=ms_object, 21 | 22 | public=list( 23 | 24 | initialize=function(token, tenant=NULL, properties=NULL) 25 | { 26 | self$type <- "site" 27 | private$api_type <- "sites" 28 | super$initialize(token, tenant, properties) 29 | }, 30 | 31 | list_drives=function() {}, # ... 32 | 33 | get_drive=function(drive_id=NULL) {}, # ... 34 | 35 | list_subsites=function() {}, # ... 36 | 37 | get_list=function(list_name=NULL, list_id=NULL) {}, # ... 38 | 39 | print=function(...) 40 | { 41 | cat("\n", sep="") 42 | cat(" directory id:", self$properties$id, "\n") 43 | cat(" web link:", self$properties$webUrl, "\n") 44 | cat(" description:", self$properties$description, "\n") 45 | cat("---\n") 46 | cat(format_public_methods(self)) 47 | invisible(self) 48 | } 49 | )) 50 | ``` 51 | 52 | Note the following: 53 | 54 | - The `initialize()` method of your class should take 3 arguments: the OAuth2 token for authenticating with Graph, the name of the AAD tenant, and the list of properties for this object as obtained from the Graph endpoint. It should set 2 fields: `self$type` contains a human-readable name for this type of object, and `private$api_type` contains the object type as it appears in the URL of a Graph API request. It should then call the superclass method to complete the initialisation. `initialize()` itself should not contact the Graph endpoint; it should merely create and populate the R6 object given the response from a previous request. 55 | 56 | - The `print()` method is optional and should display any properties that can help identify this object to a human reader. 57 | 58 | You can read the code of the existing classes such as `az_user`, `az_app` etc to see how to call the API. The `do_operation()` method should suffice for any regular communication with the Graph endpoint. 59 | 60 | ## Register the class with `register_graph_class` 61 | 62 | Having defined your new class, call `register_graph_class` so that AzureGraph becomes aware of it and can automatically use it to populate object lists. If you are writing a new package, the `register_graph_class` call should go in your package's `.onLoad` startup function. For example, registering the `ms_site` SharePoint class looks like this. 63 | 64 | ```r 65 | .onLoad <- function(libname, pkgname) 66 | { 67 | register_graph_class("site", ms_site, 68 | function(props) grepl("sharepoint", props$id, fixed=TRUE)) 69 | 70 | # ... other startup code ... 71 | } 72 | ``` 73 | 74 | `register_graph_class` takes 3 arguments: 75 | 76 | - The name of the object class, as it appears in the [Microsoft Graph online documentation](https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0). 77 | - The R6 class generator object, as defined in the previous section. 78 | - A check function which takes a list of properties (as returned by the Graph API) and returns TRUE/FALSE based on whether the properties are for an object of your class. This is necessary as some Graph calls that return lists of objects do not always include explicit metadata indicating the type of each object, hence the type must be inferred from the properties. 79 | 80 | ## Add getter and setter methods 81 | 82 | Finally, so that people can use the same workflow with your class as with AzureGraph-supplied classes, you can add getter and setter methods to `ms_graph` and any other classes for which it's appropriate. Again, if you're writing a package, this should happen in the `.onLoad` function. 83 | 84 | In the case of `ms_site`, it's appropriate to add a getter method not just to `ms_graph`, but also the `ms_group` class. This is because SharePoint sites have associated user groups, hence it's useful to be able to retrieve a site given the object for a group. The relevant code in the `.onLoad` function looks like this (slightly simplified): 85 | 86 | ```r 87 | .onLoad <- function(libname, pkgname) 88 | { 89 | # ... 90 | 91 | ms_graph$set("public", "get_sharepoint_site", overwrite=TRUE, 92 | function(site_url=NULL, site_id=NULL) 93 | { 94 | op <- if(is.null(site_url) && !is.null(site_id)) 95 | file.path("sites", site_id) 96 | else if(!is.null(site_url) && is.null(site_id)) 97 | { 98 | site_url <- httr::parse_url(site_url) 99 | file.path("sites", paste0(site_url$hostname, ":"), site_url$path) 100 | } 101 | else stop("Must supply either site ID or URL") 102 | 103 | ms_site$new(self$token, self$tenant, self$call_graph_endpoint(op)) 104 | }) 105 | 106 | az_group$set("public", "get_sharepoint_site", overwrite=TRUE, 107 | function() 108 | { 109 | res <- self$do_operation("sites/root") 110 | ms_site$new(self$token, self$tenant, res) 111 | }) 112 | 113 | # ... 114 | } 115 | ``` 116 | 117 | Once this is done, the object for a SharePoint site can be instantiated as follows: 118 | 119 | ```r 120 | library(AzureGraph) 121 | library(Microsoft365R) 122 | 123 | gr <- get_graph_login() 124 | 125 | # directly from the Graph client 126 | mysite1 <- gr$get_sharepoint_site("https://mytenant.sharepoint.com/sites/my-site-name") 127 | 128 | # or via a group 129 | mygroup <- gr$get_group("my-group-guid") 130 | mysite2 <- mygroup$get_sharepoint_site() 131 | ``` 132 | -------------------------------------------------------------------------------- /vignettes/intro.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Introduction to AzureGraph" 3 | author: Hong Ooi 4 | output: rmarkdown::html_vignette 5 | vignette: > 6 | %\VignetteIndexEntry{Introduction} 7 | %\VignetteEngine{knitr::rmarkdown} 8 | %\VignetteEncoding{utf8} 9 | --- 10 | 11 | [Microsoft Graph](https://learn.microsoft.com/en-us/graph/overview) is a comprehensive framework for accessing data in various online Microsoft services, including Azure Active Directory (AAD), Office 365, OneDrive, Teams, and more. AzureGraph is a simple R6-based interface to the Graph REST API, and is the companion package to [AzureRMR](https://github.com/Azure/AzureRMR) and [AzureAuth](https://github.com/Azure/AzureAuth). 12 | 13 | Currently, AzureGraph aims to provide an R interface only to the AAD part, with a view to supporting R interoperability with Azure: registered apps and service principals, users and groups. However, it can be extended to support other services; for more information, see the "Extending AzureGraph" vignette. 14 | 15 | ## Authentication 16 | 17 | The first time you authenticate with a given Azure Active Directory tenant, you call `create_graph_login()` and supply your credentials. AzureGraph will prompt you for permission to create a special data directory in which to cache the obtained authentication token and AD Graph login. Once this information is saved on your machine, it can be retrieved in subsequent R sessions with `get_graph_login()`. Your credentials will be automatically refreshed so you don't have to reauthenticate. 18 | 19 | ```r 20 | library(AzureGraph) 21 | 22 | # authenticate with AAD 23 | # - on first login, call create_graph_login() 24 | # - on subsequent logins, call get_graph_login() 25 | gr <- create_graph_login() 26 | ``` 27 | 28 | See the "Authentication basics" vignette for more details on how to authenticate with AzureGraph. 29 | 30 | ## Users and groups 31 | 32 | The basic classes for interacting with user accounts and groups are `az_user` and `az_group`. To instantiate these, call the `get_user` and `get_group` methods of the login client object. You can also list the users and groups with the `list_users` and `list_groups` methods. 33 | 34 | ```r 35 | # account of the logged-in user (if you authenticated via the default method) 36 | me <- gr$get_user() 37 | 38 | # alternative: supply a GUID, name or email address 39 | me2 <- gr$get_user(email="hongooi@microsoft.com") 40 | 41 | # lists of users and groups (may be large!) 42 | gr$list_users() 43 | gr$list_groups() 44 | 45 | # IDs of my groups 46 | head(me$list_group_memberships()) 47 | #> [1] "98326d14-365a-4257-b0f1-5c3ce3104f75" "b21e5600-8ac5-407b-8774-396168150210" 48 | #> [3] "be42ef66-5c13-48cb-be5c-21e563e333ed" "dd58be5a-1eac-47bd-ab78-08a452a08ea0" 49 | #> [5] "4c2bfcfe-5012-4136-ab33-f10389f2075c" "a45fbdbe-c365-4478-9366-f6f517027a22" 50 | 51 | # a specific group 52 | (grp <- gr$get_group("82d27e38-026b-4e5d-ba1a-a0f5a21a2e85")) 53 | #> 54 | #> directory id: 82d27e38-026b-4e5d-ba1a-a0f5a21a2e85 55 | #> description: ADS AP on Microsoft Teams. 56 | #> - Instant communication. 57 | #> - Share files/links/codes/... 58 | #> - Have fun. :) 59 | ``` 60 | 61 | The actual properties of an object are stored as a list in the `properties` field: 62 | 63 | ```r 64 | # properties of a user account 65 | names(me$properties) 66 | #> [1] "@odata.context" "id" "deletedDateTime" 67 | #> [4] "accountEnabled" "ageGroup" "businessPhones" 68 | #> [7] "city" "createdDateTime" "companyName" 69 | #> [10] "consentProvidedForMinor" "country" "department" 70 | #> [13] "displayName" "employeeId" "faxNumber" 71 | #> ... 72 | 73 | me$properties$companyName 74 | #> [1] "MICROSOFT PTY LIMITED" 75 | 76 | # properties of a group 77 | names(grp$properties) 78 | #> [1] "@odata.context" "id" "deletedDateTime" 79 | #> [4] "classification" "createdDateTime" "description" 80 | #> [7] "displayName" "expirationDateTime" "groupTypes" 81 | #> [10] "mail" "mailEnabled" "mailNickname" 82 | #> [13] "membershipRule" "membershipRuleProcessingState" "onPremisesLastSyncDateTime" 83 | #> ... 84 | ``` 85 | 86 | You can apply a filter to the `list_users` and `list_groups` methods, to cut down on the number of results. The filter should be a supported [OData expression](https://learn.microsoft.com/en-us/graph/query-parameters#filter-parameter). For example, this will filter the list of users down to your own account: 87 | 88 | ```r 89 | # get my own name 90 | my_name <- me$properties$displayName 91 | 92 | gr$list_users(filter=sprintf("displayName eq '%s'", my_name)) 93 | ``` 94 | 95 | You can also view any directory objects that you own and/or created, via the `list_owned_objects` and `list_registered_objects` methods of the user object. These accept a `type` argument to filter the list of objects by the specified type(s). 96 | 97 | ```r 98 | me$list_owned_objects(type="application") 99 | #> [[1]] 100 | #> 101 | #> app id: 5af7bc65-8834-4ee6-90df-e7271a12cc62 102 | #> directory id: 132ce21b-ebb9-4e75-aa04-ad9155bb921f 103 | #> domain: microsoft.onmicrosoft.com 104 | 105 | me$list_owned_objects(type="group") 106 | #> [[1]] 107 | #> 108 | #> directory id: 82d27e38-026b-4e5d-ba1a-a0f5a21a2e85 109 | #> description: ADS AP on Microsoft Teams. 110 | #> - Instant communication. 111 | #> - Share files/links/codes/... 112 | #> - Have fun. :) 113 | #> 114 | #> [[2]] 115 | #> 116 | #> directory id: 4e237eed-5f9b-4abd-830b-9322cb472b66 117 | #> description: ANZ Data Science V-Team 118 | #> 119 | #> ... 120 | ``` 121 | 122 | ## Registered apps and service principals 123 | 124 | To get the details for a registered app, use the `get_app` or `create_app` methods of the login client object. These return an object of class `az_app`. The first method retrieves an existing app, while the second creates a new app. 125 | 126 | ```r 127 | # an existing app 128 | gr$get_app("5af7bc65-8834-4ee6-90df-e7271a12cc62") 129 | #> 130 | #> app id: 5af7bc65-8834-4ee6-90df-e7271a12cc62 131 | #> directory id: 132ce21b-ebb9-4e75-aa04-ad9155bb921f 132 | #> domain: microsoft.onmicrosoft.com 133 | 134 | # create a new app 135 | (appnew <- gr$create_app("AzureRnewapp")) 136 | #> 137 | #> app id: 1751d755-71b1-40e7-9f81-526d636c1029 138 | #> directory id: be11df41-d9f1-45a0-b460-58a30daaf8a9 139 | #> domain: microsoft.onmicrosoft.com 140 | ``` 141 | 142 | By default, creating a new app will also generate a strong password with a duration of two years, and create a corresponding service principal in your AAD tenant. You can retrieve this with the `get_service_principal` method, which returns an object of class `az_service_principal`. 143 | 144 | ```r 145 | appnew$get_service_principal() 146 | #> 147 | #> app id: 1751d755-71b1-40e7-9f81-526d636c1029 148 | #> directory id: 7dcc9602-2325-4912-a32e-03e262ffd240 149 | #> app tenant: 72f988bf-86f1-41af-91ab-2d7cd011db47 150 | 151 | # or directly from the login client (supply the app ID in this case) 152 | gr$get_service_principal("1751d755-71b1-40e7-9f81-526d636c1029") 153 | #> 154 | #> app id: 1751d755-71b1-40e7-9f81-526d636c1029 155 | #> directory id: 7dcc9602-2325-4912-a32e-03e262ffd240 156 | #> app tenant: 72f988bf-86f1-41af-91ab-2d7cd011db47 157 | ``` 158 | 159 | To update an app, call its `update` method. For example, use this to set a redirect URL or change its permissions. Consult the Microsoft Graph documentation for what properties you can update. 160 | 161 | ```r 162 | #' # set a public redirect URL 163 | newapp$update(publicClient=list(redirectUris=I("http://localhost:1410"))) 164 | ``` 165 | 166 | One app property you _cannot_ change with `update` is its password. As a security measure, app passwords are auto-generated on the server, rather than being specified manually. To manage an app's password, call the `add_password` and `remove_password` methods. 167 | 168 | ```r 169 | #' # add a password 170 | newapp$add_password() 171 | 172 | #' remove a password 173 | pwd_id <- newapp$properties$passwordCredentials[[1]]$keyId 174 | newapp$remove_password(pwd_id) 175 | ``` 176 | 177 | Similarly, to manage an app's certificate for authentication, call the `add_certificate` and `remove_certificate` methods. 178 | 179 | ```r 180 | #' add a certificate: 181 | #' can be specified as a filename, openssl::cert object, AzureKeyVault::stored_cert object, 182 | #' or raw or character vector 183 | newapp$add_certificate("cert.pem") 184 | 185 | #' remove a certificate 186 | cert_id <- newapp$properties$keyCredentials[[1]]$keyId 187 | newapp$remove_certificate(cert_id) 188 | ``` 189 | 190 | ## Common methods 191 | 192 | The classes described above inherit from the `az_object` class, which represents an arbitrary object in Azure Active Directory. This has the following methods: 193 | 194 | - `list_group_memberships()`: Return the IDs of all groups this object is a member of. 195 | - `list_object_memberships()`: Return the IDs of all groups, administrative units and directory roles this object is a member of. 196 | 197 | In turn, the `az_object` class inherits from `ms_object`, which is a base class to represent any object (not just an AAD object) in Microsoft Graph. This has the following methods: 198 | 199 | - `delete(confirm=TRUE)`: Delete an object. By default, ask for confirmation first. 200 | - `update(...)`: Update the object information in Azure Active Directory (mentioned above when updating an app). 201 | - `do_operation(...)`: Carry out an arbitrary operation on the object. 202 | - `sync_fields()`: Synchronise the R object with the data in Azure Active Directory. 203 | - `get_list_pager()`: Returns a pager object for iterating through the items in a list of results. See the "Batching and paging" vignette for more information on this topic. 204 | 205 | In particular, the `do_operation` method allows you to call the Graph REST endpoint directly. This means that even if AzureGraph doesn't support the operation you want to perform, you can do it manually. For example, if you want to retrieve information on your OneDrive: 206 | 207 | ```r 208 | # get my OneDrive 209 | me$do_operation("drive") 210 | 211 | # list the files in my OneDrive root folder 212 | me$do_operation("drive/root/children") 213 | ``` 214 | 215 | ## See also 216 | 217 | See the following links on Microsoft Docs for more information. 218 | 219 | - [Microsoft Graph](https://learn.microsoft.com/en-us/graph/overview) 220 | - [Graph REST API (beta)](https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-beta) 221 | --------------------------------------------------------------------------------