WCF: Passing Username and password to another service (No Https) - c#

I have to Create a WCF service (ServiceWrapper), which references another WCF Service(RealService).
I want Clients of ServiceWrapper to pass Username/password in authentication request.
The Operations of ServiceWrapper Call RealService. I need to pass the recieved Username/passowrd to Authenticate with RealSerivce and then call its Operations.
I need to host the service on Http and not Https(SSL/TLS).
Question: How to use the Client Credentails Recieved by a Service to authenticate with a Referenced Service without using Https(SSL/TLS)?

Your can use SOAP security. There are two SecurityModes for you - Message, TransportWithMessageCredential.
You should configure security mode (UserName) in <binding> section like this
<security mode="TransportWithMessageCredential">
<transport clientCredentialType="" />
<message clientCredentialType="UserName" />
</security>
Next, you should specify custom validator in <behavior> section
<behavior name="CommonBehavior">
<serviceMetadata />
<serviceDebug includeExceptionDetailInFaults="True"/>
<serviceCredentials>
<userNameAuthentication userNamePasswordValidationMode="Custom"
customUserNamePasswordValidatorType="Megatec.MasterTourService.CustomUserNameValidator, Megatec.MasterTourService"/>
<serviceCertificate findValue="WCFServer" storeLocation="LocalMachine"
storeName="My" x509FindType="FindBySubjectName"/>
<clientCertificate>
<authentication certificateValidationMode="PeerTrust" />
</clientCertificate>
</serviceCredentials>
</behavior>
In your custom validator you can access and store user name and password, which were given as creditionals for ServiceWrapper.
using System.ServiceModel;
using System.IdentityModel.Selectors;
namespace MyService
{
public class CustomUserNameValidator : UserNamePasswordValidator
{
public override void Validate(string userName, string password)
{
if (!(userName == "testMan" && password == "pass"))
throw new FaultException("Incorrect login or password");
// save your Usermame and Password for future usage.
}
}
}
When you need to access RealService, you can use userName and password as credentials, like my example below:
private readonly Dictionary<Type, Object> channelFactoryDictionary = new Dictionary<Type, Object>();
private ChannelFactory<T> GetChannelFactory<T>() where T : class
{
if (channelFactoryDictionary.Keys.Contains(typeof(T)))
return channelFactoryDictionary[typeof(T)] as ChannelFactory<T>;
var channelFactory = new ChannelFactory<T>("*");
channelFactory.Credentials.UserName.UserName = userName;
channelFactory.Credentials.UserName.Password = password;
channelFactoryDictionary.Add(typeof(T), channelFactory);
return channelFactory;
}

If SSL is not an option, you will need to use SOAP message security (SecurityMode = Message).
http://msdn.microsoft.com/en-us/library/ms733137.aspx

Related

Unable to define user/password for WCF service

