diff --git a/.editorconfig b/.editorconfig index 5b3ad04..55aff3e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,3 +2,4 @@ # MA0003: Add argument name to improve readability dotnet_diagnostic.MA0003.severity = none +csharp_style_prefer_switch_expression=false:suggestion diff --git a/FabAccessAPI/Auth.cs b/FabAccessAPI/Auth.cs index 1af0cd1..94d218c 100644 --- a/FabAccessAPI/Auth.cs +++ b/FabAccessAPI/Auth.cs @@ -1,175 +1,78 @@ using FabAccessAPI.Schema; using S22.Sasl; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; -using Exception = System.Exception; +using FabAccessAPI.Exceptions; +using System.Linq; namespace FabAccessAPI { - /// Authentication Identity - /// - /// Under the hood a string because the form depends heavily on the method - public struct AuthCId - { - public string Id { get; private set; } - - public AuthCId(string id) : this() { Id = id; } - } - - /// Authorization Identity - /// - /// This identity is internal to FabAccess and completely independent from the authentication - /// method or source - public struct AuthZId - { - /// Main User ID. Generally an user name or similar - public string Uid; - - /// Sub user ID. - /// - /// Can change scopes for permissions, e.g. having a +admin account with more permissions than - /// the default account and +dashboard et.al. accounts that have restricted permissions for - /// their applications - public string Subuid; - - /// Realm this account originates. - /// - /// The Realm is usually described by a domain name but local policy may dictate an unrelated - /// mapping - public string Realm; - } - - /// Authentication/Authorization user object. - /// - /// This struct contains the user as is passed to the actual authentication/authorization - /// subsystems - /// - public struct AuthUser - { - /// Contains the Authentication ID used - /// - /// The authentication ID is an identifier for the authentication exchange. This is different - /// than the ID of the user to be authenticated; for example when using x509 the authcid is - /// the dn of the certificate, when using GSSAPI the authcid is of form `@` - public AuthCId Authcid; - - /// Contains the Authorization ID - /// - /// This is the identifier of the user to *authenticate as*. This in several cases is different - /// to the `authcid`: - /// If somebody wants to authenticate as somebody else, su-style. - /// If a person wants to authenticate as a higher-permissions account, e.g. foo may set authzid foo+admin - /// to split normal user and "admin" accounts. - /// If a method requires a specific authcid that is different from the identifier of the user - /// to authenticate as, e.g. GSSAPI, x509 client certificates, API TOKEN authentication. - public AuthZId Authzid; - - /// Contains the authentication method used - /// - /// For the most part this is the SASL method - public string AuthMethod; - - /// Method-specific key-value pairs - /// - /// Each method can use their own key-value pairs. - /// E.g. EXTERNAL encodes the actual method used (x509 client certs, UID/GID for unix sockets, - /// ...) - public Dictionary Kvs; - - } - - // Authentication has two parts: Granting the authentication itself and then performing the - // authentication. - // Granting the authentication checks if - // a) the given authcid fits with the given (authMethod, kvs). In general a failure here indicates - // a programming failure — the authcid come from the same source as that tuple - // b) the given authcid may authenticate as the given authzid. E.g. if a given client certificate - // has been configured for that user, if a GSSAPI user maps to a given user, - public enum AuthError - { - /// Authentication ID is bad/unknown/.. - BadAuthcid, - /// Authorization ID is unknown/.. - BadAuthzid, - /// Authorization ID is not of form user+uid@realm - MalformedAuthzid, - /// User may not use that authorization id - NotAllowedAuthzid, - - } - - public class UnauthorizedException : Exception { } - public class UnsupportedMechanismException : Exception { } - /// - /// THIS IS VERY INCOMPLETE! + /// Authenticate with SASL /// public class Auth { - #region Log - //private static readonly log4net.ILog _Log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); + #region Private Fields + private readonly IAuthentication _AuthCap; #endregion - private readonly IAuthenticationSystem _AuthCap; - public Auth(IAuthenticationSystem authCap) + #region Constructors + public Auth(IAuthentication authCap) { _AuthCap = authCap; } + #endregion - public Task> GetMechanisms() + #region Methods + public async Task Authenticate(string mech, Dictionary properties) { - return _AuthCap.Mechanisms(); - } - - public async Task Authenticate(string mech, Dictionary properties) - { - SaslMechanism? m = SaslFactory.Create(mech); + SaslMechanism? saslMechanism = SaslFactory.Create(mech); foreach (KeyValuePair entry in properties) { - m.Properties.Add(entry.Key, entry.Value); + saslMechanism.Properties.Add(entry.Key, entry.Value); } - Request.initialResponse? initialResponse = new Request.initialResponse(); - if (m.HasInitial) + byte[] data = new byte[0]; + + if (saslMechanism.HasInitial) { - initialResponse.Initial = m.GetResponse(new byte[0]); + data = saslMechanism.GetResponse(new byte[0]); } - Request? req = new Request + Response? response = await _AuthCap.Step(data); + while (!saslMechanism.IsCompleted) { - Mechanism = m.Name, - InitialResponse = initialResponse - }; - - Response? resp = await _AuthCap.Start(req); - while (!m.IsCompleted) - { - if (resp.which == Response.WHICH.Challence) + if(response.Failed != null) { - byte[]? additional = m.GetResponse(resp.Challence.ToArray()); - resp = await _AuthCap.Step(additional); + switch (response.Failed.Code) + { + case Response.Error.badMechanism: + throw new BadMechanismException(); + case Response.Error.invalidCredentials: + throw new InvalidCredentialsException(); + case Response.Error.aborted: + case Response.Error.failed: + default: + throw new AuthenticationFailedException(response.Failed.AdditionalData.ToArray()); + } + } + if(response.Successful != null) + { + return response.Successful.Session; + } + if(response.Challenge != null) + { + byte[]? additional = saslMechanism.GetResponse(response.Challenge.ToArray()); + response = await _AuthCap.Step(additional); } else { - break; + throw new AuthenticationFailedException(); } } - if (resp.which == Response.WHICH.Outcome) - { - if (resp.Outcome.Result == Response.Result.successful) - { - return true; - } - else - { - //TODO: Provide meaningful info about auth failure - return false; - } - } - - return false; + throw new AuthenticationFailedException(); } + #endregion } } diff --git a/FabAccessAPI/Connection.cs b/FabAccessAPI/Connection.cs index c35ee7f..00d9246 100644 --- a/FabAccessAPI/Connection.cs +++ b/FabAccessAPI/Connection.cs @@ -1,4 +1,5 @@ using Capnp.Rpc; +using FabAccessAPI.Exceptions; using FabAccessAPI.Schema; using System; using System.Collections.Generic; @@ -24,12 +25,13 @@ namespace FabAccessAPI { RpcClient = rpcClient; _BootstrapCap = RpcClient.GetMain(); - //_Log.Debug($"Done bootstraping API connection."); } #endregion #region Fields public TcpRpcClient? RpcClient { get; } = null; + + public Session? Session { get; private set; } = null; #endregion #region Methods @@ -40,48 +42,21 @@ namespace FabAccessAPI /// The desired authentication mechanism /// Key-Value data specific to the mechanism /// - public async Task Auth(string mech, Dictionary kvs, CancellationToken cancellationToken_ = default) + public async Task Auth(string mech, Dictionary kvs, CancellationToken cancellationToken_ = default) { - if(_Auth == null) - { - IAuthenticationSystem? authCap = await _BootstrapCap.AuthenticationSystem(cancellationToken_).ConfigureAwait(false); - _Auth = new Auth(authCap); - } - - IReadOnlyList? mechs = await _Auth.GetMechanisms(); + IReadOnlyList? mechs = await _BootstrapCap.Mechanisms(); if (!mechs.Contains(mech)) { throw new UnsupportedMechanismException(); } - return await _Auth.Authenticate(mech, kvs); - } + if (_Auth == null) + { + IAuthentication? authCap = await _BootstrapCap.CreateSession(mech, cancellationToken_).ConfigureAwait(false); + _Auth = new Auth(authCap); + } - /// - /// Get a wrapped capability to interact with machines - /// - /// A wrapped capability to interact with machines - public async Task AccessMachineSystem() - { - return await _BootstrapCap.MachineSystem(); - } - - /// - /// Get a wrapped capability to interact with users - /// - /// A wrapped capability to interact with users - public async Task AccessUserSystem() - { - return await _BootstrapCap.UserSystem(); - } - - /// - /// Get a wrapped capability to interact with permissions - /// - /// A wrapped capability to interact with permissions - public async Task AccessPermissionSystem() - { - return await _BootstrapCap.PermissionSystem(); + Session = await _Auth.Authenticate(mech, kvs); } #endregion } diff --git a/FabAccessAPI/Exceptions/AuthenticationFailedException.cs b/FabAccessAPI/Exceptions/AuthenticationFailedException.cs new file mode 100644 index 0000000..cef9c6d --- /dev/null +++ b/FabAccessAPI/Exceptions/AuthenticationFailedException.cs @@ -0,0 +1,28 @@ +using System; + +namespace FabAccessAPI.Exceptions +{ + public class AuthenticationFailedException : Exception + { + #region Constructors + public AuthenticationFailedException(byte[] additionalData = null) + { + AdditionalData = additionalData; + } + + public AuthenticationFailedException(byte[] additionalData, string message) : base(message) + { + AdditionalData = additionalData; + } + + public AuthenticationFailedException(byte[] additionalData, string message, Exception inner) : base(message, inner) + { + AdditionalData = additionalData; + } + #endregion + + #region Fields + public byte[]? AdditionalData { get; } + #endregion + } +} diff --git a/FabAccessAPI/Exceptions/BadMechanismException.cs b/FabAccessAPI/Exceptions/BadMechanismException.cs new file mode 100644 index 0000000..087ddbe --- /dev/null +++ b/FabAccessAPI/Exceptions/BadMechanismException.cs @@ -0,0 +1,22 @@ +using System; + +namespace FabAccessAPI.Exceptions +{ + public class BadMechanismException : Exception + { + public BadMechanismException() + { + + } + + public BadMechanismException(string message) : base(message) + { + + } + + public BadMechanismException(string message, Exception inner) : base(message, inner) + { + + } + } +} diff --git a/FabAccessAPI/Exceptions/InvalidCredentialsException.cs b/FabAccessAPI/Exceptions/InvalidCredentialsException.cs new file mode 100644 index 0000000..3da236e --- /dev/null +++ b/FabAccessAPI/Exceptions/InvalidCredentialsException.cs @@ -0,0 +1,22 @@ +using System; + +namespace FabAccessAPI.Exceptions +{ + public class InvalidCredentialsException : Exception + { + public InvalidCredentialsException() + { + + } + + public InvalidCredentialsException(string message) : base(message) + { + + } + + public InvalidCredentialsException(string message, Exception inner) : base(message, inner) + { + + } + } +} diff --git a/FabAccessAPI/Exceptions/UnsupportedMechanismException.cs b/FabAccessAPI/Exceptions/UnsupportedMechanismException.cs new file mode 100644 index 0000000..1f62d09 --- /dev/null +++ b/FabAccessAPI/Exceptions/UnsupportedMechanismException.cs @@ -0,0 +1,22 @@ +using System; + +namespace FabAccessAPI.Exceptions +{ + public class UnsupportedMechanismException : Exception + { + public UnsupportedMechanismException() + { + + } + + public UnsupportedMechanismException(string message) : base(message) + { + + } + + public UnsupportedMechanismException(string message, Exception inner) : base(message, inner) + { + + } + } +} diff --git a/FabAccessAPI/schema b/FabAccessAPI/schema index c855646..c9283eb 160000 --- a/FabAccessAPI/schema +++ b/FabAccessAPI/schema @@ -1 +1 @@ -Subproject commit c855646a90958ae575d58be074d187acb9f8f4fa +Subproject commit c9283ebd696ed6dd428a7c3d24820889f7ab4bf3 diff --git a/FabAccessAPI_Test/Connection_Test.cs b/FabAccessAPI_Test/Connection_Test.cs new file mode 100644 index 0000000..a63149a --- /dev/null +++ b/FabAccessAPI_Test/Connection_Test.cs @@ -0,0 +1,35 @@ +using Capnp.Rpc; +using FabAccessAPI; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace FabAccessAPI_Test +{ + public class Connection_Test + { + [TestCase("test.fab-access.org", 59661)] + public async Task Connect(string host, int port) + { + TcpRpcClient tcpRpcClient = new TcpRpcClient(); + tcpRpcClient.Connect(host, port); + await tcpRpcClient.WhenConnected; + + Connection connection = new Connection(tcpRpcClient); + } + + [TestCase("test.fab-access.org", 59661, "Testuser", "secret")] + public async Task Authenticate_PLAIN(string host, int port, string username, string password) + { + TcpRpcClient tcpRpcClient = new TcpRpcClient(); + tcpRpcClient.Connect(host, port); + await tcpRpcClient.WhenConnected; + + Connection connection = new Connection(tcpRpcClient); + await connection.Auth("PLAIN", new Dictionary(StringComparer.Ordinal) { { "Username", username }, { "Password", password } }); + + Assert.IsNotNull(connection.Session); + } + } +} \ No newline at end of file