Article REST Scopes

Custom REST Scopes

If you have created custom REST endpoints then as of 12.1 you should also be creating custom scopes for those endpoints.   Otherwise they appear as uncategorized and controlling access becomes more of a challenge.

To create scopes you must first define a new plugin the implements IRestScopeGroups

namespace Telligent.Evolution.Extensibility.Rest.Version2
{
    public interface IRestScopeGroups : IPlugin
    {
        void RegisterScopeGroups(IRestScopeGroupRegistrationController controller);
    }
}

This can be a standalone plugin however since this needs to be enabled along with any custom REST endpoints, it's better to actually combine the IRestScopeGroups implementation with the IRestEndpoints plugin that will use it.   At minimum these two plugins should be part of a common IPluginGroup.

When creating custom scopes, you are really just defining a scope group and its entities. The individual scopes are generated using the types of routes associated with them.  It is important before trying to create custom scopes you first read and understand what rest scopes are and how they are defined.

Here is an example of an IRestEndpoints implementation that defines a custom scope group and entity by extending IRestEndpoints with IRestScopeGroups:

public class CustomRestEndpointsWithScopeGroup : ITranslatablePlugin, IRestScopeGroups,IRestEndpoints
{

		private  static string _groupId => "mygroup";
		private static string _entityId => "myentity";

        #region IPlugin
        public string Name => "Custom Rest Scopes";

		public string Description => "Defines the available REST scopes for Custom REST APIs";

		public void Initialize()
		{

		}
        #endregion

        #region IRestEndpoints
        public void Register(IRestEndpointController restRoutes)
		{
			var getDoc = new RestEndpointDocumentation() { EndpointDocumentation = new RestEndpointDocumentationAttribute { Resource = "Samples", Action = "Get" } };
			getDoc.RequestDocumentation.Add(new RestRequestDocumentationAttribute { Name = "id", Type = typeof(int), Description = "Id of the entity to return.", Required = RestRequired.Required });
			restRoutes.Add(2, "sampleendpoint/sampleentity/{id}", Extensibility.Rest.Version2.HttpMethod.Get, (request, response) =>
			{
				response.StatusCode = 200;	
			},
			new RestRouteCreateOptions() { 
					Documentation=getDoc,
				    ScopeEntityAssignments= new List<RestScopeEntityAssignment>() {new RestScopeEntityAssignment(_groupId,_entityId) }
				}
			);

			var postDoc = new RestEndpointDocumentation() { EndpointDocumentation = new RestEndpointDocumentationAttribute { Resource = "Samples", Action = "Get" } };
			//Additional parameters should be defined in documentation
			restRoutes.Add(2, "sampleendpoint/sampleentity", Extensibility.Rest.Version2.HttpMethod.Post, (request, response) =>
			{
				response.StatusCode = 201;
			},
			new RestRouteCreateOptions()
			{
				Documentation = postDoc,
				ScopeEntityAssignments = new List<RestScopeEntityAssignment>() { new RestScopeEntityAssignment(_groupId, _entityId) }
			}
			);
		}
        #endregion

        #region ITranslatablePlugin
        private ITranslatablePluginController _translations = null;
		public void SetController(ITranslatablePluginController controller)
		{
			_translations = controller;
		}
		public Translation[] DefaultTranslations
		{
			get
			{
				var enUS = new Translation("en-us");

				enUS.Set("Custom_ScopeGroup_Name", "My Custom Scope");
				enUS.Set("Custom_ScopeGroup_Description", "A custom scope group.");
				enUS.Set("Custom_Entity_Name", "myentity");
	
				return new[] { enUS };
			}
		}
        #endregion
        public void RegisterScopeGroups(IRestScopeGroupRegistrationController controller)
		{

			var newScopeEntities = new List<RestScopeEntity>();
			newScopeEntities.Add(new RestScopeEntity(_entityId, () => _translations.GetLanguageResourceValue("Custom_Entity_Name")));
		
			controller.Add(_groupId,
				() => _translations.GetLanguageResourceValue("Custom_ScopeGroup_Name"),
				() => _translations.GetLanguageResourceValue("Custom_ScopeGroup_Description"),
				newScopeEntities
			   );

	    }


}

