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 readonly 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.
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 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>(). Because scopes in general are long lived and static in terms of their lifespan, 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 conditionally loaded. 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, we need to assign that endpoint to a 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