diff --git a/README.md b/README.md index c9411e0..55d2c39 100644 --- a/README.md +++ b/README.md @@ -111,13 +111,36 @@ Notes: * `tags` will be described further down #### Configure via code +Configure the base properties via code. New: You can add security settings to your api (see also the new Security-Methodattribute) + ```csharp -SwaggerWcfEndpoint.Configure(new SwaggerWcf.Models.Info +var info = new Info +{ +Description = "Sample Service to test SwaggerWCF", +Version = "0.0.1" +// etc +}; + +var security = new SecurityDefinitions { - Description = "Sample Service to test SwaggerWCF", - Version = "0.0.1" - // etc -}); + { + "api-gateway", new SecurityAuthorization + { + Type = "oauth2", + Name = "api-gateway", + Description = "Forces authentication with credentials via an api gateway", + Flow = "password", + Scopes = new Dictionary + { + { "author", "use author scope"}, + { "admin", "use admin scope"}, + }, + AuthorizationUrl = "http://yourapi.net/oauth/token" + } + } +}; + +SwaggerWcfEndpoint.Configure(info, security); ``` ### Step 5: Decorate WCF services interfaces @@ -204,6 +227,7 @@ Note: make sure you add at least the `DataContract` and `DataMember` attributes | `SwaggerWcfDefinition` | `Class` | Configure a data type | `ExternalDocsDescription`, `ExternalDocsUrl` | | `SwaggerWcfReturnType` | `Method` | Override method return type | `ReturnType` | | `SwaggerWcfContentTypes` | `Method` | Override consume/produce content-types | `ConsumeTypes`, `ProduceTypes` | +| `SwaggerWcfSecurity` | `Method` | Add security background to this method | `SecurityDefinitionName`, `params Scopes` | ## Tags diff --git a/src/SwaggerWcf.Test.Service/Global.asax.cs b/src/SwaggerWcf.Test.Service/Global.asax.cs index 80f0888..898251a 100644 --- a/src/SwaggerWcf.Test.Service/Global.asax.cs +++ b/src/SwaggerWcf.Test.Service/Global.asax.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.ServiceModel.Activation; using System.Web; using System.Web.Routing; +using SwaggerWcf.Models; namespace SwaggerWcf.Test.Service { @@ -11,6 +13,34 @@ protected void Application_Start(object sender, EventArgs e) { RouteTable.Routes.Add(new ServiceRoute("v1/rest", new WebServiceHostFactory(), typeof(BookStore))); RouteTable.Routes.Add(new ServiceRoute("api-docs", new WebServiceHostFactory(), typeof(SwaggerWcfEndpoint))); + + var info = new Info + { + Description = "Sample Service to test SwaggerWCF", + Version = "0.0.1" + // etc + }; + + var security = new SecurityDefinitions + { + { + "api-gateway", new SecurityAuthorization + { + Type = "oauth2", + Name = "api-gateway", + Description = "Forces authentication with credentials via an api gateway", + Flow = "implicit", + Scopes = new Dictionary + { + { "fu", "use fu scope"}, + { "bar", "use bar scope"}, + }, + AuthorizationUrl = "http://yourapi.net/oauth/token" + } + } + }; + + SwaggerWcfEndpoint.Configure(info, security); } protected void Session_Start(object sender, EventArgs e) diff --git a/src/SwaggerWcf.Test.Service/IStore.cs b/src/SwaggerWcf.Test.Service/IStore.cs index c3a460a..8276eb6 100644 --- a/src/SwaggerWcf.Test.Service/IStore.cs +++ b/src/SwaggerWcf.Test.Service/IStore.cs @@ -11,6 +11,7 @@ public interface IStore { #region /books + [SwaggerWcfSecurity("api-gateway", "fu", "bar")] [SwaggerWcfPath("Create book", "Create a book on the store")] // default Method for WebInvoke is POST [WebInvoke(UriTemplate = "/books", BodyStyle = WebMessageBodyStyle.Wrapped, //Method = "POST", diff --git a/src/SwaggerWcf/Attributes/SwaggerWcfHeaderAttribute.cs b/src/SwaggerWcf/Attributes/SwaggerWcfHeaderAttribute.cs index a93eb87..6a17ca6 100644 --- a/src/SwaggerWcf/Attributes/SwaggerWcfHeaderAttribute.cs +++ b/src/SwaggerWcf/Attributes/SwaggerWcfHeaderAttribute.cs @@ -5,7 +5,7 @@ namespace SwaggerWcf.Attributes /// /// Describe a parameter /// - [AttributeUsage(AttributeTargets.Method)] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public class SwaggerWcfHeaderAttribute : Attribute { /// diff --git a/src/SwaggerWcf/Attributes/SwaggerWcfSecurityAttribute.cs b/src/SwaggerWcf/Attributes/SwaggerWcfSecurityAttribute.cs new file mode 100644 index 0000000..fb6c8e4 --- /dev/null +++ b/src/SwaggerWcf/Attributes/SwaggerWcfSecurityAttribute.cs @@ -0,0 +1,33 @@ +using System; + +namespace SwaggerWcf.Attributes +{ + /// + /// Attribute to link operation to a security definition + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public class SwaggerWcfSecurityAttribute : Attribute + { + /// + /// Specify security definition for this operation + /// + /// Name of the Security Definition + /// Scopes of the security definition + public SwaggerWcfSecurityAttribute(string securityDefinitionName, params string[] scopes) + { + SecurityDefinitionName = securityDefinitionName; + SecurityDefinitionScopes = scopes; + } + + /// + /// Name of the Security Definition + /// + public string SecurityDefinitionName { get; set; } + + /// + /// Scopes of the Security Definition + /// + public string[] SecurityDefinitionScopes { get; set; } + + } +} \ No newline at end of file diff --git a/src/SwaggerWcf/Models/PathAction.cs b/src/SwaggerWcf/Models/PathAction.cs index a3b5f66..a77dc81 100644 --- a/src/SwaggerWcf/Models/PathAction.cs +++ b/src/SwaggerWcf/Models/PathAction.cs @@ -40,7 +40,7 @@ public PathAction() public bool Deprecated { get; set; } - //public List security { get; set; } + public List> Security { get; set; } public void Serialize(JsonWriter writer) { @@ -137,12 +137,38 @@ public void Serialize(JsonWriter writer) } writer.WriteEndArray(); } + if (Deprecated) { writer.WritePropertyName("deprecated"); writer.WriteValue(Deprecated); } + if (Security != null && Security.Any()) + { + writer.WritePropertyName("security"); + writer.WriteStartArray(); + + foreach (var security in Security) + { + writer.WriteStartObject(); + writer.WritePropertyName(security.Key); + writer.WriteStartArray(); + + if (security.Value != null && security.Value.Any()) + { + foreach (var scopename in security.Value) + { + writer.WriteValue(scopename); + } + } + writer.WriteEndArray(); + writer.WriteEndObject(); + } + + writer.WriteEndArray(); + } + writer.WriteEndObject(); } } diff --git a/src/SwaggerWcf/Models/SecurityAuthorization.cs b/src/SwaggerWcf/Models/SecurityAuthorization.cs new file mode 100644 index 0000000..453321e --- /dev/null +++ b/src/SwaggerWcf/Models/SecurityAuthorization.cs @@ -0,0 +1,118 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace SwaggerWcf.Models +{ + public class SecurityAuthorization + { + /// + /// (Required) The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2". + /// + public string Type { get; set; } + + /// + /// A short description for security scheme. + /// + public string Description { get; set; } + + /// + /// (Required) The name of the header or query parameter to be used. + /// WARNING: Use only, when equals "apiKey" + /// + public string Name { get; set; } + + /// + /// (Required) The location of the API key. Valid values are "query" or "header". + /// WARNING: Use only, when equals "apiKey" + /// + public string In { get; set; } + + /// + /// (Required) The flow used by the OAuth2 security scheme. Valid values are "implicit", "password", "application" or "accessCode". + /// WARNING: Use only, when equals "oauth2" + /// + public string Flow { get; set; } + + /// + /// (Required) The authorization URL to be used for this flow. This SHOULD be in the form of a URL. + /// WARNING: Use only, when equals "oauth2" and equals "implicit" or "accessCode" + /// + public string AuthorizationUrl { get; set; } + + /// + /// (Required) The token URL to be used for this flow. This SHOULD be in the form of a URL. + /// WARNING: Use only, when equals "oauth2" and equals "password" or "application" or "accessCode" + /// + public string TokenUrl { get; set; } + + /// + /// (Required) The available scopes for the OAuth2 security scheme. + /// This maps between a name of a scope to a short description of it (as the value of the property). + /// WARNING: Use only, when equals "oauth2" + /// + public Dictionary Scopes { get; set; } + + public void Serialize(JsonWriter writer) + { + writer.WriteStartObject(); + + if (Type != null) + { + writer.WritePropertyName("type"); + writer.WriteValue(Type); + } + + if (Description != null) + { + writer.WritePropertyName("description"); + writer.WriteValue(Description); + } + + if (Name != null) + { + writer.WritePropertyName("name"); + writer.WriteValue(Name); + } + + if (In != null) + { + writer.WritePropertyName("in"); + writer.WriteValue(In); + } + + if (Flow != null) + { + writer.WritePropertyName("flow"); + writer.WriteValue(Flow); + } + + if (AuthorizationUrl != null) + { + writer.WritePropertyName("authorizationUrl"); + writer.WriteValue(AuthorizationUrl); + } + + if (TokenUrl != null) + { + writer.WritePropertyName("tokenUrl"); + writer.WriteValue(TokenUrl); + } + + if (Scopes != null) + { + writer.WritePropertyName("scopes"); + writer.WriteStartObject(); + + foreach (var scope in Scopes) + { + writer.WritePropertyName(scope.Key); + writer.WriteValue(scope.Value); + } + + writer.WriteEndObject(); + } + + writer.WriteEndObject(); + } + } +} diff --git a/src/SwaggerWcf/Models/SecurityDefinitions.cs b/src/SwaggerWcf/Models/SecurityDefinitions.cs new file mode 100644 index 0000000..1f5b933 --- /dev/null +++ b/src/SwaggerWcf/Models/SecurityDefinitions.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace SwaggerWcf.Models +{ + public class SecurityDefinitions : Dictionary + { + //This is just a wrapper + } +} \ No newline at end of file diff --git a/src/SwaggerWcf/Models/Service.cs b/src/SwaggerWcf/Models/Service.cs index a7e0e0b..70fc9c7 100644 --- a/src/SwaggerWcf/Models/Service.cs +++ b/src/SwaggerWcf/Models/Service.cs @@ -25,6 +25,8 @@ public Service() public List Definitions { get; set; } + public SecurityDefinitions SecurityDefinitions { get; set; } + public void Serialize(JsonWriter writer) { writer.WriteStartObject(); @@ -58,6 +60,13 @@ public void Serialize(JsonWriter writer) writer.WritePropertyName("definitions"); WriteDefinitions(writer); } + + if (SecurityDefinitions != null && SecurityDefinitions.Any()) + { + writer.WritePropertyName("securityDefinitions"); + WriteSecurityDefinitions(writer); + } + writer.WriteEndObject(); } @@ -80,5 +89,16 @@ private void WriteDefinitions(JsonWriter writer) } writer.WriteEndObject(); } + + private void WriteSecurityDefinitions(JsonWriter writer) + { + writer.WriteStartObject(); + foreach (var d in SecurityDefinitions) + { + writer.WritePropertyName(d.Key); + d.Value.Serialize(writer); + } + writer.WriteEndObject(); + } } } diff --git a/src/SwaggerWcf/Support/Mapper.cs b/src/SwaggerWcf/Support/Mapper.cs index a7505f2..a1600de 100644 --- a/src/SwaggerWcf/Support/Mapper.cs +++ b/src/SwaggerWcf/Support/Mapper.cs @@ -186,7 +186,7 @@ internal IEnumerable> GetActions(MethodInfo[] targetMe PathAction operation = new PathAction { Id = httpMethod.ToLowerInvariant(), - Summary = HttpUtility.HtmlEncode(summary), + Summary = summary, Description = HttpUtility.HtmlEncode(description), Tags = methodTags.Where(t => !t.HideFromSpec).Select(t => HttpUtility.HtmlEncode(t.TagName)).ToList(), @@ -195,7 +195,8 @@ internal IEnumerable> GetActions(MethodInfo[] targetMe Deprecated = deprecated, OperationId = HttpUtility.HtmlEncode(operationId), ExternalDocs = externalDocs, - Responses = GetResponseCodes(implementation, declaration, wrappedResponse, definitionsTypesList) + Responses = GetResponseCodes(implementation, declaration, wrappedResponse, definitionsTypesList), + Security = GetMethodSecurity(implementation, declaration) // Schemes = TODO: how to get available schemes for this WCF service? (schemes: http/https) }; @@ -665,6 +666,27 @@ private static Type GetTaskInnerType(Type type) return type.BaseType == typeof(Task) ? type.GetGenericArguments()[0] : type; } + private static List> GetMethodSecurity(MethodInfo implementation, MethodInfo declaration) + { + var securityMethodAttributes = implementation.GetCustomAttributes().ToList(); + var securityInterfaceAttributes = declaration.GetCustomAttributes().ToList(); + + var securityAttributes = securityMethodAttributes.Concat(securityInterfaceAttributes).ToList(); + + if (securityAttributes.Any() == false) + return null; + + var security = new List>(); + + foreach (var securityAttribute in securityAttributes) + { + security.Add(new KeyValuePair(securityAttribute.SecurityDefinitionName, securityAttribute.SecurityDefinitionScopes)); + } + + + return security; + } + public static Type GetEnumerableType(Type type) { if (type == null) diff --git a/src/SwaggerWcf/SwaggerWcf.csproj b/src/SwaggerWcf/SwaggerWcf.csproj index 17f48f1..5829711 100644 --- a/src/SwaggerWcf/SwaggerWcf.csproj +++ b/src/SwaggerWcf/SwaggerWcf.csproj @@ -56,11 +56,13 @@ + + @@ -82,6 +84,7 @@ + diff --git a/src/SwaggerWcf/SwaggerWcfEndpoint.cs b/src/SwaggerWcf/SwaggerWcfEndpoint.cs index be6dfda..e3dc140 100644 --- a/src/SwaggerWcf/SwaggerWcfEndpoint.cs +++ b/src/SwaggerWcf/SwaggerWcfEndpoint.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Net; using System.ServiceModel.Activation; @@ -42,10 +43,11 @@ public static void SetCustomGetFile(GetFileCustomDelegate getFileCustom) Support.StaticContent.GetFileCustom = getFileCustom; } - public static void Configure(Info info) + public static void Configure(Info info, SecurityDefinitions securityDefinitions = null) { Init(); Service.Info = info; + Service.SecurityDefinitions = securityDefinitions; } public Stream GetSwaggerFile()