I have a WCF web service working with basic authentification.
I want to define a user/password for this service. So I wrote my web.config to user basic authentification :
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="UsernameWithTransportCredentialOnly">
<security mode="TransportCredentialOnly">
<transport clientCredentialType="Basic"/>
</security>
</binding>
</basicHttpBinding>
</bindings>
<behaviors>
<serviceBehaviors>
<behavior name="ServiceWithMetaData">
<serviceMetadata httpGetEnabled="true" httpsGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="true" />
<serviceAuthorization serviceAuthorizationManagerType="InterfaceWS.CredentialsChecker,App_Code.CredentialsChecker"/>
</behavior>
</serviceBehaviors>
</behaviors>
<services>
<service behaviorConfiguration="ServiceWithMetaData" name="InterfaceWS.MyService" >
<endpoint
address="https://localhost:44336/MyService.svc"
binding="basicHttpBinding"
bindingConfiguration="UsernameWithTransportCredentialOnly"
name="BasicEndpoint"
contract="InterfaceWS.IErpService">
</endpoint>
</service>
</services>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true"
multipleSiteBindingsEnabled="false" />
</system.serviceModel>
I created a class inherited from ServiceAuthorizationManager :
namespace InterfaceWS
{
public class CredentialsChecker : ServiceAuthorizationManager
{
protected override bool CheckAccessCore(OperationContext operationContext)
{
//Extract the Authorization header, and parse out the credentials converting the Base64 string:
var authHeader = WebOperationContext.Current.IncomingRequest.Headers["Authorization"];
if ((authHeader != null) && (authHeader != string.Empty))
{
var svcCredentials = System.Text.ASCIIEncoding.ASCII
.GetString(Convert.FromBase64String(authHeader.Substring(6)))
.Split(':');
var user = new
{
Name = svcCredentials[0],
Password = svcCredentials[1]
};
if ((user.Name == "testuser" && user.Password == "testpassword"))
{
//User is authrized and originating call will proceed
return true;
}
else
{
//not authorized
return false;
}
}
else
{
//No authorization header was provided, so challenge the client to provide before proceeding:
WebOperationContext.Current.OutgoingResponse.Headers.Add("WWW-Authenticate: Basic realm=\"MyWCFService\"");
//Throw an exception with the associated HTTP status code equivalent to HTTP status 401
throw new WebFaultException(HttpStatusCode.Unauthorized);
}
}
public override bool CheckAccess(OperationContext operationContext)
{
return false;
}
}
}
But the CheckAccessCore is never reached and i'm unable to connect to my Service. What did I do wrong ?
serviceAuthorization is about granting access to particular resources based on user credentials. You want authentication.
You can use serviceCredentials/userNameAuthentication tags in the configuration.
Example of configuration is given here: https://learn.microsoft.com/en-us/dotnet/framework/wcf/samples/membership-and-role-provider

WCF REST service url routing based on query parameters

