Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,15 @@ private async ValueTask FormatInternalAsync(
response.Headers.Vary = varyHeaderValue;
}

if (statusCode is (int)HttpStatusCode.Unauthorized
&& result.ContextData.TryGetValue(
ExecutionContextData.WwwAuthenticateHeaderValue,
out var challengeValue)
&& challengeValue is string challengeHeaderValue)
{
response.Headers.WWWAuthenticate = challengeHeaderValue;
}

OnWriteResponseHeaders(operationResult, format, response.Headers);

await format.Formatter.FormatAsync(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,9 @@ public static class ExecutionContextData
/// The key to get the Vary header value from the context data.
/// </summary>
public const string VaryHeaderValue = "HotChocolate.Caching.VaryHeaderValue";

/// <summary>
/// The key to get the WWW-Authenticate header value from the context data.
/// </summary>
public const string WwwAuthenticateHeaderValue = "HotChocolate.Execution.Transport.WwwAuthenticateHeaderValue";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace HotChocolate.Types.Composite;

/// <summary>
/// The @authenticated directive declares that accessing the annotated member requires an
/// authenticated client.
/// </summary>
[DirectiveType(
DirectiveNames.Authenticated.Name,
DirectiveLocation.FieldDefinition
| DirectiveLocation.Object
| DirectiveLocation.Interface
| DirectiveLocation.Enum
| DirectiveLocation.Scalar,
IsRepeatable = false)]
[GraphQLDescription("The @authenticated directive requires the client to be authenticated.")]
public sealed class Authenticated
{
private const string Name = $"@{DirectiveNames.Authenticated.Name}";

private Authenticated()
{
}

/// <inheritdoc />
public override string ToString() => Name;

/// <summary>
/// The singleton instance of the <see cref="Authenticated"/> directive.
/// </summary>
public static Authenticated Instance { get; } = new();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System.Reflection;
using HotChocolate.Types.Descriptors;
using HotChocolate.Utilities;

namespace HotChocolate.Types.Composite;

/// <summary>
/// Applies the @authenticated directive to the annotated member.
/// </summary>
[AttributeUsage(
AttributeTargets.Class
| AttributeTargets.Struct
| AttributeTargets.Interface
| AttributeTargets.Enum
| AttributeTargets.Method
| AttributeTargets.Property,
AllowMultiple = false)]
public sealed class AuthenticatedAttribute : DescriptorAttribute
{
protected internal override void TryConfigure(
IDescriptorContext context,
IDescriptor descriptor,
ICustomAttributeProvider? attributeProvider)
{
switch (descriptor)
{
case IObjectTypeDescriptor desc:
desc.Authenticated();
break;

case IObjectFieldDescriptor desc:
desc.Authenticated();
break;

case IInterfaceTypeDescriptor desc:
desc.Authenticated();
break;

case IInterfaceFieldDescriptor desc:
desc.Authenticated();
break;

case IEnumTypeDescriptor desc:
desc.Authenticated();
break;

case IScalarTypeDescriptor desc:
desc.Authenticated();
break;

default:
throw ThrowHelper.AuthorizationDirective_UnsupportedDescriptor(
DirectiveNames.Authenticated.Name,
attributeProvider,
descriptor);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
namespace HotChocolate.Types.Composite;

/// <summary>
/// Provides descriptor extensions to apply the @authenticated directive.
/// </summary>
public static class AuthenticatedDescriptorExtensions
{
/// <summary>
/// Applies the @authenticated directive to the object type.
/// </summary>
/// <param name="descriptor">
/// The object type descriptor to apply the directive to.
/// </param>
/// <returns>
/// The object type descriptor with the directive applied.
/// </returns>
/// <exception cref="ArgumentNullException">
/// The <paramref name="descriptor"/> is <c>null</c>.
/// </exception>
public static IObjectTypeDescriptor Authenticated(this IObjectTypeDescriptor descriptor)
{
ArgumentNullException.ThrowIfNull(descriptor);
return descriptor.Directive(Composite.Authenticated.Instance);
}

/// <summary>
/// Applies the @authenticated directive to the object field.
/// </summary>
/// <param name="descriptor">
/// The object field descriptor to apply the directive to.
/// </param>
/// <returns>
/// The object field descriptor with the directive applied.
/// </returns>
/// <exception cref="ArgumentNullException">
/// The <paramref name="descriptor"/> is <c>null</c>.
/// </exception>
public static IObjectFieldDescriptor Authenticated(this IObjectFieldDescriptor descriptor)
{
ArgumentNullException.ThrowIfNull(descriptor);
return descriptor.Directive(Composite.Authenticated.Instance);
}

/// <summary>
/// Applies the @authenticated directive to the interface type.
/// </summary>
/// <param name="descriptor">
/// The interface type descriptor to apply the directive to.
/// </param>
/// <returns>
/// The interface type descriptor with the directive applied.
/// </returns>
/// <exception cref="ArgumentNullException">
/// The <paramref name="descriptor"/> is <c>null</c>.
/// </exception>
public static IInterfaceTypeDescriptor Authenticated(this IInterfaceTypeDescriptor descriptor)
{
ArgumentNullException.ThrowIfNull(descriptor);
return descriptor.Directive(Composite.Authenticated.Instance);
}

/// <summary>
/// Applies the @authenticated directive to the interface field.
/// </summary>
/// <param name="descriptor">
/// The interface field descriptor to apply the directive to.
/// </param>
/// <returns>
/// The interface field descriptor with the directive applied.
/// </returns>
/// <exception cref="ArgumentNullException">
/// The <paramref name="descriptor"/> is <c>null</c>.
/// </exception>
public static IInterfaceFieldDescriptor Authenticated(this IInterfaceFieldDescriptor descriptor)
{
ArgumentNullException.ThrowIfNull(descriptor);
return descriptor.Directive(Composite.Authenticated.Instance);
}

/// <summary>
/// Applies the @authenticated directive to the enum type.
/// </summary>
/// <param name="descriptor">
/// The enum type descriptor to apply the directive to.
/// </param>
/// <returns>
/// The enum type descriptor with the directive applied.
/// </returns>
/// <exception cref="ArgumentNullException">
/// The <paramref name="descriptor"/> is <c>null</c>.
/// </exception>
public static IEnumTypeDescriptor Authenticated(this IEnumTypeDescriptor descriptor)
{
ArgumentNullException.ThrowIfNull(descriptor);
return descriptor.Directive(Composite.Authenticated.Instance);
}

/// <summary>
/// Applies the @authenticated directive to the scalar type.
/// </summary>
/// <param name="descriptor">
/// The scalar type descriptor to apply the directive to.
/// </param>
/// <returns>
/// The scalar type descriptor with the directive applied.
/// </returns>
/// <exception cref="ArgumentNullException">
/// The <paramref name="descriptor"/> is <c>null</c>.
/// </exception>
public static IScalarTypeDescriptor Authenticated(this IScalarTypeDescriptor descriptor)
{
ArgumentNullException.ThrowIfNull(descriptor);
return descriptor.Directive(Composite.Authenticated.Instance);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using HotChocolate.Internal;
using HotChocolate.Types.Descriptors;
using HotChocolate.Types.Descriptors.Configurations;
using HotChocolate.Utilities;

namespace HotChocolate.Types.Composite;

internal static class AuthorizationGroups
{
public static IReadOnlyList<string> CreateGroup(
string directiveName,
IEnumerable<string> values,
string paramName)
{
ArgumentNullException.ThrowIfNull(values);

var set = new SortedSet<string>(StringComparer.Ordinal);

foreach (var value in values)
{
if (string.IsNullOrEmpty(value))
{
throw ThrowHelper.AuthorizationDirective_ValueIsEmpty(directiveName, paramName);
}

set.Add(value);
}

if (set.Count == 0)
{
throw ThrowHelper.AuthorizationDirective_GroupIsEmpty(directiveName, paramName);
}

return [.. set];
}

public static IReadOnlyList<IReadOnlyList<string>> Canonicalize(
string directiveName,
IEnumerable<IEnumerable<string>> groups,
string paramName)
{
ArgumentNullException.ThrowIfNull(groups);

var result = new List<IReadOnlyList<string>>();

foreach (var group in groups)
{
var canonical = CreateGroup(directiveName, group, paramName);

if (!result.Exists(existing => existing.SequenceEqual(canonical)))
{
result.Add(canonical);
}
}

if (result.Count == 0)
{
throw ThrowHelper.AuthorizationDirective_GroupIsEmpty(directiveName, paramName);
}

result.Sort(CompareGroups);
return result;
}

public static void AddGroup<TDirective>(
IDirectiveConfigurationProvider configuration,
ITypeInspector typeInspector,
string directiveName,
string[] values,
string paramName,
Func<TDirective, IReadOnlyList<IReadOnlyList<string>>> getGroups,
Func<IReadOnlyList<IReadOnlyList<string>>, TDirective> create)
where TDirective : class
{
ArgumentNullException.ThrowIfNull(values);

var group = CreateGroup(directiveName, values, paramName);
var groups = new List<IReadOnlyList<string>>();

for (var i = configuration.Directives.Count - 1; i >= 0; i--)
{
if (configuration.Directives[i].Value is TDirective existing)
{
groups.AddRange(getGroups(existing));
configuration.Directives.RemoveAt(i);
}
}

groups.Add(group);
configuration.AddDirective(create(groups), typeInspector);
}

private static int CompareGroups(IReadOnlyList<string> x, IReadOnlyList<string> y)
{
var length = Math.Min(x.Count, y.Count);

for (var i = 0; i < length; i++)
{
var result = string.CompareOrdinal(x[i], y[i]);

if (result != 0)
{
return result;
}
}

return x.Count.CompareTo(y.Count);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
namespace HotChocolate.Types.Composite;

/// <summary>
/// The @policy directive declares the policy groups of which at least one must be fully
/// satisfied to access the annotated member.
/// </summary>
[DirectiveType(
DirectiveNames.Policy.Name,
DirectiveLocation.FieldDefinition
| DirectiveLocation.Object
| DirectiveLocation.Interface
| DirectiveLocation.Enum
| DirectiveLocation.Scalar,
IsRepeatable = false)]
[GraphQLDescription(
"""
The @policy directive requires the client to satisfy all policies of at least one of
the policy groups.
""")]
public sealed class Policy
{
private const string Name = $"@{DirectiveNames.Policy.Name}";

/// <summary>
/// Initializes a new instance of <see cref="Policy"/>.
/// </summary>
/// <param name="policies">
/// The policy groups. All policies of a group are required together, and any single group
/// is sufficient. Groups are stored in canonical order.
/// </param>
public Policy(IReadOnlyList<IReadOnlyList<string>> policies)
{
Policies = AuthorizationGroups.Canonicalize(
DirectiveNames.Policy.Name,
policies,
nameof(policies));
}

/// <summary>
/// Gets the policy groups in canonical order.
/// </summary>
[GraphQLName(DirectiveNames.Policy.Arguments.Policies)]
[GraphQLDescription("The policy groups of which at least one must be fully satisfied.")]
[GraphQLType<NonNullType<ListType<NonNullType<ListType<NonNullType<StringType>>>>>>]
public IReadOnlyList<IReadOnlyList<string>> Policies { get; }

/// <inheritdoc />
public override string ToString()
=> $"{Name}(policies: [{string.Join(", ", Policies.Select(g => $"[{string.Join(", ", g.Select(s => $"\"{s}\""))}]"))}])";
}
Loading
Loading