You may be using standardized solutions like Fabric Unified Admin Monitoring (FUAM) or any other templated solution that comes with a semantic model. As part of transparency within your organization, you decided to share the insights gathered with others in the organization by adjusting the solution to apply your own security setup on top.
However, after running an update of the template, you’ve overwritten your custom security configuration and reapplying costs a lot of time, again and again after each update. Why don’t we just script this security? In this blog I will share how you can deploy security configurations to semantic models and assign users to these roles.

The challenge
Th current challenge describing this time, is something that came along when I recently updated a Fabric Unified Admin Monitoring (FUAM) installation to the latest version. In this case, it was decided long ago to share the insights in this report with content owners within the organization. This was done by applying row-level-security to the semantic model, which is a customization compared to the default setup provided as template.
Row-level-security in this case, allowed us to filter down the data in the semantic model back to only what is relevant for the user we shared with, and that person is responsible for. From a principle of least privilege as possible, a key configuration for us. However, after each update (and we’re expecting more updates in the future) the new version of the semantic model will be deployed. As a result, the old semantic model will be replaced for a new one. Reconfiguring security will be a manual effort requiring additional time as part of the update process. As manual exercise, it’s also a risk to make mistakes in the configuration by forgetting to assign a user to a role or missing a filter expression on a table.
I think, this should be doable in an easier way and repeatable fashion! Not only as a one off, but also rerun the configuration regularly, that in case someone manually adjusted the security configuration, it will automatically fall back to the agreed setup.
Semantic link
As many have figured by now, I’m a huge fan of Fabric Semantic Link. In other words, connecting from a Fabric notebook to your semantic model for both read and write operations. Semantic Link in combination with the Semantic Link Labs extension is a rich toolbox that helps us to automate tasks like the challenge described. In below section, I will describe how I used this toolbox to solve the challenge addressed. I will skip loading the libraries in below detailed explanation.
For your reuse, I made the full notebook available on my GitHub so you can easily download and adjust to your needs. For those who like a more detailed explanation, keep on reading!
All connections to the semantic model (dataset) and workspace are parameterized in my notebook. I primarily do this for reusability of the notebook so I can easily copy-paste the solution to other workspaces and run it with minor adjustment. It starts with setting up these parameters.
semanticmodel_name = "AW2020" # specify semantic model name or id
workspace_id = fabric.get_workspace_id() # Retrieves the workspace id in which the notebook is running
In this scenario, we had to use the Labs extension in order to make changes to the Tabular Object Model (TOM) using the TOM wrapper. This allows for many more advanced transformations. Below code snippet defines a structure to work with the TOM model.
# Create a read/write TOM connection
# This cell takes the semantic model definition in memory
tw = tom.TOMWrapper(
dataset = semanticmodel_name,
workspace = workspace_id,
readonly = False
)
Next, was defining the security as I want to add it to the semantic model. What we have to understand is the that we first need to define a role, before we can setup security filters (RLS / OLS) to the role. I’ve done this in a notebook cell where I defined the role, added a role description (not even possible in Power BI, even better I can properly document it here!) and the members I want to add to the role. In below example I’m only using named users, but it is also possible to use Entra ID security groups.
rolename = "test" # Name of the security role in the model
roledescription = "test description" # description for the security role in the model
members = ["user1@example.com", "user2@example.com", "user3@example.com"] # list of users to add
member_type = "User" # User or Group (in case of Entra security groups)
If you want to regularly run the notebook (for example every week) to (re-)apply security configurations, you may run into errors where the role already exist. Therefore, I used a setup where I catch this error in case the role already exists.
# Create role if not existing yet
try:
tw.add_role(
role_name = rolename,
model_permission = None, # Defaults to read
description = roledescription
)
print(f"✅ Role '{rolename}' successfully created.")
except Exception as e:
if "already exists" in str(e):
print(f"⚠️ Role '{rolename}' already exists — continuing with members and RLS.")
else:
raise
As this only creates the role, I still need to add the role memberships. As multiple members can be part of a role, below section loops through all the members specified in the parameter cell before. Each member will be added individually.
# Add each member (skip existing ones)
for member in members:
try:
tw.add_role_member(
role_name = rolename,
member = member,
role_member_type = member_type
)
print(f"👤 Added member '{member}' to role '{rolename}'.")
except Exception as e:
# catch any duplicate membership errors
if "already exists in the collection" in str(e) or "already part of role" in str(e):
print(f"ℹ️ Member '{member}' already part of role '{rolename}', skipping.")
else:
raise
So far, we only added a role and assigned members. But whoever is part of this role, can still see all the data. We still need to add the expression to the role to limit the data. Below examples use row-level-security only, however other Semantic Link Labs functions allow you to add object-level-security roles as well. Again, I start with a simple parameter cell.
table = "DimProduct"
expression = 'DimProduct[Color] = "Blue"'
In this example, I take the assumption a proper semantic model has been built following to best practices, in which a star schema is used. Assuming so, the defined filter will automatically flow from one table to another crossing the relationship. Please note that relationship direction and type can influence the actual behavior. The setting to apply security filters in both directions might need to be enabled as well. If so, additional steps in must be added to the notebook to change this property.
# Set or update RLS
try:
tw.set_rls(
role_name = rolename,
table_name = table,
filter_expression = expression
)
print(f"🔒 RLS filter set for role '{rolename}'.")
except Exception as e:
print(f"⚠️ Failed to set RLS filter: {e}")
So far, all changes have been made to the Tabular Object Model (TOM) in memory. Finally, the changes have to be saved back to the semantic model with a last step.
# This step saves the changes back to the model in the workspace
tw.model.SaveChanges()
Wrap up
Although the solution explained above works for my scenario, I realize this might not work for all scenarios. Though, I believe it’s a start that could help many and with small adjustments it could be used for many more scenarios.
A few assumptions have been made in defining this notebook. The relationships, their direction and cross-filter behavior have a major impact on the reach of the defined security expression. With adjustments to the script, relationships can be validated and adjusted if required. Also, only row-level-security has been specified in this example, in which I used the assumption that the expression will only hit one table. In case of more complex scenarios, the step to apply the security filters has to be applied in a for-each loop.
Let me know if you think this was useful, or you rather stick to a manual process?
Pingback: Automating Semantic Model Security via Semantic Link – Curated SQL