Since WCF routing doesn't support routing for REST services, I created a REST service that has one enpoint which accepts all incoming requests and than redirects those requests based on the query parameters.
I did this by following this article http://blog.tonysneed.com/2012/04/24/roll-your-own-rest-ful-wcf-router/.
This approach works for passing through requests and returning the results. The problem is whenever I get an error, like a 404, from the actual service the message that is returned to the client is a 400 (Bad Request).
What I would like to have is a routing proxy that actually just redirects the calls to the real service based on the query and returns all the errors to the client as they come from the real service.
Is this even the right approach to what I'm trying to accomplish, or are there easier or better solutions?
Any help is appreciated!
In the following I added what my code looks like.
app.config:
<!--
System.net
-->
<system.net>
<settings>
<servicePointManager expect100Continue="false" useNagleAlgorithm="false" />
</settings>
<connectionManagement>
<add address="*" maxconnection="24" />
</connectionManagement>
</system.net>
<!--
System.ServiceModel
-->
<system.serviceModel>
<!--
Services
-->
<services>
<service name="RoutingGateway.RoutingService">
<endpoint address="/api/routing" binding="webHttpBinding" bindingConfiguration="secureWebHttpBinding" contract="RoutingGateway.IRoutingService" behaviorConfiguration="RESTBehaviour" />
</service>
</services>
<client>
<endpoint binding="webHttpBinding" bindingConfiguration="secureWebHttpBinding" contract="RoutingGateway.IRoutingService" name="routingService" behaviorConfiguration="RESTBehaviour" />
</client>
<!--
Bindings
-->
<bindings>
<webHttpBinding>
<binding name="secureWebHttpBinding" hostNameComparisonMode="StrongWildcard" maxReceivedMessageSize="2147483647" transferMode="Streamed">
<security mode="Transport">
<transport clientCredentialType="None" />
</security>
</binding>
</webHttpBinding>
</bindings>
<!--
Behaviors
-->
<behaviors>
<endpointBehaviors>
<behavior name="RESTBehaviour">
<dispatcherSynchronization asynchronousSendEnabled="true" />
<webHttp helpEnabled="true" />
</behavior>
</endpointBehaviors>
<serviceBehaviors>
<behavior>
<!-- To avoid disclosing metadata information, set the value below to false before deployment -->
<serviceMetadata httpsGetEnabled="false" />
<!-- To receive exception details in faults for debugging purposes, set the value below to true. Set to false before deployment to avoid disclosing exception information -->
<serviceDebug includeExceptionDetailInFaults="false" />
<!-- Enable Throttling -->
<serviceThrottling maxConcurrentCalls="100" maxConcurrentInstances="100" maxConcurrentSessions="100" />
</behavior>
</serviceBehaviors>
</behaviors>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
</system.serviceModel>
IRoutingService.cs:
[ServiceContract(Namespace = "https://test/api/routing")]
public interface IRoutingService
{
[OperationContract(Action = "*", ReplyAction = "*")]
[WebInvoke(UriTemplate = "*", Method = "*")]
Message ProcessRequest(Message requestMessage);
}
RoutingService.cs:
public Message ProcessRequest(Message requestMessage)
{
ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true;
Uri originalRequestUri = WebOperationContext.Current.IncomingRequest.UriTemplateMatch.RequestUri;
// Gets the URI depending on the query parameters
Uri uri = GetUriForRequest(requestMessage);
// Select rest client endpoint
string endpoint = "routingService";
// Create channel factory
var factory = new ChannelFactory<IRoutingService>(endpoint);
Uri requestUri = new Uri(uri, originalRequestUri.PathAndQuery);
factory.Endpoint.Address = new EndpointAddress(requestUri);
requestMessage.Headers.To = requestUri;
// Create client channel
_client = factory.CreateChannel();
// Begin request
Message result = _client.ProcessRequest(requestMessage);
return result;
}
I ended up catching all CommunicationExceptions and then rethrowing WebFaultExceptions with the appropriate messages and status codes.
Here is the code:
Message result = null;
try
{
result = _client.ProcessRequest(requestMessage);
}
catch (CommunicationException ex)
{
if (ex.InnerException == null ||
!(ex.InnerException is WebException))
{
throw new WebFaultException<string>("An unknown internal Server Error occurred.",
HttpStatusCode.InternalServerError);
}
else
{
var webException = ex.InnerException as WebException;
var webResponse = webException.Response as HttpWebResponse;
if (webResponse == null)
{
throw new WebFaultException<string>(webException.Message, HttpStatusCode.InternalServerError);
}
else
{
var responseStream = webResponse.GetResponseStream();
string message = string.Empty;
if (responseStream != null)
{
using (StreamReader sr = new StreamReader(responseStream))
{
message = sr.ReadToEnd();
}
throw new WebFaultException<string>(message, webResponse.StatusCode);
}
else
{
throw new WebFaultException<string>(webException.Message, webResponse.StatusCode);
}
}
}
}

How authenticate signed-in user via self-hosted WCF REST API

