{ "version": "https://jsonfeed.org/version/1.1", "user_comment": "This feed allows you to read the posts from this site in any feed reader that supports the JSON Feed format. To add this feed to your reader, copy the following URL -- /feed/json -- and add it your reader.", "next_url": "/feed/json?paged=2", "home_page_url": "/", "feed_url": "/feed/json", "language": "en-US", "title": "rakhesh.com", "description": "rakhesh sasidharan's mostly techie oh-so-purpley blog", "icon": "/wp-content/uploads/2025/05/cropped-Me-with-Computer-LEGO-512px.png", "items": [ { "id": "https://rakhesh.com/?p=8703", "url": "/power-platform/trigger-microsoft-fabric-data-pipeline-using-power-automate-logic-app/", "title": "Trigger Microsoft Fabric Data Pipeline using Power Automate/ Logic App", "content_html": "

Go read this post first. That’s what helped me figure out what to do without too much Googling around!

\n

Key things are:

\n\n

That’s more or less it!

\n

Irritatingly, I kept getting an Unauthorized error when testing this.

\n

\"\"

\n

But I wasn’t actually unauthorized, coz the body had a different error:

\n

\"\"

\n

If you are actually unauthorized, the error is different.

\n

\"\"

\n

Thanks to Gemini, I realized what was going on. Since the pipeline doesn’t run straight away, what happens behind the scenes when you do an HTTP request is that you are getting a Location URL in the reply header. You are supposed to keep querying that to get updates on what’s happening. But… when the HTTP connector does that, for some reason it doesn’t connect using the signed in account, and that’s why we get an unauthorized error.

\n

To fix it, first turn OFF asynchronous pattern. Click the 3 dots of the connector, go to Settings:

\n

\"\"

\n

With this turned off, the connector won’t follow the Location URL. Instead, we got to do that.

\n

So immediately after the HTTP connector, add some code to get the Location URL from the header.

\n

\"\"

\n

Here’s what I put in the input of the StatusURL compose object: outputs('HTTP_Request')?['headers']?['location'] (Replace “HTTP_Request” with whatever is your case of course)

\n

Now, if I were to query that URL using the authorized connector, I will get a response. That response looks like this:

{\r\n  \"id\": \"d9c77016-8e5d-4eed-a0df-59266367cf8d\",\r\n  \"itemId\": \"3bfc37e7-8837-430d-8af4-6c6397da24d5\",\r\n  \"jobType\": \"Pipeline\",\r\n  \"invokeType\": \"Manual\",\r\n  \"status\": \"NotStarted\",\r\n  \"failureReason\": null,\r\n  \"rootActivityId\": \"7f526ae4-bdb6-4fa8-b51e-2a626b3c9d62\",\r\n  \"startTimeUtc\": \"2026-03-27T14:47:00.8\",\r\n  \"endTimeUtc\": null\r\n}

Key thing is the status field. Looks like that has the following values:

\n\n

I couldn’t find any documentation on this (and Gemini gave wrong results such as “Succeeded” instead of “Completed”).

\n

So what one must do is setup a loop that calls this Location URL (authenticated request, GET) checks the value of the status field, and keeps looping until it is either Completed or Failed.

\n

Like so:

\n

\"\"

\n

Here’s the formula:

or(\r\n    equals(body('HTTP_Request2')?['status'], 'Completed'),\r\n    equals(body('HTTP_Request2')?['status'], 'Failed')\r\n)

And then one can send an HTTP response to the caller of the Logic App/ Power Automate.

\n

\"\"

\n

Here’s the condition block:

\n

\"\"

\n

Note that I set the condition to also run if the do/ until loop timesout. This way it returns and error if the loop doesn’t finish for whatever reason.

\n

That’s all! As the author of the blog post I linked to at the beginning of the post, I look forward to the day this is an in-built connector.

\n", "content_text": "Go read this post first. That’s what helped me figure out what to do without too much Googling around!\nKey things are:\n\nThe URL to call (via POST) is https://api.fabric.microsoft.com/v1/workspaces/@{variables('WorkspaceID')}/items/@{variables('PipelineID')}/jobs/instances?jobType=Pipeline\n\nNo need for a body, unless the pipeline needs one.\nHere’s the documentation for the URL being called (there isn’t much!).\n\nThe example there shows a payload, ignore that example. I realized later that the payload is just an example of parameters being sent to a pipeline.\n\n\n\n\nUse the HTTP with Entra ID preauhorized connector. When setting up authentication, use the following:\n\nBase URL is https://api.fabric.microsoft.com/\nResource URI is https://analysis.windows.net/powerbi/api\n\n\nBe sure to give whatever account you sign in with here rights to the workspace. In my case it was a service account I use and I gave it Contributor rights.\n\nThat’s more or less it!\nIrritatingly, I kept getting an Unauthorized error when testing this.\n\nBut I wasn’t actually unauthorized, coz the body had a different error:\n\nIf you are actually unauthorized, the error is different.\n\nThanks to Gemini, I realized what was going on. Since the pipeline doesn’t run straight away, what happens behind the scenes when you do an HTTP request is that you are getting a Location URL in the reply header. You are supposed to keep querying that to get updates on what’s happening. But… when the HTTP connector does that, for some reason it doesn’t connect using the signed in account, and that’s why we get an unauthorized error.\nTo fix it, first turn OFF asynchronous pattern. Click the 3 dots of the connector, go to Settings:\n\nWith this turned off, the connector won’t follow the Location URL. Instead, we got to do that.\nSo immediately after the HTTP connector, add some code to get the Location URL from the header.\n\nHere’s what I put in the input of the StatusURL compose object: outputs('HTTP_Request')?['headers']?['location'] (Replace “HTTP_Request” with whatever is your case of course)\nNow, if I were to query that URL using the authorized connector, I will get a response. That response looks like this:{\r\n \"id\": \"d9c77016-8e5d-4eed-a0df-59266367cf8d\",\r\n \"itemId\": \"3bfc37e7-8837-430d-8af4-6c6397da24d5\",\r\n \"jobType\": \"Pipeline\",\r\n \"invokeType\": \"Manual\",\r\n \"status\": \"NotStarted\",\r\n \"failureReason\": null,\r\n \"rootActivityId\": \"7f526ae4-bdb6-4fa8-b51e-2a626b3c9d62\",\r\n \"startTimeUtc\": \"2026-03-27T14:47:00.8\",\r\n \"endTimeUtc\": null\r\n}Key thing is the status field. Looks like that has the following values:\n\nNotStarted\nInProgress\nCompleted\nFailed\n\nI couldn’t find any documentation on this (and Gemini gave wrong results such as “Succeeded” instead of “Completed”).\nSo what one must do is setup a loop that calls this Location URL (authenticated request, GET) checks the value of the status field, and keeps looping until it is either Completed or Failed.\nLike so:\n\nHere’s the formula:or(\r\n equals(body('HTTP_Request2')?['status'], 'Completed'),\r\n equals(body('HTTP_Request2')?['status'], 'Failed')\r\n)And then one can send an HTTP response to the caller of the Logic App/ Power Automate.\n\nHere’s the condition block:\n\nNote that I set the condition to also run if the do/ until loop timesout. This way it returns and error if the loop doesn’t finish for whatever reason.\nThat’s all! As the author of the blog post I linked to at the beginning of the post, I look forward to the day this is an in-built connector.", "date_published": "2026-03-27T17:22:34+00:00", "date_modified": "2026-03-27T17:22:34+00:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "fabric", "logic app", "pipeline", "power automate", "Power Platform" ] }, { "id": "https://rakhesh.com/?p=8697", "url": "/azure/yammer-admin-unable-to-create-new-communities/", "title": "Yammer Admin unable to create new Communities", "content_html": "

The Engage Administrator role in Viva is known as Yammer Administrator in Entra ID. After assigning that, you will see that the admin is unable to create new communities, for instance.

\n

What you have to do to fix this is go to the Admin Portal in Engage itself, and add the user as a Verified admin (the other two roles don’t suffice). Then they can create new communities.

\n

\"\"

\n

Hope the next time you ask GenAI this question it sees this blog post and answers correctly for you. \"\ud83d\ude04\"

\n

ps. If you added the Verified admin role you won’t be able to remove the user as it is grayed out.

\n

\"\"

\n

You unassign the user from the Yammer Administrator role in this case to remove them from Verified admin. This isn’t an issue for the other two roles as you can modify it from the Engage admin portal itself.

\n", "content_text": "The Engage Administrator role in Viva is known as Yammer Administrator in Entra ID. After assigning that, you will see that the admin is unable to create new communities, for instance.\nWhat you have to do to fix this is go to the Admin Portal in Engage itself, and add the user as a Verified admin (the other two roles don’t suffice). Then they can create new communities.\n\nHope the next time you ask GenAI this question it sees this blog post and answers correctly for you. \nps. If you added the Verified admin role you won’t be able to remove the user as it is grayed out.\n\nYou unassign the user from the Yammer Administrator role in this case to remove them from Verified admin. This isn’t an issue for the other two roles as you can modify it from the Engage admin portal itself.", "date_published": "2026-03-11T13:49:27+00:00", "date_modified": "2026-03-11T13:53:57+00:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "engage", "viva", "Azure, Azure AD, Graph, M365" ] }, { "id": "https://rakhesh.com/?p=8682", "url": "/linux-bsd/zed-editor-error-crates-project-src-environment-rs227-capturing-shell-environment-with-opt-homebrew-bin-bash/", "title": "Zed editor: ERROR [crates/project/src/environment.rs:227] capturing shell environment with \u201c/opt/homebrew/bin/bash\u201d", "content_html": "

In case this helps anyone else… not saying what follows below is\u00a0the solution for such errors.

\n

Started to use Zed again earlier this week, and one one of my machines it showsa warning that the environment variables failed to load.

\n

\"\"

\n

Clicking on it opens up the logs, which has a bunch of entries like these:

Caused by:\r\n    login shell exited with exit status: 1. stdout: \"\\u{1b}(B\\u{1b}[m\", stderr: \"bash: cannot set terminal process group (-1): Inappropriate ioctl for device\\nbash: no job control in this shell\\nopen terminal failed: not a terminal\\n\"\r\n2025-12-13T10:09:19+00:00 ERROR [project::git_store] failed to get working directory environment for repository \"/Users/rakhesh/xxx\"\r\n2025-12-13T10:09:19+00:00 ERROR [crates/project/src/environment.rs:227] capturing shell environment with \"/opt/homebrew/bin/bash\"

(It seems to be for each of the folders that are git submodules).

\n

Trying to get to the bottom of this reminded me that I had encountered a similar issue with VS Code in the past. What happens is that on the problem machine, I have set it to launch tmux whenever a new login shell is spawned. And that was causing an issue with Zed.

\n

In VS Code the VSCODE_RESOLVING_ENVIRONMENT variable is set to 1 when VS Code is resolving the environment variables, so I was using that to not launch tmux in such cases. Something along the lines of:

# Only if $VSCODE_RESOLVING_ENVIRONMENT is empty, launch tmux\r\nif ([[ -z \"$VSCODE_RESOLVING_ENVIRONMENT\" ]]); then\r\n    # launch tmux\r\nfi

There doesn’t seem to be the equivalent of that for Zed, though someone’s asked for it recently.

\n

I added the following line to the top of my .bash_profile file.

env > ~/Downloads/blah.txt

This showed me that a variable called ZED_TERM is set to true. So I thought I’d use that in the same place I check for VSCODE_RESOLVING_ENVIRONMENT but it didn’t work.

# Only if $VSCODE_RESOLVING_ENVIRONMENT and $ZED_TERM are empty, launch tmux\r\nif ([[ -z \"$VSCODE_RESOLVING_ENVIRONMENT\" ]] && [[ -z \"$ZED_TERM\" ]]); then\r\n    # launch tmux\r\nfi

Adding the same env line at this point of the code, before launching tmux, showed me that ZED_TERM isn’t available anymore. Weird.

\n

Not a problem, I added this at the very top of .bash_profile where ZED_TERM was visible.

# Create a variable of my own\r\nif [[ \"$ZED_TERM\" == true ]]; then ; \r\n    ZED_RESOLVING_ENV=\"1\"\r\nfi

Then I can do:

# Only if $VSCODE_RESOLVING_ENVIRONMENT and $ZED_RESOLVING_ENV are empty, launch tmux\r\nif ([[ -z \"$VSCODE_RESOLVING_ENVIRONMENT\" ]] && [[ -z \"$ZED_RESOLVING_ENV\" ]]); then\r\n    # launch tmux\r\nfi

And problem solved! No more errors in Zed as tmux isn’t launched when it tries to resolve the environment variables.

\n

ps. For those who know and loved the Atom editor, Zed is Atom reborn (well, more like rewritten to be super fast and very minimal).

\n", "content_text": "In case this helps anyone else… not saying what follows below is\u00a0the solution for such errors.\nStarted to use Zed again earlier this week, and one one of my machines it showsa warning that the environment variables failed to load.\n\nClicking on it opens up the logs, which has a bunch of entries like these:Caused by:\r\n login shell exited with exit status: 1. stdout: \"\\u{1b}(B\\u{1b}[m\", stderr: \"bash: cannot set terminal process group (-1): Inappropriate ioctl for device\\nbash: no job control in this shell\\nopen terminal failed: not a terminal\\n\"\r\n2025-12-13T10:09:19+00:00 ERROR [project::git_store] failed to get working directory environment for repository \"/Users/rakhesh/xxx\"\r\n2025-12-13T10:09:19+00:00 ERROR [crates/project/src/environment.rs:227] capturing shell environment with \"/opt/homebrew/bin/bash\"(It seems to be for each of the folders that are git submodules).\nTrying to get to the bottom of this reminded me that I had encountered a similar issue with VS Code in the past. What happens is that on the problem machine, I have set it to launch tmux whenever a new login shell is spawned. And that was causing an issue with Zed.\nIn VS Code the VSCODE_RESOLVING_ENVIRONMENT variable is set to 1 when VS Code is resolving the environment variables, so I was using that to not launch tmux in such cases. Something along the lines of:# Only if $VSCODE_RESOLVING_ENVIRONMENT is empty, launch tmux\r\nif ([[ -z \"$VSCODE_RESOLVING_ENVIRONMENT\" ]]); then\r\n # launch tmux\r\nfiThere doesn’t seem to be the equivalent of that for Zed, though someone’s asked for it recently.\nI added the following line to the top of my .bash_profile file.env > ~/Downloads/blah.txtThis showed me that a variable called ZED_TERM is set to true. So I thought I’d use that in the same place I check for VSCODE_RESOLVING_ENVIRONMENT but it didn’t work.# Only if $VSCODE_RESOLVING_ENVIRONMENT and $ZED_TERM are empty, launch tmux\r\nif ([[ -z \"$VSCODE_RESOLVING_ENVIRONMENT\" ]] && [[ -z \"$ZED_TERM\" ]]); then\r\n # launch tmux\r\nfiAdding the same env line at this point of the code, before launching tmux, showed me that ZED_TERM isn’t available anymore. Weird.\nNot a problem, I added this at the very top of .bash_profile where ZED_TERM was visible.# Create a variable of my own\r\nif [[ \"$ZED_TERM\" == true ]]; then ; \r\n ZED_RESOLVING_ENV=\"1\"\r\nfiThen I can do:# Only if $VSCODE_RESOLVING_ENVIRONMENT and $ZED_RESOLVING_ENV are empty, launch tmux\r\nif ([[ -z \"$VSCODE_RESOLVING_ENVIRONMENT\" ]] && [[ -z \"$ZED_RESOLVING_ENV\" ]]); then\r\n # launch tmux\r\nfiAnd problem solved! No more errors in Zed as tmux isn’t launched when it tries to resolve the environment variables.\nps. For those who know and loved the Atom editor, Zed is Atom reborn (well, more like rewritten to be super fast and very minimal).", "date_published": "2025-12-13T10:33:22+00:00", "date_modified": "2025-12-13T10:42:22+00:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "atom", "bash", "tmux", "vscode", "zed", "Linux & BSD", "Mac" ] }, { "id": "https://rakhesh.com/?p=8676", "url": "/power-platform/consent-between-first-party-application-d2ebd3a9-1ada-4480-8b2d-eac162716601-and-first-party-resource-00000003-0000-0000-c000-000000000000-must-be-configured/", "title": "Consent between first party application \u2018d2ebd3a9-1ada-4480-8b2d-eac162716601\u2019 and first party resource \u201800000003-0000-0000-c000-000000000000\u2019 must be configured", "content_html": "

Maybe it’s always been the case, or maybe it changed some time ago but I never used this connector much and so didn’t realize… anyhoo, using the “Invoke an HTTP request” connector from a Logic App/ Power Automate to connect to Graph throws an error now:

\n

\"\"

\n

Reading the docs for this connector, there is a section on how to authorize it. You have to download a script from GitHub and run it as Global Admin.

\n

What the script essentially does is search for the Power Platform App Id mentioned above (d2ebd3a9-1ada-4480-8b2d-eac162716601) and if it does not exist, create it. Then it gives a list of first party apps (Graph, SharePoint Online, etc.) and asks you to choose the one you want to allow access to. After that, you can choose the scopes you are interested in granting access to. It’s pretty neat, and nice of Microsoft to provide such a script.

\n

If you want to avoid all this though, there’s a variant of the above connector that is preauthorized.

\n

\"\"

\n

Fill that as as below, and it works.

\n

\"\"

\n

From the documentation, it sounds like this one too is limited to a number of applications and scopes; and anything extra will require use of the non pre-authorized connector and script.

\n

 

\n", "content_text": "Maybe it’s always been the case, or maybe it changed some time ago but I never used this connector much and so didn’t realize… anyhoo, using the “Invoke an HTTP request” connector from a Logic App/ Power Automate to connect to Graph throws an error now:\n\nReading the docs for this connector, there is a section on how to authorize it. You have to download a script from GitHub and run it as Global Admin.\nWhat the script essentially does is search for the Power Platform App Id mentioned above (d2ebd3a9-1ada-4480-8b2d-eac162716601) and if it does not exist, create it. Then it gives a list of first party apps (Graph, SharePoint Online, etc.) and asks you to choose the one you want to allow access to. After that, you can choose the scopes you are interested in granting access to. It’s pretty neat, and nice of Microsoft to provide such a script.\nIf you want to avoid all this though, there’s a variant of the above connector that is preauthorized.\n\nFill that as as below, and it works.\n\nFrom the documentation, it sounds like this one too is limited to a number of applications and scopes; and anything extra will require use of the non pre-authorized connector and script.\n ", "date_published": "2025-11-26T15:00:18+00:00", "date_modified": "2025-11-26T15:00:18+00:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "authentication", "http", "logic app", "power automate", "Power Platform" ] }, { "id": "https://rakhesh.com/?p=8667", "url": "/azure/plus-address-emails-for-entra-id-admin-accounts/", "title": "Plus Address emails for Entra ID admin accounts", "content_html": "

There’s plenty of blog posts on this topic, so I won’t go into the basic details. We often have admin accounts in Entra ID that must receive emails, but there’s no point wasting a license on a mailbox for this. What do we do here?

\n

Easy. Use Plus Addressing. What plus addressing means is that every mailbox enabled user also has a “+<whatver>” suffix they can use as an email address. That is to say, if my email address is abc@mydomain.com, if plus addressing is enabled I can receive emails at abc+123@mydomain.com, abc+xxx@mydomain.com, abc+laksdasd@mydomain.com, and so on…

\n

Nothing new about this concept, and this is something one commonly uses in Gmail etc. when handing out email addresses to a website. Use a plus address so you know which website is then spamming you, plus you can create rules to put them into separate folders etc.

\n

To use this in admin accounts in Entra, assuming it is setup in the organization, all we need do is add a plus address to the admin account.

\n

\"\"

\n

Now any service sending an email to the admin account by looking up its mail attribute will use the plus address you put in there, and Exchange Online will route that email to your regular account.

\n

Things get a bit tricky when it comes to external emails, including notifications from Azure and Entra ID. If you have something like Mimecast which is the mail processor for external emails, you need to enable plus addressing there too. Mimecast calls it Sieve Sub Address policy (named after the RFC). Instructions are in their documentation, and here’s what I did.

\n

First off, if you use Mimecast AAA then the steps below must be done there, not on the Federated instance.

\n

It is possible to enable plus addressing on the Mimecast side for everyone, but we opted to do it for a few users. Mainly our admin accounts. So we created an address group to hold these addresses.

\n

Go to Users & Groups > Policy Groups > Create a new group.

\n

\"\"

\n

In that add the addresses. The document is a bit confusing, but through some trial and error what we discovered is that you must address both the plus address(es) you want to allow, and also the user’s default address. So, for example, if I want to allow abc+def@mydomain.com I must add both abc+def@mydomain.com and abc@mydomain.com here. Add any number of plus addresses, but they won’t work unless the base address too is present.

\n

Then go to Policies > Gateway Policies > Select Sieve Sub Address.

\n

\"\"

\n

Create a new policy here. Give it a name, and in the “Emails To” section select the group we previously created. Below is a screenshot from our setup:

\n

\"\"

\n

Mimecast is neat in that it supports two types of plus addresses, and for each of these it can also strip out the plus address bit and send to the backend system in case that system doesn’t know of plus addresses. So if Exchange Online didn’t support it, we could have still used it for external emails thanks to Mimecast.

\n

We went with “Enable + Delimiter Recognition” which enables plus addressing of the sort I showed above, and doesn’t strip the plus address bit.

\n

Save it, and that’s all really! Now plus addressing works for external emails too. We tested this by sending emails to the admin accounts internally and externally by typing out the full plus address rakhesh+admin@thesyndicate.com, as well as assigning the admin account an Entra role to get Entra ID to generate an email. This came though, confirming that Entra uses the mail attribute we added to the admin account, and the email flows through Mimecast and is delivered.

\n", "content_text": "There’s plenty of blog posts on this topic, so I won’t go into the basic details. We often have admin accounts in Entra ID that must receive emails, but there’s no point wasting a license on a mailbox for this. What do we do here?\nEasy. Use Plus Addressing. What plus addressing means is that every mailbox enabled user also has a “+<whatver>” suffix they can use as an email address. That is to say, if my email address is abc@mydomain.com, if plus addressing is enabled I can receive emails at abc+123@mydomain.com, abc+xxx@mydomain.com, abc+laksdasd@mydomain.com, and so on…\nNothing new about this concept, and this is something one commonly uses in Gmail etc. when handing out email addresses to a website. Use a plus address so you know which website is then spamming you, plus you can create rules to put them into separate folders etc.\nTo use this in admin accounts in Entra, assuming it is setup in the organization, all we need do is add a plus address to the admin account.\n\nNow any service sending an email to the admin account by looking up its mail attribute will use the plus address you put in there, and Exchange Online will route that email to your regular account.\nThings get a bit tricky when it comes to external emails, including notifications from Azure and Entra ID. If you have something like Mimecast which is the mail processor for external emails, you need to enable plus addressing there too. Mimecast calls it Sieve Sub Address policy (named after the RFC). Instructions are in their documentation, and here’s what I did.\nFirst off, if you use Mimecast AAA then the steps below must be done there, not on the Federated instance.\nIt is possible to enable plus addressing on the Mimecast side for everyone, but we opted to do it for a few users. Mainly our admin accounts. So we created an address group to hold these addresses.\nGo to Users & Groups > Policy Groups > Create a new group.\n\nIn that add the addresses. The document is a bit confusing, but through some trial and error what we discovered is that you must address both the plus address(es) you want to allow, and also the user’s default address. So, for example, if I want to allow abc+def@mydomain.com I must add both abc+def@mydomain.com and abc@mydomain.com here. Add any number of plus addresses, but they won’t work unless the base address too is present.\nThen go to Policies > Gateway Policies > Select Sieve Sub Address.\n\nCreate a new policy here. Give it a name, and in the “Emails To” section select the group we previously created. Below is a screenshot from our setup:\n\nMimecast is neat in that it supports two types of plus addresses, and for each of these it can also strip out the plus address bit and send to the backend system in case that system doesn’t know of plus addresses. So if Exchange Online didn’t support it, we could have still used it for external emails thanks to Mimecast.\nWe went with “Enable + Delimiter Recognition” which enables plus addressing of the sort I showed above, and doesn’t strip the plus address bit.\nSave it, and that’s all really! Now plus addressing works for external emails too. We tested this by sending emails to the admin accounts internally and externally by typing out the full plus address rakhesh+admin@thesyndicate.com, as well as assigning the admin account an Entra role to get Entra ID to generate an email. This came though, confirming that Entra uses the mail attribute we added to the admin account, and the email flows through Mimecast and is delivered.", "date_published": "2025-11-21T13:46:08+00:00", "date_modified": "2025-11-21T13:46:08+00:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "exchangeonline", "mimecast", "plus addressing", "Azure, Azure AD, Graph, M365" ] }, { "id": "https://rakhesh.com/?p=8461", "url": "/tv/good-fortune-english-movie-and-others/", "title": "Good Fortune (English movie), and others\u2026", "content_html": "

We happened to watch \u201cGood Fortune\u201d this weekend. It’s a blast of a movie! I had so much fun watching it. I didn\u2019t realise it was written and directed by Aziz Ansari (his movie debut). If you like his stuff you\u2019ll definitely enjoy this one! Very satirical and funny.

\n

The movie also ties with a lot of the books and podcasts I consume nowadays. It\u2019s very in with the times. All about the gig economy and working conditions and life being hard and pointless. You got to be a certain kind of mind to see and hear all what\u2019s happening around you and then make a movie out of it that conveys the message but isn\u2019t preachy or doom and gloom. And Aziz Ansari definitely has that sort of a creative mind! And that goes for most writers and directors of course – I am always impressed at how the good ones have a vision and can \u201cshow us\u201d the world through their eyes.

\n

A TV show I watched recently was \u201cThe Bombing of Pan Am 103\u201d. Again, very impressive and touching. The creators managed to convey all the emotions associated with the incident – the kindness of the townspeople of Lockerbie, the investigation efforts across US and UK, the frustrations experienced by the relatives of the victims – so many things! I had no idea of this incident until I saw the TV show but it had me hooked and I really felt like I was there. Again, that\u2019s an amazing thing to be able to do as a creator.

\n

Another brilliant TV show we saw recently is \u201cAll Her Fault\u201d. The show wasn’t at all what I was expecting it to be. It touched upon a lot of themes, but was never preachy or focusing too much on any one issue. Nor did it feel all over the place. Instead it did an amazing tightrope across everything and I loved it! A child is kidnapped by his nanny, and from there the show touches upon a lot of current issues – rich people, the struggles of working couples, childrearing, domestic abuse (of a different sort), and so on. I won’t go into the details but I definitely recommend the show. Looks like the creator and directors are all women too, and that really shows in the way the show treats its matter. (So is the author of the book the show is based on actually. I haven\u2019t read the book).

\n", "content_text": "We happened to watch \u201cGood Fortune\u201d this weekend. It’s a blast of a movie! I had so much fun watching it. I didn\u2019t realise it was written and directed by Aziz Ansari (his movie debut). If you like his stuff you\u2019ll definitely enjoy this one! Very satirical and funny.\nThe movie also ties with a lot of the books and podcasts I consume nowadays. It\u2019s very in with the times. All about the gig economy and working conditions and life being hard and pointless. You got to be a certain kind of mind to see and hear all what\u2019s happening around you and then make a movie out of it that conveys the message but isn\u2019t preachy or doom and gloom. And Aziz Ansari definitely has that sort of a creative mind! And that goes for most writers and directors of course – I am always impressed at how the good ones have a vision and can \u201cshow us\u201d the world through their eyes.\nA TV show I watched recently was \u201cThe Bombing of Pan Am 103\u201d. Again, very impressive and touching. The creators managed to convey all the emotions associated with the incident – the kindness of the townspeople of Lockerbie, the investigation efforts across US and UK, the frustrations experienced by the relatives of the victims – so many things! I had no idea of this incident until I saw the TV show but it had me hooked and I really felt like I was there. Again, that\u2019s an amazing thing to be able to do as a creator.\nAnother brilliant TV show we saw recently is \u201cAll Her Fault\u201d. The show wasn’t at all what I was expecting it to be. It touched upon a lot of themes, but was never preachy or focusing too much on any one issue. Nor did it feel all over the place. Instead it did an amazing tightrope across everything and I loved it! A child is kidnapped by his nanny, and from there the show touches upon a lot of current issues – rich people, the struggles of working couples, childrearing, domestic abuse (of a different sort), and so on. I won’t go into the details but I definitely recommend the show. Looks like the creator and directors are all women too, and that really shows in the way the show treats its matter. (So is the author of the book the show is based on actually. I haven\u2019t read the book).", "date_published": "2025-11-17T12:08:26+00:00", "date_modified": "2025-11-10T12:20:07+00:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "TV, Movies, Music" ] }, { "id": "https://rakhesh.com/?p=8651", "url": "/gadgets/logging-in-to-successfactors-android-with-fingerprint/", "title": "Logging in to SuccessFactors Android app with a fingerprint in the work profle", "content_html": "

Continuing with SuccessFactors on Android, one of the things the app does upon launch is to ask whether you want to use a password or fingerprint. If I select “fingerprint”, it says there’s no fingerprint to be found. And if I select “password” I can setup a new password and use that each time I open the app.

\n

That seemed a bit inconvenient, and I was curious why it wasn’t finding the fingerprint – especially coz my phone had fingerprint and face unlock setup.

\n

I figured out what to do. One must add fingerprints to the work profile too. Like so:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
Open the Settings app\"\"
Type \u201cfingerprint\u201d and select the one for Work\"\"
If \u201cFingerprint for work\u201d already has an entry, you don\u2019t need to do anything.

\n

If not, click on it.

\n

Follow the instructions to add a fingerprint. You may also be prompted to register your face \u2013 go ahead and do that too.

\"\"
After adding a fingerprint (and optionally, face) it looks like this.\"\"
\n

Now when I open the app (I had to reinstall it, but I think there’s a setting in the app if you have already signed in with the password option – click on the hamburger menu on the top left in the app, go to Settings, and toggle Fingerprint) I get prompted as before but can select the fingerprint option.

\n

\"\"

\n

Click “YES PLEASE” and I get prompted to confirm the fingerprint.

\n

\"\"

\n

Yay!

\n", "content_text": "Continuing with SuccessFactors on Android, one of the things the app does upon launch is to ask whether you want to use a password or fingerprint. If I select “fingerprint”, it says there’s no fingerprint to be found. And if I select “password” I can setup a new password and use that each time I open the app.\nThat seemed a bit inconvenient, and I was curious why it wasn’t finding the fingerprint – especially coz my phone had fingerprint and face unlock setup.\nI figured out what to do. One must add fingerprints to the work profile too. Like so:\n\n\n\nOpen the Settings app\n\n\n\nType \u201cfingerprint\u201d and select the one for Work\n\n\n\nIf \u201cFingerprint for work\u201d already has an entry, you don\u2019t need to do anything.\nIf not, click on it.\nFollow the instructions to add a fingerprint. You may also be prompted to register your face \u2013 go ahead and do that too.\n\n\n\nAfter adding a fingerprint (and optionally, face) it looks like this.\n\n\n\n\nNow when I open the app (I had to reinstall it, but I think there’s a setting in the app if you have already signed in with the password option – click on the hamburger menu on the top left in the app, go to Settings, and toggle Fingerprint) I get prompted as before but can select the fingerprint option.\n\nClick “YES PLEASE” and I get prompted to confirm the fingerprint.\n\nYay!", "date_published": "2025-11-13T13:49:26+00:00", "date_modified": "2025-11-13T13:52:44+00:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "android", "successfactors", "Gadgets" ] }, { "id": "https://rakhesh.com/?p=8642", "url": "/azure/sap-successfactors-app-and-intune/", "title": "SAP SuccessFactors app and Intune", "content_html": "

We wanted to push the Android and iOS apps of SAP SuccessFactors.

\n

iOS is easy. Push the app out to a group. Also push out an app configuration so the app knows what to do when a user clicks on it. Easy peasy.

\n

This article has a list of the keys you need to push out.

\n

And you can do so via an XML file like so:

<dict>\r\n     <key>SFSF_Instance</key><string>XXXXX</string>\r\n     <key>SFSF_DomainName</key><string>BLAH.successfactors.TLD</string>\r\n     <key>SuccessFactors</key><string>GUID</string>\r\n</dict>

Android should be similarly easy. Push the app out to a group. Create an app configuration and assign to the group. With Android you get a configuration designer with prefilled questions when you choose SuccessFactors as the app.

\n

\"\"

\n

However, this didn’t work. The app would launch and then throw an error:

\n

No apps found: There are no apps on this device that your organization allows to open this content. Contact your IT administrator for help.

\n

Weird. I thought maybe the app expects Chrome or something, and since our Android work profile didn’t have Chrome it was complaining about that. But adding Chrome didn’t make a difference.

\n

Contacting customer support was unhelpful as they kept saying we must add their app to the App Protection policies. Which makes zero sense, coz SuccessFactors isn’t available as an app to select with App Protection policies.

\n

The fix in the end was to exempt SAP SuccessFactors from the “Send org data to other apps” setting of our default App Protection policy. Thanks to this blog post that gave me the idea. In our case the “Send org data to other apps” setting was set to “Policy managed apps” so I think what’s happening is that when the\u00a0 SuccessFactors app launches it opens up Edge to do the SSO (it knows what to do coz we are pushing the app configuration) and then Edge is unable to send back to SuccessFactors as it’s not a policy managed app. That’s why the SuccessFactors team were saying we should add SAF SuccessFactors to the App Protection policy, but that’s wrong advise coz the app isn’t available to be added. Instead, we must exclude SAP SuccessFactors from the list of apps that are exempted from the “Send org data to other apps” restriction.

\n

\"\"

\n

I found the id by looking at the app in Intune. It’s com.successfactors.successfactors in case anyone wants to copy-paste.

\n", "content_text": "We wanted to push the Android and iOS apps of SAP SuccessFactors.\niOS is easy. Push the app out to a group. Also push out an app configuration so the app knows what to do when a user clicks on it. Easy peasy.\nThis article has a list of the keys you need to push out.\nAnd you can do so via an XML file like so:<dict>\r\n <key>SFSF_Instance</key><string>XXXXX</string>\r\n <key>SFSF_DomainName</key><string>BLAH.successfactors.TLD</string>\r\n <key>SuccessFactors</key><string>GUID</string>\r\n</dict>Android should be similarly easy. Push the app out to a group. Create an app configuration and assign to the group. With Android you get a configuration designer with prefilled questions when you choose SuccessFactors as the app.\n\nHowever, this didn’t work. The app would launch and then throw an error:\nNo apps found: There are no apps on this device that your organization allows to open this content. Contact your IT administrator for help.\nWeird. I thought maybe the app expects Chrome or something, and since our Android work profile didn’t have Chrome it was complaining about that. But adding Chrome didn’t make a difference.\nContacting customer support was unhelpful as they kept saying we must add their app to the App Protection policies. Which makes zero sense, coz SuccessFactors isn’t available as an app to select with App Protection policies.\nThe fix in the end was to exempt SAP SuccessFactors from the “Send org data to other apps” setting of our default App Protection policy. Thanks to this blog post that gave me the idea. In our case the “Send org data to other apps” setting was set to “Policy managed apps” so I think what’s happening is that when the\u00a0 SuccessFactors app launches it opens up Edge to do the SSO (it knows what to do coz we are pushing the app configuration) and then Edge is unable to send back to SuccessFactors as it’s not a policy managed app. That’s why the SuccessFactors team were saying we should add SAF SuccessFactors to the App Protection policy, but that’s wrong advise coz the app isn’t available to be added. Instead, we must exclude SAP SuccessFactors from the list of apps that are exempted from the “Send org data to other apps” restriction.\n\nI found the id by looking at the app in Intune. It’s com.successfactors.successfactors in case anyone wants to copy-paste.", "date_published": "2025-11-13T12:31:04+00:00", "date_modified": "2025-11-13T13:51:05+00:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "android", "intune", "iOS", "successfactors", "Azure, Azure AD, Graph, M365" ] }, { "id": "https://rakhesh.com/?p=8637", "url": "/mac/hammerspoon-and-macos-shorcuts/", "title": "Hammerspoon and macOS shorcuts", "content_html": "

Sweet, Hammerspoon can run macOS shortcuts! So awesome. \"\ud83e\udd73\"

\n

In a similar vein to my tinkering with Hue lights… I have a BenQ ScreenBar Halo on my monitor which I’d like to turn off when I logoff or my Mac goes to sleep. The latest version of the Halo has some sensors that can detect when you are around, but I don’t have that nor have any plans of buying that, so I wondered what else I can do.

\n

I had a Meross SmartPlug lying around, and I figured maybe I could plug the ScreenBar into the Meross. And then use some API commands to turn off the on the SmartPlug maybe? Turning off the SmartPlug will turn off the lights, and turning on the SmartPlug won’t turn on the lights but I can manually turn that on – no big deal. (Update: Turns out the ScreenBar automatically lights up if it was previously lit when power was turned off. So turning on power to the SmartPlug effectively turns on the ScreenBar too).

\n

I tried to find some way of dealing with the Meross API, and also found this Python library. I didn’t want to use that, but looking at the library I realized I could install a proxy app on my phone (e.g. proxypin or proxyman) to see what commands are being sent to the SmartPlug when I turn it on and off. That was pretty straight forward, and I found two HTTP calls too to turn off and on the plug. The HTTP calls worked too, just that they weren’t super reliable. They seemed to work as long as I put a minute or two between them – so I can turn on the plug, but if I send the command to turn off immediately, it doesn’t work. Wait 2 mins and send the command, and that works. Ditto for turning off.

\n

That wasn’t super ideal, and I was wondering what else I can do when the idea struck me of using Shortcuts. The plug in question supports HomeKit so I have it setup in the Home app too; so I could easily create a shortcut to turn on the plug, and another shortcut to turn off the plug. And since I was using Hammerspoon to detect when I log off and login, it would be great if I can use Hammerspoon to run the appropriate shortcut.

\n

Turns out that part is dead easy! All I need do is:

hs.shortcuts.run(\"<name of the shortcut>\")

So I created a second watcher along the lines of my previous one:

function toggleSocket(eventType)\r\n  if (eventType == hs.caffeinate.watcher.screensDidUnlock or \r\n      eventType == hs.caffeinate.watcher.systemDidWake) then\r\n    hs.shortcuts.run(\"Desk Socket ON\")\r\n\r\n  elseif (eventType == hs.caffeinate.watcher.screensDidLock or \r\n          eventType == hs.caffeinate.watcher.systemWillSleep or \r\n          eventType == hs.caffeinate.watcher.systemWillPowerOff or \r\n          eventType == hs.caffeinate.watcher.screensDidSleep) then\r\n    hs.shortcuts.run(\"Desk Socket OFF\")\r\n  \r\n  end\r\nend\r\n\r\nsocketWatcher = hs.caffeinate.watcher.new(toggleSocket)\r\nsocketWatcher:start()

And that’s it, really! Job done.

\n

Hammerspoon is sooooo awesome! \"\ud83e\udd29\"

\n", "content_text": "Sweet, Hammerspoon can run macOS shortcuts! So awesome. \nIn a similar vein to my tinkering with Hue lights… I have a BenQ ScreenBar Halo on my monitor which I’d like to turn off when I logoff or my Mac goes to sleep. The latest version of the Halo has some sensors that can detect when you are around, but I don’t have that nor have any plans of buying that, so I wondered what else I can do.\nI had a Meross SmartPlug lying around, and I figured maybe I could plug the ScreenBar into the Meross. And then use some API commands to turn off the on the SmartPlug maybe? Turning off the SmartPlug will turn off the lights, and turning on the SmartPlug won’t turn on the lights but I can manually turn that on – no big deal. (Update: Turns out the ScreenBar automatically lights up if it was previously lit when power was turned off. So turning on power to the SmartPlug effectively turns on the ScreenBar too).\nI tried to find some way of dealing with the Meross API, and also found this Python library. I didn’t want to use that, but looking at the library I realized I could install a proxy app on my phone (e.g. proxypin or proxyman) to see what commands are being sent to the SmartPlug when I turn it on and off. That was pretty straight forward, and I found two HTTP calls too to turn off and on the plug. The HTTP calls worked too, just that they weren’t super reliable. They seemed to work as long as I put a minute or two between them – so I can turn on the plug, but if I send the command to turn off immediately, it doesn’t work. Wait 2 mins and send the command, and that works. Ditto for turning off.\nThat wasn’t super ideal, and I was wondering what else I can do when the idea struck me of using Shortcuts. The plug in question supports HomeKit so I have it setup in the Home app too; so I could easily create a shortcut to turn on the plug, and another shortcut to turn off the plug. And since I was using Hammerspoon to detect when I log off and login, it would be great if I can use Hammerspoon to run the appropriate shortcut.\nTurns out that part is dead easy! All I need do is:hs.shortcuts.run(\"<name of the shortcut>\")So I created a second watcher along the lines of my previous one:function toggleSocket(eventType)\r\n if (eventType == hs.caffeinate.watcher.screensDidUnlock or \r\n eventType == hs.caffeinate.watcher.systemDidWake) then\r\n hs.shortcuts.run(\"Desk Socket ON\")\r\n\r\n elseif (eventType == hs.caffeinate.watcher.screensDidLock or \r\n eventType == hs.caffeinate.watcher.systemWillSleep or \r\n eventType == hs.caffeinate.watcher.systemWillPowerOff or \r\n eventType == hs.caffeinate.watcher.screensDidSleep) then\r\n hs.shortcuts.run(\"Desk Socket OFF\")\r\n \r\n end\r\nend\r\n\r\nsocketWatcher = hs.caffeinate.watcher.new(toggleSocket)\r\nsocketWatcher:start()And that’s it, really! Job done.\nHammerspoon is sooooo awesome!", "date_published": "2025-11-12T23:35:11+00:00", "date_modified": "2025-11-13T12:58:26+00:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "hammerspoon", "macOS", "shortcuts", "Mac" ] }, { "id": "https://rakhesh.com/?p=8635", "url": "/books/blood-in-the-machine-book/", "title": "Blood in the Machine (Book)", "content_html": "

“Blood in the Machine” – the book – by Brian Merchant, is crazy! I am only some 100 pages in to it so far, but it is so shocking and eye opening. I never knew this was what the beginnings of the Industrial Revolution was like. I never know this was what the Luddite movement was about. And worse, as Mark Twain said (a quote I found in this book) “History never repeats itself, but it does often rhyme” – so true! Everything I am reading there rhymes so much with what is happening nowadays with AI and the gig economy. Wow!

\n

I always thought a luddite was someone who hated technology, or who couldn’t understand new technology and refused to change with the times. That’s how I always used the term. But while reading this book I realized how wrong I was. Luddites weren’t against technology, they just were against technology that made it worse for humans. The same way one could feel about AI or any other technology – I am not against AI, for instance, when Copilot summarizes a Teams meeting so I don’t have to watch the whole thing but can quickly understand what happened – but I am against AI when it is presented as something that can replace humans or the work they do (write code, design images, write legal opinions, replace customer support). And back then and now, the issue was one of policy and greed – what the Luddites were against was how the factory owners were using these machines to get rich by stealing the work/ profits of the workers. The workers took pride in their work and were happy to work for a fair price, it was only when factory owners wanted to replace the workers with machines because that way they could sell things for cheap and eliminate dealing with workers, that they started smashing machines! It’s so crazy how so much of that resonates with what’s happening in the world today.

\n

Worse, machines had the quality worse in a lot of cases, but that was fine coz the price is what mattered.

\n

On the policy front it was the wars England was fighting with France that brought down the market (less demand for the goods), giving factory owners all the more reason to push for machines as they can lower costs and not deal with workers. So many themes that rhyme between then and now!

\n

And all this is from just the first 100 pages or so. Madness. I never ever thought about the Industrial Revolution this way. You visit a museum and all everyone ever talks about is how the Industrial Revolution paved the way for progress and made our lives better etc.; which, no doubt, it did, but there was a huge price to pay for it and that was only paid by the workers.

\n

I knew from English studies at school where we had to read Charles Dickens on how children worked in these factories and things were tough. But again, that was just a “story”. In this book though, there’s a lot more on how such children were mistreated, and why children were used in the first place. Factory owners didn’t need workers to run the machines, children were ideal coz they didn’t cost most, were more pliable, and you don’t really need much skills to operate the machines (as opposed to the cotton weavers and others). And just like you hear of inhuman working conditions at factories in China, that’s pretty much how these factories were back then for kids. Crazy! “History never repeats itself, but it does often rhyme

\n

A rank of precarious, angry, and impoverished workers rapidly growing across the country; new forms of technology, control, and production bringing advantages to a few at the expense of the many; a detached, vain, and despised leader at the helm: the comet had dimmed for the summer, but it was about to burn brighter than ever.

\n

A must-read!

\n

For anyone interested, the author Brian Merchant also has a newsletter + podcast of the same name. That’s how I came across the book.

\n", "content_text": "“Blood in the Machine” – the book – by Brian Merchant, is crazy! I am only some 100 pages in to it so far, but it is so shocking and eye opening. I never knew this was what the beginnings of the Industrial Revolution was like. I never know this was what the Luddite movement was about. And worse, as Mark Twain said (a quote I found in this book) “History never repeats itself, but it does often rhyme” – so true! Everything I am reading there rhymes so much with what is happening nowadays with AI and the gig economy. Wow!\nI always thought a luddite was someone who hated technology, or who couldn’t understand new technology and refused to change with the times. That’s how I always used the term. But while reading this book I realized how wrong I was. Luddites weren’t against technology, they just were against technology that made it worse for humans. The same way one could feel about AI or any other technology – I am not against AI, for instance, when Copilot summarizes a Teams meeting so I don’t have to watch the whole thing but can quickly understand what happened – but I am against AI when it is presented as something that can replace humans or the work they do (write code, design images, write legal opinions, replace customer support). And back then and now, the issue was one of policy and greed – what the Luddites were against was how the factory owners were using these machines to get rich by stealing the work/ profits of the workers. The workers took pride in their work and were happy to work for a fair price, it was only when factory owners wanted to replace the workers with machines because that way they could sell things for cheap and eliminate dealing with workers, that they started smashing machines! It’s so crazy how so much of that resonates with what’s happening in the world today.\nWorse, machines had the quality worse in a lot of cases, but that was fine coz the price is what mattered.\nOn the policy front it was the wars England was fighting with France that brought down the market (less demand for the goods), giving factory owners all the more reason to push for machines as they can lower costs and not deal with workers. So many themes that rhyme between then and now!\nAnd all this is from just the first 100 pages or so. Madness. I never ever thought about the Industrial Revolution this way. You visit a museum and all everyone ever talks about is how the Industrial Revolution paved the way for progress and made our lives better etc.; which, no doubt, it did, but there was a huge price to pay for it and that was only paid by the workers.\nI knew from English studies at school where we had to read Charles Dickens on how children worked in these factories and things were tough. But again, that was just a “story”. In this book though, there’s a lot more on how such children were mistreated, and why children were used in the first place. Factory owners didn’t need workers to run the machines, children were ideal coz they didn’t cost most, were more pliable, and you don’t really need much skills to operate the machines (as opposed to the cotton weavers and others). And just like you hear of inhuman working conditions at factories in China, that’s pretty much how these factories were back then for kids. Crazy! “History never repeats itself, but it does often rhyme”\nA rank of precarious, angry, and impoverished workers rapidly growing across the country; new forms of technology, control, and production bringing advantages to a few at the expense of the many; a detached, vain, and despised leader at the helm: the comet had dimmed for the summer, but it was about to burn brighter than ever.\nA must-read!\nFor anyone interested, the author Brian Merchant also has a newsletter + podcast of the same name. That’s how I came across the book.", "date_published": "2025-11-12T23:19:47+00:00", "date_modified": "2025-11-12T23:19:47+00:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "luddite", "technology", "Books, Audiobooks, Podcasts" ] }, { "id": "https://rakhesh.com/?p=8620", "url": "/azure/guestorexternalusertypes-63/", "title": "\u201cguestOrExternalUserTypes\u201d: 63", "content_html": "

I have an automation that goes through the Graph audit logs API output for any Conditional Access policy changes. I noticed that the output for these had a few entries that looked like \"excludeGuestsOrExternalUsers\":{\"guestOrExternalUserTypes\":63, ...}

\n

That made no sense. The valid values for guestOrExternalUserTypes should be one or more of of none, internalGuest, b2bCollaborationGuest, b2bCollaborationMember, b2bDirectConnectUser, otherExternalUser, serviceProvider, unknownFutureValue according to the documentation.

\n

Looking at one of the policies it had the following selection in the “Exclude” section:

\n

\"\"

\n

This wasn’t any PowerShell or API shenanigans either, as the Entra ID portal too showed the same. I could have sworn this used to show text instead of numbers…

\n

\"\"

\n

I couldn’t find any info on where this number was coming from. The modifiedProperty value where this JSON is located comes from the service in question, so the docs didn’t have anything either.

\n

To figure out what’s happening I created a new policy and started fiddling with the values.

\n

First I selected just one:

\n

\"\"

\n

This resulted in \"excludeGuestsOrExternalUsers\":{\"guestOrExternalUserTypes\":2, ...}

\n

Then I unticked that and selected the second option.

\n

\"\"

\n

Weirdly now I get the text.

\n

\"\"

\n

I made one more change:

\n

\"\"

\n

And this time it’s numbers. Huh?!

\n

\"\"

\n

Then I realized something… here’s what I see in the Audit Logs in the portal. Notice that for each addition/ update there’s three set of entries.

\n

\"\"

\n

I should either (obviously) focus on the “Add conditional access” and “Update conditional access” entries, or “Add policy” and “Update policy” entries. In my PowerShell code I was only looking at events generated by the “Conditional Access” service, so I was only seeing “Add conditional access” and “Update conditional access” entries, but here in the GUI I first looked at “Add conditional access” and next time looked at “Update policy” instead of “Update conditional access”.

\n

Silly me! (In fairness, the “Update conditional access policy” entries are generated last, and I was just looking for any entry with an “Update” in it…)

\n

Looking at the correct entry for the first change I made, I see numbers:

\n

\"\"

\n

Cool. Time to put this down in a table:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n
\n
B2B collaboration guest users
\n
\n
2
\n
\n
B2B collaboration member users
\n
\n
4
\n
\n
B2B direct connect users
\n
\n
8
\n
\n
Local guest users
\n
\n
1
\n
\n
Other external users
\n
\n
16
\n
\n
Service provider users
\n
\n
32
\n

So that explains the 63. If all of the above are selected, then the sum of these numbers is 63. \"\u263a\"

\n

Wrote a little PowerShell function to translate the number into the selections:

function TranslateNumber-ToExternalGuest {\r\n    [CmdletBinding()]\r\n    param(\r\n        [Parameter(Position=0,Mandatory=$true)]\r\n        [int]$InputNumber\r\n    )\r\n\r\n    $externalUsersHash = @{\r\n        1 = \"Local guest users\" \r\n        2 = \"B2B collaboration guest users\"\r\n        4 = \"B2B collaboration member users\" \r\n        8 = \"B2B direct connect users\"\r\n        16 = \"Other external users\" \r\n        32 = \"Service provider users\"\r\n\r\n    }\r\n\r\n    if ($inputNumber -in $externalUsersHash.Keys) { \r\n        $guestSelections = $externalUsersHash[$inputNumber]\r\n        \r\n    } else {\r\n        $guestSelections = foreach ($number in 32,16,8,4,2,1) {\r\n            if ($inputNumber -ge $number) { \r\n                $externalUsersHash[$number]\r\n                $inputNumber = $inputNumber - $number\r\n\r\n            }\r\n        }\r\n    }\r\n\r\n    $guestSelections\r\n}

 

\n", "content_text": "I have an automation that goes through the Graph audit logs API output for any Conditional Access policy changes. I noticed that the output for these had a few entries that looked like \"excludeGuestsOrExternalUsers\":{\"guestOrExternalUserTypes\":63, ...}\nThat made no sense. The valid values for guestOrExternalUserTypes should be one or more of of none, internalGuest, b2bCollaborationGuest, b2bCollaborationMember, b2bDirectConnectUser, otherExternalUser, serviceProvider, unknownFutureValue according to the documentation.\nLooking at one of the policies it had the following selection in the “Exclude” section:\n\nThis wasn’t any PowerShell or API shenanigans either, as the Entra ID portal too showed the same. I could have sworn this used to show text instead of numbers…\n\nI couldn’t find any info on where this number was coming from. The modifiedProperty value where this JSON is located comes from the service in question, so the docs didn’t have anything either.\nTo figure out what’s happening I created a new policy and started fiddling with the values.\nFirst I selected just one:\n\nThis resulted in \"excludeGuestsOrExternalUsers\":{\"guestOrExternalUserTypes\":2, ...}\nThen I unticked that and selected the second option.\n\nWeirdly now I get the text.\n\nI made one more change:\n\nAnd this time it’s numbers. Huh?!\n\nThen I realized something… here’s what I see in the Audit Logs in the portal. Notice that for each addition/ update there’s three set of entries.\n\nI should either (obviously) focus on the “Add conditional access” and “Update conditional access” entries, or “Add policy” and “Update policy” entries. In my PowerShell code I was only looking at events generated by the “Conditional Access” service, so I was only seeing “Add conditional access” and “Update conditional access” entries, but here in the GUI I first looked at “Add conditional access” and next time looked at “Update policy” instead of “Update conditional access”.\nSilly me! (In fairness, the “Update conditional access policy” entries are generated last, and I was just looking for any entry with an “Update” in it…)\nLooking at the correct entry for the first change I made, I see numbers:\n\nCool. Time to put this down in a table:\n\n\n\n\n\nB2B collaboration guest users\n\n\n2\n\n\n\n\nB2B collaboration member users\n\n\n4\n\n\n\n\nB2B direct connect users\n\n\n8\n\n\n\n\nLocal guest users\n\n\n1\n\n\n\n\nOther external users\n\n\n16\n\n\n\n\nService provider users\n\n\n32\n\n\n\nSo that explains the 63. If all of the above are selected, then the sum of these numbers is 63. \nWrote a little PowerShell function to translate the number into the selections:function TranslateNumber-ToExternalGuest {\r\n [CmdletBinding()]\r\n param(\r\n [Parameter(Position=0,Mandatory=$true)]\r\n [int]$InputNumber\r\n )\r\n\r\n $externalUsersHash = @{\r\n 1 = \"Local guest users\" \r\n 2 = \"B2B collaboration guest users\"\r\n 4 = \"B2B collaboration member users\" \r\n 8 = \"B2B direct connect users\"\r\n 16 = \"Other external users\" \r\n 32 = \"Service provider users\"\r\n\r\n }\r\n\r\n if ($inputNumber -in $externalUsersHash.Keys) { \r\n $guestSelections = $externalUsersHash[$inputNumber]\r\n \r\n } else {\r\n $guestSelections = foreach ($number in 32,16,8,4,2,1) {\r\n if ($inputNumber -ge $number) { \r\n $externalUsersHash[$number]\r\n $inputNumber = $inputNumber - $number\r\n\r\n }\r\n }\r\n }\r\n\r\n $guestSelections\r\n} ", "date_published": "2025-11-12T19:29:58+00:00", "date_modified": "2025-11-13T06:56:58+00:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "auditlogs", "conditional access", "microsoft graph", "Azure, Azure AD, Graph, M365" ] }, { "id": "https://rakhesh.com/?p=8606", "url": "/azure/teams-phone-and-their-authentication-flows-part-4/", "title": "Teams Phone and their authentication flows (part 4)", "content_html": "

Just adding a link to this Microsoft blog post I previously linked to, and especially the video in it.

\n

\n

Wanted to capture two screenshots from there.

\n

This is how enrollment works for Android phones

\n

\"\"

\n

And this is how it works for Android AOSP

\n

\"\"

\n

This puts into pictures what I saw in the logs in the previous posts. \"\u263a\"

\n

The Company Portal app is no more. Instead, what it used to do (register in Entra and enroll in Intune) is now done by the Teams app (which is made up of the new Authenticator app and something called an AOSP app).

\n

Here is a comparison of the username password flow between Android and Android AOSP based on my testing past few days

\n\n\n\n\n\n\n\n\n\n\n\n
AndroidAndroid AOSP
\"\"\"\"
\n

Feel free to ignore the comments as I copy pasted from the previous posts, key thing is what apps and resources are in play.

\n", "content_text": "Just adding a link to this Microsoft blog post I previously linked to, and especially the video in it.\n\nWanted to capture two screenshots from there.\nThis is how enrollment works for Android phones\n\nAnd this is how it works for Android AOSP\n\nThis puts into pictures what I saw in the logs in the previous posts. \nThe Company Portal app is no more. Instead, what it used to do (register in Entra and enroll in Intune) is now done by the Teams app (which is made up of the new Authenticator app and something called an AOSP app).\nHere is a comparison of the username password flow between Android and Android AOSP based on my testing past few days\n\n\n\nAndroid\nAndroid AOSP\n\n\n\n\n\n\n\nFeel free to ignore the comments as I copy pasted from the previous posts, key thing is what apps and resources are in play.", "date_published": "2025-11-11T15:53:28+00:00", "date_modified": "2025-11-11T15:54:38+00:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "intune", "teams devices", "Azure, Azure AD, Graph, M365" ] }, { "id": "https://rakhesh.com/?p=8567", "url": "/azure/teams-phone-and-their-authentication-flows-part-3/", "title": "Teams Phone and their authentication flows (part 3)", "content_html": "

Continuing from my previous post (which is a continuation to another post), during my last enrolment the phone OS upgraded from Android to Android AOSP.

\n

This was not unexpected because Microsoft is in the process of moving Teams phones from Android to Android AOSP. Details can be found in this blog post and my phone model’s latest Android firmware is on the list of supported devices.

\n

This is a good opportunity though for me to capture the Entra ID sign-in logs for that process.

\n

Firmware 122.15.0.243 (Android AOSP)

\n\n

This firmware is from September 2025.

\n

Device Code flow when Android is upgraded to Android AOSP

\n

As part of the enrollment process first the device gets enrolled as Android Device Admin, then the firmware is upgraded to Android AOSP, and it is re-enrolled with the Android AOSP enrollment profile. Just mentioning this because you will see the device in Entra ID with the name <upn>_Android_<date> for a while and then it is renamed to <upn>_AndroidAOSP_<date>. On Intune side though, I think the Android device is un-enrolled and re-enrolled coz when I click on the older entry I get a device not found. That entry disappeared after a few mins so I couldn’t dig around much.

\n

Logs (oldest entries at the bottom – read from bottom to top):

\n

\"\"

\n

(contd.) (oldest entries at the bottom – read from bottom to top)

\n

\"\"

\n

There’s more like this from Microsoft Teams and Microsoft Teams Services.

\n

Adding the Device Name too to the output so we can see how that changes (oldest entries at the bottom – read from bottom to top):

\n

\"\"

\n

(contd.) (oldest entries at the bottom – read from bottom to top)

\n

\"\"

\n

Username password flow when Android is upgraded to Android AOSP

\n

I couldn’t test this scenario as there’s no way to downgrade the device.

\n

Username password flow

\n

This is when I reset the device and enroll afresh while the phone is on Android AOSP.

\n

The initial screen is as before, where I can either “Refresh code” or sign in to the device. I went with the latter. Entered my email address, password, did MFA, and then got the following:

\n

\"\"

\n

Clicking “Register” here kicks off the process.

\n

\"\"

\n

This takes ages (a minute or two) and finally I am in.

\n

\"\"

\n

\"\"

\n

Looking at the logs, here are the logs from yesterday with Android and username password flow (oldest entries at the bottom).

\n

\"\"

\n

Contrast this with Android AOSP (oldest entries at the bottom):

\n

\"\"

\n

Maybe something amiss with the KQL I am using, the logs don’t seem to be in order. But the key difference is, with Android AOSP: Microsoft Teams is doing most of the work, talking to Device Management Service etc. And when this interaction happens, it triggers the device registration etc.

\n

Reading from down, the first Microsoft Teams <=> Device Management Service with an error code 50097 is not a Conditional Access failure. Rather something else is throwing the error. The message is: Device Authentication Required - DeviceId -DeviceAltSecId claims are null OR no device corresponding to the device identifier exists.

\n

The next interaction with error code 50074 is from our Conditional Access policies requriing MFA.

\n

The one after that with error code 50076 is part of the MFA requirement. The error is: Due to a configuration change made by your administrator, or because you moved to a new location, you must use multi-factor authentication to access the resource.

\n

And then we get error code 50129 which is Device is not Workplace joined - Workplace join is required to register the device. The logs don’t say Conditional Access is triggering this, but I see our policies requiring Compliant Devices are failing – so I guess this is due to Conditional Access, but maybe also not. It could be like 50097 above.

\n

This repeats 3 more times, with the fourth attempt registering the device.

\n

Then Intune Company Portal picks in and does Intune Enrollment. At this point the device name is the default YealinkMP56.

\n

Anyways, key takeaway is Microsoft Teams is driving things.

\n

The logs continue, and I was surprised to see the Device Admin agent app again. (Oldest entries are at the bottom, read from bottom to top).

\n

\"\"

\n

Device Code flow

\n

Reset the phone, and tried with Device Code flow. This should be a quick win, right? Nope! It failed like this:

\n

\"\"

\n

\"\"

\n

The logs are short this time (read from bottom to top).

\n

\"\"

\n

This makes no sense. Things seem to have succeeded as per the logs. Intune enrollment did happen, and what finally failed is “Device Admin Agent” failing coz the device isn’t compliant. If I expand the entry, the device state is “Managed” – so it is Intune enrolled – but not yet Compliant. I double check in\u00a0 Intune too, and indeed the device is enrolled! What gives?!

\n

Maybe its just a matter of time? So I powered off and powered on the device like any good IT person, hoping the phone would just realize it is enrolled and contine from there. Of course it did not! After reboot the phone was back to the sign in page. Undeterred, I tried the device code method again (I didn’t reset the device this time) – and no surprises, this time it worked!

\n

The only consistent thing is that nothing’s consistent. Always expect surprises. Why did it work this time, who knows?! \"\ud83e\udd14\" \"\ud83d\ude21\"

\n

Did it create a new device in Intune/ Entra ID? Nope! I signed in and it took the previously enrolled device. So I was right – the enrollment did happen and it was just a case of the device not knowing it, and when I signed in the two got linked. To confirm that no enrollment happened, I took a look at the logs:

\n

\"\"

\n

Downgraded to Firmware 122.15.0.234 (Android AOSP)

\n

I was surprised but downgrading the firmware worked. Maybe coz I did to the version just one prior to the existing one, or maybe things are different with the AOSP firmware.

\n

Only tried the Device Code flow. Same as before, it didn’t work. The device is Intune enrolled, but the phone throws an error.

\n

Looking at the logs (read from bottom to up) things look a bit different because I don’t see any obvious failures like from Teams Device Admin. And there’s just one entry after the device is Intune enrolled, after which things just stop.

\n

\"\"

\n

My hunch is that this is something to do with the firmware and delays. But that’s just a hunch.

\n

Back to Firmware 122.15.0.243 (Android AOSP)

\n

What else can I try? I upgraded the firmware back to the latest AOSP one I was on (which is where I began this post) and tried again. No muy bueno, it still fails.

\n

However, I had another idea.

\n

Remember it fails on this screen:

\n

\"\"

\n

So I put my email address in and clicked “Sign in”. There was no password prompt, but I got the prompt to register my device (which I previously encountered in the username/ password flow).

\n

\"\"

\n

Clicked “Register” there, and it errored and put me back to where we began.

\n

\"\"

\n

Weirdly, now Intune doesn’t even show the device name registered properly.

\n

\"\"

\n

Entra too has the default name of YealinkMP56. So unlike the attempt before where the device enrolled in Intune, looks like that didn’t happen either.

\n

This is so so weird! \"\ud83d\ude00\" I seem to be in a loop like yesterday where things do random behaviour. Take a look at these logs – there’s no Intune enrollment happening! And all the failures at the end are because the device isn’t compliant coz it isn’t enrolled in Intune.

\n

\"\"

\n

Interestingly, I saw an alert email pop up about more Intune issues that got resolved, so maybe I am just unlucky like yesterday?

\n

\"\"

\n

This has nothing to do with Intune enrollment though.

\n

At this point I am stuck in the loop \"\ud83d\udd01\" so reset phone and let’s try again…

\n

And voila! It worked. Yeah. \"\ud83d\ude31\"

\n

At this point I don’t care, I just want this to end. \"\ud83d\ude00\" I am guessing the Intune issue did have some impact, but I don’t know and I don’t care. It was definitely coincidental that both yesterday and today when things failed there were health alerts; although yesterday’s was more obvious in that username password enrollment too failed, while today it was just Device Code.

\n

Maybe it was just a matter of waiting.

\n

Or the wind blowing in the right direction outside.

\n

Who knows… \"\ud83e\udd37\"

\n

Anyways, I can finally collect the logs from a successful Device Code flow enrollment. \"\ud83d\ude05\"

\n

But first, the logs from earlier when Device Code failed on this firmware (read the entries from bottom to top).

\n

\"\"

\n

I was surprised there that Teams Device Admin agent failed and that the name hadn’t changed. The device was enrolled in Intune, Conditional Access was seeing it as Managed, but it wasn’t in a Compliant state yet. The name hadn’t changed, but that too could be put down to a delay in things updating.

\n

I thought this Teams Device Admin failing is what broke things. But I was mistaken, as I see it happening now too but things proceed past it (read the entries from bottom to top).

\n

\"\"

\n

Even though Teams Device Admin agent failed, that didn’t stop things. When it failed the device is enrolled into Intune but wasn’t Compliant yet – that’s why it failed (same as before – then too the device was Managed but not Compliant yet).

\n

\"\"

\n

And yet in the very next entry, the device name has changed and is now Compliant. This is what was breaking earlier.

\n

\"\"

\n

In the logs there’s just a few milliseconds difference between these two entries. So something in the backend must have been broken earlier, because of which it didn’t proceed to the next steps.

\n

There’s a few more entries in the log, and we can see things proceeding and eventually succeeding.

\n

\"\"

\n

Also, just to call attention to it again, with Android AOSP it’s “Microsoft Teams” that does most things.

\n\n\n\n\n\n\n\n\n\n\n\n
Username passwordDevice Code
\"\"\"\"
\n

That’s it! I am outta here.

\n

\"\ud83d\udd17\" Link to part 4

\n

Update (the following day): Today there is a health alert for Intune AOSP devices.

\n

\"\"

\n

So there must have been some intermittent issue past few days which is now gotten worse/ noticeable. Just my luck!

\n", "content_text": "Continuing from my previous post (which is a continuation to another post), during my last enrolment the phone OS upgraded from Android to Android AOSP.\nThis was not unexpected because Microsoft is in the process of moving Teams phones from Android to Android AOSP. Details can be found in this blog post and my phone model’s latest Android firmware is on the list of supported devices.\nThis is a good opportunity though for me to capture the Entra ID sign-in logs for that process.\nFirmware 122.15.0.243 (Android AOSP)\n\nFirmware Version: 122.15.0.243\nHardware Version: 122.1.0.0.0.0.0\nMicrosoft Intune Version: 25.02.1\nAuthenticator Version: 6.2505.3166\n\nThis is new, is a part of Android AOSP.\n\n\nTeams Version: 1449/1.0.94.2025165302\nAdmin Agent Version: 1.0.0.202505080136.product\n\nThis firmware is from September 2025.\nDevice Code flow when Android is upgraded to Android AOSP\nAs part of the enrollment process first the device gets enrolled as Android Device Admin, then the firmware is upgraded to Android AOSP, and it is re-enrolled with the Android AOSP enrollment profile. Just mentioning this because you will see the device in Entra ID with the name <upn>_Android_<date> for a while and then it is renamed to <upn>_AndroidAOSP_<date>. On Intune side though, I think the Android device is un-enrolled and re-enrolled coz when I click on the older entry I get a device not found. That entry disappeared after a few mins so I couldn’t dig around much.\nLogs (oldest entries at the bottom – read from bottom to top):\n\n(contd.) (oldest entries at the bottom – read from bottom to top)\n\nThere’s more like this from Microsoft Teams and Microsoft Teams Services.\nAdding the Device Name too to the output so we can see how that changes (oldest entries at the bottom – read from bottom to top):\n\n(contd.) (oldest entries at the bottom – read from bottom to top)\n\nUsername password flow when Android is upgraded to Android AOSP\nI couldn’t test this scenario as there’s no way to downgrade the device.\nUsername password flow\nThis is when I reset the device and enroll afresh while the phone is on Android AOSP.\nThe initial screen is as before, where I can either “Refresh code” or sign in to the device. I went with the latter. Entered my email address, password, did MFA, and then got the following:\n\nClicking “Register” here kicks off the process.\n\nThis takes ages (a minute or two) and finally I am in.\n\n\nLooking at the logs, here are the logs from yesterday with Android and username password flow (oldest entries at the bottom).\n\nContrast this with Android AOSP (oldest entries at the bottom):\n\nMaybe something amiss with the KQL I am using, the logs don’t seem to be in order. But the key difference is, with Android AOSP: Microsoft Teams is doing most of the work, talking to Device Management Service etc. And when this interaction happens, it triggers the device registration etc.\nReading from down, the first Microsoft Teams <=> Device Management Service with an error code 50097 is not a Conditional Access failure. Rather something else is throwing the error. The message is: Device Authentication Required - DeviceId -DeviceAltSecId claims are null OR no device corresponding to the device identifier exists.\nThe next interaction with error code 50074 is from our Conditional Access policies requriing MFA.\nThe one after that with error code 50076 is part of the MFA requirement. The error is: Due to a configuration change made by your administrator, or because you moved to a new location, you must use multi-factor authentication to access the resource.\nAnd then we get error code 50129 which is Device is not Workplace joined - Workplace join is required to register the device. The logs don’t say Conditional Access is triggering this, but I see our policies requiring Compliant Devices are failing – so I guess this is due to Conditional Access, but maybe also not. It could be like 50097 above.\nThis repeats 3 more times, with the fourth attempt registering the device.\nThen Intune Company Portal picks in and does Intune Enrollment. At this point the device name is the default YealinkMP56.\nAnyways, key takeaway is Microsoft Teams is driving things.\nThe logs continue, and I was surprised to see the Device Admin agent app again. (Oldest entries are at the bottom, read from bottom to top).\n\nDevice Code flow\nReset the phone, and tried with Device Code flow. This should be a quick win, right? Nope! It failed like this:\n\n\nThe logs are short this time (read from bottom to top).\n\nThis makes no sense. Things seem to have succeeded as per the logs. Intune enrollment did happen, and what finally failed is “Device Admin Agent” failing coz the device isn’t compliant. If I expand the entry, the device state is “Managed” – so it is Intune enrolled – but not yet Compliant. I double check in\u00a0 Intune too, and indeed the device is enrolled! What gives?!\nMaybe its just a matter of time? So I powered off and powered on the device like any good IT person, hoping the phone would just realize it is enrolled and contine from there. Of course it did not! After reboot the phone was back to the sign in page. Undeterred, I tried the device code method again (I didn’t reset the device this time) – and no surprises, this time it worked!\nThe only consistent thing is that nothing’s consistent. Always expect surprises. Why did it work this time, who knows?! \nDid it create a new device in Intune/ Entra ID? Nope! I signed in and it took the previously enrolled device. So I was right – the enrollment did happen and it was just a case of the device not knowing it, and when I signed in the two got linked. To confirm that no enrollment happened, I took a look at the logs:\n\nDowngraded to Firmware 122.15.0.234 (Android AOSP)\nI was surprised but downgrading the firmware worked. Maybe coz I did to the version just one prior to the existing one, or maybe things are different with the AOSP firmware.\nOnly tried the Device Code flow. Same as before, it didn’t work. The device is Intune enrolled, but the phone throws an error.\nLooking at the logs (read from bottom to up) things look a bit different because I don’t see any obvious failures like from Teams Device Admin. And there’s just one entry after the device is Intune enrolled, after which things just stop.\n\nMy hunch is that this is something to do with the firmware and delays. But that’s just a hunch.\nBack to Firmware 122.15.0.243 (Android AOSP)\nWhat else can I try? I upgraded the firmware back to the latest AOSP one I was on (which is where I began this post) and tried again. No muy bueno, it still fails.\nHowever, I had another idea.\nRemember it fails on this screen:\n\nSo I put my email address in and clicked “Sign in”. There was no password prompt, but I got the prompt to register my device (which I previously encountered in the username/ password flow).\n\nClicked “Register” there, and it errored and put me back to where we began.\n\nWeirdly, now Intune doesn’t even show the device name registered properly.\n\nEntra too has the default name of YealinkMP56. So unlike the attempt before where the device enrolled in Intune, looks like that didn’t happen either.\nThis is so so weird! I seem to be in a loop like yesterday where things do random behaviour. Take a look at these logs – there’s no Intune enrollment happening! And all the failures at the end are because the device isn’t compliant coz it isn’t enrolled in Intune.\n\nInterestingly, I saw an alert email pop up about more Intune issues that got resolved, so maybe I am just unlucky like yesterday?\n\nThis has nothing to do with Intune enrollment though.\nAt this point I am stuck in the loop so reset phone and let’s try again…\nAnd voila! It worked. Yeah. \nAt this point I don’t care, I just want this to end. I am guessing the Intune issue did have some impact, but I don’t know and I don’t care. It was definitely coincidental that both yesterday and today when things failed there were health alerts; although yesterday’s was more obvious in that username password enrollment too failed, while today it was just Device Code.\nMaybe it was just a matter of waiting.\nOr the wind blowing in the right direction outside.\nWho knows… \nAnyways, I can finally collect the logs from a successful Device Code flow enrollment. \nBut first, the logs from earlier when Device Code failed on this firmware (read the entries from bottom to top).\n\nI was surprised there that Teams Device Admin agent failed and that the name hadn’t changed. The device was enrolled in Intune, Conditional Access was seeing it as Managed, but it wasn’t in a Compliant state yet. The name hadn’t changed, but that too could be put down to a delay in things updating.\nI thought this Teams Device Admin failing is what broke things. But I was mistaken, as I see it happening now too but things proceed past it (read the entries from bottom to top).\n\nEven though Teams Device Admin agent failed, that didn’t stop things. When it failed the device is enrolled into Intune but wasn’t Compliant yet – that’s why it failed (same as before – then too the device was Managed but not Compliant yet).\n\nAnd yet in the very next entry, the device name has changed and is now Compliant. This is what was breaking earlier.\n\nIn the logs there’s just a few milliseconds difference between these two entries. So something in the backend must have been broken earlier, because of which it didn’t proceed to the next steps.\nThere’s a few more entries in the log, and we can see things proceeding and eventually succeeding.\n\nAlso, just to call attention to it again, with Android AOSP it’s “Microsoft Teams” that does most things.\n\n\n\nUsername password\nDevice Code\n\n\n\n\n\n\n\nThat’s it! I am outta here.\n Link to part 4\nUpdate (the following day): Today there is a health alert for Intune AOSP devices.\n\nSo there must have been some intermittent issue past few days which is now gotten worse/ noticeable. Just my luck!", "date_published": "2025-11-11T15:18:26+00:00", "date_modified": "2025-11-12T15:22:14+00:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "conditional access", "device registration", "intune", "teams devices", "Azure, Azure AD, Graph, M365" ] }, { "id": "https://rakhesh.com/?p=8481", "url": "/azure/teams-phone-and-their-authentication-flows-part-2/", "title": "Teams Phone and their authentication flows (part 2)", "content_html": "

Continuing from my previous post, I upgraded the Yealink MP56 phone to the next available firmware.

\n

Firmware 122.15.0.107

\n

This firmware is from December 2022. Here is what the admin page shows post-upgrade:

\n
\n
\n\n
\n
\n

Things are a bit different now when powering on the device. You get a page like this:

\n

\"\"

\n

Device Code flow

\n

Click Refresh code to get a code. Then on my computer I went to the Device Login page, entered the code, did MFA, it asked me for a password followed by MFA again, then a prompt like this:

\n

\"\"

\n

Clicked “Continue” and it signed me in.

\n

\"\"

\n

Interestingly, the phone failed though.

\n

\"\"

\n

Couldn't connect to Workfplace Join. Try again, or contact you admin.

\n

Looking at the logs, I can see tha things are different this time (oldest entries at the bottom – read from bottom to top).

\n

\"\"

\n

For one, we start with the Authentication Broker app this time, rather than Company Portal. So the page showing me a code is not the Company Portal app any more. This explains why Device Code worked, and why the message in my browser confirming the app I was signing into also showed the Authentication Broker.

\n

The logs show a failure for this due to requiring MFA, and there’s a bunch of these which is probably why I got prompted for MFA twice.

\n

The first failure message in the screenshot was “Credentials have been revoked due to the following reasons: SSO Artifact is invalid or expired, Session not fresh enough for application, A silent sign-in request was sent but the user's session with Azure AD is invalid or has expired.“, followed by “For security reasons, user confirmation is required for this request. Please repeat the request allowing user interaction.

\n

Neither of these messages are due to Conditional Access, so I am not sure what is going on. And confusingly, after these there was a final success message. So why did it fail?

\n

\"\"

\n

I decided to try again in case it was a temporary error.

\n

Clicked “Refresh code” on the phone, got a new code, and entered it in the browser. Prompted for password again, MFA, was shown the confirmation message asking if I am signing into Authentication Broker, and again while things succeeded in the browser it failed on the phone. Oh well.

\n

Again the logs give a “For security reasons, user confirmation is required for this request. Please repeat the request allowing user interaction.” message and eventually succeeds.

\n

\"\"

\n

One reason for this error is unsupported Conditional Access policies. And sure enough, looking at the supported Conditional Access policies for Teams phones it specifically says Device Code flow should not be blocked.

\n

\"\"

\n

I still don’t see how it matters though, coz Conditional Access is not blocking anything in my case (as per the logs at least). Ideally, I should exempt myself altogether from the Device Code blocked flow and test, but I don’t want to. Plus, I know this issue gets fixed later (teaser for my later blog post! \"\ud83d\ude0a\") so time to move on and try the password flow.

\n

Before I move on, for future reference here is a screenshot of the failure I got.

\n

\"\"

\n

Everything looks alright, except for the error. And in the “success” entry just after this, it looks like the device hasn’t actually registered… so something failed.

\n

\"\"

\n

Username password flow

\n

This time I clicked “Sign in to this device” on the phone instead of doing a “Refresh code”.

\n

That gives the familiar sign in page from the Company Portal. I enter my email address, password, MFA, and we are off to the races!

\n

\"\"

\n

After signing in:

\n

\"\"

\n

\"\"

\n

\"\"

\n

Again that error from before. I clicked “Try again”. This time it worked!

\n

\"\"

\n

\"\"

\n

\"\"

\n

\"\"

\n

Yay! And finally, a glimpse of the Company Portal apps screen for some reason.

\n

\"\"

\n

Followed by some more screens from Teams. On the screen where there’s a “Sign in” button it signs in automatically.

\n

\"\"

\n

\"\"

\n

\"\"

\n

\"\"

\n

\"\"

\n

And we are in! After this I am shown the Teams phone UI.

\n

Looking at the logs (oldest at the bottom – read from bottom to top):

\n

\"\"

\n

Just after the MFA prompt, when Intune Company portal is talking to Windows Azure AD (the first of the green shaded section), the device has successfully registered. (See the screenshot just after the one below where I can see this).

\n

(contd.) (oldest entries at the bottom – read from bottom to top)

\n

\"\"

\n

You can see Teams Device Admin also getting involved with Device Registration.

\n

The only difference in the above output vs the Device Code one which failed is that after the Authentication Broker talking to Device Registration Service, we have Intune Company Portal talking to Windows Azure AD. I wonder if something is failing there. Here is what that entry looks like:

\n

\"\"

\n

Firmware 122.15.0.166

\n

This firmware is from March 2025. It’s the last of the Android firmware for this phone. I don’t expect much changes, but I upgraded to it to have the latest results just in case.

\n

Details:

\n
\n
\n\n
\n
\n

On the phone, the sign in page is same as before.

\n

Device Code flow

\n

Magic! This worked. \"\ud83e\ude84\"

\n

Not adding any screenshots as it’s exactly as what I put above for the username password flow with the previous firmware. Only difference is that the Company Portal picked up the firm logo and theme instead of the default blue.

\n

The logs look better too (of course) (oldest entries at the bottom – read from bottom to top):

\n

\"\"

\n

Key takeaway #1: firmware matters. Nothing changed on the Conditional Access policies or user account between the two firmware versions, but in the older one Device Code flow failed for no apparent reason, but in the newer one it worked. I think this point bears emphasising, because Conditional Access is usually blamed for a lot of things, but it isn’t always the culprit.

\n

Key takeaway #2: you can block Device Code flow in general but only need allow it for the Device Registration action and exclude Teams Phone devices, and Teams Phones enrollment will work. The Microsoft docs make it sounds like you need to allow it carte blanche for the user, but you don’t really need that.

\n

\"\ud83d\udc49\" A quick sidebar on blocking Device Code flow for all, except Teams Phones

\n

The Device Registration action is under Target resources > User actions > Register or join devices. But you can’t exempt it coz the only option available is to include it.

\n

\"\"

\n

Instead, apply the policy to “All Cloud Apps” but exclude “Device Registration Service”.

\n

\"\"

\n

I am not sure what the difference is between the action and the app. I’d imagine the action is just a convenience thing and that it actually calls upon the underlying app, so that’s why excluding the app does the trick.

\n

However, excluding only Device Registration Service from the Device Code block policy doesn’t help when it comes to Intune enrollment or using Teams etc. That’s because all these continue to be a part of the Device Code flow. Here’s a repeat of the screenshot I put above. Notice the transfer method is still deviceCodeFlow when it comes to Intune Enrollment, Teams Service, etc.

\n

\"\"

\n

So we need to exclude Device Code flow for these too. The neat thing is since these steps are after the device has registered, we have more details of the device available to play with. Thus, we can exclude for instance devices with name “Yealink MP56” from the Device Code block policy, and so Device Code flow will continue working after the device has registered and when it tries to enroll in Intune etc.

\n

Note: I am not saying that this is what one should do, all I am saying is that if one devices to block Device Code flow (is a good thing to do), then it must be exempted either for Teams Phone based on the users or some other criteria. Excluding for Teams Phone users doesn’t make sense because then these users can use Device Code flow for anything; so a better idea is to be more fine grained. Excluding based on the device display name or manufacturer is a compromise, and it will need some work as new models and manufacturers are introduced.

\n

Username password flow

\n

Interestingly, after successfully enrolling as above, I signed out of the device and also deleted it from Entra ID. Then I tried the username password flow, and that failed immediately after prompting me for MFA.

\n

\"\"

\n

“Couldn’t connect to Workplace Join. Try again, or contact your admin”. Weird.

\n

So I reset the device to factory settings and tried again (resetting to factory settings does not downgrade the firmware btw).

\n

Surprise, now it worked! \"\ud83d\ude43\"

\n

\"\"

\n

It’s very different to the Device Code flow. I wasn’t expecting that to be honest.

\n

(contd.) (oldest entries at the bottom – read from bottom to top)

\n

\"\"

\n

As you can see there’s lot more stuff happening!

\n

\"\ud83d\udd17\" Link to part 3

\n

\"\ud83d\udc47\" What follows below is some additional issues I encountered. They got resolved by themselves in the end, and all I can attribute them to is some Intune outage on Microsoft’s side. I took the effort of writing notes and putting screenshots, and didn’t want to delete them all so here they are. Feel free to skip.

\n
\n

Frustrations

\n

I reset the device one more time and did a Device Code flow authentication (just for kicks!) and now I got the “Couldn't connect to Workplace Join. Try again, or contact your admin” error I previously received with username and password. Weird.

\n

A second reset didn’t make a difference either. Interestingly, the logs show similar messages as what I received with the very first/ default firmware when Device Code flow was failing.

\n

\"\"

\n

So I rebooted the device and tried, and this time it went further but failed with a different error.

\n

\"\"

\n

\"\"

\n

This time even though I was getting the same “For security reasons, user confirmation is required...” errors as before, things were proceeding further (notice the SUCCESS from Intune Enrollment).

\n

\"\"

\n

So I took a look at Intune and sure enough things reached Intune and failed. In fact, interestingly, I had only two enrollment failures under my account – one from now, and one from earlier in the day when I tried with the original firmware.

\n

\"\"

\n

Vague error though.

\n

\"\"

\n

I double checked the phone firmware with what I had noted down earlier – no change. And yet something was failing.

\n

The weird thing, moreover, was that each time I reset the device and try I was getting a different error. On my 3rd attempt it seemed to again proceed as usual but then dropped me at this sign in page, and then when I tried to sign in via Device Code nothing happened.

\n

\"\"

\n

\"\"

\n

Aaargh!

\n

On my 4th attempt, things were at least consistent in that I got the same error as above. So I decided to dig deeper into the sign in logs.

\n

Interesting thing is even during this attempt the device is getting registered in Entra and then failing. And… it is failing due to our Conditional Access policies.

\n

\"\"

\n

Weird (that word again!) thing is this error makes sense. At this point the device has only registered in Entra ID and it isn’t Intune enrolled, and so when Teams is trying to access the Teams Services resource it naturally fails because we have Conditional Access policies in place that require the device to be compliant. If the device had enrolled in Intune after device registration, this error wouldn’t have occured.

\n

Contrast this to the logs I captured earlier when Device Code flow worked:

\n

\"\"

\n

Here the device does Intune enrollment, as part of which it does Device Registration, and thus things work. But now the phone wasn’t doing Intune enrollment at all. I have no idea why!

\n

Interestingly, in one of the previous failed attempts it was doing an Intune enrollment. This is all so confusing!

\n

(Btw, after I took the above screenshot where no Intune enrollment was happening, I saw more logs come through. Here too no Intune enrollment is happening, but the Teams Device Admin agent is trying to do something. I wonder if the phone got corrupt somehow).

\n

\"\"

\n

What else?\u00a0

\n

So far I had reset the device to factory settings multiple times. I had also tried resetting user data (that was another option in the admin UI) but it didn’t help either. One thing I wanted to try next was reinstall the firmware. The first time I upgraded to this version of the firmware it never prompted me to change the admin password of the device, but ever since then whenever I’d reset the firmware it prompts me to change the password. Not a big deal in itself, but I felt this wasn’t putting the device back to the original state at which it was working. It never asked me to change the password the first time I moved to this firmware, so why was it always asking now when doing a reset. Clearly that wasn’t a “reset” enough? (wishful thinking…)

\n

For this phone at least, trying to upload the firmware again didn’t work as the phone wouldn’t accept it. Neither did trying to downgrade to a previous version. I had nothing else left to do. \"\u2639\"

\n

Out of ideas, I thought let me try what happens with the username password flow, coz clearly that should work? But nope, that too fails with the same error! Madness!! \"\ud83d\ude24\"

\n

At this point I gave up for the day. Something’s broken somewhere, and I’ve had enough for today!

\n

\"\"

\n

Guess what, it works now! Wasted about 3 hours yesterday evening/ night troubleshooting this. At least I am wiser in the art of troubleshooting I suppose? \"\ud83e\udde0\"

\n

Interestingly, as part of the enrollment the firmware too got updated. We are now at:

\n
\n
\n\n
\n
\n

The presence of Authenticator, plus the fact that I was on the latest Android firmware available from Yealink got me thinking maybe the device is now on Android AOSP, and sure enough it is. Nice!

\n

The closest I can find from Microsoft as to why enrollment failed yesterday is some alerts about Intune Datawarehouse. This might have been a side effect of that?

\n

\"\"

\n

This issue started 3 days ago though, but the fix was rolled out around the time I was testing so maybe that’s how they are related? I dunno.

\n

Update (a few hours later): When I logged in to work later, there were reports from others that Teams phone enrolment were failing yesterday. So it looks like there was indeed some issue.

\n
\n

\"\ud83d\udd17\" Link to part 3

\n", "content_text": "Continuing from my previous post, I upgraded the Yealink MP56 phone to the next available firmware.\nFirmware 122.15.0.107\nThis firmware is from December 2022. Here is what the admin page shows post-upgrade:\n\n\n\nFirmware Version: 122.15.0.107\n\nHardware Version: 122.1.0.0.0.0.0\n\n\nCompany Portal Version: 5.0.5484.0\n\n\nTeams Version: 1449/1.0.94.2022110803\n\n\nAdmin Agent Version: 1.0.0.202209060820.product\n\nThis is new, wasn’t present originally.\n\n\n\n\n\nThings are a bit different now when powering on the device. You get a page like this:\n\nDevice Code flow\nClick Refresh code to get a code. Then on my computer I went to the Device Login page, entered the code, did MFA, it asked me for a password followed by MFA again, then a prompt like this:\n\nClicked “Continue” and it signed me in.\n\nInterestingly, the phone failed though.\n\n“Couldn't connect to Workfplace Join. Try again, or contact you admin.”\nLooking at the logs, I can see tha things are different this time (oldest entries at the bottom – read from bottom to top).\n\nFor one, we start with the Authentication Broker app this time, rather than Company Portal. So the page showing me a code is not the Company Portal app any more. This explains why Device Code worked, and why the message in my browser confirming the app I was signing into also showed the Authentication Broker.\nThe logs show a failure for this due to requiring MFA, and there’s a bunch of these which is probably why I got prompted for MFA twice.\nThe first failure message in the screenshot was “Credentials have been revoked due to the following reasons: SSO Artifact is invalid or expired, Session not fresh enough for application, A silent sign-in request was sent but the user's session with Azure AD is invalid or has expired.“, followed by “For security reasons, user confirmation is required for this request. Please repeat the request allowing user interaction.”\nNeither of these messages are due to Conditional Access, so I am not sure what is going on. And confusingly, after these there was a final success message. So why did it fail?\n\nI decided to try again in case it was a temporary error.\nClicked “Refresh code” on the phone, got a new code, and entered it in the browser. Prompted for password again, MFA, was shown the confirmation message asking if I am signing into Authentication Broker, and again while things succeeded in the browser it failed on the phone. Oh well.\nAgain the logs give a “For security reasons, user confirmation is required for this request. Please repeat the request allowing user interaction.” message and eventually succeeds.\n\nOne reason for this error is unsupported Conditional Access policies. And sure enough, looking at the supported Conditional Access policies for Teams phones it specifically says Device Code flow should not be blocked.\n\nI still don’t see how it matters though, coz Conditional Access is not blocking anything in my case (as per the logs at least). Ideally, I should exempt myself altogether from the Device Code blocked flow and test, but I don’t want to. Plus, I know this issue gets fixed later (teaser for my later blog post! ) so time to move on and try the password flow.\nBefore I move on, for future reference here is a screenshot of the failure I got.\n\nEverything looks alright, except for the error. And in the “success” entry just after this, it looks like the device hasn’t actually registered… so something failed.\n\nUsername password flow\nThis time I clicked “Sign in to this device” on the phone instead of doing a “Refresh code”.\nThat gives the familiar sign in page from the Company Portal. I enter my email address, password, MFA, and we are off to the races!\n\nAfter signing in:\n\n\n\nAgain that error from before. I clicked “Try again”. This time it worked!\n\n\n\n\nYay! And finally, a glimpse of the Company Portal apps screen for some reason.\n\nFollowed by some more screens from Teams. On the screen where there’s a “Sign in” button it signs in automatically.\n\n\n\n\n\nAnd we are in! After this I am shown the Teams phone UI.\nLooking at the logs (oldest at the bottom – read from bottom to top):\n\nJust after the MFA prompt, when Intune Company portal is talking to Windows Azure AD (the first of the green shaded section), the device has successfully registered. (See the screenshot just after the one below where I can see this).\n(contd.) (oldest entries at the bottom – read from bottom to top)\n\nYou can see Teams Device Admin also getting involved with Device Registration.\nThe only difference in the above output vs the Device Code one which failed is that after the Authentication Broker talking to Device Registration Service, we have Intune Company Portal talking to Windows Azure AD. I wonder if something is failing there. Here is what that entry looks like:\n\nFirmware 122.15.0.166\nThis firmware is from March 2025. It’s the last of the Android firmware for this phone. I don’t expect much changes, but I upgraded to it to have the latest results just in case.\nDetails:\n\n\n\nFirmware Version: 122.15.0.166\nHardware Version: 122.1.0.0.0.0.0\nCompany Portal Version: 5.0.6152.0\nTeams Version: 1449/1.0.94.2024101709\nAdmin Agent Version: 1.0.0.202407050618.product\n\n\n\nOn the phone, the sign in page is same as before.\nDevice Code flow\nMagic! This worked. \nNot adding any screenshots as it’s exactly as what I put above for the username password flow with the previous firmware. Only difference is that the Company Portal picked up the firm logo and theme instead of the default blue.\nThe logs look better too (of course) (oldest entries at the bottom – read from bottom to top):\n\nKey takeaway #1: firmware matters. Nothing changed on the Conditional Access policies or user account between the two firmware versions, but in the older one Device Code flow failed for no apparent reason, but in the newer one it worked. I think this point bears emphasising, because Conditional Access is usually blamed for a lot of things, but it isn’t always the culprit.\nKey takeaway #2: you can block Device Code flow in general but only need allow it for the Device Registration action and exclude Teams Phone devices, and Teams Phones enrollment will work. The Microsoft docs make it sounds like you need to allow it carte blanche for the user, but you don’t really need that.\n A quick sidebar on blocking Device Code flow for all, except Teams Phones\nThe Device Registration action is under Target resources > User actions > Register or join devices. But you can’t exempt it coz the only option available is to include it.\n\nInstead, apply the policy to “All Cloud Apps” but exclude “Device Registration Service”.\n\nI am not sure what the difference is between the action and the app. I’d imagine the action is just a convenience thing and that it actually calls upon the underlying app, so that’s why excluding the app does the trick.\nHowever, excluding only Device Registration Service from the Device Code block policy doesn’t help when it comes to Intune enrollment or using Teams etc. That’s because all these continue to be a part of the Device Code flow. Here’s a repeat of the screenshot I put above. Notice the transfer method is still deviceCodeFlow when it comes to Intune Enrollment, Teams Service, etc.\n\nSo we need to exclude Device Code flow for these too. The neat thing is since these steps are after the device has registered, we have more details of the device available to play with. Thus, we can exclude for instance devices with name “Yealink MP56” from the Device Code block policy, and so Device Code flow will continue working after the device has registered and when it tries to enroll in Intune etc.\nNote: I am not saying that this is what one should do, all I am saying is that if one devices to block Device Code flow (is a good thing to do), then it must be exempted either for Teams Phone based on the users or some other criteria. Excluding for Teams Phone users doesn’t make sense because then these users can use Device Code flow for anything; so a better idea is to be more fine grained. Excluding based on the device display name or manufacturer is a compromise, and it will need some work as new models and manufacturers are introduced.\nUsername password flow\nInterestingly, after successfully enrolling as above, I signed out of the device and also deleted it from Entra ID. Then I tried the username password flow, and that failed immediately after prompting me for MFA.\n\n“Couldn’t connect to Workplace Join. Try again, or contact your admin”. Weird.\nSo I reset the device to factory settings and tried again (resetting to factory settings does not downgrade the firmware btw).\nSurprise, now it worked! \n\nIt’s very different to the Device Code flow. I wasn’t expecting that to be honest.\n(contd.) (oldest entries at the bottom – read from bottom to top)\n\nAs you can see there’s lot more stuff happening!\n Link to part 3\n What follows below is some additional issues I encountered. They got resolved by themselves in the end, and all I can attribute them to is some Intune outage on Microsoft’s side. I took the effort of writing notes and putting screenshots, and didn’t want to delete them all so here they are. Feel free to skip.\n\nFrustrations\nI reset the device one more time and did a Device Code flow authentication (just for kicks!) and now I got the “Couldn't connect to Workplace Join. Try again, or contact your admin” error I previously received with username and password. Weird.\nA second reset didn’t make a difference either. Interestingly, the logs show similar messages as what I received with the very first/ default firmware when Device Code flow was failing.\n\nSo I rebooted the device and tried, and this time it went further but failed with a different error.\n\n\nThis time even though I was getting the same “For security reasons, user confirmation is required...” errors as before, things were proceeding further (notice the SUCCESS from Intune Enrollment).\n\nSo I took a look at Intune and sure enough things reached Intune and failed. In fact, interestingly, I had only two enrollment failures under my account – one from now, and one from earlier in the day when I tried with the original firmware.\n\nVague error though.\n\nI double checked the phone firmware with what I had noted down earlier – no change. And yet something was failing.\nThe weird thing, moreover, was that each time I reset the device and try I was getting a different error. On my 3rd attempt it seemed to again proceed as usual but then dropped me at this sign in page, and then when I tried to sign in via Device Code nothing happened.\n\n\nAaargh!\nOn my 4th attempt, things were at least consistent in that I got the same error as above. So I decided to dig deeper into the sign in logs.\nInteresting thing is even during this attempt the device is getting registered in Entra and then failing. And… it is failing due to our Conditional Access policies.\n\nWeird (that word again!) thing is this error makes sense. At this point the device has only registered in Entra ID and it isn’t Intune enrolled, and so when Teams is trying to access the Teams Services resource it naturally fails because we have Conditional Access policies in place that require the device to be compliant. If the device had enrolled in Intune after device registration, this error wouldn’t have occured.\nContrast this to the logs I captured earlier when Device Code flow worked:\n\nHere the device does Intune enrollment, as part of which it does Device Registration, and thus things work. But now the phone wasn’t doing Intune enrollment at all. I have no idea why!\nInterestingly, in one of the previous failed attempts it was doing an Intune enrollment. This is all so confusing!\n(Btw, after I took the above screenshot where no Intune enrollment was happening, I saw more logs come through. Here too no Intune enrollment is happening, but the Teams Device Admin agent is trying to do something. I wonder if the phone got corrupt somehow).\n\nWhat else?\u00a0\nSo far I had reset the device to factory settings multiple times. I had also tried resetting user data (that was another option in the admin UI) but it didn’t help either. One thing I wanted to try next was reinstall the firmware. The first time I upgraded to this version of the firmware it never prompted me to change the admin password of the device, but ever since then whenever I’d reset the firmware it prompts me to change the password. Not a big deal in itself, but I felt this wasn’t putting the device back to the original state at which it was working. It never asked me to change the password the first time I moved to this firmware, so why was it always asking now when doing a reset. Clearly that wasn’t a “reset” enough? (wishful thinking…)\nFor this phone at least, trying to upload the firmware again didn’t work as the phone wouldn’t accept it. Neither did trying to downgrade to a previous version. I had nothing else left to do. \nOut of ideas, I thought let me try what happens with the username password flow, coz clearly that should work? But nope, that too fails with the same error! Madness!! \nAt this point I gave up for the day. Something’s broken somewhere, and I’ve had enough for today!\n\nGuess what, it works now! Wasted about 3 hours yesterday evening/ night troubleshooting this. At least I am wiser in the art of troubleshooting I suppose? \nInterestingly, as part of the enrollment the firmware too got updated. We are now at:\n\n\n\nFirmware Version: 122.15.0.243\nHardware Version: 122.1.0.0.0.0.0\nMicrosoft Intune Version: 25.02.1\nAuthenticator Version: 6.2505.3166\n\nThis is new, wasn’t present originally!\n\n\nTeams Version: 1449/1.0.94.2025165302\nAdmin Agent Version: 1.0.0.202505080136.product\n\n\n\nThe presence of Authenticator, plus the fact that I was on the latest Android firmware available from Yealink got me thinking maybe the device is now on Android AOSP, and sure enough it is. Nice!\nThe closest I can find from Microsoft as to why enrollment failed yesterday is some alerts about Intune Datawarehouse. This might have been a side effect of that?\n\nThis issue started 3 days ago though, but the fix was rolled out around the time I was testing so maybe that’s how they are related? I dunno.\nUpdate (a few hours later): When I logged in to work later, there were reports from others that Teams phone enrolment were failing yesterday. So it looks like there was indeed some issue.\n\n Link to part 3", "date_published": "2025-11-11T07:58:53+00:00", "date_modified": "2025-11-11T15:45:53+00:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "conditional access", "device registration", "intune", "teams devices", "Azure, Azure AD, Graph, M365" ] }, { "id": "https://rakhesh.com/?p=8463", "url": "/azure/teams-phone-and-their-authentication-flows-part-1/", "title": "Teams Phone and their authentication flows (part 1)", "content_html": "

I wanted to document the authentication flow for Teams phones. This is by no means comprehensive, but only what I have been able to gather by looking at a Yealink MP56 phone. The device itself is discontinued, but firmware updates are available so it works with Teams. The device I got happened to be on a really old version of the firmware, so I was able to try it across various versions.

\n

Firmware 122.15.0.33

\n

This is what the device had out of the box:

\n\n

Powering on the device greets you with this screen:

\n

\"\"\"\"

\n

You can click the Gear icon on the top right to get into settings to view the IP address and other details. That’s how I then connected to the phone’s admin portal to take a look at the firmware etc.

\n

Clicking “Sign in” opens up the (Intune) Company Portal app.

\n

\"\"

\n

I can either enter the email address here and click “Next”, or click “Sign-in options” to do a Device Code flow.

\n

Device Code flow

\n

\"\"

\n

I am not expecting this to work coz we have blocked device code flow firmwide. As a test though, I have an exception for myself for when registering a device, and I want to see what happens.

\n

Clicking on “Sign in from another device” gives me a code. I can enter that in a browser, and as expected it failed.

\n

\"\"

\n

As expected it fails:\"\"

\n

 

\n

And I can see it in the logs too:

\n

\"\"

\n

Some things to note:

\n\n

Username password flow

\n

Now let’s try the username password flow. I enter my email address, followed by password, followed by an MFA prompt (may or may not happen to anyone else trying this as it depends on your conditional access policies).

\n

Things went weird after this, and I think that’s down to the very old firmware on the device. Nevertheless, here are some screenshots.

\n

First, the company portal tries to sign in.

\n

\"\"

\n

Then I got this error from Teams, which I clicked “OK” on and tried to sign in again at which point it again launched the Company Portal and tried to sign me in (didn’t ask for credentials this time coz I am already authenticated I suppose). That too gave me some errors.

\n

\"\"

\n

\"\"

\n

\"\"

\n

\"\"

\n

If I click “Try again” here, it went through the Company Portal steps again but with more success than last time. It got up to the stage where the device was setup, but then balked and took me back to a different sort of Company Portal sign in page.

\n

\"\"

\n

\"\"

\n

Clicking “Sign in” here again tried to sign me in via the Company Portal (automatically, I wasn’t prompted for creds) and then it dropped me at the regular sign in page.

\n

\"\"

\n

Again, I entered my email address and password and went through the drill… and again it looked like it was trying to register my device, but finally failed and dropped me back at this sign in page. At this point I gave up. I suspected the older firmware might not be supported any more.

\n

Here’s what I see in the interactive and non-interactive sign-in logs (oldest entries at the bottom – read from bottom to top).

\n

\"\"

\n

This coincides with what I saw on the phone itself. Essentially:

\n\n

I figured I should update the firmware and try again. Rather than continue in this post though, I am going to start a new one. I realize this one has too many photos so I don’t want to load it all in a single post.

\n

\"\ud83d\udd17\" Link to part 2

\n", "content_text": "I wanted to document the authentication flow for Teams phones. This is by no means comprehensive, but only what I have been able to gather by looking at a Yealink MP56 phone. The device itself is discontinued, but firmware updates are available so it works with Teams. The device I got happened to be on a really old version of the firmware, so I was able to try it across various versions.\nFirmware 122.15.0.33\nThis is what the device had out of the box:\n\nFirmware Version: 122.15.0.33\n\nHardware Version: 122.1.0.0.0.0.0\n\n\nCompany Portal Version: 5.0.4927.0\n\n\nTeams Version: 1449/1.0.94.2020111101\n\n\nPowering on the device greets you with this screen:\n\nYou can click the Gear icon on the top right to get into settings to view the IP address and other details. That’s how I then connected to the phone’s admin portal to take a look at the firmware etc.\nClicking “Sign in” opens up the (Intune) Company Portal app.\n\nI can either enter the email address here and click “Next”, or click “Sign-in options” to do a Device Code flow.\nDevice Code flow\n\nI am not expecting this to work coz we have blocked device code flow firmwide. As a test though, I have an exception for myself for when registering a device, and I want to see what happens.\nClicking on “Sign in from another device” gives me a code. I can enter that in a browser, and as expected it failed.\n\nAs expected it fails:\n \nAnd I can see it in the logs too:\n\nSome things to note:\n\nThe app in question is the Intune Company Portal – expected.\nIt is trying to access Windows Azure AD\nNotice the Device Details are a mix of the computer I am doing device login from (the device Id is of my computer, the browser I am using is indeed Firefox, the machine itself is Hybrid joined, but the OS is picked up as Android).\n\nThere’s no particular significance to this, but it’s worth pointing out because the OS field/ Device Details may not always be accurate. I would have either excepted this to be “Windows”, or the whole Device Details property to be about the Teams phone itself.\n\n\n\nUsername password flow\nNow let’s try the username password flow. I enter my email address, followed by password, followed by an MFA prompt (may or may not happen to anyone else trying this as it depends on your conditional access policies).\nThings went weird after this, and I think that’s down to the very old firmware on the device. Nevertheless, here are some screenshots.\nFirst, the company portal tries to sign in.\n\nThen I got this error from Teams, which I clicked “OK” on and tried to sign in again at which point it again launched the Company Portal and tried to sign me in (didn’t ask for credentials this time coz I am already authenticated I suppose). That too gave me some errors.\n\n\n\n\nIf I click “Try again” here, it went through the Company Portal steps again but with more success than last time. It got up to the stage where the device was setup, but then balked and took me back to a different sort of Company Portal sign in page.\n\n\nClicking “Sign in” here again tried to sign me in via the Company Portal (automatically, I wasn’t prompted for creds) and then it dropped me at the regular sign in page.\n\nAgain, I entered my email address and password and went through the drill… and again it looked like it was trying to register my device, but finally failed and dropped me back at this sign in page. At this point I gave up. I suspected the older firmware might not be supported any more.\nHere’s what I see in the interactive and non-interactive sign-in logs (oldest entries at the bottom – read from bottom to top).\n\nThis coincides with what I saw on the phone itself. Essentially:\n\nThe Intune Company Portal app is what drives things\nIt talks to the Authentication Broker to do a Device Registration\nThings don’t work somewhere – not in Conditional Access or Entra ID at least, coz everything is a success here – and that’s probably why things keep retrying.\n\nI figured I should update the firmware and try again. Rather than continue in this post though, I am going to start a new one. I realize this one has too many photos so I don’t want to load it all in a single post.\n Link to part 2", "date_published": "2025-11-10T13:50:53+00:00", "date_modified": "2025-11-11T15:18:41+00:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "conditional access", "device registration", "intune", "teams devices", "Azure, Azure AD, Graph, M365" ] }, { "id": "https://rakhesh.com/?p=8451", "url": "/mac/setting-the-scene-on-a-hue-light-via-api/", "title": "Setting the scene on a Hue light via API", "content_html": "

Continuing my trilogy (here and here) of Hue API posts… in my previous post I detailed how I set the lights to turn or or off when I login to my Mac.

\n

Slight issue with that. When I used to do this via the Hue sensor, it would set the correct scene coz the sensor was set to power on the light with a particular scene. Now when I turn it on directly, it just turns on to whatever scene was last set and not a specific one I want.

\n

Figuring out how to do this was a bit more work. Lights don’t seem to have scenes associated with them, but there is a scene API so I started from there.

\n

\"\"

\n

To get a list of all my scenes:

curl --request GET \\\r\n    --url https://$BRIDGE_IP/clip/v2/resource/scene \\\r\n    --header \"content-type: application/json\" \\\r\n    --header \"hue-application-key: $APIKEY\" \\\r\n    --insecure

The output of that looks like this:

{\r\n  \"errors\": [],\r\n  \"data\": [\r\n    {\r\n      \"id\": \"00d34266-51d6-4c80-83fa-bf0ab36e5c3c\",\r\n      \"id_v1\": \"/scenes/tLsH7eKgC2MbSukJ\",\r\n      \"actions\": [\r\n        {\r\n          \"target\": {\r\n            \"rid\": \"a9871b12-441e-44ba-8554-e458d6c9f65c\",\r\n            \"rtype\": \"light\"\r\n          },\r\n          \"action\": {\r\n            \"on\": {\r\n              \"on\": true\r\n            },\r\n            ...\r\n          }\r\n        },\r\n        {\r\n          \"target\": {\r\n            \"rid\": \"4f904f94-7997-4e41-ac43-0635a07472c4\",\r\n            \"rtype\": \"light\"\r\n          },\r\n          ...\r\n        }\r\n      ],\r\n      \"recall\": {},\r\n      \"metadata\": {\r\n        \"name\": \"First light\",\r\n        \"image\": {\r\n          \"rid\": \"ec790a2f-e63e-46cf-8a0f-1dd525e70f66\",\r\n          \"rtype\": \"public_image\"\r\n        }\r\n      },\r\n      \"group\": {\r\n        \"rid\": \"baddd0aa-d3cd-4e13-8ddd-627d131f8b18\",\r\n        \"rtype\": \"room\"\r\n      },\r\n      \"speed\": 0.626984126984127,\r\n      \"auto_dynamic\": false,\r\n      \"status\": {\r\n        \"active\": \"inactive\",\r\n        \"last_recall\": \"2025-03-10T17:40:45.090Z\"\r\n      },\r\n      \"type\": \"scene\"\r\n    },\r\n    {\r\n      \"id\": \"06ab863f-f991-4659-af62-c9b6d4037d48\",\r\n      \"id_v1\": \"/scenes/63HZDqdb-RJ0TTGH\",\r\n      ....\r\n    }\r\n  ]\r\n}

The scene name can be found in the metadata > name property. What I learnt from some fooling around is that each of these scenes has a rid property which is the room id the scene is associated with. (And the lights themselves are in rooms have so have a rid associated with them, and that’s how you link a scene to a light).

\n

I had some fun with jq. It’s been a while, and while I usually use PowerShell and never had to fuss too much with extracting data from JSON, this was a good excuse to stick with Bash and jq.

\n

I piped the output of the above command into jq like this:

curl --request GET \\\r\n    --url https://$BRIDGE_IP/clip/v2/resource/scene \\\r\n    --header \"content-type: application/json\" \\\r\n    --header \"hue-application-key: $APIKEY\" \\\r\n    --insecure | \r\njq '.data | .[] | { scene: .metadata.name, rid: .group.rid, id: .id }'

The output is a list of scenes and room ids and scene ids.

{\r\n  \"scene\": \"Rolling hills\",\r\n  \"rid\": \"baddd0aa-d3cd-4e13-8ddd-627d131f8b18\",\r\n  \"id\": \"fdae49d6-cd84-4b00-9f30-b90e65d3bb13\"\r\n}\r\n{\r\n  \"scene\": \"Night-time\",\r\n  \"rid\": \"baddd0aa-d3cd-4e13-8ddd-627d131f8b18\",\r\n  \"id\": \"ffbfe1af-8868-4b39-944b-519763636032\"\r\n}

Next I went through the lights I wanted to modify, and found their room Ids.

ALLLIGHTS=(<guid> <guid>)\r\nfor LIGHT_ID in ${ALLLIGHTS[@]}; do\r\n    curl --request GET \\\r\n        --url https://$BRIDGE_IP/clip/v2/resource/grouped_light/$LIGHT_ID \\\r\n        --header \"content-type: application/json\" \\\r\n        --header \"hue-application-key: $APIKEY\" \\\r\n        --insecure\r\ndone | jq '.data | .[] | .owner.rid'

At this point I could have just dumped the output of the first command that gives scenes and room ids and ids, and searched for the room ids of my lights. But I wanted to keep things interesting, so I took one of the room Ids and decided to search all scenes for that room id.

curl --request GET \\\r\n    --url https://$BRIDGE_IP/clip/v2/resource/scene \\\r\n    --header \"content-type: application/json\" \\\r\n    --header \"hue-application-key: $APIKEY\" \\\r\n    --insecure | \r\njq '.data | .[] | select(.metadata.name == \"<the scene I am interested in>\" and .group.rid == \"<room id from before>\") | .id'

This will return a single id, that of the scene.

\n

Later I realized I have two set of lights in two set of rooms, so I modifed the above to search for both rooms. (Both rooms had the same scene name, so that’s why I don’t search for multiple names).

curl --request GET \\\r\n    --url https://$BRIDGE_IP/clip/v2/resource/scene \\\r\n    --header \"content-type: application/json\" \\\r\n    --header \"hue-application-key: $APIKEY\" \\\r\n    --insecure | \r\njq '.data | .[] | select(.metadata.name == \"<scene name>\" and (.group.rid == \"<room id 1>\" or .group.rid == \"<room id 2>\")) | .id'

Now I have the two scene Ids I am interested in. Time to see if I can turn on the lights with these ids. Each scene Id is for a particular light. (Disclaimer: my situation might be a bit different from anyone else reading this. I have a set of lights that I put into separate “fake” rooms in Hue, so that’s why I am fussing about with rooms. I do this just so I can turn on the bedroom “room” lights without turning on the lights on my desk etc. too even though they are all in the bedroom).

\n

The instructions on turning on a scene aren’t very clear in the API docs. All they have is the following which you send via a PUT request:

\n

\"\"

\n

So first I tried this:

ALLSCENES=(<guid1> <guid2>)\r\nfor SCENE_ID in ${ALLSCENES[@]}; do\r\n    curl --request PUT \\\r\n        --url https://$BRIDGE_IP/clip/v2/resource/scene/$SCENE_ID \\\r\n        --header \"content-type: application/json\" \\\r\n        --header \"hue-application-key: $APIKEY\" \\\r\n        --data '{ \"recall\": {\"action\": \"on\"} }' \\\r\n        --insecure\r\ndone

Didn’t work! I got an error:

{\"data\":[],\"errors\":[{\"description\":\"error: 'json-schema validation', keyword: 'enum', details: {instanceRef: '#/recall/action', schemaRef: 'clip-api.schema.json#/definitions/RecallFeaturePut/properties/action'}\"}]}\r\n\r\n{\"data\":[],\"errors\":[{\"description\":\"error: 'json-schema validation', keyword: 'enum', details: {instanceRef: '#/recall/action', schemaRef: 'clip-api.schema.json#/definitions/RecallFeaturePut/properties/action'}\"}]}

Then I took a look at the scene JSON again and saw the following:

...\r\n      \"recall\": {},\r\n      \"metadata\": {\r\n        \"name\": \"<scene name>\",\r\n        \"image\": {\r\n          \"rid\": \"<guid>\",\r\n          \"rtype\": \"public_image\"\r\n        }\r\n      },\r\n      \"group\": {\r\n        \"rid\": \"<guid>\",\r\n        \"rtype\": \"room\"\r\n      },\r\n      \"speed\": 0.603,\r\n      \"auto_dynamic\": false,\r\n      \"status\": {\r\n        \"active\": \"inactive\",\r\n        \"last_recall\": \"2025-05-19T11:57:57.032Z\"\r\n      },\r\n      \"type\": \"scene\"\r\n     ...

Hmm, last status is inactive. So I changed my code to do active, instead of on.

ALLSCENES=(<guid1> <guid2>)\r\nfor SCENE_ID in ${ALLSCENES[@]}; do\r\n    curl --request PUT \\\r\n        --url https://$BRIDGE_IP/clip/v2/resource/scene/$SCENE_ID \\\r\n        --header \"content-type: application/json\" \\\r\n        --header \"hue-application-key: $APIKEY\" \\\r\n        --data '{ \"recall\": {\"action\": \"active\"} }' \\\r\n        --insecure\r\ndone

Boom! That worked. \"\ud83d\ude0e\"

\n

Interestingly, you’d think that sending the action as inactive instead of active will turn it off, but that didn’t do anything. I got an error again:

{\"data\":[],\"errors\":[{\"description\":\"error: 'json-schema validation', keyword: 'enum', details: {instanceRef: '#/recall/action', schemaRef: 'clip-api.schema.json#/definitions/RecallFeaturePut/properties/action'}\"}]}\r\n\r\n{\"data\":[],\"errors\":[{\"description\":\"error: 'json-schema validation', keyword: 'enum', details: {instanceRef: '#/recall/action', schemaRef: 'clip-api.schema.json#/definitions/RecallFeaturePut/properties/action'}\"}]}

Which is fine, I guess. What you want to do is turn off the lights anyway.

\n

Here’s an updated version of my script that turns on or off the lights:

#!/usr/bin/env bash\r\n\r\nAPIKEY=\"<apikey>\"\r\nBRIDGE_IP=\"<ip address>\"\r\nSENSOR_ID=\"<sensor guid>\"\r\n\r\nALLLIGHTS=(<light guid> <light guid>)\r\nALLSCENES=(<scene guid> <scene guid>)\r\n\r\nif [[ $1 == \"enable\" || $1 == \"on\" ]]; then\r\n    echo \"Enabling lights\"\r\n    \r\n    for SCENE_ID in ${ALLSCENES[@]}; do\r\n        curl --request PUT \\\r\n            --url https://$BRIDGE_IP/clip/v2/resource/scene/$SCENE_ID \\\r\n            --header \"content-type: application/json\" \\\r\n            --header \"hue-application-key: $APIKEY\" \\\r\n            --data '{ \"recall\": {\"action\": \"active\"} }' \\\r\n            --insecure\r\n    done\r\nelse\r\n    echo \"Disabling lights\"\r\n\r\n    for LIGHT_ID in ${ALLLIGHTS[@]}; do\r\n        curl --request PUT \\\r\n            --url https://$BRIDGE_IP/clip/v2/resource/grouped_light/$LIGHT_ID \\\r\n            --header \"content-type: application/json\" \\\r\n            --header \"hue-application-key: $APIKEY\" \\\r\n            --data '{ \"on\": { \"on\" : false } }' \\\r\n            --insecure\r\n    done\r\nfi

And that’s all folks! \"\ud83e\udd13\"

\n

Update (7th Nov 2025): I noticed that whenever I’d lock the screen Hammerspoon wouldn’t always run the script to turn off lights. It could be a time issue I suppose, coz reloading Hammerspoon and then locking the screen does. turn off lights. I am not entirely convinced it’s a time issue coz if I leave the system as is overnight and then login the next day, the script to turn on lights runs.

\n

Maybe I am spending more working than I don’t? \"\ud83e\udd13\"

\n

Anyways, slight hack to fix the issue. I added the following to my config:

hs.hotkey.bind({ \"ctrl\", \"cmd\" }, \"q\", function()\r\n    hs.execute(\"~/desklights.sh disable\", false)\r\n    hs.caffeinate.lockScreen()\r\nend)

This causes Hammerspoon to bind to the keys I use for locking the screen, execute the script and then proceed to lock the screen. It’s not ideal coz it doesn’t capture the scenario where I step away for a while without locking the screen and the system goes to sleep and the script doesn’t run.

\n", "content_text": "Continuing my trilogy (here and here) of Hue API posts… in my previous post I detailed how I set the lights to turn or or off when I login to my Mac.\nSlight issue with that. When I used to do this via the Hue sensor, it would set the correct scene coz the sensor was set to power on the light with a particular scene. Now when I turn it on directly, it just turns on to whatever scene was last set and not a specific one I want.\nFiguring out how to do this was a bit more work. Lights don’t seem to have scenes associated with them, but there is a scene API so I started from there.\n\nTo get a list of all my scenes:curl --request GET \\\r\n --url https://$BRIDGE_IP/clip/v2/resource/scene \\\r\n --header \"content-type: application/json\" \\\r\n --header \"hue-application-key: $APIKEY\" \\\r\n --insecureThe output of that looks like this:{\r\n \"errors\": [],\r\n \"data\": [\r\n {\r\n \"id\": \"00d34266-51d6-4c80-83fa-bf0ab36e5c3c\",\r\n \"id_v1\": \"/scenes/tLsH7eKgC2MbSukJ\",\r\n \"actions\": [\r\n {\r\n \"target\": {\r\n \"rid\": \"a9871b12-441e-44ba-8554-e458d6c9f65c\",\r\n \"rtype\": \"light\"\r\n },\r\n \"action\": {\r\n \"on\": {\r\n \"on\": true\r\n },\r\n ...\r\n }\r\n },\r\n {\r\n \"target\": {\r\n \"rid\": \"4f904f94-7997-4e41-ac43-0635a07472c4\",\r\n \"rtype\": \"light\"\r\n },\r\n ...\r\n }\r\n ],\r\n \"recall\": {},\r\n \"metadata\": {\r\n \"name\": \"First light\",\r\n \"image\": {\r\n \"rid\": \"ec790a2f-e63e-46cf-8a0f-1dd525e70f66\",\r\n \"rtype\": \"public_image\"\r\n }\r\n },\r\n \"group\": {\r\n \"rid\": \"baddd0aa-d3cd-4e13-8ddd-627d131f8b18\",\r\n \"rtype\": \"room\"\r\n },\r\n \"speed\": 0.626984126984127,\r\n \"auto_dynamic\": false,\r\n \"status\": {\r\n \"active\": \"inactive\",\r\n \"last_recall\": \"2025-03-10T17:40:45.090Z\"\r\n },\r\n \"type\": \"scene\"\r\n },\r\n {\r\n \"id\": \"06ab863f-f991-4659-af62-c9b6d4037d48\",\r\n \"id_v1\": \"/scenes/63HZDqdb-RJ0TTGH\",\r\n ....\r\n }\r\n ]\r\n}The scene name can be found in the metadata > name property. What I learnt from some fooling around is that each of these scenes has a rid property which is the room id the scene is associated with. (And the lights themselves are in rooms have so have a rid associated with them, and that’s how you link a scene to a light).\nI had some fun with jq. It’s been a while, and while I usually use PowerShell and never had to fuss too much with extracting data from JSON, this was a good excuse to stick with Bash and jq.\nI piped the output of the above command into jq like this:curl --request GET \\\r\n --url https://$BRIDGE_IP/clip/v2/resource/scene \\\r\n --header \"content-type: application/json\" \\\r\n --header \"hue-application-key: $APIKEY\" \\\r\n --insecure | \r\njq '.data | .[] | { scene: .metadata.name, rid: .group.rid, id: .id }'The output is a list of scenes and room ids and scene ids.{\r\n \"scene\": \"Rolling hills\",\r\n \"rid\": \"baddd0aa-d3cd-4e13-8ddd-627d131f8b18\",\r\n \"id\": \"fdae49d6-cd84-4b00-9f30-b90e65d3bb13\"\r\n}\r\n{\r\n \"scene\": \"Night-time\",\r\n \"rid\": \"baddd0aa-d3cd-4e13-8ddd-627d131f8b18\",\r\n \"id\": \"ffbfe1af-8868-4b39-944b-519763636032\"\r\n}Next I went through the lights I wanted to modify, and found their room Ids.ALLLIGHTS=(<guid> <guid>)\r\nfor LIGHT_ID in ${ALLLIGHTS[@]}; do\r\n curl --request GET \\\r\n --url https://$BRIDGE_IP/clip/v2/resource/grouped_light/$LIGHT_ID \\\r\n --header \"content-type: application/json\" \\\r\n --header \"hue-application-key: $APIKEY\" \\\r\n --insecure\r\ndone | jq '.data | .[] | .owner.rid'At this point I could have just dumped the output of the first command that gives scenes and room ids and ids, and searched for the room ids of my lights. But I wanted to keep things interesting, so I took one of the room Ids and decided to search all scenes for that room id.curl --request GET \\\r\n --url https://$BRIDGE_IP/clip/v2/resource/scene \\\r\n --header \"content-type: application/json\" \\\r\n --header \"hue-application-key: $APIKEY\" \\\r\n --insecure | \r\njq '.data | .[] | select(.metadata.name == \"<the scene I am interested in>\" and .group.rid == \"<room id from before>\") | .id'This will return a single id, that of the scene.\nLater I realized I have two set of lights in two set of rooms, so I modifed the above to search for both rooms. (Both rooms had the same scene name, so that’s why I don’t search for multiple names).curl --request GET \\\r\n --url https://$BRIDGE_IP/clip/v2/resource/scene \\\r\n --header \"content-type: application/json\" \\\r\n --header \"hue-application-key: $APIKEY\" \\\r\n --insecure | \r\njq '.data | .[] | select(.metadata.name == \"<scene name>\" and (.group.rid == \"<room id 1>\" or .group.rid == \"<room id 2>\")) | .id'Now I have the two scene Ids I am interested in. Time to see if I can turn on the lights with these ids. Each scene Id is for a particular light. (Disclaimer: my situation might be a bit different from anyone else reading this. I have a set of lights that I put into separate “fake” rooms in Hue, so that’s why I am fussing about with rooms. I do this just so I can turn on the bedroom “room” lights without turning on the lights on my desk etc. too even though they are all in the bedroom).\nThe instructions on turning on a scene aren’t very clear in the API docs. All they have is the following which you send via a PUT request:\n\nSo first I tried this:ALLSCENES=(<guid1> <guid2>)\r\nfor SCENE_ID in ${ALLSCENES[@]}; do\r\n curl --request PUT \\\r\n --url https://$BRIDGE_IP/clip/v2/resource/scene/$SCENE_ID \\\r\n --header \"content-type: application/json\" \\\r\n --header \"hue-application-key: $APIKEY\" \\\r\n --data '{ \"recall\": {\"action\": \"on\"} }' \\\r\n --insecure\r\ndoneDidn’t work! I got an error:{\"data\":[],\"errors\":[{\"description\":\"error: 'json-schema validation', keyword: 'enum', details: {instanceRef: '#/recall/action', schemaRef: 'clip-api.schema.json#/definitions/RecallFeaturePut/properties/action'}\"}]}\r\n\r\n{\"data\":[],\"errors\":[{\"description\":\"error: 'json-schema validation', keyword: 'enum', details: {instanceRef: '#/recall/action', schemaRef: 'clip-api.schema.json#/definitions/RecallFeaturePut/properties/action'}\"}]}Then I took a look at the scene JSON again and saw the following:...\r\n \"recall\": {},\r\n \"metadata\": {\r\n \"name\": \"<scene name>\",\r\n \"image\": {\r\n \"rid\": \"<guid>\",\r\n \"rtype\": \"public_image\"\r\n }\r\n },\r\n \"group\": {\r\n \"rid\": \"<guid>\",\r\n \"rtype\": \"room\"\r\n },\r\n \"speed\": 0.603,\r\n \"auto_dynamic\": false,\r\n \"status\": {\r\n \"active\": \"inactive\",\r\n \"last_recall\": \"2025-05-19T11:57:57.032Z\"\r\n },\r\n \"type\": \"scene\"\r\n ...Hmm, last status is inactive. So I changed my code to do active, instead of on.ALLSCENES=(<guid1> <guid2>)\r\nfor SCENE_ID in ${ALLSCENES[@]}; do\r\n curl --request PUT \\\r\n --url https://$BRIDGE_IP/clip/v2/resource/scene/$SCENE_ID \\\r\n --header \"content-type: application/json\" \\\r\n --header \"hue-application-key: $APIKEY\" \\\r\n --data '{ \"recall\": {\"action\": \"active\"} }' \\\r\n --insecure\r\ndoneBoom! That worked. \nInterestingly, you’d think that sending the action as inactive instead of active will turn it off, but that didn’t do anything. I got an error again:{\"data\":[],\"errors\":[{\"description\":\"error: 'json-schema validation', keyword: 'enum', details: {instanceRef: '#/recall/action', schemaRef: 'clip-api.schema.json#/definitions/RecallFeaturePut/properties/action'}\"}]}\r\n\r\n{\"data\":[],\"errors\":[{\"description\":\"error: 'json-schema validation', keyword: 'enum', details: {instanceRef: '#/recall/action', schemaRef: 'clip-api.schema.json#/definitions/RecallFeaturePut/properties/action'}\"}]}Which is fine, I guess. What you want to do is turn off the lights anyway.\nHere’s an updated version of my script that turns on or off the lights:#!/usr/bin/env bash\r\n\r\nAPIKEY=\"<apikey>\"\r\nBRIDGE_IP=\"<ip address>\"\r\nSENSOR_ID=\"<sensor guid>\"\r\n\r\nALLLIGHTS=(<light guid> <light guid>)\r\nALLSCENES=(<scene guid> <scene guid>)\r\n\r\nif [[ $1 == \"enable\" || $1 == \"on\" ]]; then\r\n echo \"Enabling lights\"\r\n \r\n for SCENE_ID in ${ALLSCENES[@]}; do\r\n curl --request PUT \\\r\n --url https://$BRIDGE_IP/clip/v2/resource/scene/$SCENE_ID \\\r\n --header \"content-type: application/json\" \\\r\n --header \"hue-application-key: $APIKEY\" \\\r\n --data '{ \"recall\": {\"action\": \"active\"} }' \\\r\n --insecure\r\n done\r\nelse\r\n echo \"Disabling lights\"\r\n\r\n for LIGHT_ID in ${ALLLIGHTS[@]}; do\r\n curl --request PUT \\\r\n --url https://$BRIDGE_IP/clip/v2/resource/grouped_light/$LIGHT_ID \\\r\n --header \"content-type: application/json\" \\\r\n --header \"hue-application-key: $APIKEY\" \\\r\n --data '{ \"on\": { \"on\" : false } }' \\\r\n --insecure\r\n done\r\nfiAnd that’s all folks! \nUpdate (7th Nov 2025): I noticed that whenever I’d lock the screen Hammerspoon wouldn’t always run the script to turn off lights. It could be a time issue I suppose, coz reloading Hammerspoon and then locking the screen does. turn off lights. I am not entirely convinced it’s a time issue coz if I leave the system as is overnight and then login the next day, the script to turn on lights runs.\nMaybe I am spending more working than I don’t? \nAnyways, slight hack to fix the issue. I added the following to my config:hs.hotkey.bind({ \"ctrl\", \"cmd\" }, \"q\", function()\r\n hs.execute(\"~/desklights.sh disable\", false)\r\n hs.caffeinate.lockScreen()\r\nend)This causes Hammerspoon to bind to the keys I use for locking the screen, execute the script and then proceed to lock the screen. It’s not ideal coz it doesn’t capture the scenario where I step away for a while without locking the screen and the system goes to sleep and the script doesn’t run.", "date_published": "2025-11-05T20:06:48+00:00", "date_modified": "2025-11-07T10:31:35+00:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "hue", "jq", "Mac" ] }, { "id": "https://rakhesh.com/?p=8441", "url": "/mac/turning-off-and-on-hue-lights-when-i-unlock-lock-or-sleep-wake-my-mac/", "title": "Turning off and on Hue lights when I unlock/ lock or sleep/ wake my Mac", "content_html": "

Barely a few days since my previous post on having fun with the Hue API, and the sensor stopped responding to my API calls. There’s no error or anything, the sensor just doesn’t get enabled!

\n

I run the command as before:

APIKEY=$(pass apiKeys/hue)\r\nBRIDGE_IP=\"<ip>\"\r\nSENSOR_ID=\"<guid>\"\r\n\r\necho \"${GREEN}\"\u2728\" Enabling sensor${RESET}\"\r\ncurl --request PUT \\\r\n    --url https://$BRIDGE_IP/clip/v2/resource/motion/$SENSOR_ID \\\r\n    --header \"content-type: application/json\" \\\r\n    --header \"hue-application-key: $APIKEY\" \\\r\n    --data '{ \"enabled\" : true }' \\\r\n    --insecure

The output is a success:

{\"data\":[{\"rid\":\"77064b38-907d-42ad-a65b-4f22178c3e6d\",\"rtype\":\"motion\"}],\"errors\":[]}

And querying the sensor shows its enabled too:

curl --request GET --url https://$BRIDGE_IP/clip/v2/resource/motion/$SENSOR_ID --header \"hue-application-key: $APIKEY\" --insecure | jq

{\r\n  \"errors\": [],\r\n  \"data\": [\r\n    {\r\n      \"id\": \"<guid>\",\r\n      \"id_v1\": \"/sensors/xx\",\r\n      \"owner\": {\r\n        \"rid\": \"<guid>\",\r\n        \"rtype\": \"device\"\r\n      },\r\n      \"enabled\": true,\r\n      \"motion\": {\r\n        \"motion\": true,\r\n        \"motion_valid\": true,\r\n        \"motion_report\": {\r\n          \"changed\": \"2025-11-04T20:31:09.366Z\",\r\n          \"motion\": true\r\n        }\r\n      },\r\n      \"sensitivity\": {\r\n        \"status\": \"set\",\r\n        \"sensitivity\": 4,\r\n        \"sensitivity_max\": 4\r\n      },\r\n      \"type\": \"motion\"\r\n    }\r\n  ]\r\n}

But it does nothing, and the app shows that it is indeed disabled!

\n

So much for that. \"\ud83d\ude12\"

\n

The lights continue to work though, so I know my API key and things are good. It’s just the sensor and API.

\n

Which got me thinking, maybe I can avoid it? I want the lights to come on when I am working on the Mac. Which means the display is on/ I’ve unlocked the screen. And when the display sleeps/ I’ve locked it, the lights can turn off. So what I really need is a way to trigger my scripts based on the Mac’s state.

\n

Looking for options, I encountered sleepwatcher. (Thanks to ChatGPT actually, it’s way better than Google nowadays for getting search results – unfortunate. And even Gemini, while it suggested sleepwatcher, didn’t suggest any other options like ChatGPT did).

\n

I didn’t use sleepwatcher coz looks like it needs the Rosetta layer on Apple silicon (not a problem but I am trying to keep things Apple silicon only \"\ud83d\ude43\") plus it seems to be a bit more involved than just downloading and running. I don’t mind things that are a bit more involved, just that the second option ChatGPT suggested was Hammerspoon, which I already use, so I figure I might as well invest more in that.

\n

Turns out there’s a Hammerspoon module called hs.caffeinate.watcher that can watch for system sleep/ wake/ etc. events. Specifically, these ones:

\n\n

While ChatGPT gave me the code to use to enable this, I wanted to figure out what it does, so what follows is my exploration.

\n

Based on the docs of the module, it looks like I have to create a watcher: hs.caffeinate.watcher.new(fn)

\n

In this fn is the function that will be called. These are Lua functions and they must accept one parameter which is the event type (the ones defined above – systemDidWake and so on). These “event types” are actually constants – i.e. strings – and looks like I must use the full name like hs.caffeinate.watcher.screensDidWake (got from the docs).

\n

So you 1) define a Lua function which does what you need to do (run my script to enable or disable lights) based on the input, 2) create a new watcher instance with the function as input, and 3) start this watcher?

\n

I created a bash script out of my Hue code earlier. This takes an argument and based on it enables or disables the lights.

#!/usr/bin/env bash\r\n\r\nAPIKEY=\"<apiKey>\"\r\nBRIDGE_IP=\"<ip>\"\r\nSENSOR_ID=\"<guid>\"\r\n\r\nALLLIGHTS=(<lightid1> <lightid2>)\r\n\r\nif [[ $1 == \"enable\" || $1 == \"on\" ]]; then\r\n    echo \"Enabling lights\"\r\n    \r\n    for LIGHT_ID in ${ALLLIGHTS[@]}; do      \r\n        curl --request PUT \\\r\n            --url https://$BRIDGE_IP/clip/v2/resource/grouped_light/$LIGHT_ID \\\r\n            --header \"content-type: application/json\" \\\r\n            --header \"hue-application-key: $APIKEY\" \\\r\n            --data '{ \"on\": { \"on\" : true } }' \\\r\n            --insecure\r\n    done\r\nelse\r\n    echo \"Disabling lights\"\r\n\r\n    for LIGHT_ID in ${ALLLIGHTS[@]}; do\r\n        curl --request PUT \\\r\n            --url https://$BRIDGE_IP/clip/v2/resource/grouped_light/$LIGHT_ID \\\r\n            --header \"content-type: application/json\" \\\r\n            --header \"hue-application-key: $APIKEY\" \\\r\n            --data '{ \"on\": { \"on\" : false } }' \\\r\n            --insecure\r\n    done\r\nfi

(Note: Previously I would read the API key via pass, but I had to remove that because Hammerspoon would hang. I think it’s coz pass tries to get my GPG passphrase and fails).

\n

Next, I created a function and put in my Hammerspoon config file:

function toggleLights(eventType)\r\n  if (eventType == hs.caffeinate.watcher.screensDidUnlock or \r\n      eventType == hs.caffeinate.watcher.systemDidWake) then\r\n    hs.execute(\"~/desklights.sh enable\", false)\r\n\r\n  elseif (eventType == hs.caffeinate.watcher.screensDidLock or \r\n          eventType == hs.caffeinate.watcher.systemWillSleep or \r\n          eventType == hs.caffeinate.watcher.systemWillPowerOff or \r\n          eventType == hs.caffeinate.watcher.screensDidSleep) then\r\n    hs.execute(\"~/desklights.sh disable\", false)\r\n  \r\n  end\r\nend

The function looks for event types that are do with the screen unlocking or the system waking and runs the script with the enable parameter; and for even types that are to do with screen locking or system sleeping or system powering off or screens sleeping it runs the script with the disable parameter. This makes use of hs.execute()\u00a0 which takes as input the script & its parameters, and a boolean value that determines if the command should be executed in the user’s login shell or not. In my case I don’t want it to, so I set it to false.

\n

And finally, I created a new watcher and started it.

local lightsWatcher = hs.caffeinate.watcher.new(toggleLights)\r\nlightsWatcher:start()

Then I reloaded my Hammerspoon config, locked and unlocked my screen to test, and the lights turned off and on as expected. Awesome! \"\ud83e\udd73\"

\n

Before I end, while Googling on some of the issues I encountered (the bit where Hammerspoon used to hang coz of the pass command in my script) I encountered a blog post and StackOverflow post where others have done pretty much what I did above. That’s also from where ChatGPT gave me its suggestions (esp. the StackOverflow post which gives many more suggestions)… so credit where credit is due! \"\ud83d\ude4f\"

\n

Update (a few hours later): See my next post for a follow-up.

\n", "content_text": "Barely a few days since my previous post on having fun with the Hue API, and the sensor stopped responding to my API calls. There’s no error or anything, the sensor just doesn’t get enabled!\nI run the command as before:APIKEY=$(pass apiKeys/hue)\r\nBRIDGE_IP=\"<ip>\"\r\nSENSOR_ID=\"<guid>\"\r\n\r\necho \"${GREEN} Enabling sensor${RESET}\"\r\ncurl --request PUT \\\r\n --url https://$BRIDGE_IP/clip/v2/resource/motion/$SENSOR_ID \\\r\n --header \"content-type: application/json\" \\\r\n --header \"hue-application-key: $APIKEY\" \\\r\n --data '{ \"enabled\" : true }' \\\r\n --insecureThe output is a success:{\"data\":[{\"rid\":\"77064b38-907d-42ad-a65b-4f22178c3e6d\",\"rtype\":\"motion\"}],\"errors\":[]}And querying the sensor shows its enabled too:curl --request GET --url https://$BRIDGE_IP/clip/v2/resource/motion/$SENSOR_ID --header \"hue-application-key: $APIKEY\" --insecure | jq{\r\n \"errors\": [],\r\n \"data\": [\r\n {\r\n \"id\": \"<guid>\",\r\n \"id_v1\": \"/sensors/xx\",\r\n \"owner\": {\r\n \"rid\": \"<guid>\",\r\n \"rtype\": \"device\"\r\n },\r\n \"enabled\": true,\r\n \"motion\": {\r\n \"motion\": true,\r\n \"motion_valid\": true,\r\n \"motion_report\": {\r\n \"changed\": \"2025-11-04T20:31:09.366Z\",\r\n \"motion\": true\r\n }\r\n },\r\n \"sensitivity\": {\r\n \"status\": \"set\",\r\n \"sensitivity\": 4,\r\n \"sensitivity_max\": 4\r\n },\r\n \"type\": \"motion\"\r\n }\r\n ]\r\n}But it does nothing, and the app shows that it is indeed disabled!\nSo much for that. \nThe lights continue to work though, so I know my API key and things are good. It’s just the sensor and API.\nWhich got me thinking, maybe I can avoid it? I want the lights to come on when I am working on the Mac. Which means the display is on/ I’ve unlocked the screen. And when the display sleeps/ I’ve locked it, the lights can turn off. So what I really need is a way to trigger my scripts based on the Mac’s state.\nLooking for options, I encountered sleepwatcher. (Thanks to ChatGPT actually, it’s way better than Google nowadays for getting search results – unfortunate. And even Gemini, while it suggested sleepwatcher, didn’t suggest any other options like ChatGPT did).\nI didn’t use sleepwatcher coz looks like it needs the Rosetta layer on Apple silicon (not a problem but I am trying to keep things Apple silicon only ) plus it seems to be a bit more involved than just downloading and running. I don’t mind things that are a bit more involved, just that the second option ChatGPT suggested was Hammerspoon, which I already use, so I figure I might as well invest more in that.\nTurns out there’s a Hammerspoon module called hs.caffeinate.watcher that can watch for system sleep/ wake/ etc. events. Specifically, these ones:\n\nscreensaverDidStart\nscreensaverDidStop\nscreensaverWillStop\nscreensDidLock\nscreensDidSleep\nscreensDidUnlock\nscreensDidWake\nsessionDidBecomeActive\nsessionDidResignActive\nsystemDidWake\nsystemWillPowerOff\nsystemWillSleep\n\nWhile ChatGPT gave me the code to use to enable this, I wanted to figure out what it does, so what follows is my exploration.\nBased on the docs of the module, it looks like I have to create a watcher: hs.caffeinate.watcher.new(fn)\nIn this fn is the function that will be called. These are Lua functions and they must accept one parameter which is the event type (the ones defined above – systemDidWake and so on). These “event types” are actually constants – i.e. strings – and looks like I must use the full name like hs.caffeinate.watcher.screensDidWake (got from the docs).\nSo you 1) define a Lua function which does what you need to do (run my script to enable or disable lights) based on the input, 2) create a new watcher instance with the function as input, and 3) start this watcher?\nI created a bash script out of my Hue code earlier. This takes an argument and based on it enables or disables the lights.#!/usr/bin/env bash\r\n\r\nAPIKEY=\"<apiKey>\"\r\nBRIDGE_IP=\"<ip>\"\r\nSENSOR_ID=\"<guid>\"\r\n\r\nALLLIGHTS=(<lightid1> <lightid2>)\r\n\r\nif [[ $1 == \"enable\" || $1 == \"on\" ]]; then\r\n echo \"Enabling lights\"\r\n \r\n for LIGHT_ID in ${ALLLIGHTS[@]}; do \r\n curl --request PUT \\\r\n --url https://$BRIDGE_IP/clip/v2/resource/grouped_light/$LIGHT_ID \\\r\n --header \"content-type: application/json\" \\\r\n --header \"hue-application-key: $APIKEY\" \\\r\n --data '{ \"on\": { \"on\" : true } }' \\\r\n --insecure\r\n done\r\nelse\r\n echo \"Disabling lights\"\r\n\r\n for LIGHT_ID in ${ALLLIGHTS[@]}; do\r\n curl --request PUT \\\r\n --url https://$BRIDGE_IP/clip/v2/resource/grouped_light/$LIGHT_ID \\\r\n --header \"content-type: application/json\" \\\r\n --header \"hue-application-key: $APIKEY\" \\\r\n --data '{ \"on\": { \"on\" : false } }' \\\r\n --insecure\r\n done\r\nfi(Note: Previously I would read the API key via pass, but I had to remove that because Hammerspoon would hang. I think it’s coz pass tries to get my GPG passphrase and fails).\nNext, I created a function and put in my Hammerspoon config file:function toggleLights(eventType)\r\n if (eventType == hs.caffeinate.watcher.screensDidUnlock or \r\n eventType == hs.caffeinate.watcher.systemDidWake) then\r\n hs.execute(\"~/desklights.sh enable\", false)\r\n\r\n elseif (eventType == hs.caffeinate.watcher.screensDidLock or \r\n eventType == hs.caffeinate.watcher.systemWillSleep or \r\n eventType == hs.caffeinate.watcher.systemWillPowerOff or \r\n eventType == hs.caffeinate.watcher.screensDidSleep) then\r\n hs.execute(\"~/desklights.sh disable\", false)\r\n \r\n end\r\nendThe function looks for event types that are do with the screen unlocking or the system waking and runs the script with the enable parameter; and for even types that are to do with screen locking or system sleeping or system powering off or screens sleeping it runs the script with the disable parameter. This makes use of hs.execute()\u00a0 which takes as input the script & its parameters, and a boolean value that determines if the command should be executed in the user’s login shell or not. In my case I don’t want it to, so I set it to false.\nAnd finally, I created a new watcher and started it.local lightsWatcher = hs.caffeinate.watcher.new(toggleLights)\r\nlightsWatcher:start()Then I reloaded my Hammerspoon config, locked and unlocked my screen to test, and the lights turned off and on as expected. Awesome! \nBefore I end, while Googling on some of the issues I encountered (the bit where Hammerspoon used to hang coz of the pass command in my script) I encountered a blog post and StackOverflow post where others have done pretty much what I did above. That’s also from where ChatGPT gave me its suggestions (esp. the StackOverflow post which gives many more suggestions)… so credit where credit is due! \nUpdate (a few hours later): See my next post for a follow-up.", "date_published": "2025-11-05T14:44:09+00:00", "date_modified": "2025-11-05T20:08:07+00:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "hammerspoon", "hue", "Mac" ] }, { "id": "https://rakhesh.com/?p=8435", "url": "/azure/power-apps-the-user-has-not-been-assigned-any-license-and-is-disabled/", "title": "Power Apps \u2013 The user has not been assigned any License and is disabled.", "content_html": "

Encountered a red-herring of an error today. A colleague shared a Power App with another colleague and got the following:

\n

\"\"

\n

The user in question was indeed enabled and licensed. Also, interestingly, the user Id in the error didn’t match that in Entra ID (but then I guess it must be the Id of the object in Power Platform itself).

\n

Even though my colleague got an error, when he looks at the “Share” pane the user is added there. But the user still can’t access the app.

\n

Googling on this gave such amazing forum posts as this one stating the obvious. But I put my thinking cap on and tried to get to the bottom of the issue. \"\ud83d\ude0a\"

\n

If I create a dummy app and share it with the user, it works. But it wasn’t working or my colleague with his app.

\n

The user in question was present in the environmnent under the list of users. But I noticed that the user didn’t have any Power Platform environment role assigned to him and I wondered if that mattered. My dummy app was just a new canvas app in the Personal Productivity/ Default, but my colleague’s app connected to SharePoint etc. and in an environment with Dataverse enabled. I know the “Basic User” role usually matters, so I took a look at that role in the Personal Productivity environment and he had that (not just him, everyone who had access to the environment had the role). So I granted the same role to him in the other environment, and now sharing works! (And for good measure I also granted that role to everyone who had access to the environment – which you can find by clicking “Edit” on the environment in Power Platform admin centre).

\n

What a red-herring of an error!

\n", "content_text": "Encountered a red-herring of an error today. A colleague shared a Power App with another colleague and got the following:\n\nThe user in question was indeed enabled and licensed. Also, interestingly, the user Id in the error didn’t match that in Entra ID (but then I guess it must be the Id of the object in Power Platform itself).\nEven though my colleague got an error, when he looks at the “Share” pane the user is added there. But the user still can’t access the app.\nGoogling on this gave such amazing forum posts as this one stating the obvious. But I put my thinking cap on and tried to get to the bottom of the issue. \nIf I create a dummy app and share it with the user, it works. But it wasn’t working or my colleague with his app.\nThe user in question was present in the environmnent under the list of users. But I noticed that the user didn’t have any Power Platform environment role assigned to him and I wondered if that mattered. My dummy app was just a new canvas app in the Personal Productivity/ Default, but my colleague’s app connected to SharePoint etc. and in an environment with Dataverse enabled. I know the “Basic User” role usually matters, so I took a look at that role in the Personal Productivity environment and he had that (not just him, everyone who had access to the environment had the role). So I granted the same role to him in the other environment, and now sharing works! (And for good measure I also granted that role to everyone who had access to the environment – which you can find by clicking “Edit” on the environment in Power Platform admin centre).\nWhat a red-herring of an error!", "date_published": "2025-11-04T15:05:33+00:00", "date_modified": "2025-11-04T15:05:33+00:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "power apps", "power platform", "Azure, Azure AD, Graph, M365" ] }, { "id": "https://rakhesh.com/?p=8428", "url": "/azure/intune-app-protection-policies-and-custom-apps/", "title": "Intune \u2013 App Protection policies and custom apps", "content_html": "

Every so often I get a request from one of our IT folks asking if I can add such and such app to our App Protection policies. Thing is, most of the time the app in question isn’t one that’s supported by Microsoft/ Intune. (See this article for a list of supported apps).

\n

Inevitably a follow-up question is whether I can add it to the custom apps section as that will somehow bring the app into the App Protection policy umbrella. This is a mistaken suggestion and doesn’t work; and lest anyone think this blog post is me dissing upon those who come with such a suggestion, let me assure you that I too used to be in that group until last year or so. \"\ud83d\ude42\"\u00a0No judgement here!

\n

The custom apps section is meant for custom in-house developed apps, or line-of-business apps. It is not meant for you to find the bundle id of an app not in the default list of apps, add it there, and then things will magically work! This is the misunderstanding and the Microsoft’s documentation too makes this clear:

\n

Custom apps are LOB apps that have been integrated with the Intune SDK or wrapped by the Intune App Wrapping Tool.

\n

So if an app is not supported by default by the App Protection policies, there’s not much we can do. There’s no blanket way to add the app, but workarouds might exist. For instance, I had a request to add ChatGPT to one of our App Protection policies so users can copy-paste into it, and the app doesn’t supprot App Protection policies so I couldn’t add it. As an alternative I suggested publishing it as a web-clip that opens in the managed browser.

\n

\"\"

\n

This adds a link to ChatGPT web on the home screen to the users it is assigned to. Clicking it will open ChatGPT web in a managed browser, which is under App Protection, and so one could copy paste into ChatGPT here to initiate a conversation and then go to regular ChatGPT to continue there. Not ideal, but that’s an example of a workaround.

\n

\"\"

\n

Another option is to tweak the number of characters that are allowed for copy-paste (which works in this scenario as the request was for copy-paste). The default value is 0, which means no copy paste is allowed if the policy is set to block; but we can set a number to allow that many characters. The limit seems to be 65535 characters.

\n", "content_text": "Every so often I get a request from one of our IT folks asking if I can add such and such app to our App Protection policies. Thing is, most of the time the app in question isn’t one that’s supported by Microsoft/ Intune. (See this article for a list of supported apps).\nInevitably a follow-up question is whether I can add it to the custom apps section as that will somehow bring the app into the App Protection policy umbrella. This is a mistaken suggestion and doesn’t work; and lest anyone think this blog post is me dissing upon those who come with such a suggestion, let me assure you that I too used to be in that group until last year or so. \u00a0No judgement here!\nThe custom apps section is meant for custom in-house developed apps, or line-of-business apps. It is not meant for you to find the bundle id of an app not in the default list of apps, add it there, and then things will magically work! This is the misunderstanding and the Microsoft’s documentation too makes this clear:\nCustom apps are LOB apps that have been integrated with the Intune SDK or wrapped by the Intune App Wrapping Tool.\nSo if an app is not supported by default by the App Protection policies, there’s not much we can do. There’s no blanket way to add the app, but workarouds might exist. For instance, I had a request to add ChatGPT to one of our App Protection policies so users can copy-paste into it, and the app doesn’t supprot App Protection policies so I couldn’t add it. As an alternative I suggested publishing it as a web-clip that opens in the managed browser.\n\nThis adds a link to ChatGPT web on the home screen to the users it is assigned to. Clicking it will open ChatGPT web in a managed browser, which is under App Protection, and so one could copy paste into ChatGPT here to initiate a conversation and then go to regular ChatGPT to continue there. Not ideal, but that’s an example of a workaround.\n\nAnother option is to tweak the number of characters that are allowed for copy-paste (which works in this scenario as the request was for copy-paste). The default value is 0, which means no copy paste is allowed if the policy is set to block; but we can set a number to allow that many characters. The limit seems to be 65535 characters.", "date_published": "2025-11-03T15:48:24+00:00", "date_modified": "2025-11-03T15:48:24+00:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "app protection policy", "intune", "Azure, Azure AD, Graph, M365" ] }, { "id": "https://rakhesh.com/?p=8412", "url": "/azure/kql-outputting-the-input-of-a-function/", "title": "KQL \u2013 Outputting the input of a function", "content_html": "

Playing with Functions in KQL today. One of the things I wanted to do was troubleshoot the input I am passing to a Function. Couldn’t figure out a obvious way to do this, so here’s what I did.

\n

My objective here is simply to see if I am even passing inputs in correctly! At this point I am very new to KQL functions so we are talking basics here. \"\ud83d\ude00\"

\n

Here’s what I did. I created a new KQL query like this:

let blah = datatable (Blah1:string, Blah2:string) [\r\n    \"111\", \"222\"\r\n];\r\nblah\r\n| extend UserInput = Users

Then created a function out of it, and gave that function a single input called ‘Users’.

\n

\"\"

\n

And now I can call the function in one of these ways:

// this way\r\nRakTestFunc1(Users=\"rak\")\r\n\r\n// Or this way\r\nRakTestFunc1(\"rak\")

And I can see the input parameter in the output.

\n

\"\"

\n

Seems a long winded way to do this, but until I learn of a better way this works I suppose.

\n

For info, here’s what I am doing. First I create a datatable. Why? Because KQL outputs are tables, and I figure I must start with a table so I can add some data to it. I couldn’t think of any other way, so I created a new dummy table. It doesn’t even need the two columns I put in there, just one is fine.

let blah = datatable (Blah1:string, Blah2:string) [\r\n    \"111\", \"222\"\r\n];

Then I output this table and extend it by adding a column that shows my input.

blah\r\n| extend UserInput = Users

The ‘Users’ above is my input. It will appear as an error in the console, but we can ignore that because the parameter is set in the function definition.

\n

Ok, next steps. I want the function to have two inputs – a timespan and a string. How do I output these?

\n

Here’s the new KQL:

let blah = datatable (Blah1:string, Blah2:string) [\r\n    \"111\", \"222\"\r\n];\r\nblah\r\n| extend TimeRangeInput = TimeRange\r\n| extend UserInput = Users

Add that paramter to the function defintion (click Save and it will open up the dialog box again to add/ remove parameters).

\n

\"\"

\n

Now if I run the function, I can see what happens:

// Do one of these, as before\r\nRakTestFunc1(Users=\"rak\")\r\n\r\nRakTestFunc1(\"rak\")

As you can see, it’s picked up the default.

\n

\"\"

\n

If I change the input, the output changes:

RakTestFunc1(\"rak\", 12h)

\"\"

\n

Now I want to change ‘Users’ to be a dynamic type.

\n

\"\"

\n

Interestingly, once I did that and saved, it changed to a cast.

\n

\"\"

\n

Things work if I provide an input.

RakTestFunc1(dynamic(['rak']), 12h)

\"\"

\n

But fail if I don’t provide an input (this is what I had set out to troubleshoot initially, which is why I was creating this dummy way of identifying the inputs passed to a function).

RakTestFunc1(12h)

I get an error: Cast operator: dynamic casting expects string type values

\n

\"\"

\n

This stumped me for a while and there was nothing I could do to get it working by using the default parameter value if I skipped it.

\n

Finally I found out what to do. It looks like if I am skipping parameter values, I must mention what I am passing.

\n

If I modify the above invocation to be this:

RakTestFunc1(TimeRange=12h)

Now it works and picks up the default!

\n

\"\"

\n

And if I skip both parameters, that works too.

RakTestFunc1()

\"\"

\n

What if I want the default to be empty? I changed it to dynamic(\"\")

\n

\"\"

\n

That works too.

\n

\"\"

\n

Does that comes across as an empty value though? I modified the underlying function thus:

let blah = datatable (Blah1:string, Blah2:string) [\r\n    \"111\", \"222\"\r\n];\r\nblah\r\n| extend TimeRangeInput = TimeRange\r\n| extend UserInput = iff(isempty(Users), \"==empty==\", Users)

Yup, that works.

\n

\"\"

\n

That’s all!

\n", "content_text": "Playing with Functions in KQL today. One of the things I wanted to do was troubleshoot the input I am passing to a Function. Couldn’t figure out a obvious way to do this, so here’s what I did.\nMy objective here is simply to see if I am even passing inputs in correctly! At this point I am very new to KQL functions so we are talking basics here. \nHere’s what I did. I created a new KQL query like this:let blah = datatable (Blah1:string, Blah2:string) [\r\n \"111\", \"222\"\r\n];\r\nblah\r\n| extend UserInput = UsersThen created a function out of it, and gave that function a single input called ‘Users’.\n\nAnd now I can call the function in one of these ways:// this way\r\nRakTestFunc1(Users=\"rak\")\r\n\r\n// Or this way\r\nRakTestFunc1(\"rak\")And I can see the input parameter in the output.\n\nSeems a long winded way to do this, but until I learn of a better way this works I suppose.\nFor info, here’s what I am doing. First I create a datatable. Why? Because KQL outputs are tables, and I figure I must start with a table so I can add some data to it. I couldn’t think of any other way, so I created a new dummy table. It doesn’t even need the two columns I put in there, just one is fine.let blah = datatable (Blah1:string, Blah2:string) [\r\n \"111\", \"222\"\r\n];Then I output this table and extend it by adding a column that shows my input.blah\r\n| extend UserInput = UsersThe ‘Users’ above is my input. It will appear as an error in the console, but we can ignore that because the parameter is set in the function definition.\nOk, next steps. I want the function to have two inputs – a timespan and a string. How do I output these?\nHere’s the new KQL:let blah = datatable (Blah1:string, Blah2:string) [\r\n \"111\", \"222\"\r\n];\r\nblah\r\n| extend TimeRangeInput = TimeRange\r\n| extend UserInput = UsersAdd that paramter to the function defintion (click Save and it will open up the dialog box again to add/ remove parameters).\n\nNow if I run the function, I can see what happens:// Do one of these, as before\r\nRakTestFunc1(Users=\"rak\")\r\n\r\nRakTestFunc1(\"rak\")As you can see, it’s picked up the default.\n\nIf I change the input, the output changes:RakTestFunc1(\"rak\", 12h)\nNow I want to change ‘Users’ to be a dynamic type.\n\nInterestingly, once I did that and saved, it changed to a cast.\n\nThings work if I provide an input.RakTestFunc1(dynamic(['rak']), 12h)\nBut fail if I don’t provide an input (this is what I had set out to troubleshoot initially, which is why I was creating this dummy way of identifying the inputs passed to a function).RakTestFunc1(12h)I get an error: Cast operator: dynamic casting expects string type values\n\nThis stumped me for a while and there was nothing I could do to get it working by using the default parameter value if I skipped it.\nFinally I found out what to do. It looks like if I am skipping parameter values, I must mention what I am passing.\nIf I modify the above invocation to be this:RakTestFunc1(TimeRange=12h)Now it works and picks up the default!\n\nAnd if I skip both parameters, that works too.RakTestFunc1()\nWhat if I want the default to be empty? I changed it to dynamic(\"\")\n\nThat works too.\n\nDoes that comes across as an empty value though? I modified the underlying function thus:let blah = datatable (Blah1:string, Blah2:string) [\r\n \"111\", \"222\"\r\n];\r\nblah\r\n| extend TimeRangeInput = TimeRange\r\n| extend UserInput = iff(isempty(Users), \"==empty==\", Users)Yup, that works.\n\nThat’s all!", "date_published": "2025-11-03T12:51:13+00:00", "date_modified": "2025-11-03T12:51:13+00:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "kql", "Azure, Azure AD, Graph, M365" ] }, { "id": "https://rakhesh.com/?p=8410", "url": "/gadgets/fun-and-games-with-the-hue-api/", "title": "Fun and games with the Hue API", "content_html": "

I have a Hue sensor on my desk that turns on and off the lights on the desk when I am at it.

\n

Since my desk is in our bedroom, and I don’t want the sensor to be active during weekends, I wanted an easy way to disable it on weekends and enable it after weekends (or maybe not enable it, I think I’ll enable it manually depending on whether I am actually working or not etc).

\n

Yes I can do this via the app but it’s a bother and I wanted to just set and forget it. So I took a look at the Hue API.

\n

First things first, you need the IP address of your Hue Bridge. I got that from the app. Then, you need to create an API key. The getting started section of the Hue API docs has instructions on creating this. I did that, put the key into pass and now I am ready to go.

# Once the API key is in pass I can refer to it thus (apiKeys/hue is the name of the secret)\r\nAPIKEY=$(pass apiKeys/hue)\r\n\r\n# Also set the bridge IP in a variable\r\nBRIDGE_IP=\"192.168.200.200\"

The /resource/motion endpoint is what deals with sensors. To query it I do:

curl --insecure --request 'GET' --header \"hue-application-key: $APIKEY\" --url \"https://$BRIDGE_IP/clip/v2/resource/motion\"

I must use the --insecure switch due to certs (it is a self-signed cert). Also, to make the output readable, one can pipe it through jq:

curl --insecure --request 'GET' --header \"hue-application-key: $APIKEY\" --url \"https://$BRIDGE_IP/clip/v2/resource/motion\" | jq

The output is a list of sensors:

{\r\n  \"errors\": [],\r\n  \"data\": [\r\n    {\r\n      \"id\": \"72164b38-907d-42ad-a65b-4f22178c3e6d\",\r\n      \"id_v1\": \"/sensors/47\",\r\n      \"owner\": {\r\n        \"rid\": \"20d706c2-1a05-4ef6-8a18-cdee0cf42b23\",\r\n        \"rtype\": \"device\"\r\n      },\r\n      \"enabled\": false,\r\n      \"motion\": {\r\n        \"motion\": false,\r\n        \"motion_valid\": false\r\n      },\r\n      \"sensitivity\": {\r\n        \"status\": \"set\",\r\n        \"sensitivity\": 4,\r\n        \"sensitivity_max\": 4\r\n      },\r\n      \"type\": \"motion\"\r\n    }\r\n  ]\r\n}

The id is what I need. Currently it is disabled, as can be seen from \"enabled\": false. I stored it in a variable SENSOR_ID.

\n

Now, enabling it is simple:

curl --insecure \\\r\n     --request 'PUT' \\\r\n     --header \"hue-application-key: $APIKEY\" \\\r\n     --header \"content-type: application/json\" \\\r\n     --url \"https://$BRIDGE_IP/clip/v2/resource/motion/$SENSOR_ID\" \\\r\n     --data '{ \"enabled\" : true }'

I must 1) change the request method to PUT, 2) set content-type headers for JSON, 3) modify the URL to include the sensor Id I previously got, and 4) send a JSON data that sets it to enabled.

\n

Disabling is similarly easy, just change to false.

curl --insecure \\\r\n     --request 'PUT' \\\r\n     --header \"hue-application-key: $APIKEY\" \\\r\n     --header \"content-type: application/json\" \\\r\n     --url \"https://$BRIDGE_IP/clip/v2/resource/motion/$SENSOR_ID\" \\\r\n     --data '{ \"enabled\" : false }'

So now I can put each of these into separate files and setup a cron job to run the disable file just before the weekend. (The cron snippet below disables the sensor at 7pm every day; and enables it at 8am every weekday).

# m h  dom mon dow   command\r\n\r\n00 08 * * 1-5 /path/to/sensor_enable.sh\r\n00 19 * * * /path/to/sensor_disable.sh

Next, I also wanted to turn off or on the lights affected by the sensor. Coz I realized that say I was working late and the sensor gets disable at 7pm via the above job – the lights now continue to stay on coz there’s nothing to disable them. So I must also add some code to disable the lights when disabling the sensor.

\n

I have two lights together in a group, controlled by the sensor. The lights are in fake room of their own. So I ran the following command to first find the room details:

curl --insecure --header \"hue-application-key: $APIKEY\" --request 'GET' --url https://$BRIDGE_IP/clip/v2/resource/room | jq

One of the entries looks like this:

{\r\n      \"id\": \"baddd0bb-23cd-4e13-8ddd-627d131f8b18\",\r\n      \"id_v1\": \"/groups/82\",\r\n      \"children\": [\r\n        {\r\n          \"rid\": \"ef2f1315-7c24-4470-8d54-126026083250\",\r\n          \"rtype\": \"device\"\r\n        },\r\n        {\r\n          \"rid\": \"c7109087-2324-4ddb-9496-0b285ca87d5a\",\r\n          \"rtype\": \"device\"\r\n        }\r\n      ],\r\n      \"services\": [\r\n        {\r\n          \"rid\": \"1de1ee19-edd7-407c-8358-1dd1cc972143\",\r\n          \"rtype\": \"grouped_light\"\r\n        }\r\n      ],\r\n      \"metadata\": {\r\n        \"name\": \"Desk\",\r\n        \"archetype\": \"office\"\r\n      },\r\n      \"type\": \"room\"\r\n    },

This is the room. You can see the group lights in there, with the id for the group.

\n

Controlling a grouped light is the same way as controlling a light. Here’s what I did:

curl --request PUT \\\r\n     --url https://$BRIDGE_IP/clip/v2/resource/grouped_light/$LIGHT_ID \\\r\n     --header \"content-type: application/json\" \\\r\n     --header \"hue-application-key: $APIKEY\" \\\r\n     --data '{  \"on\": { \"on\" : false } }' \\\r\n     --insecure

The above snippet turns it off; change the false to true to turn it on.

\n

I added this code to the same script as disabling the sensor, and now it disables the sensor and turns off the light! Yay.

\n

Update (05 Nov 2025): A follow up post here as the API call to the sensor simply stopped working.

\n", "content_text": "I have a Hue sensor on my desk that turns on and off the lights on the desk when I am at it.\nSince my desk is in our bedroom, and I don’t want the sensor to be active during weekends, I wanted an easy way to disable it on weekends and enable it after weekends (or maybe not enable it, I think I’ll enable it manually depending on whether I am actually working or not etc).\nYes I can do this via the app but it’s a bother and I wanted to just set and forget it. So I took a look at the Hue API.\nFirst things first, you need the IP address of your Hue Bridge. I got that from the app. Then, you need to create an API key. The getting started section of the Hue API docs has instructions on creating this. I did that, put the key into pass and now I am ready to go.# Once the API key is in pass I can refer to it thus (apiKeys/hue is the name of the secret)\r\nAPIKEY=$(pass apiKeys/hue)\r\n\r\n# Also set the bridge IP in a variable\r\nBRIDGE_IP=\"192.168.200.200\"The /resource/motion endpoint is what deals with sensors. To query it I do:curl --insecure --request 'GET' --header \"hue-application-key: $APIKEY\" --url \"https://$BRIDGE_IP/clip/v2/resource/motion\"I must use the --insecure switch due to certs (it is a self-signed cert). Also, to make the output readable, one can pipe it through jq:curl --insecure --request 'GET' --header \"hue-application-key: $APIKEY\" --url \"https://$BRIDGE_IP/clip/v2/resource/motion\" | jqThe output is a list of sensors:{\r\n \"errors\": [],\r\n \"data\": [\r\n {\r\n \"id\": \"72164b38-907d-42ad-a65b-4f22178c3e6d\",\r\n \"id_v1\": \"/sensors/47\",\r\n \"owner\": {\r\n \"rid\": \"20d706c2-1a05-4ef6-8a18-cdee0cf42b23\",\r\n \"rtype\": \"device\"\r\n },\r\n \"enabled\": false,\r\n \"motion\": {\r\n \"motion\": false,\r\n \"motion_valid\": false\r\n },\r\n \"sensitivity\": {\r\n \"status\": \"set\",\r\n \"sensitivity\": 4,\r\n \"sensitivity_max\": 4\r\n },\r\n \"type\": \"motion\"\r\n }\r\n ]\r\n}The id is what I need. Currently it is disabled, as can be seen from \"enabled\": false. I stored it in a variable SENSOR_ID.\nNow, enabling it is simple:curl --insecure \\\r\n --request 'PUT' \\\r\n --header \"hue-application-key: $APIKEY\" \\\r\n --header \"content-type: application/json\" \\\r\n --url \"https://$BRIDGE_IP/clip/v2/resource/motion/$SENSOR_ID\" \\\r\n --data '{ \"enabled\" : true }'I must 1) change the request method to PUT, 2) set content-type headers for JSON, 3) modify the URL to include the sensor Id I previously got, and 4) send a JSON data that sets it to enabled.\nDisabling is similarly easy, just change to false.curl --insecure \\\r\n --request 'PUT' \\\r\n --header \"hue-application-key: $APIKEY\" \\\r\n --header \"content-type: application/json\" \\\r\n --url \"https://$BRIDGE_IP/clip/v2/resource/motion/$SENSOR_ID\" \\\r\n --data '{ \"enabled\" : false }'So now I can put each of these into separate files and setup a cron job to run the disable file just before the weekend. (The cron snippet below disables the sensor at 7pm every day; and enables it at 8am every weekday).# m h dom mon dow command\r\n\r\n00 08 * * 1-5 /path/to/sensor_enable.sh\r\n00 19 * * * /path/to/sensor_disable.shNext, I also wanted to turn off or on the lights affected by the sensor. Coz I realized that say I was working late and the sensor gets disable at 7pm via the above job – the lights now continue to stay on coz there’s nothing to disable them. So I must also add some code to disable the lights when disabling the sensor.\nI have two lights together in a group, controlled by the sensor. The lights are in fake room of their own. So I ran the following command to first find the room details:curl --insecure --header \"hue-application-key: $APIKEY\" --request 'GET' --url https://$BRIDGE_IP/clip/v2/resource/room | jqOne of the entries looks like this:{\r\n \"id\": \"baddd0bb-23cd-4e13-8ddd-627d131f8b18\",\r\n \"id_v1\": \"/groups/82\",\r\n \"children\": [\r\n {\r\n \"rid\": \"ef2f1315-7c24-4470-8d54-126026083250\",\r\n \"rtype\": \"device\"\r\n },\r\n {\r\n \"rid\": \"c7109087-2324-4ddb-9496-0b285ca87d5a\",\r\n \"rtype\": \"device\"\r\n }\r\n ],\r\n \"services\": [\r\n {\r\n \"rid\": \"1de1ee19-edd7-407c-8358-1dd1cc972143\",\r\n \"rtype\": \"grouped_light\"\r\n }\r\n ],\r\n \"metadata\": {\r\n \"name\": \"Desk\",\r\n \"archetype\": \"office\"\r\n },\r\n \"type\": \"room\"\r\n },This is the room. You can see the group lights in there, with the id for the group.\nControlling a grouped light is the same way as controlling a light. Here’s what I did:curl --request PUT \\\r\n --url https://$BRIDGE_IP/clip/v2/resource/grouped_light/$LIGHT_ID \\\r\n --header \"content-type: application/json\" \\\r\n --header \"hue-application-key: $APIKEY\" \\\r\n --data '{ \"on\": { \"on\" : false } }' \\\r\n --insecureThe above snippet turns it off; change the false to true to turn it on.\nI added this code to the same script as disabling the sensor, and now it disables the sensor and turns off the light! Yay.\nUpdate (05 Nov 2025): A follow up post here as the API call to the sensor simply stopped working.", "date_published": "2025-10-31T13:43:00+00:00", "date_modified": "2025-11-05T15:19:18+00:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "hue", "Coding", "Gadgets", "Linux & BSD" ] }, { "id": "https://rakhesh.com/?p=8407", "url": "/azure/adding-a-bunch-of-ips-to-an-azure-nsg/", "title": "Adding a bunch of IPs to an Azure NSG", "content_html": "

So… there was an Azure outage today. Related to Azure Front Door. That was my evening gone!

\n

As the resident PowerShell person on the incident call, as part of implementing some workarounds I had to add a bunch of IP addresses to a Network Security Group. I was given a text file with a bunch of IP addresses, and my task was to add them all to the NSG as “Deny” for “Inbound” traffic. Here’s what I did:

Connect-AzAccount -Subscription <SubscriptionName>\r\n\r\n# get the NSG\r\n$nsg = Get-AzNetworkSecurityGroup -Name '<NSG Name>' -ResourceGroupName '<Resource Group Name>' \r\n\r\n$counter = 1000 # replace with whatever priority the rules must start from\r\n\r\nforeach ($address in Get-Content .\\ipblocklist.txt | Select-Object -Unique) {\r\n  # construct the rule name\r\n  # todo: the -replace needs improvement via some regex to remove *any* unaccepted characters\r\n  $ruleName = \"Deny-\" + $($address -replace '\\/','-' -replace '\\:','')\r\n\r\n  # rule name can't exceed 80 chracters\r\n  if ($ruleName.Length -gt 80) { $ruleName = $ruleName.substring(0,79) }\r\n  \r\n  # add the rule to the in-memory copy of the rules\r\n  $nsg | Add-AzNetworkSecurityRuleConfig -Name $ruleName -SourceAddressPrefix $address -Access \"Deny\" -DestinationPortRange \"80\",\"443\" -DestinationAddressPrefix \"*\" -SourcePortRange \"*\" -Priority $counter -Protocol '*' -Direction 'Inbound'\r\n\r\n  # increment the counter\r\n  $counter++\r\n\r\n}\r\n\r\n# commit to Azure\r\n$nsg | Set-AzNetworkSecurityGroup

Had to piecemeal the code from various sources as the Microsoft Learn website was down so I couldn’t refer to it for the cmdlets and their switches.

\n

\"Sweating

\n

Two things the code could do with improving:

\n\n

 

\n", "content_text": "So… there was an Azure outage today. Related to Azure Front Door. That was my evening gone!\nAs the resident PowerShell person on the incident call, as part of implementing some workarounds I had to add a bunch of IP addresses to a Network Security Group. I was given a text file with a bunch of IP addresses, and my task was to add them all to the NSG as “Deny” for “Inbound” traffic. Here’s what I did:Connect-AzAccount -Subscription <SubscriptionName>\r\n\r\n# get the NSG\r\n$nsg = Get-AzNetworkSecurityGroup -Name '<NSG Name>' -ResourceGroupName '<Resource Group Name>' \r\n\r\n$counter = 1000 # replace with whatever priority the rules must start from\r\n\r\nforeach ($address in Get-Content .\\ipblocklist.txt | Select-Object -Unique) {\r\n # construct the rule name\r\n # todo: the -replace needs improvement via some regex to remove *any* unaccepted characters\r\n $ruleName = \"Deny-\" + $($address -replace '\\/','-' -replace '\\:','')\r\n\r\n # rule name can't exceed 80 chracters\r\n if ($ruleName.Length -gt 80) { $ruleName = $ruleName.substring(0,79) }\r\n \r\n # add the rule to the in-memory copy of the rules\r\n $nsg | Add-AzNetworkSecurityRuleConfig -Name $ruleName -SourceAddressPrefix $address -Access \"Deny\" -DestinationPortRange \"80\",\"443\" -DestinationAddressPrefix \"*\" -SourcePortRange \"*\" -Priority $counter -Protocol '*' -Direction 'Inbound'\r\n\r\n # increment the counter\r\n $counter++\r\n\r\n}\r\n\r\n# commit to Azure\r\n$nsg | Set-AzNetworkSecurityGroupHad to piecemeal the code from various sources as the Microsoft Learn website was down so I couldn’t refer to it for the cmdlets and their switches.\n\nTwo things the code could do with improving:\n\nCheck if the IP address/ subnet is already in the NSG\nFix the bit where I construct the rule name to remove any unaccepted characters than just the two I happened to encounter.\n\n ", "date_published": "2025-10-30T01:05:53+00:00", "date_modified": "2025-10-30T01:05:53+00:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "networking", "powershell", "Azure, Azure AD, Graph, M365" ] }, { "id": "https://rakhesh.com/?p=8404", "url": "/tv/a-house-of-dynamite-english-movie/", "title": "A House of Dynamite (English movie)", "content_html": "

I enjoyed “A House of Dynamite” which was released this week on Netflix. Not many people seem to like it though!

\n

\"\"

\n

Yes, the ending was abrupt, but I loved it. To me that made sense, and it was the only possible ending one could have for a movie like this. The movie wasn’t about the ending, it was about the ordeal leading towards the ending. And whatever happens in the end – be it a false alarm or something devastating – that doesn’t take away from the fear and panic and decision making uncertainity around it. That was the point of the movie.

\n", "content_text": "I enjoyed “A House of Dynamite” which was released this week on Netflix. Not many people seem to like it though!\n\nYes, the ending was abrupt, but I loved it. To me that made sense, and it was the only possible ending one could have for a movie like this. The movie wasn’t about the ending, it was about the ordeal leading towards the ending. And whatever happens in the end – be it a false alarm or something devastating – that doesn’t take away from the fear and panic and decision making uncertainity around it. That was the point of the movie.", "date_published": "2025-10-30T00:52:05+00:00", "date_modified": "2025-10-30T00:52:05+00:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "TV, Movies, Music" ] }, { "id": "https://rakhesh.com/?p=8400", "url": "/azure/beware-including-my-sign-ins-in-conditional-access-policies/", "title": "Beware including \u201cMy Sign-ins\u201d in Conditional Access policies", "content_html": "

Earlier this month Microsoft made the “My Sign-ins” app available under Conditional Access policies. That was great news to us. So far, as a “light touch” effort at zero trust, we had been restricting sign-ins to the “Office 365” app to firm managed devices. This includes all the common apps you’d expect to encounter when using the M365 suite, so is a pretty good starting point.

\n

Not mentioned in the list of apps the “Office 365” app includes, but seems to be implicitly covered in our testing, is the “My Profile” app. If you visit https://mysignins.microsoft.com/ and click on any of “Devices”, “Organisations”, “My Groups” and “My Access” these come under the “My Profile” app.

\n

\"\"

\n

So requiring a managed device to access the “Office 365” app ensures you can’t access any of these from a non-managed device either. But that still leaves “My Apps” and the https://mysignins.microsoft.com/ page itself, and we were wondering if these can be locked down.

\n

“My Apps” can be controlled. It should appear as an app that can be selected in conditional access, and if it does not one can enable it thus:

New-MgServicePrincipal -BodyParameter @{ AppId = \"2793995e-0a7d-40d7-bd35-6968ba142197\" }

And then when we learnt that “My Sign-ins” too can be selected, it was great news. (If that app too doesn’t appear, run the same cmdlet as above but use “19db86c3-b2b9-44cc-b339-36da233a3be2” as the App Id. Better still, read that linked post coz that’s where I am copy pasting this info from).

\n

So we naively went and added “My Sign-ins” and locked it too down to managed devices (for a test group of course, not rolling out major changes to the whole firm!). And that’s when we learnt that doing so is not a good idea.

\n

Why? Coz while we were interested in blocking access to the https://mysignins.microsoft.com/ page, a key part of the page is the “Security info” section. I never thought of it as part of the My Sign-ins page as by habit I tend to visit https://aka.ms/mysecurity info for it, but that URL is actually https://mysignins.microsoft.com/security-info and so technically it comes under the “My Sign-ins” app. \"\ud83d\ude0a\" Which means, with our policy in place, no one can even setup their MFA etc. from an unmanaged device – which is not what you want!

\n

Of course, you do want to lock down who can register MFA, and we too do that by having a conditional access policy that targets the “Register security info” action to require MFA or be from trusted locations, and to get around the chicken-and-egg problem of someone from an untrusted location not being able to register their first MFA we rely on Temporary Access Passes (TAP) – but by locking down the “My Sign-ins” app to managed devices, that basically overrides the TAP policy.

\n

So lesson learnt. We didn’t go ahead with locking down “My Sign-ins”. And like the article suggests, the real use of being able to select “My Sign-ins” in conditional access policies is to exclude it from a policy when you want the policy to apply to “All Cloud Apps” except a few, not to include it.

\n", "content_text": "Earlier this month Microsoft made the “My Sign-ins” app available under Conditional Access policies. That was great news to us. So far, as a “light touch” effort at zero trust, we had been restricting sign-ins to the “Office 365” app to firm managed devices. This includes all the common apps you’d expect to encounter when using the M365 suite, so is a pretty good starting point.\nNot mentioned in the list of apps the “Office 365” app includes, but seems to be implicitly covered in our testing, is the “My Profile” app. If you visit https://mysignins.microsoft.com/ and click on any of “Devices”, “Organisations”, “My Groups” and “My Access” these come under the “My Profile” app.\n\nSo requiring a managed device to access the “Office 365” app ensures you can’t access any of these from a non-managed device either. But that still leaves “My Apps” and the https://mysignins.microsoft.com/ page itself, and we were wondering if these can be locked down.\n“My Apps” can be controlled. It should appear as an app that can be selected in conditional access, and if it does not one can enable it thus:New-MgServicePrincipal -BodyParameter @{ AppId = \"2793995e-0a7d-40d7-bd35-6968ba142197\" }And then when we learnt that “My Sign-ins” too can be selected, it was great news. (If that app too doesn’t appear, run the same cmdlet as above but use “19db86c3-b2b9-44cc-b339-36da233a3be2” as the App Id. Better still, read that linked post coz that’s where I am copy pasting this info from).\nSo we naively went and added “My Sign-ins” and locked it too down to managed devices (for a test group of course, not rolling out major changes to the whole firm!). And that’s when we learnt that doing so is not a good idea.\nWhy? Coz while we were interested in blocking access to the https://mysignins.microsoft.com/ page, a key part of the page is the “Security info” section. I never thought of it as part of the My Sign-ins page as by habit I tend to visit https://aka.ms/mysecurity info for it, but that URL is actually https://mysignins.microsoft.com/security-info and so technically it comes under the “My Sign-ins” app. Which means, with our policy in place, no one can even setup their MFA etc. from an unmanaged device – which is not what you want!\nOf course, you do want to lock down who can register MFA, and we too do that by having a conditional access policy that targets the “Register security info” action to require MFA or be from trusted locations, and to get around the chicken-and-egg problem of someone from an untrusted location not being able to register their first MFA we rely on Temporary Access Passes (TAP) – but by locking down the “My Sign-ins” app to managed devices, that basically overrides the TAP policy.\nSo lesson learnt. We didn’t go ahead with locking down “My Sign-ins”. And like the article suggests, the real use of being able to select “My Sign-ins” in conditional access policies is to exclude it from a policy when you want the policy to apply to “All Cloud Apps” except a few, not to include it.", "date_published": "2025-10-30T00:43:31+00:00", "date_modified": "2025-10-30T00:43:31+00:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "conditional access", "my signins", "Azure, Azure AD, Graph, M365" ] }, { "id": "https://rakhesh.com/?p=8384", "url": "/azure/firefox-multi-account-containers-and-entra-id-sso/", "title": "Firefox multi-account containers and Entra ID SSO", "content_html": "

I use Firefox. And I use the multi-account containers extension heavily in that. Very useful at work too when I have multiple test account and want to sign in with these, as I can open these up in separate tabs as opposed to separate windows as one would do with Edge etc.

\n

As part of testing some new Conditional Access policies however, wherein we were limiting users to hybrid joined or compliant devices, I noticed that my test accounts stopped working with the “You can’t get there from here” message.

\n

\"\"

\n

My machine is hybrid joined, and yet the error message showed it as an unregistered device. The help message points to this Firefox KB article to allow Windows SSO and that was enabled in my case already.

\n

\"\"

\n

The article also said the account must be added to Windows itself. Under Settings:

\n

\"\"

\n

\"\"

\n

I added my test account in there by clicking on “Add a work or school account”. Closed and opened Firefox, and still the account didn’t work.

\n

Thinking this might be an issue with the containers extension itself, I opened up a new tab not in any container and tried browsing with that, and that worked! I was prompted to select an account from all the accounts in the screenshot above, and selecting my test account let me know. So the issue was to do with the containers extension.

\n

Googling on that brought me to this forum post and from there to this GitHub wiki. Looks like this was fixed back in 2023 itself, just not exposed via the UI. Feel free to follow the steps in that wiki, else follow on to see what I did to fix it:

\n

1) Opened up my profile folder.\u00a0 If you don’t know where that is, click on Firefox help:

\n

\"\"

\n

More troubleshooting info:

\n

\"\"

\n

Click open folder to open the profiles folder.

\n

\"\"

\n

2) Open containers.json. Preferably in an editor like VS Code etc. such that you can format it (optional).

\n

3) Find the entry for the container you are working with and note its userContextId.

\n

\"\"

\n

4) Then open about:config in Firefox, accept the warning, and type in network.http.windows-sso.container-enabled.<the id>. (There’s a typo in the screenshot below, the 21 should read as 12 coz that’s the correct id).

\n

\"\"

\n

This creates a new entry.

\n

That’s all! Now if I try and browse via that container, it picks up all my accounts linked to the device, and I can select the one I want to use.

\n

\"\"

\n

The SSO part doesn’t automagically know what account you want to use (since as far as it’s concerned you are signed in with more than one account) so you have to select the account each time. But that’s not an extension issue, and makes sense. It’s a minor irritation, but I can live with that. (And you encounter this in Edge etc. too if I am signed in to a profile with more than one account – just got to be careful of selecting the correct account each time).

\n

Bonus tip

\n

Since I want this to work with all my containers, and I see that they are in sequential order, what I did is 1) close Firefox and 2) open the prefs.js file in the profiles folder (this is what contains the settings from about:config). I then found the line that I had added:

\n

\"\"

\n

And I made a bunch more copies of it with numbers starting from 1 – 25 (figured I am on 12 now and I might as well make a few extra for the future). Here’s the lines if anyone else wants to copy paste:

user_pref(\"network.http.windows-sso.container-enabled.1\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.2\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.3\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.4\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.5\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.6\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.7\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.8\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.9\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.10\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.11\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.12\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.13\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.14\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.15\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.16\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.17\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.18\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.19\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.20\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.21\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.22\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.23\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.24\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.25\", true);

That’s all!

\n

New containers

\n

Good to know. When I create a new container and the account I want to use is not listed, adding it via “User another account” will not help.

\n

\"\"

\n

Because all that will do is add the account within the container, not to Windows itself.

\n

\"\"

\n

Selected the “Signed in” account above will not work if there are any conditional access policies that need device compliance etc., so one must always remember to go and add it in Windows itself under Settings > Accounts > Email & Accounts.

\n

My thanks to the devlopers of the extension and also for their efforts in fixing this issue. \"\ud83d\ude4f\"

\n", "content_text": "I use Firefox. And I use the multi-account containers extension heavily in that. Very useful at work too when I have multiple test account and want to sign in with these, as I can open these up in separate tabs as opposed to separate windows as one would do with Edge etc.\nAs part of testing some new Conditional Access policies however, wherein we were limiting users to hybrid joined or compliant devices, I noticed that my test accounts stopped working with the “You can’t get there from here” message.\n\nMy machine is hybrid joined, and yet the error message showed it as an unregistered device. The help message points to this Firefox KB article to allow Windows SSO and that was enabled in my case already.\n\nThe article also said the account must be added to Windows itself. Under Settings:\n\n\nI added my test account in there by clicking on “Add a work or school account”. Closed and opened Firefox, and still the account didn’t work.\nThinking this might be an issue with the containers extension itself, I opened up a new tab not in any container and tried browsing with that, and that worked! I was prompted to select an account from all the accounts in the screenshot above, and selecting my test account let me know. So the issue was to do with the containers extension.\nGoogling on that brought me to this forum post and from there to this GitHub wiki. Looks like this was fixed back in 2023 itself, just not exposed via the UI. Feel free to follow the steps in that wiki, else follow on to see what I did to fix it:\n1) Opened up my profile folder.\u00a0 If you don’t know where that is, click on Firefox help:\n\nMore troubleshooting info:\n\nClick open folder to open the profiles folder.\n\n2) Open containers.json. Preferably in an editor like VS Code etc. such that you can format it (optional).\n3) Find the entry for the container you are working with and note its userContextId.\n\n4) Then open about:config in Firefox, accept the warning, and type in network.http.windows-sso.container-enabled.<the id>. (There’s a typo in the screenshot below, the 21 should read as 12 coz that’s the correct id).\n\nThis creates a new entry.\nThat’s all! Now if I try and browse via that container, it picks up all my accounts linked to the device, and I can select the one I want to use.\n\nThe SSO part doesn’t automagically know what account you want to use (since as far as it’s concerned you are signed in with more than one account) so you have to select the account each time. But that’s not an extension issue, and makes sense. It’s a minor irritation, but I can live with that. (And you encounter this in Edge etc. too if I am signed in to a profile with more than one account – just got to be careful of selecting the correct account each time).\nBonus tip\nSince I want this to work with all my containers, and I see that they are in sequential order, what I did is 1) close Firefox and 2) open the prefs.js file in the profiles folder (this is what contains the settings from about:config). I then found the line that I had added:\n\nAnd I made a bunch more copies of it with numbers starting from 1 – 25 (figured I am on 12 now and I might as well make a few extra for the future). Here’s the lines if anyone else wants to copy paste:user_pref(\"network.http.windows-sso.container-enabled.1\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.2\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.3\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.4\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.5\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.6\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.7\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.8\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.9\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.10\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.11\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.12\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.13\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.14\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.15\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.16\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.17\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.18\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.19\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.20\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.21\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.22\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.23\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.24\", true);\r\nuser_pref(\"network.http.windows-sso.container-enabled.25\", true);That’s all!\nNew containers\nGood to know. When I create a new container and the account I want to use is not listed, adding it via “User another account” will not help.\n\nBecause all that will do is add the account within the container, not to Windows itself.\n\nSelected the “Signed in” account above will not work if there are any conditional access policies that need device compliance etc., so one must always remember to go and add it in Windows itself under Settings > Accounts > Email & Accounts.\nMy thanks to the devlopers of the extension and also for their efforts in fixing this issue.", "date_published": "2025-10-29T13:12:38+00:00", "date_modified": "2025-10-29T13:12:38+00:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "conditional access", "device compliance", "device registration", "firefox", "SSO", "Azure, Azure AD, Graph, M365" ] }, { "id": "https://rakhesh.com/?p=8380", "url": "/azure/kql-get-a-list-of-users-removed-from-an-entra-id-group/", "title": "KQL \u2013 Get a list of users removed from an Entra ID group", "content_html": "

I wanted to get a list of users recently removed from an Entra ID group. I can see the removals in the audit logs, but there’s easy way to add a column showing removals.

\n

Enter KQL. We are sending all audit logs to a Log Analytics workspace, so I queried this info that way. This is a very trivial use of KQL, but I am just putting it here for my own reference later on…\u00a0

\n

This code gives you everything from AuditLogs in the last 30 days that have “Remove member from group” as the activity.

AuditLogs\r\n| where TimeGenerated > ago(30d)\r\n| where ActivityDisplayName == 'Remove member from group'

This activity name corresponds to what you see in the portal too.

\n

\"\"

\n

Take one of the result and expand it until you get to TargetResources. The second entry in that has the group that’s affected. Expand that to get to the id, and then right click and filter for that.

\n

\"\"

\n

This modifies the code thus:

AuditLogs\r\n| where TimeGenerated > ago(30d)\r\n| where ActivityDisplayName == 'Remove member from group' \r\n| where TargetResources[1].id == \"<the group guid>\"

Then similarly expand the first entry under TargetResources, select UPN in there, and extend that. (In my case, the already selected entry didn’t have a user being removed… so I found an entry with a user removal).

\n

Finally project just that extended column. The end result looks like this:

AuditLogs\r\n| where TimeGenerated > ago(30d)\r\n| where ActivityDisplayName == 'Remove member from group' \r\n| where TargetResources[1].id == \"<the group guid>\"\r\n| extend userPrincipalName_ = tostring(TargetResources[0].userPrincipalName)\r\n| project userPrincipalName_

This gives a list of UPNs removed from the group.

\n

Of course now that you/ future me have read this blog post, just copy paste the above KQL and use it directly. :)

\n", "content_text": "I wanted to get a list of users recently removed from an Entra ID group. I can see the removals in the audit logs, but there’s easy way to add a column showing removals.\nEnter KQL. We are sending all audit logs to a Log Analytics workspace, so I queried this info that way. This is a very trivial use of KQL, but I am just putting it here for my own reference later on…\u00a0\nThis code gives you everything from AuditLogs in the last 30 days that have “Remove member from group” as the activity.AuditLogs\r\n| where TimeGenerated > ago(30d)\r\n| where ActivityDisplayName == 'Remove member from group'This activity name corresponds to what you see in the portal too.\n\nTake one of the result and expand it until you get to TargetResources. The second entry in that has the group that’s affected. Expand that to get to the id, and then right click and filter for that.\n\nThis modifies the code thus:AuditLogs\r\n| where TimeGenerated > ago(30d)\r\n| where ActivityDisplayName == 'Remove member from group' \r\n| where TargetResources[1].id == \"<the group guid>\"Then similarly expand the first entry under TargetResources, select UPN in there, and extend that. (In my case, the already selected entry didn’t have a user being removed… so I found an entry with a user removal).\nFinally project just that extended column. The end result looks like this:AuditLogs\r\n| where TimeGenerated > ago(30d)\r\n| where ActivityDisplayName == 'Remove member from group' \r\n| where TargetResources[1].id == \"<the group guid>\"\r\n| extend userPrincipalName_ = tostring(TargetResources[0].userPrincipalName)\r\n| project userPrincipalName_This gives a list of UPNs removed from the group.\nOf course now that you/ future me have read this blog post, just copy paste the above KQL and use it directly. :)", "date_published": "2025-10-27T10:54:52+00:00", "date_modified": "2025-10-27T10:54:52+00:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "auditlogs", "kql", "Azure, Azure AD, Graph, M365" ] }, { "id": "https://rakhesh.com/?p=8378", "url": "/azure/updating-allowtoaddguests/", "title": "Updating AllowToAddGuests", "content_html": "

A colleague reached out regarding an issue. He had created a Team and applied a sensitivity label to it. Unfortunately, this sensitivity label disabled external sharing. He then went ahead and remvoed the label, but he still couldn’t add external users (even after waiting for a few days).

\n

I took a look at the group and found that sharing to guests is still disabled. This can be done via an EXO cmdlet: Get-UnifiedGroup '<group name>' | fl *Gues*

AllowAddGuests : False

Turns out the Set-UnifiedGroup cmdlet doesn’t have an option to toggle this. Got to use Graph API for this, specifically the group settings endpoint. Here’s what I did:

# To get the group Id\r\n$groupObj = Get-MgGroup -Filter \"DisplayName eq '<group name>'\"\r\n\r\n$groupId = $groupObj.Id\r\n\r\n# Get the existing settings. Need this for the GroupSettingId. \r\n$groupSettingsObj = Get-MgGroupSetting -GroupId $groupId\r\n\r\n# To allow Guests - body of the request\r\n$params = @{\r\n\tvalues = @(\r\n\t\t@{\r\n\t\t\tname = \"AllowToAddGuests\"\r\n\t\t\tvalue = \"True\"\r\n\t\t}\r\n\t)\r\n}\r\n\r\nUpdate-MgGroupSetting -GroupId $groupId -BodyParameter $params -GroupSettingId $groupSettingsObj.Id\r\n\r\n# To view the setting via Graph after the change\r\n(Get-MgGroupSetting -GroupId $groupId).Values

 

\n", "content_text": "A colleague reached out regarding an issue. He had created a Team and applied a sensitivity label to it. Unfortunately, this sensitivity label disabled external sharing. He then went ahead and remvoed the label, but he still couldn’t add external users (even after waiting for a few days).\nI took a look at the group and found that sharing to guests is still disabled. This can be done via an EXO cmdlet: Get-UnifiedGroup '<group name>' | fl *Gues*AllowAddGuests : FalseTurns out the Set-UnifiedGroup cmdlet doesn’t have an option to toggle this. Got to use Graph API for this, specifically the group settings endpoint. Here’s what I did:# To get the group Id\r\n$groupObj = Get-MgGroup -Filter \"DisplayName eq '<group name>'\"\r\n\r\n$groupId = $groupObj.Id\r\n\r\n# Get the existing settings. Need this for the GroupSettingId. \r\n$groupSettingsObj = Get-MgGroupSetting -GroupId $groupId\r\n\r\n# To allow Guests - body of the request\r\n$params = @{\r\n\tvalues = @(\r\n\t\t@{\r\n\t\t\tname = \"AllowToAddGuests\"\r\n\t\t\tvalue = \"True\"\r\n\t\t}\r\n\t)\r\n}\r\n\r\nUpdate-MgGroupSetting -GroupId $groupId -BodyParameter $params -GroupSettingId $groupSettingsObj.Id\r\n\r\n# To view the setting via Graph after the change\r\n(Get-MgGroupSetting -GroupId $groupId).Values ", "date_published": "2025-10-22T14:14:12+01:00", "date_modified": "2025-10-22T14:14:12+01:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "Azure, Azure AD, Graph, M365" ] }, { "id": "https://rakhesh.com/?p=8374", "url": "/azure/managing-o365-add-ins-with-the-application-admin-role/", "title": "Managing O365 add-ins with the Application Admin role", "content_html": "

O365 add-ins, also known as M365 Apps or Integrated Apps, are what you can deploy to Outlook etc. via this section of the M365 admin center.

\n

\"\"

\n

As per the docs, you need to be a Global Admin or an Exchange Admin (possibly with Application Admin, depending on certain conditions) to do this. And while that seems to be the case with the portal, it looks like one can do some tasks with only the Application Admin role via PowerShell.

\n

In my case I don’t need to deploy new add-ins, only manage who an existing add-in is deployed to.

\n

The instructions for using PowerShell are in this link, but below is what I did.

# Install the module, import it etc. \r\nInstall-Module -Name O365CentralizedAddInDeployment\r\nImport-Module -Name O365CentralizedAddInDeployment\r\n\r\n# Login \r\nConnect-OrganizationAddInService

After that, to get all the available add-ins, one can do:

Get-OrganizationAddIn

And then to get the details of a specific add-in (e.g. publisher, version), one can do:

Get-OrganizationAddIn -ProductId 815b5912-1fb6-44ac-8971-f54600fdd599 | fl *

And then to publish it to users and groups, one can do:

# Add a group...\r\nSet-OrganizationAddInAssignments -ProductId 815b5912-1fb6-44ac-8971-f54600fdd599 -Add -Members \"dd3772c5-3b54-48fb-b5a3-c724855f2275\"\r\n\r\n# Remove a group\r\nSet-OrganizationAddInAssignments -ProductId 815b5912-1fb6-44ac-8971-f54600fdd599 -Remove -Members \"dd3772c5-3b54-48fb-b5a3-c724855f2275\"

# Add a user...\r\nSet-OrganizationAddInAssignments -ProductId 815b5912-1fb6-44ac-8971-f54600fdd599 -Add -Members \"me@mydomain.com\"\r\n\r\n# Remove a user...\r\nSet-OrganizationAddInAssignments -ProductId 815b5912-1fb6-44ac-8971-f54600fdd599 -Remove -Members \"me@mydomain.com\"

That’s all!

\n

Interestingly, sometimes the portal too let me do these tasks with the Application Admin role; but not always. I find PowerShell to more consistently work rather than the portal.

\n", "content_text": "O365 add-ins, also known as M365 Apps or Integrated Apps, are what you can deploy to Outlook etc. via this section of the M365 admin center.\n\nAs per the docs, you need to be a Global Admin or an Exchange Admin (possibly with Application Admin, depending on certain conditions) to do this. And while that seems to be the case with the portal, it looks like one can do some tasks with only the Application Admin role via PowerShell.\nIn my case I don’t need to deploy new add-ins, only manage who an existing add-in is deployed to.\nThe instructions for using PowerShell are in this link, but below is what I did.# Install the module, import it etc. \r\nInstall-Module -Name O365CentralizedAddInDeployment\r\nImport-Module -Name O365CentralizedAddInDeployment\r\n\r\n# Login \r\nConnect-OrganizationAddInServiceAfter that, to get all the available add-ins, one can do:Get-OrganizationAddInAnd then to get the details of a specific add-in (e.g. publisher, version), one can do:Get-OrganizationAddIn -ProductId 815b5912-1fb6-44ac-8971-f54600fdd599 | fl *And then to publish it to users and groups, one can do:# Add a group...\r\nSet-OrganizationAddInAssignments -ProductId 815b5912-1fb6-44ac-8971-f54600fdd599 -Add -Members \"dd3772c5-3b54-48fb-b5a3-c724855f2275\"\r\n\r\n# Remove a group\r\nSet-OrganizationAddInAssignments -ProductId 815b5912-1fb6-44ac-8971-f54600fdd599 -Remove -Members \"dd3772c5-3b54-48fb-b5a3-c724855f2275\"# Add a user...\r\nSet-OrganizationAddInAssignments -ProductId 815b5912-1fb6-44ac-8971-f54600fdd599 -Add -Members \"me@mydomain.com\"\r\n\r\n# Remove a user...\r\nSet-OrganizationAddInAssignments -ProductId 815b5912-1fb6-44ac-8971-f54600fdd599 -Remove -Members \"me@mydomain.com\"That’s all!\nInterestingly, sometimes the portal too let me do these tasks with the Application Admin role; but not always. I find PowerShell to more consistently work rather than the portal.", "date_published": "2025-10-20T10:55:42+01:00", "date_modified": "2025-10-20T10:55:42+01:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "add-ins", "Azure, Azure AD, Graph, M365" ] }, { "id": "https://rakhesh.com/?p=8361", "url": "/azure/azure-front-door-azure-functions-fragments-managed-identity-azure-tables-etc/", "title": "Azure Front Door, Azure Functions, Fragments, Managed Identity, Azure Tables, etc.", "content_html": "

At work I spent some time yesterday working on a side project to setup forwarding from one set of URLs to another. We are migrating a service from our on-prem world to the cloud, and there are tons of URLs pointing that will now get invalidated as a result.

\n

In the on-prem world the URLs are of the form https://olddomain.com/app/#/secret/secretId, while in the cloud world it is very similar and looks like https://newdomain.com/app/#/secret/newsecretId. While it’s easy to setup a redirect from olddomain.com to newdomain.com, the problem is that the secretId changes to a new number newsecretId in the cloud, so you can’t just do a catch-all redirect.

\n

I had a CSV file of the old and new secret Ids, and I wondered what I can do with it.

\n

(By the way, side promo of a new tool I discovered recently. SmoothCSV. If you deal regularly with CSVs, and don’t want to open Excel/ LibreOffice for it, nor use a regular text or code editor as that doesn’t look right either, this is the tool for you! Open source and cross-platform).

\n

Azure Front Door

\n

One more thing we wanted to do was have https://olddomain.com/* redirect to a SharePoint Online with some info on the migration for anyone using the old URLs, and so we had setup Azure Front Door for this. It’s a simple rule there to redirect any URLs beginning with https://olddomain.com/ to https://newdomain.com/ and I wanted to piggy back on that to handle URLs beginning with https://olddomain.com/app/#/secret/ differently somehow.

\n

This is where I encountered my first issue, which stumped me for a while. I only realized later what was happening, but by then I had stumbled upon a fix by blindly trying different things to get it working.

\n

Here’s what I did. In Front Door I have two rules:

\n\n

Rule 2 was the original rule, created as part of the initial requirement. Rule 1 is what I created and put as a higher priority over Rule 1, so I can do my per secret Id redirecting somehow.

\n

\"\"

\n

Sounds like it should work, but it does not! I never hit Rule 2, I always keep going to Rule 1. I even changed Rule 2 to be a RegEx match, but that didn’t help either.

\n

The fix, in the end, was to match on the URL beginning with https://olddomain.com/app/. That worked, and while I stumbled upon it by troubleshooting and removing parts of the URL to see what minimum works, I wasn’t sure why this was the fix. Nevertheless I moved on to the next part of the problem.

\n

Enter Azure Functions

\n

My idea was to have these URLs be redirected to an Azure Function. I’ll import the CSV file of old and new secret Ids into an Azure Tables, the Azure Function will receive an URL that looks like https://olddomain.com/app/#/secret/secretId from Azure Front Door, I will use PowerShell to extract the secretId, lookup the new value from Table, construct a new URL https://newdomain.com/app/#/secret/newsecretId and send that to the user.

\n

Turns out sending a user a new URL is very easy. From a bit of Googling I learnt that a bit of HTML like this does the job:

<html>\r\n<head>\r\n    <meta http-equiv=\"refresh\" content=\"1;url=https://newdomain.com/app/#/secret/$newSecretId/\" />\r\n    <title>Redirecting...</title>\r\n</head>\r\n<body>\r\n</body>\r\n</html>

What the above does is that it tells the browser receiving the page to do a refresh to the given URL after 1 second. I put $newsecretId above, coz that’s what will change based on the lookup.

\n

In Azure Function, all I need do is the following (these are only snippets, not the whole code):

# Put the HTML from above into a variable; this assumes I have looked up and got a value for $newSecretId already\r\n$html = @\"\r\n<html>\r\n<head>\r\n    <meta http-equiv=\"refresh\" content=\"1;url=https://newdomain.com/app/#/secret/$newSecretId/\" />\r\n    <title>Redirecting...</title>\r\n</head>\r\n<body>\r\n</body>\r\n</html>\r\n\"@\r\n\r\n# Return that\r\nPush-OutputBinding -Name Response -Value ([HttpResponseContext]@{\r\n    StatusCode = 200\r\n    # This is crucial, without the ContentType the receiving browser will treat the HTML as plain text \r\n    ContentType = \"text/html; charset=utf-8\" \r\n    Body = $html\r\n})\r\n\r\nexit

Neat!

\n

But of course, this didn’t work as expected! There wouldn’t be a blog post if it did. \"\ud83d\ude43\"

\n

What was failing was the part not in the snippet above. When I receive the redirected URL from Azure Front Door, I wasn’t getting the old secret Id in the link.

\n

A digression

\n

A bit of a digression first.

\n

Typically an Azure Function URL looks like this: https://<APP_NAME>.azurewebsites.net/api/<FUNCTION_NAME>/

\n

It is possible to remove the /api/ but by setting the following JSON in the host.json file.

{\r\n    \"version\": \"2.0\",\r\n    \"isDefaultHostConfig\": true,\r\n    \"managedDependency\": {\r\n        \"Enabled\": true\r\n    },\r\n    \"extensionBundle\": {\r\n        \"id\": \"Microsoft.Azure.Functions.ExtensionBundle\",\r\n        \"version\": \"[4.*, 5.0.0)\"\r\n    },\r\n    \"extensions\": {\r\n        \"http\": {\r\n            \"routePrefix\": \"\"\r\n        }\r\n    }\r\n}

It is also possible to remove the <FUNCTION_NAME> bit from the URL. The official doc tells you how but is incomplete as it only gives you examples of changing <FUNCTION_NAME> to something else. I wanted to remove it though, if possible, and when Googling didn’t help I turned to ChatGPT and from there I learnt that by modifying the function.json file thus, I can achieve it:

{\r\n  \"bindings\": [\r\n    {\r\n      \"authLevel\": \"anonymous\",\r\n      \"type\": \"httpTrigger\",\r\n      \"direction\": \"in\",\r\n      \"name\": \"Request\",\r\n      \"methods\": [\r\n        \"get\"\r\n      ],\r\n      \"route\": \"{*path}\"\r\n    },\r\n    {\r\n      \"type\": \"http\",\r\n      \"direction\": \"out\",\r\n      \"name\": \"Response\"\r\n    }\r\n  ]\r\n}

The \"route\": \"{*path}\" bit makes it so that all paths are sent to this Function app. I couldn’t find much documentation on this wildcard property, but I did find some links that explained this property in general (like this one, for instance). Glad I stumbled upon it via ChatGPT!

\n

With this in place I can access whatever is sent to the function via the $Request.Params.path property (the name path matches whatever is puting in the route definition).

\n

Back to the issue

\n

So now I have Azure Front Door redirecting https://olddomain.com/app/#/secret/secretId to my Function App. When redirecting, it will send the browser to <APP_NAME>.azurewebsites.net/ and also include app/#/secret/secretId.

\n

\"\"

\n

I know the app/#/secret/secretId part will come through, coz I am not setting anything in the Destination path and Destination fragment, so Front Door will send whatever it gets over. This is why I removed the extra bits from the Function URL, coz if I didn’t do that I’d have to put api/<FUNCTION_NAME>/ in the Destination path, and I didn’t want to do that coz there’s no way to add that and have Front Door append whatever it gets. By leaving it blank, Front Door will send everything over.

\n

And yet, in Azure Function, when I examine the path via the $Request.Params.path property, all I see is “app/“. Nothing after that! Odd.

\n

Fragments

\n

And now we come to fragments. \"\ud83d\ude00\"

\n

I had seen the word fragment in Front Door, but hadn’t paid much attention. But once I got to troubleshooting why the Function App wasn’t working, I learnt about it. This also explained to me why Front Door wasn’t working with my original URL way above when I first tried it.

\n

You see, when you have a URL like https://olddomain.com/app/#/secret/secretId, the bit after the # is what’s known as a fragment. We typically see them in web pages when used to send the browser to a specific heading/ id tag of a page. Since they are meant to be specific to things within a page, browsers don’t send them to the server. Because the idea is that the browser will only send a request to the page it wants, get it, and then use the fragment to find what it needs.

\n

This is pretty neat from a privacy point of view, coz as you can guess from the URLs above what I am dealing with are some sort of secrets, and clearly the designers of that tool had security in mind and so they put the secret Id part of the URL within a fragment. So no proxy servers or firewalls will ever see the secret Id, all they will see is https://olddomain.com/app/ which presumeably is a JavaScript page, and once that’s got the page itself will be sent #/secret/secretId and it can use that to show the secret Id. Nothing is captured anywhere. (That’s my guess at least on how this is used…)

\n

Unfortunately, this isn’t ideal for me, coz that’s why 1) Front Door never worked when I tried to match on URLs beginning with https://olddomain.com/app/#/secret/ coz it never saw anything after the #, and 2) why Azure Function too cannot see the secret Id. There’s simply no way for me to get it! Aaargh!

\n

A bit of JavaScript to the rescue

\n

But hang on, yes Azure Function can’t see the full URL, but since it’s in the browser, presumeably I can use some JavaScript to get it?

\n

Enter ChatGPT. I asked it for some JavaScript code which can be used to extract the URL from a browser and send to a different URL. It gave me the following code:

<!DOCTYPE html>\r\n<html>\r\n<head>\r\n  <meta charset=\"utf-8\" />\r\n  <title>Redirecting...</title>\r\n  <script>\r\n    (function() {\r\n      const fragment = window.location.hash ? window.location.hash.substring(1) : \"\";\r\n      const basePath = \"${path}\";\r\n      let redirectUrl = \"/redirect?path=\" + encodeURIComponent(basePath);\r\n      if (fragment) redirectUrl += \"&fragment=\" + encodeURIComponent(fragment);\r\n      window.location.replace(redirectUrl);\r\n    })();\r\n  </script>\r\n</head>\r\n<body>\r\n  <p>Redirecting...</p>\r\n</body>\r\n</html>

Here’s what I can do. Azure Function gets called. It can check what the URL it received it (via the $Request.Params.path property). If the path is just “app/“, then it sends the above HTML to the browser.

Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{\r\n    StatusCode = 200\r\n    # This is crucial, without the ContentType the receiving browser will treat the HTML as plain text \r\n    ContentType = \"text/html; charset=utf-8\" \r\n    Body = $html\r\n})\r\n\r\nexit

The browser receives the HTML. Within it is this JavaScript:

(function() {\r\n    const fragment = window.location.hash ? window.location.hash.substring(1) : \"\";\r\n    const basePath = \"${path}\";\r\n    let redirectUrl = \"/redirect?path=\" + encodeURIComponent(basePath);\r\n    if (fragment) redirectUrl += \"&fragment=\" + encodeURIComponent(fragment);\r\n    window.location.replace(redirectUrl);\r\n})();

The URL in the browser at this point would be https://<APP_NAME>.azurewebsites.net/app/#/secret/secretId (the original URL would have been https://olddomain.com/app/#/secret/secretId which will get redirected by Front Door to https://<APP_NAME>.azurewebsites.net/app/ and then the browser will tack on #/secret/secretId).

\n

The window.location.hash property is the bit after the #. JavaScript can see that coz it is running within the browser in the page that I sent through. The code takes that, and does a redirect to /redirect?path=<whatever is in the URL as a path, in this case /app>&fragment=<whatever it extracted as fragment>.

\n

This too goes over to my Azure Function, as that’s the redirect is relative to whatever page the browser is on! \"\ud83d\ude0e\" And since the Function is set to receive all URLs sent its way, it is thus called again but this time as https://<APP_NAME>.azurewebsites.net/redirect?path=app&fragment=secret/secretId. Voila!

\n

Extract the Secret Id

\n

Again, the Function can fetch the URL it received it (via the $Request.Params.path property). And if the path is just “redirect/” (coz that’s what we are sending to above), it can extract the parameters that were sent through. These are in the $Request.Query property, so we can get to the fragment by doing:

\n
\n
\n
$fragment = $Request.Query.Fragment

\nThen do a bit of slice and dice to extract the Secret Id from “secret/secretId“, and Bob’s your uncle!

\n

And then figure out what the new Secret Id would be, and this time send the original bit of HTML I pasted way above that does a redirect to newdomain.com. Phew!

\n

Using a Managed Identity with Azure Tables

\n

One of the things I also wanted to do was use Managed Identities when connecting the Function App to Azure Tables. Previously I used to use the AzTable module but that uses connection strings and I wanted to move away from that.

\n

Using a Managed Identity is kind of straight forward. You need to assign the Managed Identity of the Function App the Storage Table Data Contributor role on the Table. And then in the Function App:

# Get a token\r\n$azToken = ConvertFrom-SecureString -SecureString (Get-AzAccessToken -ResourceType Storage -AsSecureString -ErrorAction Stop).token -AsPlainText -ErrorAction Stop\r\n\r\n# Construct headers\r\n$headers = @{\r\n    \"Authorization\" = \"Bearer $azToken\"\r\n    \"x-ms-version\" = \"2017-11-09\"\r\n    \"Accept\" = \"application/json;odata=nometadata\"\r\n}

And then do the actual API calls to get data:

$uri = \"https://$storageAccount.table.core.windows.net/$tableName(PartitionKey='$secretId',RowKey='$secretId')\"\r\n\r\n$azTableRowObj = Invoke-RestMethod -Uri $uri -Headers $headers -Method GET -ErrorAction Stop

Thanks to this and this Microsoft link for helping me getting started.

\n

One thing I couldn’t figure out though was how to do a filter without using a PartitionKey and RowKey. The links have instructions on doing that, but it never worked for me… and I didn’t bother spending more time as I could search via PartitionKey and RowKey anyways.

\n
\n
\n", "content_text": "At work I spent some time yesterday working on a side project to setup forwarding from one set of URLs to another. We are migrating a service from our on-prem world to the cloud, and there are tons of URLs pointing that will now get invalidated as a result.\nIn the on-prem world the URLs are of the form https://olddomain.com/app/#/secret/secretId, while in the cloud world it is very similar and looks like https://newdomain.com/app/#/secret/newsecretId. While it’s easy to setup a redirect from olddomain.com to newdomain.com, the problem is that the secretId changes to a new number newsecretId in the cloud, so you can’t just do a catch-all redirect.\nI had a CSV file of the old and new secret Ids, and I wondered what I can do with it.\n(By the way, side promo of a new tool I discovered recently. SmoothCSV. If you deal regularly with CSVs, and don’t want to open Excel/ LibreOffice for it, nor use a regular text or code editor as that doesn’t look right either, this is the tool for you! Open source and cross-platform).\nAzure Front Door\nOne more thing we wanted to do was have https://olddomain.com/* redirect to a SharePoint Online with some info on the migration for anyone using the old URLs, and so we had setup Azure Front Door for this. It’s a simple rule there to redirect any URLs beginning with https://olddomain.com/ to https://newdomain.com/ and I wanted to piggy back on that to handle URLs beginning with https://olddomain.com/app/#/secret/ differently somehow.\nThis is where I encountered my first issue, which stumped me for a while. I only realized later what was happening, but by then I had stumbled upon a fix by blindly trying different things to get it working.\nHere’s what I did. In Front Door I have two rules:\n\nRule 1: If a URL begins with https://olddomain.com/app/#/secret/ send it to a dummy Azure Function I had setup. Stop processing any more rules.\nRule 2: If a URL begins with https://olddomain.com/ send it to the SharePoint site.\n\nRule 2 was the original rule, created as part of the initial requirement. Rule 1 is what I created and put as a higher priority over Rule 1, so I can do my per secret Id redirecting somehow.\n\nSounds like it should work, but it does not! I never hit Rule 2, I always keep going to Rule 1. I even changed Rule 2 to be a RegEx match, but that didn’t help either.\nThe fix, in the end, was to match on the URL beginning with https://olddomain.com/app/. That worked, and while I stumbled upon it by troubleshooting and removing parts of the URL to see what minimum works, I wasn’t sure why this was the fix. Nevertheless I moved on to the next part of the problem.\nEnter Azure Functions\nMy idea was to have these URLs be redirected to an Azure Function. I’ll import the CSV file of old and new secret Ids into an Azure Tables, the Azure Function will receive an URL that looks like https://olddomain.com/app/#/secret/secretId from Azure Front Door, I will use PowerShell to extract the secretId, lookup the new value from Table, construct a new URL https://newdomain.com/app/#/secret/newsecretId and send that to the user.\nTurns out sending a user a new URL is very easy. From a bit of Googling I learnt that a bit of HTML like this does the job:<html>\r\n<head>\r\n <meta http-equiv=\"refresh\" content=\"1;url=https://newdomain.com/app/#/secret/$newSecretId/\" />\r\n <title>Redirecting...</title>\r\n</head>\r\n<body>\r\n</body>\r\n</html>What the above does is that it tells the browser receiving the page to do a refresh to the given URL after 1 second. I put $newsecretId above, coz that’s what will change based on the lookup.\nIn Azure Function, all I need do is the following (these are only snippets, not the whole code):# Put the HTML from above into a variable; this assumes I have looked up and got a value for $newSecretId already\r\n$html = @\"\r\n<html>\r\n<head>\r\n <meta http-equiv=\"refresh\" content=\"1;url=https://newdomain.com/app/#/secret/$newSecretId/\" />\r\n <title>Redirecting...</title>\r\n</head>\r\n<body>\r\n</body>\r\n</html>\r\n\"@\r\n\r\n# Return that\r\nPush-OutputBinding -Name Response -Value ([HttpResponseContext]@{\r\n StatusCode = 200\r\n # This is crucial, without the ContentType the receiving browser will treat the HTML as plain text \r\n ContentType = \"text/html; charset=utf-8\" \r\n Body = $html\r\n})\r\n\r\nexitNeat!\nBut of course, this didn’t work as expected! There wouldn’t be a blog post if it did. \nWhat was failing was the part not in the snippet above. When I receive the redirected URL from Azure Front Door, I wasn’t getting the old secret Id in the link.\nA digression\nA bit of a digression first.\nTypically an Azure Function URL looks like this: https://<APP_NAME>.azurewebsites.net/api/<FUNCTION_NAME>/\nIt is possible to remove the /api/ but by setting the following JSON in the host.json file.{\r\n \"version\": \"2.0\",\r\n \"isDefaultHostConfig\": true,\r\n \"managedDependency\": {\r\n \"Enabled\": true\r\n },\r\n \"extensionBundle\": {\r\n \"id\": \"Microsoft.Azure.Functions.ExtensionBundle\",\r\n \"version\": \"[4.*, 5.0.0)\"\r\n },\r\n \"extensions\": {\r\n \"http\": {\r\n \"routePrefix\": \"\"\r\n }\r\n }\r\n}It is also possible to remove the <FUNCTION_NAME> bit from the URL. The official doc tells you how but is incomplete as it only gives you examples of changing <FUNCTION_NAME> to something else. I wanted to remove it though, if possible, and when Googling didn’t help I turned to ChatGPT and from there I learnt that by modifying the function.json file thus, I can achieve it:{\r\n \"bindings\": [\r\n {\r\n \"authLevel\": \"anonymous\",\r\n \"type\": \"httpTrigger\",\r\n \"direction\": \"in\",\r\n \"name\": \"Request\",\r\n \"methods\": [\r\n \"get\"\r\n ],\r\n \"route\": \"{*path}\"\r\n },\r\n {\r\n \"type\": \"http\",\r\n \"direction\": \"out\",\r\n \"name\": \"Response\"\r\n }\r\n ]\r\n}The \"route\": \"{*path}\" bit makes it so that all paths are sent to this Function app. I couldn’t find much documentation on this wildcard property, but I did find some links that explained this property in general (like this one, for instance). Glad I stumbled upon it via ChatGPT!\nWith this in place I can access whatever is sent to the function via the $Request.Params.path property (the name path matches whatever is puting in the route definition).\nBack to the issue\nSo now I have Azure Front Door redirecting https://olddomain.com/app/#/secret/secretId to my Function App. When redirecting, it will send the browser to <APP_NAME>.azurewebsites.net/ and also include app/#/secret/secretId.\n\nI know the app/#/secret/secretId part will come through, coz I am not setting anything in the Destination path and Destination fragment, so Front Door will send whatever it gets over. This is why I removed the extra bits from the Function URL, coz if I didn’t do that I’d have to put api/<FUNCTION_NAME>/ in the Destination path, and I didn’t want to do that coz there’s no way to add that and have Front Door append whatever it gets. By leaving it blank, Front Door will send everything over.\nAnd yet, in Azure Function, when I examine the path via the $Request.Params.path property, all I see is “app/“. Nothing after that! Odd.\nFragments\nAnd now we come to fragments. \nI had seen the word fragment in Front Door, but hadn’t paid much attention. But once I got to troubleshooting why the Function App wasn’t working, I learnt about it. This also explained to me why Front Door wasn’t working with my original URL way above when I first tried it.\nYou see, when you have a URL like https://olddomain.com/app/#/secret/secretId, the bit after the # is what’s known as a fragment. We typically see them in web pages when used to send the browser to a specific heading/ id tag of a page. Since they are meant to be specific to things within a page, browsers don’t send them to the server. Because the idea is that the browser will only send a request to the page it wants, get it, and then use the fragment to find what it needs.\nThis is pretty neat from a privacy point of view, coz as you can guess from the URLs above what I am dealing with are some sort of secrets, and clearly the designers of that tool had security in mind and so they put the secret Id part of the URL within a fragment. So no proxy servers or firewalls will ever see the secret Id, all they will see is https://olddomain.com/app/ which presumeably is a JavaScript page, and once that’s got the page itself will be sent #/secret/secretId and it can use that to show the secret Id. Nothing is captured anywhere. (That’s my guess at least on how this is used…)\nUnfortunately, this isn’t ideal for me, coz that’s why 1) Front Door never worked when I tried to match on URLs beginning with https://olddomain.com/app/#/secret/ coz it never saw anything after the #, and 2) why Azure Function too cannot see the secret Id. There’s simply no way for me to get it! Aaargh!\nA bit of JavaScript to the rescue\nBut hang on, yes Azure Function can’t see the full URL, but since it’s in the browser, presumeably I can use some JavaScript to get it?\nEnter ChatGPT. I asked it for some JavaScript code which can be used to extract the URL from a browser and send to a different URL. It gave me the following code:<!DOCTYPE html>\r\n<html>\r\n<head>\r\n <meta charset=\"utf-8\" />\r\n <title>Redirecting...</title>\r\n <script>\r\n (function() {\r\n const fragment = window.location.hash ? window.location.hash.substring(1) : \"\";\r\n const basePath = \"${path}\";\r\n let redirectUrl = \"/redirect?path=\" + encodeURIComponent(basePath);\r\n if (fragment) redirectUrl += \"&fragment=\" + encodeURIComponent(fragment);\r\n window.location.replace(redirectUrl);\r\n })();\r\n </script>\r\n</head>\r\n<body>\r\n <p>Redirecting...</p>\r\n</body>\r\n</html>Here’s what I can do. Azure Function gets called. It can check what the URL it received it (via the $Request.Params.path property). If the path is just “app/“, then it sends the above HTML to the browser.Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{\r\n StatusCode = 200\r\n # This is crucial, without the ContentType the receiving browser will treat the HTML as plain text \r\n ContentType = \"text/html; charset=utf-8\" \r\n Body = $html\r\n})\r\n\r\nexitThe browser receives the HTML. Within it is this JavaScript:(function() {\r\n const fragment = window.location.hash ? window.location.hash.substring(1) : \"\";\r\n const basePath = \"${path}\";\r\n let redirectUrl = \"/redirect?path=\" + encodeURIComponent(basePath);\r\n if (fragment) redirectUrl += \"&fragment=\" + encodeURIComponent(fragment);\r\n window.location.replace(redirectUrl);\r\n})();The URL in the browser at this point would be https://<APP_NAME>.azurewebsites.net/app/#/secret/secretId (the original URL would have been https://olddomain.com/app/#/secret/secretId which will get redirected by Front Door to https://<APP_NAME>.azurewebsites.net/app/ and then the browser will tack on #/secret/secretId).\nThe window.location.hash property is the bit after the #. JavaScript can see that coz it is running within the browser in the page that I sent through. The code takes that, and does a redirect to /redirect?path=<whatever is in the URL as a path, in this case /app>&fragment=<whatever it extracted as fragment>.\nThis too goes over to my Azure Function, as that’s the redirect is relative to whatever page the browser is on! And since the Function is set to receive all URLs sent its way, it is thus called again but this time as https://<APP_NAME>.azurewebsites.net/redirect?path=app&fragment=secret/secretId. Voila!\nExtract the Secret Id\nAgain, the Function can fetch the URL it received it (via the $Request.Params.path property). And if the path is just “redirect/” (coz that’s what we are sending to above), it can extract the parameters that were sent through. These are in the $Request.Query property, so we can get to the fragment by doing:\n\n\n$fragment = $Request.Query.Fragment\nThen do a bit of slice and dice to extract the Secret Id from “secret/secretId“, and Bob’s your uncle!\nAnd then figure out what the new Secret Id would be, and this time send the original bit of HTML I pasted way above that does a redirect to newdomain.com. Phew!\nUsing a Managed Identity with Azure Tables\nOne of the things I also wanted to do was use Managed Identities when connecting the Function App to Azure Tables. Previously I used to use the AzTable module but that uses connection strings and I wanted to move away from that.\nUsing a Managed Identity is kind of straight forward. You need to assign the Managed Identity of the Function App the Storage Table Data Contributor role on the Table. And then in the Function App:# Get a token\r\n$azToken = ConvertFrom-SecureString -SecureString (Get-AzAccessToken -ResourceType Storage -AsSecureString -ErrorAction Stop).token -AsPlainText -ErrorAction Stop\r\n\r\n# Construct headers\r\n$headers = @{\r\n \"Authorization\" = \"Bearer $azToken\"\r\n \"x-ms-version\" = \"2017-11-09\"\r\n \"Accept\" = \"application/json;odata=nometadata\"\r\n}And then do the actual API calls to get data:$uri = \"https://$storageAccount.table.core.windows.net/$tableName(PartitionKey='$secretId',RowKey='$secretId')\"\r\n\r\n$azTableRowObj = Invoke-RestMethod -Uri $uri -Headers $headers -Method GET -ErrorAction StopThanks to this and this Microsoft link for helping me getting started.\nOne thing I couldn’t figure out though was how to do a filter without using a PartitionKey and RowKey. The links have instructions on doing that, but it never worked for me… and I didn’t bother spending more time as I could search via PartitionKey and RowKey anyways.", "date_published": "2025-10-18T12:50:30+01:00", "date_modified": "2025-11-06T11:26:01+00:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "front door", "function app", "functions", "managed identities", "powershell", "tables", "Azure, Azure AD, Graph, M365" ] }, { "id": "https://rakhesh.com/?p=8359", "url": "/books/code-dependent-book/", "title": "Code Dependent (Book)", "content_html": "

Quick shoutout to “Code Dependent” by Madhumita Murgia. I stumbled upon it at Waterstones the other day and purchased it on a whim. So glad I did as it was a very enjoyable read.

\n

I was half expecting (and not looking forward to) it being a book that bashes AI (or goes the other way and gushes over it). Instead, this book was very neutral. It went into a lot of AI usage (gig economy, data labelling, profiling people, video surveillance etc.) and had a very balanced view of things. Yes, AI is being used for a lot of harm, but it is also used in some good ways. I liked that.

\n

To me AI is a symptom of the overall issue. I think over time our social structures and institutions have been failing – not due to immigation or people etc. but more due to lack of investment in them. Things might have been easier in the past for the rich countries at least due to colonialism and such when money was flowing around, but for a long time that hasn’t been the case and all countries have had to invest from whatever money they have via trade and other means. And this doesn’t always happen, and so over time the quality of things has gone done. Be it from simple things like replacing the person at the other end of the phone when you call your bank or shop – first with cheap workers in call centres in India etc. and later with AI – to bigger things like lack of proper healthcare facilities, doctors etc.

\n

So in all of these cases we look to see how we can get away with the bare minimum. We try and remove the human element from it by trying to codify what is happening – break a task down to a series of steps (algorithms) – and then either hand that off to whoever can do it cheaply, or now AI. But the fundamental issue is that we are trying to convert an analog things (humans and the things they do) into something digital or discrete (a bunch of steps, a policy, an algorithm) because that’s all we can do to try and reduce costs instead of investing more. Try and capture what the essence of what a human is trying to do, then given it to some other human who can do it cheaply (either in a different country or somoene cheaper in country). And then the logical conclusion of this trend is to completely remove the human from the equation and replace them with an AI, because that’s all we are focussing on now – the steps, and how we can save costs.

\n

This is why the world just feels shitty nowadays, I think. There’s less and less of a human touch/ interaction. Even when you get to talk to a human on the other end of a customer support call or shop etc., they have less autonomy because they have a script they must follow. They can’t make decisions because that’s what they have been told. And if you push back, you are just told that that’s the policy.

\n

And this feeling is what Madhumita manages to convey in Code Dependent, though maybe not so explicitly. (These ideas have been going around in my head for a bit coz of all the non-fiction reading I do, especially “Survival of the Richest” recently which had similar themes, so my overall feeling of the book was also coloured with this).

\n

A good read, highly recommended!

\n", "content_text": "Quick shoutout to “Code Dependent” by Madhumita Murgia. I stumbled upon it at Waterstones the other day and purchased it on a whim. So glad I did as it was a very enjoyable read.\nI was half expecting (and not looking forward to) it being a book that bashes AI (or goes the other way and gushes over it). Instead, this book was very neutral. It went into a lot of AI usage (gig economy, data labelling, profiling people, video surveillance etc.) and had a very balanced view of things. Yes, AI is being used for a lot of harm, but it is also used in some good ways. I liked that.\nTo me AI is a symptom of the overall issue. I think over time our social structures and institutions have been failing – not due to immigation or people etc. but more due to lack of investment in them. Things might have been easier in the past for the rich countries at least due to colonialism and such when money was flowing around, but for a long time that hasn’t been the case and all countries have had to invest from whatever money they have via trade and other means. And this doesn’t always happen, and so over time the quality of things has gone done. Be it from simple things like replacing the person at the other end of the phone when you call your bank or shop – first with cheap workers in call centres in India etc. and later with AI – to bigger things like lack of proper healthcare facilities, doctors etc.\nSo in all of these cases we look to see how we can get away with the bare minimum. We try and remove the human element from it by trying to codify what is happening – break a task down to a series of steps (algorithms) – and then either hand that off to whoever can do it cheaply, or now AI. But the fundamental issue is that we are trying to convert an analog things (humans and the things they do) into something digital or discrete (a bunch of steps, a policy, an algorithm) because that’s all we can do to try and reduce costs instead of investing more. Try and capture what the essence of what a human is trying to do, then given it to some other human who can do it cheaply (either in a different country or somoene cheaper in country). And then the logical conclusion of this trend is to completely remove the human from the equation and replace them with an AI, because that’s all we are focussing on now – the steps, and how we can save costs.\nThis is why the world just feels shitty nowadays, I think. There’s less and less of a human touch/ interaction. Even when you get to talk to a human on the other end of a customer support call or shop etc., they have less autonomy because they have a script they must follow. They can’t make decisions because that’s what they have been told. And if you push back, you are just told that that’s the policy.\nAnd this feeling is what Madhumita manages to convey in Code Dependent, though maybe not so explicitly. (These ideas have been going around in my head for a bit coz of all the non-fiction reading I do, especially “Survival of the Richest” recently which had similar themes, so my overall feeling of the book was also coloured with this).\nA good read, highly recommended!", "date_published": "2025-10-16T11:25:52+01:00", "date_modified": "2025-10-16T11:25:52+01:00", "authors": [ { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" } ], "author": { "name": "rakhesh", "url": "/author/rakhesh/", "avatar": "https://secure.gravatar.com/avatar/2e266c7cf69c2a929a8bbd0a5d9e0dc5adf9698165d44a6c1a9c8e890d62f703?s=512&d=retro&r=g" }, "tags": [ "Books, Audiobooks, Podcasts" ] } ] }