You will also see we implemented ITranslatablePlugin which allows for static strings to be translated.   This is not required, however because the group and entity names are displayed in the user interface, this is the recommended best practice.

Breaking It Down

First we define instance level static or read-only string values that are the scope group id and the entity id we are defining.  These values never change and we will use them later.  If your REST endpoints are part of a separate plugin you may consider making these public for use later.

private  static string _groupId => "mygroup";

private static string _entityId => "myentity";


In RegisterScopeGroups() we actually define the new scope group and its entity.

 

public void RegisterScopeGroups(IRestScopeGroupRegistrationController controller)
{

var newScopeEntities = new List<RestScopeEntity>();
newScopeEntities.Add(new RestScopeEntity(_entityId, () => _translations.GetLanguageResourceValue("Custom_Entity_Name")));
controller.Add(_groupId,
                 () => _translations.GetLanguageResourceValue("Custom_ScopeGroup_Name"),
                 () => _translations.GetLanguageResourceValue("Custom_ScopeGroup_Description"),
                newScopeEntities

             );

}


Even though it’s not illustrated here you can register multiple scope groups in a single plugin by simply calling IRestGroupRegistrationController.Add().  The same also applies to entities.  A scope group can and usually does contain multiple entities.  In the example we instantiate a list of RestScopeEntity, add the entity or entities we want to assign to the group, and pass that in to IRestGroupRegistrationController.Add() along with the scope group id, name and description.

One thing to notice is that the name and description options on groups and entities are not string properties but rather Func<string>().   Scopes in general are long lived and static in terms of their lifespan,and in order to facilitate translation as we do in this plugin, the values for these properties are required to be more dynamic so they can be loaded on access.  In this case because the Func<string>() calls our translation controller, the string returned is in the appropriate language for the accessing user, if defined.

Lastly when defining an endpoint or endpoints, we need to assign that endpoint to our scope group and entity which is done by assigning RestRouteCreateOptions.ScopeEntityAssignments to a list of ScopeEntityAssignment objects.   Note that an endpoint can belong to multiple scope groups and entities, though it's not common. 

public void Register(IRestEndpointController restRoutes)
{
    var getDoc = new RestEndpointDocumentation() { EndpointDocumentation = new RestEndpointDocumentationAttribute { Resource = "Samples", Action = "Get" } };
	getDoc.RequestDocumentation.Add(new RestRequestDocumentationAttribute { Name = "id", Type = typeof(int), Description = "Id of the entity to return.", Required = RestRequired.Required });
	restRoutes.Add(2, "sampleendpoint/sampleentity/{id}", Extensibility.Rest.Version2.HttpMethod.Get, (request, response) =>
	{
		response.StatusCode = 200;	
	},
	new RestRouteCreateOptions() { 
			Documentation=getDoc,
			ScopeEntityAssignments= new List<RestScopeEntityAssignment>() {new RestScopeEntityAssignment(_groupId,_entityId) }
		}
	);

	var postDoc = new RestEndpointDocumentation() { EndpointDocumentation = new RestEndpointDocumentationAttribute { Resource = "Samples", Action = "Get" } };
	//Additional parameters should be defined in documentation
	restRoutes.Add(2, "sampleendpoint/sampleentity", Extensibility.Rest.Version2.HttpMethod.Post, (request, response) =>
	{
		response.StatusCode = 201;
	},
	new RestRouteCreateOptions()
		{
				Documentation = postDoc,
				ScopeEntityAssignments = new List<RestScopeEntityAssignment>() { new RestScopeEntityAssignment(_groupId, _entityId) }
		}
	);
}


If you recall from the article on Rest Scopes in general, an actual scope is a 3 part string that is:

groupId.entityId.accessModifier

The access modifier is determined by the HTTP method, so endpoints that use GET are readonly access, while POST/PUT/DELETE fall under modify.  So given we have 2 endpoints, one GET and one POST, both in the same scope and entity group, we will have 2 new scopes available:

mygroup.myentity.readonly

mygroup.myentity.modify

These are the actual strings used when asking for scopes via OAuth.  You will also see these options are now available for Api Keys and for OAuth clients to configure.


Full Example