Monday, May 21, 2018

Constrained and dignified

Another day, another TFS discovery.

I was facing a minor usability issue. We have an extension with several custom menu commands for release definitions pipelines. Out of those, two only make sense for server administrators. For anybody else, they'd error out anyway. The menu was getting crowded, so I wanted to see if I could make the admin-only commands hidden for non-admin users.

In TFS extension lingo, the custom menu commands are contributions. They're described by a JSON manifest. On the surface, there's nothing in the manifest docs about conditionally visible contributions. However, a careful look at the JavaScript API docs reveals something interesting - the Contribution object, as returned from VSS.getContribution(), has a property called "constraints", and it's an array of ContributionConstraint objects.

Looking at the sources of the TFX tool reveals that "constraints" is a valid JSON element in the extension manifest, and it's expected to be an array.

Now we're getting somewhere. So, what goes into that array? One would guess, objects that correspond to the ContributionConstraint class, one with the following properties:
  • name
  • properties
  • group
  • inverse
According to the docs, the "name" property contains "name of the IContributionFilter class". IContributionFilter is not a JavaScript object. However, it is a C# interface. Poking around TFS assemblies with ILSpy reveals it in Microsoft.VisualStudio.Services.ExtensionManagement.Sdk.Server.dll, and there's a bunch of derived classes in bin\Plugins\Microsoft.VisualStudio.Services.ExtensionManagement.Sdk.Plugins.dll. Incidentally, the IContributionFilter interface has a Name property. One would presume it corresponds to the "name" property of the "constraint" JavaScript object and the manifest line.

Each of those filter classes has a name and supports a set of parameters:
  • Security (class SecurityFilter)
    • namespaceId (GUID)
    • namespaceToken (string)
    • permission (int)
    • allowSystemContext (optional bool)
    • serviceInstanceType (optional GUID)
  • ExtensionActive (class ActiveExtensionFilter)
    • extensionId (string)
  • ExtensionLicensed (class ExtensionLicensedFilter)
    • extensionId (string)
    • extension-rights (object with a set of bool-valued properties)
  • Feature (class FeatureFilter)
    • featureId (string)
  • FeatureFlag (class FeatureFlagFilter)
    • featureName (string)
  • FeatureEnabled (class LegacyFeatureEnabledFilter)
    • featureName (string)

I've worked with Security, since it directly relates to my issue. The "namespaceId" property corresponds to the GUID of the security namespace (see the contents of Tfs_Configuration.dbo.tbl_SecurityNamespace for the list of those). The "namespaceToken" property corresponds to the token of a securable object. There are many classes of those in TFS, and the rules for generating tokens are different - poke around tbl_SecurityAccessControlEntry to see. The "permission" property contains a bit mask to check - to see the bit values of rights, see the contents of  Tfs_Configuration.dbo.tbl_SecurityAction. The structure of TFS access control lists is a discussion for another day.

So the Security constraint would check if the current user has the specified rights on the specified securable object, and if not, the contribution will not be displayed. Bingo! All I had to do was find a server-level securable object that only admins could manage. A certain restricted app pool did the trick.

The system of constraints is not documented and is barely acknowledged. However, TFS makes some use of it internally - the various built-in hubs are treated like contributions and some of them have constraints.

One can designate a constraint for a custom extension, too. In the manifest, under the contribution object, a constraint could look like this:

{
  "id": "mycommand",
  "type": "ms.vss-web.action",
  "constraints": [
  {
    "name": "Security",
    "properties": {
        "namespaceId":"101EAE8C-1709-47F9-B228-0E476C35B3BA",                "namespaceToken":"AgentPools/17/",
        "permission": 27 }
  }],
  // More contribution stuff...
}

4 comments:

  1. You can also constrain based on routevalue such as:


    "constraints": [
    {
    "name": "RouteValue",
    "properties": {
    "key": "project",
    "value": "R&D"
    }
    }
    ]


    Route values can be seen in the console with:
    console.log(__vssPageContext.navigation.routeValues)

    ReplyDelete
    Replies
    1. Good find. Is that new in AzDevOps? This post goes back to TFS 2017 update some or other, IIRC.

      Delete
  2. Very nice to find your blog here which explain better than the azure devops doc! do you know if there's constraint which can apply to work item type?

    ReplyDelete
    Replies
    1. Work item type is a completely different object. what exactly are you trying to do?

      Delete