I have a self-hosted WCF RESTful API that exposes some functionality that I don't want exposed to unauthorized users. All administrators must be signed in using a custom ASP.NET membership provider to call the REST API. Currently I just send a API key which is unsecure as it can be seen by all. All calls to the REST API is done via jQuery. I'm not using TLS/SSL or other transport security mechanisms. All REST API calls are done against the same server/domain, so there are no cross-domain calls or JSONP stuff going on.
My question is, what is the best practice in my case for securing my REST API? Perhaps I should use OAuth for this - the more I read about OAuth the more it seems it is not for my scenario with jQuery.
IVeraCMS.cs:
[ServiceContract]
public interface IVeraCMS {
[OperationContract]
[WebInvoke(Method = "GET",
BodyStyle = WebMessageBodyStyle.WrappedRequest,
RequestFormat = WebMessageFormat.Json,
ResponseFormat = WebMessageFormat.Json)]
string PerformanceCounter(string API_Key);
}
VeraCMS.cs:
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple, InstanceContextMode = InstanceContextMode.PerCall,
IncludeExceptionDetailInFaults = false, MaxItemsInObjectGraph = 1000)]
public class VeraCMS : IVeraCMS
{
public string PerformanceCounter(string API_Key)
{
if (ConfigurationManager.AppSettings["API_key"] != API_Key)
throw new SecurityException("Access denied");
var procPercentage = new PerformanceCounter("Processor", "% Processor Time", "_Total");
procPercentage.NextValue();
var memPercentage = new PerformanceCounter("Memory", "Available MBytes");
memPercentage.NextValue();
const int samplingIntervalMs = 100;
Thread.Sleep(samplingIntervalMs);
var json = "{" + String.Format("\"ProcTime\":\"{0}%\",\"AvailMemory\":\"{1}MB\"" ,
procPercentage.NextValue().ToString(), memPercentage.NextValue().ToString()
) + "}";
return json;
}
}
}
Web.config:
<system.serviceModel>
<bindings>
<webHttpBinding>
<binding name="VeraWAF.WebPages.Interfaces.VeraCMS.Endpoint.Binding" maxReceivedMessageSize="4096" crossDomainScriptAccessEnabled="true" />
</webHttpBinding>
</bindings>
<services>
<service behaviorConfiguration="VeraWAF.WebPages.Interfaces.VeraCMS.Service.Behavior"
name="VeraWAF.WebPages.Interfaces.VeraCMS">
<endpoint address="" behaviorConfiguration="VeraWAF.WebPages.Interfaces.VeraCMS.Endpoint.Behavior"
binding="webHttpBinding" bindingConfiguration="VeraWAF.WebPages.Interfaces.VeraCMS.Endpoint.Binding"
contract="VeraWAF.WebPages.Interfaces.IVeraCMS" />
</service>
</services>
<behaviors>
<endpointBehaviors>
<behavior name="VeraWAF.WebPages.Interfaces.VeraCMS.Endpoint.Behavior">
<webHttp defaultOutgoingResponseFormat="Json" />
</behavior>
</endpointBehaviors>
<serviceBehaviors>
<behavior name="VeraWAF.WebPages.Interfaces.VeraCMS.Service.Behavior">
<serviceDebug includeExceptionDetailInFaults="true" />
</behavior>
</serviceBehaviors>
</behaviors>
You can do basic HTTP authentication like this:
WebServiceHost secureHost
secureHost.Credentials.UserNameAuthentication.UserNamePasswordValidationMode = UserNamePasswordValidationMode.Custom;
secureHost.Credentials.UserNameAuthentication.CustomUserNamePasswordValidator = new ClientValidator(username, password);
// Need to reference System.IdentityModel
public class ClientValidator : UserNamePasswordValidator
{
private readonly string _password;
private readonly string _username;
public ClientValidator(string username, string password)
{
_password = password;
_username = username;
}
public override void Validate(string userName, string password)
{
if (userName != _username || (password != _password))
{
WebFaultException rejectEx = new WebFaultException(HttpStatusCode.Unauthorized);
rejectEx.Data.Add("HttpStatusCode", rejectEx.StatusCode);
throw rejectEx;
}
}
}
Just keep in mind that your username and password can be easily sniffed if you are not using SSL. You can change the Validate method to fetch username and password from DB or some other service.

How to use HTTPS with WCF SessionMode.Required - simplest possible example

UPDATE (8/7/2014) - The solution to this problem was that I needed to add a class that derived from "UserNamePasswordValidator" and register it in Web.Config.
I have created a simple test WCF service and test console client application (see below for code). I am using .NET 4.5.1. I have already searched for duplicates on StackOverflow (found similar posts here and here) - however I feel that the referenced posts are potentially outdated, and also feel that my post is more limited in scope.
OK now for the example:
The solution currently uses sessions (in ITestService.cs):
[ServiceContract(SessionMode = SessionMode.Required)]
... and uses wsHttpBinding (see below app.config and web.config).
When I deploy this to a server, I am successfully able to access it via a web browser using HTTPS like this: https://myserver.com/test/testservice.svc
However, when I change the endpoint in the client app.config from:
http://localhost:20616/TestService.svc/TestService.svc
to:
https://myserver.com/test/testservice.svc
and run the console application again, I receive the error: "The provided URI scheme 'https' is invalid; expected 'http'. Parameter name: via"
My question is, what is the minimum changes I need to make for this to work, without changing SessionMode.Required?
Here is the client console application code. Please be sure to change the App.Config value for "mycomputer\Matt" to the correct value for your machine.
Program.cs
using System;
namespace TestClient
{
class Program
{
static void Main(string[] args)
{
Console.Clear();
Console.WriteLine("Attempting to log in...");
try
{
TestServiceReference.TestServiceClient client = new TestServiceReference.TestServiceClient();
bool loginSuccess = client.LogIn("admin", "password");
if (loginSuccess)
{
Console.WriteLine("Successfully logged in.");
string secretMessage = client.GetSecretData();
Console.WriteLine("Retrieved secret message: " + secretMessage);
}
else
{
Console.WriteLine("Log in failed!");
}
}
catch (Exception exc)
{
Console.WriteLine("Exception occurred: " + exc.Message);
}
Console.WriteLine("Press ENTER to quit.");
Console.ReadLine();
}
}
}
App.config:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.1"/>
</startup>
<system.serviceModel>
<bindings>
<wsHttpBinding>
<binding name="WSHttpBinding_ITestService"/>
</wsHttpBinding>
</bindings>
<client>
<endpoint address="https://myserver.com/test/testservice.svc" binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_ITestService" contract="TestServiceReference.ITestService" name="WSHttpBinding_ITestService">
<identity>
<userPrincipalName value="mycomputer\Matt"/>
</identity>
</endpoint>
<!--<endpoint address="http://localhost:20616/TestService.svc/TestService.svc" binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_ITestService" contract="TestServiceReference.ITestService" name="WSHttpBinding_ITestService">
<identity>
<userPrincipalName value="mycomputer\Matt"/>
</identity>
</endpoint>-->
</client>
</system.serviceModel>
</configuration>
WCF Service code.
ITestService.cs:
using System.ServiceModel;
namespace WcfSessionsOverHttpsTest
{
[ServiceContract(SessionMode = SessionMode.Required)]
public interface ITestService
{
[OperationContract(IsInitiating = true)]
bool LogIn(string username, string password);
[OperationContract(IsInitiating = false, IsTerminating = true)]
bool LogOut();
[OperationContract(IsInitiating = false)]
string GetSecretData();
}
}
TestService.svc:
namespace WcfSessionsOverHttpsTest
{
public class TestService : ITestService
{
public bool IsAuthenticated { get; set; }
bool ITestService.LogIn(string username, string password)
{
if (username == "admin" && password == "password")
{
IsAuthenticated = true;
return true;
}
else
{
IsAuthenticated = false;
return false;
}
}
bool ITestService.LogOut()
{
IsAuthenticated = false;
return true;
}
string ITestService.GetSecretData()
{
if (!IsAuthenticated)
{
throw new System.Security.Authentication.AuthenticationException("User has not logged in.");
}
else
{
string secretMessage = "The Red Sox are going to win the World Series in 2016";
return secretMessage;
}
}
}
}
Web.config:
<?xml version="1.0"?>
<configuration>
<appSettings>
<add key="aspnet:UseTaskFriendlySynchronizationContext" value="true"/>
</appSettings>
<system.web>
<compilation debug="true" targetFramework="4.5.1"/>
<httpRuntime targetFramework="4.5"/>
</system.web>
<system.serviceModel>
<bindings>
<wsHttpBinding>
<binding name="wsHttpEndpointBinding" closeTimeout="00:10:00" openTimeout="00:10:00" receiveTimeout="00:10:00" sendTimeout="00:10:00" maxReceivedMessageSize="2147483647">
<readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="2147483647" maxNameTableCharCount="2147483647"/>
</binding>
</wsHttpBinding>
</bindings>
<services>
<service name="WcfSessionsOverHttpsTest.TestService">
<endpoint address="/TestService.svc" binding="wsHttpBinding" bindingConfiguration="wsHttpEndpointBinding" contract="WcfSessionsOverHttpsTest.ITestService"/>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior>
<serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
</behaviors>
<protocolMapping>
<add binding="wsHttpBinding" scheme="http"/>
</protocolMapping>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true"/>
</system.serviceModel>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
<directoryBrowse enabled="true"/>
</system.webServer>
</configuration>
Thanks in advance for any help!
Matt
The solution to this problem was that I needed to add a class that derived from "UserNamePasswordValidator" and register it in Web.Config.
public class CustomUserNameValidator : UserNamePasswordValidator
{
public override void Validate(string userName, string password)
{
return;
}
}
Web.config:
<behaviors>
<serviceBehaviors>
<behavior>
<!-- To avoid disclosing metadata information, set the values below to false before deployment -->
<serviceMetadata httpsGetEnabled="true" />
<!-- To receive exception details in faults for debugging purposes, set the value below to true. Set to false before deployment to avoid disclosing exception information -->
<serviceDebug includeExceptionDetailInFaults="true" />
<serviceCredentials>
<userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="MyProgram.CustomUserNameValidator,MyProgram" />
</serviceCredentials>
</behavior>
</serviceBehaviors>
</behaviors>

WCF UserName Authentication: Can I get the Username in a custom ServiceAuthorizationManager?

I have a WCF service that is using a custom ServiceAuthorizationManager. The custom auth manager is already set up to handle Windows and Forms authentication.
However, if I connect with a client that is set to UserName auth, I can't seem to find the username anywhere.
The client code looks like this:
this.ClientCredentials.UserName.UserName = "user";
this.ClientCredentials.UserName.Password = "pass";
this.Open();
this.MyMethod(); // my actual contract method
this.Close();
Then on the server, I have my custom auth manager:
public sealed class AppAuthorizationManager : ServiceAuthorizationManager
{
public override bool CheckAccess(OperationContext operationContext, ref Message message)
{
// would like to check user/pwd here...
}
}
Is this possible?
The Thread.CurrentPrincipal is not set,
operationContext.ServiceSecurityContext.PrimaryIdentity is not set.
operationContext.ServiceSecurityContext.AuthorizationContext.ClaimSets is empty.
Is the user/pwd supposed to be available anywhere? Or do I have to add a custom UsernamePasswordValidator too?
Update: So I added a custom UserNamePasswordValidator and an IAuthorizationPolicy.
My updated WCF config looks like this:
<behavior name="Server2ServerBehavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="true" />
<serviceAuthorization principalPermissionMode="Custom" serviceAuthorizationManagerType="MyApp.AuthManager, MyApp">
<authorizationPolicies>
<add policyType="MyApp.TokenAuthorizationPolicy, MyApp" />
</authorizationPolicies>
</serviceAuthorization>
<serviceCredentials>
<userNameAuthentication customUserNamePasswordValidatorType="MyApp.PFUserNameValidator, MyApp" />
</serviceCredentials>
</behavior>
If I set a breakpoint in all 3 of those classes, WCF throws the exception:
LogonUser failed for the 'username' user. Ensure that the user has a valid Windows account.
at System.IdentityModel.Selectors.WindowsUserNameSecurityTokenAuthenticator.ValidateUserNamePasswordCore(String userName, String password)
Before any of them are run. Hmmm...
This is normally handled in the UsernamePasswordValidator - which is the only place you'll have access to the password. However, this isn't where you set the principal - that would be in the IAuthorizationPolicy's Evaluate method, which might look something like:
bool IAuthorizationPolicy.Evaluate(
EvaluationContext evaluationContext, ref object state)
{
IList<IIdentity> idents;
object identsObject;
if (evaluationContext.Properties.TryGetValue(
"Identities", out identsObject) && (idents =
identsObject as IList<IIdentity>) != null)
{
foreach (IIdentity ident in idents)
{
if (ident.IsAuthenticated &&
ident.AuthenticationType == TrustedAuthType)
{
evaluationContext.Properties["Principal"]
= //TODO our principal
return true;
}
}
}
if (!evaluationContext.Properties.ContainsKey("Principal"))
{
evaluationContext.Properties["Principal"] = //TODO anon
}
return false;
}
(where TrustedAuthType is the name of our password validator)
With this in place, the thread's principal will be set, and we can identify ourselves (and use roles-based security etc)

Categories

Resources