Browse Source

Merge pull request #332 from Squidex/feature_permissions

Feature permissions
pull/333/head
Sebastian Stehle 7 years ago
committed by GitHub
parent
commit
b5b0a362a1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .gitignore
  2. 41
      src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs
  3. 6
      src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs
  4. 12
      src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs
  5. 33
      src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs
  6. 4
      src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs
  7. 4
      src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs
  8. 39
      src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs
  9. 11
      src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs
  10. 14
      src/Squidex.Domain.Apps.Core.Model/Apps/Named.cs
  11. 91
      src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs
  12. 29
      src/Squidex.Domain.Apps.Core.Model/Apps/RoleExtension.cs
  13. 69
      src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs
  14. 6
      src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs
  15. 1
      src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  16. 33
      src/Squidex.Domain.Apps.Entities/AppProvider.cs
  17. 55
      src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs
  18. 46
      src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs
  19. 14
      src/Squidex.Domain.Apps.Entities/Apps/Commands/AddRole.cs
  20. 6
      src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs
  21. 14
      src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteRole.cs
  22. 4
      src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs
  23. 15
      src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateRole.cs
  24. 14
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs
  25. 15
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs
  26. 2
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppPatterns.cs
  27. 103
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppRoles.cs
  28. 2
      src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs
  29. 15
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs
  30. 6
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndex.cs
  31. 61
      src/Squidex.Domain.Apps.Entities/Apps/RoleExtensions.cs
  32. 74
      src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs
  33. 24
      src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs
  34. 2
      src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs
  35. 21
      src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs
  36. 7
      src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs
  37. 3
      src/Squidex.Domain.Apps.Entities/IAppProvider.cs
  38. 5
      src/Squidex.Domain.Apps.Events/Apps/AppClientUpdated.cs
  39. 5
      src/Squidex.Domain.Apps.Events/Apps/AppContributorAssigned.cs
  40. 17
      src/Squidex.Domain.Apps.Events/Apps/AppRoleAdded.cs
  41. 17
      src/Squidex.Domain.Apps.Events/Apps/AppRoleDeleted.cs
  42. 16
      src/Squidex.Domain.Apps.Events/Apps/AppRoleUpdated.cs
  43. 7
      src/Squidex.Domain.Users.MongoDb/MongoUser.cs
  44. 15
      src/Squidex.Domain.Users/UserManagerExtensions.cs
  45. 22
      src/Squidex.Infrastructure/Commands/CommandContext.cs
  46. 16
      src/Squidex.Infrastructure/Language.cs
  47. 13
      src/Squidex.Infrastructure/PubSubExtensions.cs
  48. 1
      src/Squidex.Infrastructure/Queries/FilterValue.cs
  49. 133
      src/Squidex.Infrastructure/Security/Permission.cs
  50. 91
      src/Squidex.Infrastructure/Security/PermissionSet.cs
  51. 10
      src/Squidex.Shared/Identity/ClaimsPrincipalExtensions.cs
  52. 12
      src/Squidex.Shared/Identity/SquidexClaimTypes.cs
  53. 22
      src/Squidex.Shared/Identity/SquidexRoles.cs
  54. 180
      src/Squidex.Shared/Permissions.cs
  55. 2
      src/Squidex.Shared/Users/IUser.cs
  56. 66
      src/Squidex.Shared/Users/UserExtensions.cs
  57. 1
      src/Squidex/.vscode/settings.json
  58. 1
      src/Squidex/Areas/Api/Controllers/ApiController.cs
  59. 13
      src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs
  60. 10
      src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs
  61. 16
      src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs
  62. 13
      src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs
  63. 145
      src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs
  64. 19
      src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  65. 2
      src/Squidex/Areas/Api/Controllers/Apps/Models/AddLanguageDto.cs
  66. 26
      src/Squidex/Areas/Api/Controllers/Apps/Models/AddRoleDto.cs
  67. 9
      src/Squidex/Areas/Api/Controllers/Apps/Models/AppCreatedDto.cs
  68. 26
      src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs
  69. 9
      src/Squidex/Areas/Api/Controllers/Apps/Models/AssignContributorDto.cs
  70. 10
      src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs
  71. 7
      src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs
  72. 2
      src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs
  73. 2
      src/Squidex/Areas/Api/Controllers/Apps/Models/CreateClientDto.cs
  74. 53
      src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs
  75. 29
      src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs
  76. 10
      src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateClientDto.cs
  77. 2
      src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateLanguageDto.cs
  78. 26
      src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs
  79. 2
      src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
  80. 19
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  81. 2
      src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs
  82. 8
      src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs
  83. 10
      src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs
  84. 9
      src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs
  85. 3
      src/Squidex/Areas/Api/Controllers/Contents/ContentSwaggerController.cs
  86. 34
      src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  87. 34
      src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs
  88. 4
      src/Squidex/Areas/Api/Controllers/Docs/DocsController.cs
  89. 14
      src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs
  90. 6
      src/Squidex/Areas/Api/Controllers/History/HistoryController.cs
  91. 4
      src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs
  92. 8
      src/Squidex/Areas/Api/Controllers/Ping/PingController.cs
  93. 8
      src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs
  94. 18
      src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs
  95. 20
      src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs
  96. 22
      src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs
  97. 9
      src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs
  98. 9
      src/Squidex/Areas/Api/Controllers/UI/UIController.cs
  99. 6
      src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs
  100. 6
      src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs

2
.gitignore

@ -4,6 +4,8 @@
*.vs
*.log
.awCache
.vscode
.awcache

41
src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs

@ -10,44 +10,29 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class AppClient
public sealed class AppClient : Named
{
private readonly string secret;
private readonly string name;
private readonly AppClientPermission permission;
public string Role { get; }
public string Name
{
get { return name; }
}
public string Secret
{
get { return secret; }
}
public string Secret { get; }
public AppClientPermission Permission
public AppClient(string name, string secret, string role)
: base(name)
{
get { return permission; }
}
public AppClient(string name, string secret, AppClientPermission permission)
{
Guard.NotNullOrEmpty(name, nameof(name));
Guard.NotNullOrEmpty(secret, nameof(secret));
Guard.Enum(permission, nameof(permission));
Guard.NotNullOrEmpty(role, nameof(role));
Role = role;
this.name = name;
this.secret = secret;
this.permission = permission;
Secret = secret;
}
[Pure]
public AppClient Update(AppClientPermission newPermission)
public AppClient Update(string newRole)
{
Guard.Enum(newPermission, nameof(newPermission));
Guard.NotNullOrEmpty(newRole, nameof(newRole));
return new AppClient(name, secret, newPermission);
return new AppClient(Name, Secret, newRole);
}
[Pure]
@ -55,7 +40,7 @@ namespace Squidex.Domain.Apps.Core.Apps
{
Guard.NotNullOrEmpty(newName, nameof(newName));
return new AppClient(newName, secret, permission);
return new AppClient(newName, Secret, Role);
}
}
}

6
src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs

@ -39,7 +39,7 @@ namespace Squidex.Domain.Apps.Core.Apps
{
Guard.NotNullOrEmpty(id, nameof(id));
return new AppClients(Inner.Add(id, new AppClient(id, secret, AppClientPermission.Editor)));
return new AppClients(Inner.Add(id, new AppClient(id, secret, Role.Editor)));
}
[Pure]
@ -64,7 +64,7 @@ namespace Squidex.Domain.Apps.Core.Apps
}
[Pure]
public AppClients Update(string id, AppClientPermission permission)
public AppClients Update(string id, string role)
{
Guard.NotNullOrEmpty(id, nameof(id));
@ -73,7 +73,7 @@ namespace Squidex.Domain.Apps.Core.Apps
return this;
}
return new AppClients(Inner.SetItem(id, client.Update(permission)));
return new AppClients(Inner.SetItem(id, client.Update(role)));
}
}
}

12
src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs

@ -11,27 +11,27 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class AppContributors : DictionaryWrapper<string, AppContributorPermission>
public sealed class AppContributors : DictionaryWrapper<string, string>
{
public static readonly AppContributors Empty = new AppContributors();
private AppContributors()
: base(ImmutableDictionary<string, AppContributorPermission>.Empty)
: base(ImmutableDictionary<string, string>.Empty)
{
}
public AppContributors(ImmutableDictionary<string, AppContributorPermission> inner)
public AppContributors(ImmutableDictionary<string, string> inner)
: base(inner)
{
}
[Pure]
public AppContributors Assign(string contributorId, AppContributorPermission permission)
public AppContributors Assign(string contributorId, string role)
{
Guard.NotNullOrEmpty(contributorId, nameof(contributorId));
Guard.Enum(permission, nameof(permission));
Guard.NotNullOrEmpty(role, nameof(role));
return new AppContributors(Inner.SetItem(contributorId, permission));
return new AppContributors(Inner.SetItem(contributorId, role));
}
[Pure]

33
src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs

@ -10,41 +10,26 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class AppPattern
public sealed class AppPattern : Named
{
private readonly string name;
private readonly string pattern;
private readonly string message;
public string Pattern { get; }
public string Name
{
get { return name; }
}
public string Pattern
{
get { return pattern; }
}
public string Message
{
get { return message; }
}
public string Message { get; }
public AppPattern(string name, string pattern, string message = null)
: base(name)
{
Guard.NotNullOrEmpty(name, nameof(name));
Guard.NotNullOrEmpty(pattern, nameof(pattern));
this.name = name;
this.pattern = pattern;
this.message = message;
Pattern = pattern;
Message = message;
}
[Pure]
public AppPattern Update(string name, string pattern, string message)
public AppPattern Update(string newName, string newPattern, string newMessage)
{
return new AppPattern(name, pattern, message);
return new AppPattern(newName, newPattern, newMessage);
}
}
}

4
src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs

@ -17,7 +17,7 @@ namespace Squidex.Domain.Apps.Core.Apps.Json
{
protected override void WriteValue(JsonWriter writer, AppContributors value, JsonSerializer serializer)
{
var json = new Dictionary<string, AppContributorPermission>(value.Count);
var json = new Dictionary<string, string>(value.Count);
foreach (var contributor in value)
{
@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Core.Apps.Json
protected override AppContributors ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer)
{
var json = serializer.Deserialize<Dictionary<string, AppContributorPermission>>(reader);
var json = serializer.Deserialize<Dictionary<string, string>>(reader);
return new AppContributors(json.ToImmutableDictionary());
}

4
src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs

@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Core.Apps.Json
public string Secret { get; set; }
[JsonProperty]
public AppClientPermission Permission { get; set; }
public string Role { get; set; }
public JsonAppClient()
{
@ -32,7 +32,7 @@ namespace Squidex.Domain.Apps.Core.Apps.Json
public AppClient ToClient()
{
return new AppClient(Name, Secret, Permission);
return new AppClient(Name, Secret, Role);
}
}
}

39
src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs

@ -0,0 +1,39 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Newtonsoft.Json;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Security;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Squidex.Domain.Apps.Core.Apps.Json
{
public sealed class RolesConverter : JsonClassConverter<Roles>
{
protected override void WriteValue(JsonWriter writer, Roles value, JsonSerializer serializer)
{
var json = new Dictionary<string, string[]>(value.Count);
foreach (var role in value)
{
json.Add(role.Key, role.Value.Permissions.ToIds().ToArray());
}
serializer.Serialize(writer, json);
}
protected override Roles ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer)
{
var json = serializer.Deserialize<Dictionary<string, string[]>>(reader);
return new Roles(json.ToImmutableDictionary(x => x.Key, x => new Role(x.Key, new PermissionSet(x.Value))));
}
}
}

11
src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs

@ -13,14 +13,11 @@ namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class LanguageConfig : IFieldPartitionItem
{
private static readonly Language[] DefaultFallback = new Language[0];
private readonly Language language;
private readonly Language[] languageFallbacks;
private readonly bool isOptional;
public bool IsOptional
{
get { return isOptional; }
}
public bool IsOptional { get; }
public Language Language
{
@ -56,10 +53,10 @@ namespace Squidex.Domain.Apps.Core.Apps
{
Guard.NotNull(language, nameof(language));
this.isOptional = isOptional;
IsOptional = isOptional;
this.language = language;
this.languageFallbacks = fallback ?? new Language[0];
this.languageFallbacks = fallback ?? DefaultFallback;
}
}
}

14
src/Squidex/Pipeline/AppApiAttribute.cs → src/Squidex.Domain.Apps.Core.Model/Apps/Named.cs

@ -5,15 +5,19 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.AspNetCore.Mvc;
using Squidex.Infrastructure;
namespace Squidex.Pipeline
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class AppApiAttribute : ServiceFilterAttribute
public abstract class Named
{
public AppApiAttribute()
: base(typeof(AppApiFilter))
public string Name { get; }
protected Named(string name)
{
Guard.NotNullOrEmpty(name, nameof(name));
Name = name;
}
}
}

91
src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs

@ -0,0 +1,91 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using P = Squidex.Shared.Permissions;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class Role : Named
{
public const string Editor = "Editor";
public const string Developer = "Developer";
public const string Owner = "Owner";
public const string Reader = "Reader";
private static readonly HashSet<string> DefaultRolesSet = new HashSet<string>
{
Editor,
Developer,
Owner,
Reader
};
public PermissionSet Permissions { get; }
public Role(string name, PermissionSet permissions)
: base(name)
{
Guard.NotNull(permissions, nameof(permissions));
Permissions = permissions;
}
public Role(string name, params Permission[] permissions)
: this(name, new PermissionSet(permissions))
{
}
[Pure]
public Role Update(string[] permissions)
{
return new Role(Name, new PermissionSet(permissions));
}
public static bool IsDefaultRole(string role)
{
return role != null && DefaultRolesSet.Contains(role);
}
public static Role CreateOwner(string app)
{
return new Role(Owner,
P.ForApp(P.App, app));
}
public static Role CreateEditor(string app)
{
return new Role(Editor,
P.ForApp(P.AppAssets, app),
P.ForApp(P.AppCommon, app),
P.ForApp(P.AppContents, app));
}
public static Role CreateReader(string app)
{
return new Role(Reader,
P.ForApp(P.AppAssetsRead, app),
P.ForApp(P.AppCommon, app),
P.ForApp(P.AppContentsRead, app));
}
public static Role CreateDeveloper(string app)
{
return new Role(Developer,
P.ForApp(P.AppApi, app),
P.ForApp(P.AppAssets, app),
P.ForApp(P.AppCommon, app),
P.ForApp(P.AppContents, app),
P.ForApp(P.AppPatterns, app),
P.ForApp(P.AppRules, app),
P.ForApp(P.AppSchemas, app));
}
}
}

29
src/Squidex.Domain.Apps.Core.Model/Apps/RoleExtension.cs

@ -1,29 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps
{
public static class RoleExtension
{
public static AppPermission ToAppPermission(this AppClientPermission clientPermission)
{
Guard.Enum(clientPermission, nameof(clientPermission));
return (AppPermission)Enum.Parse(typeof(AppPermission), clientPermission.ToString());
}
public static AppPermission ToAppPermission(this AppContributorPermission contributorPermission)
{
Guard.Enum(contributorPermission, nameof(contributorPermission));
return (AppPermission)Enum.Parse(typeof(AppPermission), contributorPermission.ToString());
}
}
}

69
src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs

@ -0,0 +1,69 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.Contracts;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class Roles : DictionaryWrapper<string, Role>
{
public static readonly Roles Empty = new Roles();
private Roles()
: base(ImmutableDictionary<string, Role>.Empty)
{
}
public Roles(ImmutableDictionary<string, Role> inner)
: base(inner)
{
}
[Pure]
public Roles Add(string name)
{
var newRole = new Role(name);
return new Roles(Inner.Add(name, newRole));
}
[Pure]
public Roles Remove(string name)
{
return new Roles(Inner.Remove(name));
}
[Pure]
public Roles Update(string name, params string[] permissions)
{
Guard.NotNullOrEmpty(name, nameof(name));
Guard.NotNull(permissions, nameof(permissions));
if (!TryGetValue(name, out var role))
{
return this;
}
return new Roles(Inner.SetItem(name, role.Update(permissions)));
}
public static Roles CreateDefaults(string app)
{
return new Roles(
new Dictionary<string, Role>
{
[Role.Developer] = Role.CreateDeveloper(app),
[Role.Editor] = Role.CreateEditor(app),
[Role.Owner] = Role.CreateOwner(app),
[Role.Reader] = Role.CreateReader(app)
}.ToImmutableDictionary());
}
}
}

6
src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs

@ -23,9 +23,15 @@ namespace Squidex.Domain.Apps.Core.Comments
public Comment(Guid id, Instant time, RefToken user, string text)
{
Guard.NotEmpty(id, nameof(id));
Guard.NotNull(user, nameof(user));
Guard.NotNull(text, nameof(text));
Id = id;
Time = time;
Text = text;
User = user;
}
}

1
src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj

@ -19,6 +19,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup>
<PropertyGroup>
<CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet>

33
src/Squidex.Domain.Apps.Entities/AppProvider.cs

@ -20,6 +20,8 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
namespace Squidex.Domain.Apps.Entities
{
@ -158,23 +160,46 @@ namespace Squidex.Domain.Apps.Entities
});
}
public Task<List<IAppEntity>> GetUserApps(string userId)
public Task<List<IAppEntity>> GetUserApps(string userId, PermissionSet permissions)
{
Guard.NotNull(userId, nameof(userId));
Guard.NotNull(permissions, nameof(permissions));
return localCache.GetOrCreateAsync($"GetUserApps({userId})", async () =>
{
using (Profiler.TraceMethod<AppProvider>())
{
var ids = await grainFactory.GetGrain<IAppsByUserIndex>(userId).GetAppIdsAsync();
var ids =
await Task.WhenAll(
GetAppIdsByUserAsync(userId),
GetAppIdsAsync(permissions.ToAppNames()));
var apps =
await Task.WhenAll(
ids.Select(id => grainFactory.GetGrain<IAppGrain>(id).GetStateAsync()));
await Task.WhenAll(ids
.SelectMany(x => x)
.Select(id => grainFactory.GetGrain<IAppGrain>(id).GetStateAsync()));
return apps.Where(a => IsFound(a.Value)).Select(a => a.Value).ToList();
}
});
}
private async Task<List<Guid>> GetAppIdsByUserAsync(string userId)
{
using (Profiler.TraceMethod<AppProvider>())
{
return await grainFactory.GetGrain<IAppsByUserIndex>(userId).GetAppIdsAsync();
}
}
private async Task<List<Guid>> GetAppIdsAsync(IEnumerable<string> names)
{
using (Profiler.TraceMethod<AppProvider>())
{
return await grainFactory.GetGrain<IAppsByNameIndex>(SingleGrain.Id).GetAppIdsAsync(names.ToArray());
}
}
private async Task<Guid> GetAppIdAsync(string name)
{
using (Profiler.TraceMethod<AppProvider>())

55
src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs

@ -70,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
case AssignContributor assigneContributor:
return UpdateReturnAsync(assigneContributor, async c =>
{
await GuardAppContributors.CanAssign(Snapshot.Contributors, c, userResolver, appPlansProvider.GetPlan(Snapshot.Plan?.PlanId));
await GuardAppContributors.CanAssign(Snapshot.Contributors, c, userResolver, appPlansProvider.GetPlan(Snapshot.Plan?.PlanId), Snapshot.Roles);
AssignContributor(c);
@ -96,7 +96,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
case UpdateClient updateClient:
return UpdateAsync(updateClient, c =>
{
GuardAppClients.CanUpdate(Snapshot.Clients, c);
GuardAppClients.CanUpdate(Snapshot.Clients, c, Snapshot.Roles);
UpdateClient(c);
});
@ -133,10 +133,34 @@ namespace Squidex.Domain.Apps.Entities.Apps
UpdateLanguage(c);
});
case AddRole addRole:
return UpdateAsync(addRole, c =>
{
GuardAppRoles.CanAdd(Snapshot.Roles, c);
AddRole(c);
});
case DeleteRole deleteRole:
return UpdateAsync(deleteRole, c =>
{
GuardAppRoles.CanDelete(Snapshot.Roles, c, Snapshot.Contributors, Snapshot.Clients);
DeleteRole(c);
});
case UpdateRole updateRole:
return UpdateAsync(updateRole, c =>
{
GuardAppRoles.CanUpdate(Snapshot.Roles, c);
UpdateRole(c);
});
case AddPattern addPattern:
return UpdateAsync(addPattern, c =>
{
GuardAppPattern.CanAdd(Snapshot.Patterns, c);
GuardAppPatterns.CanAdd(Snapshot.Patterns, c);
AddPattern(c);
});
@ -144,7 +168,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
case DeletePattern deletePattern:
return UpdateAsync(deletePattern, c =>
{
GuardAppPattern.CanDelete(Snapshot.Patterns, c);
GuardAppPatterns.CanDelete(Snapshot.Patterns, c);
DeletePattern(c);
});
@ -152,7 +176,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
case UpdatePattern updatePattern:
return UpdateAsync(updatePattern, c =>
{
GuardAppPattern.CanUpdate(Snapshot.Patterns, c);
GuardAppPatterns.CanUpdate(Snapshot.Patterns, c);
UpdatePattern(c);
});
@ -226,9 +250,9 @@ namespace Squidex.Domain.Apps.Entities.Apps
RaiseEvent(SimpleMapper.Map(command, new AppClientRenamed()));
}
if (command.Permission.HasValue)
if (command.Role != null)
{
RaiseEvent(SimpleMapper.Map(command, new AppClientUpdated { Permission = command.Permission.Value }));
RaiseEvent(SimpleMapper.Map(command, new AppClientUpdated { Role = command.Role }));
}
}
@ -287,6 +311,21 @@ namespace Squidex.Domain.Apps.Entities.Apps
RaiseEvent(SimpleMapper.Map(command, new AppPatternUpdated()));
}
public void AddRole(AddRole command)
{
RaiseEvent(SimpleMapper.Map(command, new AppRoleAdded()));
}
public void DeleteRole(DeleteRole command)
{
RaiseEvent(SimpleMapper.Map(command, new AppRoleDeleted()));
}
public void UpdateRole(UpdateRole command)
{
RaiseEvent(SimpleMapper.Map(command, new AppRoleUpdated()));
}
public void ArchiveApp(ArchiveApp command)
{
RaiseEvent(SimpleMapper.Map(command, new AppArchived()));
@ -327,7 +366,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
private static AppContributorAssigned CreateInitialOwner(RefToken actor)
{
return new AppContributorAssigned { ContributorId = actor.Identifier, Permission = AppContributorPermission.Owner };
return new AppContributorAssigned { ContributorId = actor.Identifier, Role = Role.Owner };
}
protected override AppState OnEvent(Envelope<IEvent> @event)

46
src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs

@ -19,8 +19,14 @@ namespace Squidex.Domain.Apps.Entities.Apps
public AppHistoryEventsCreator(TypeNameRegistry typeNameRegistry)
: base(typeNameRegistry)
{
AddEventMessage("AppContributorAssignedEvent",
"assigned {user:[Contributor]} as {[Role]}");
AddEventMessage("AppClientUpdatedEvent",
"updated client {[Id]}");
AddEventMessage<AppContributorAssigned>(
"assigned {user:[Contributor]} as {[Permission]}");
"assigned {user:[Contributor]} as {[Role]}");
AddEventMessage<AppContributorRemoved>(
"removed {user:[Contributor]} from app");
@ -57,6 +63,15 @@ namespace Squidex.Domain.Apps.Entities.Apps
AddEventMessage<AppPatternUpdated>(
"updated pattern {[Name]}");
AddEventMessage<AppRoleAdded>(
"added role {[Name]}");
AddEventMessage<AppRoleDeleted>(
"deleted role {[Name]}");
AddEventMessage<AppRoleUpdated>(
"updated role {[Name]}");
}
protected Task<HistoryEventToStore> On(AppContributorRemoved @event)
@ -74,7 +89,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
return Task.FromResult(
ForEvent(@event, channel)
.AddParameter("Contributor", @event.ContributorId).AddParameter("Permission", @event.Permission));
.AddParameter("Contributor", @event.ContributorId).AddParameter("Role", @event.Role));
}
protected Task<HistoryEventToStore> On(AppClientAttached @event)
@ -167,6 +182,33 @@ namespace Squidex.Domain.Apps.Entities.Apps
.AddParameter("PatternId", @event.PatternId));
}
protected Task<HistoryEventToStore> On(AppRoleAdded @event)
{
const string channel = "settings.roles";
return Task.FromResult(
ForEvent(@event, channel)
.AddParameter("Name", @event.Name));
}
protected Task<HistoryEventToStore> On(AppRoleUpdated @event)
{
const string channel = "settings.roles";
return Task.FromResult(
ForEvent(@event, channel)
.AddParameter("Name", @event.Name));
}
protected Task<HistoryEventToStore> On(AppRoleDeleted @event)
{
const string channel = "settings.roles";
return Task.FromResult(
ForEvent(@event, channel)
.AddParameter("Name", @event.Name));
}
protected override Task<HistoryEventToStore> CreateEventCoreAsync(Envelope<IEvent> @event)
{
return this.DispatchFuncAsync(@event.Payload, (HistoryEventToStore)null);

14
src/Squidex.Domain.Apps.Entities/Apps/Commands/AddRole.cs

@ -0,0 +1,14 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
public sealed class AddRole : AppCommand
{
public string Name { get; set; }
}
}

6
src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs

@ -5,7 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Apps;
using Roles = Squidex.Domain.Apps.Core.Apps.Role;
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
@ -13,8 +13,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
public string ContributorId { get; set; }
public bool FromRestore { get; set; }
public string Role { get; set; } = Roles.Developer;
public AppContributorPermission Permission { get; set; }
public bool FromRestore { get; set; }
}
}

14
src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteRole.cs

@ -0,0 +1,14 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
public sealed class DeleteRole : AppCommand
{
public string Name { get; set; }
}
}

4
src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs

@ -5,8 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Apps;
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
public sealed class UpdateClient : AppCommand
@ -15,6 +13,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands
public string Name { get; set; }
public AppClientPermission? Permission { get; set; }
public string Role { get; set; }
}
}

15
src/Squidex/Pipeline/MustBeAppOwnerAttribute.cs → src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateRole.cs

@ -1,19 +1,16 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Apps;
namespace Squidex.Pipeline
{
public sealed class MustBeAppOwnerAttribute : AppPermissionAttribute
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
public MustBeAppOwnerAttribute()
: base(AppPermission.Owner)
public sealed class UpdateRole : AppCommand
{
}
public string Name { get; set; }
public string[] Permissions { get; set; }
}
}

14
src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs

@ -45,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
});
}
public static void CanUpdate(AppClients clients, UpdateClient command)
public static void CanUpdate(AppClients clients, UpdateClient command, Roles roles)
{
Guard.NotNull(command, nameof(command));
@ -58,14 +58,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
e("Client id is required.", nameof(command.Id));
}
if (string.IsNullOrWhiteSpace(command.Name) && command.Permission == null)
if (string.IsNullOrWhiteSpace(command.Name) && command.Role == null)
{
e("Either name or permission must be defined.", nameof(command.Name), nameof(command.Permission));
e("Either name or role must be defined.", nameof(command.Name), nameof(command.Role));
}
if (command.Permission.HasValue && !command.Permission.Value.IsEnumValue())
if (command.Role != null && !roles.ContainsKey(command.Role))
{
e("Permission is not valid.", nameof(command.Permission));
e("Role is not valid.", nameof(command.Role));
}
if (client == null)
@ -78,9 +78,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
e("Client has already this name.", nameof(command.Name));
}
if (command.Permission == client.Permission)
if (command.Role == client.Role)
{
e("Client has already this permission.", nameof(command.Permission));
e("Client has already this role.", nameof(command.Role));
}
});
}

15
src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs

@ -7,7 +7,6 @@
using System;
using System.Linq;
using System.Security;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
@ -19,15 +18,15 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
public static class GuardAppContributors
{
public static Task CanAssign(AppContributors contributors, AssignContributor command, IUserResolver users, IAppLimitsPlan plan)
public static Task CanAssign(AppContributors contributors, AssignContributor command, IUserResolver users, IAppLimitsPlan plan, Roles roles)
{
Guard.NotNull(command, nameof(command));
return Validate.It(() => "Cannot assign contributor.", async e =>
{
if (!command.Permission.IsEnumValue())
if (!roles.ContainsKey(command.Role))
{
e("Permission is not valid.", nameof(command.Permission));
e("Role is not valid.", nameof(command.Role));
}
if (string.IsNullOrWhiteSpace(command.ContributorId))
@ -47,14 +46,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
if (string.Equals(command.ContributorId, command.Actor?.Identifier, StringComparison.OrdinalIgnoreCase) && !command.FromRestore)
{
throw new SecurityException("You cannot change your own permission.");
throw new DomainForbiddenException("You cannot change your own role.");
}
if (contributors.TryGetValue(command.ContributorId, out var existing))
{
if (existing == command.Permission)
if (existing == command.Role)
{
e("Contributor has already this permission.", nameof(command.Permission));
e("Contributor has already this role.", nameof(command.Role));
}
}
else if (plan.MaxContributors == contributors.Count)
@ -75,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
e("Contributor id is required.", nameof(command.ContributorId));
}
var ownerIds = contributors.Where(x => x.Value == AppContributorPermission.Owner).Select(x => x.Key).ToList();
var ownerIds = contributors.Where(x => x.Value == Role.Owner).Select(x => x.Key).ToList();
if (ownerIds.Count == 1 && ownerIds.Contains(command.ContributorId))
{

2
src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppPattern.cs → src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppPatterns.cs

@ -12,7 +12,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
public static class GuardAppPattern
public static class GuardAppPatterns
{
public static void CanAdd(AppPatterns patterns, AddPattern command)
{

103
src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppRoles.cs

@ -0,0 +1,103 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Linq;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
public static class GuardAppRoles
{
public static void CanAdd(Roles roles, AddRole command)
{
Guard.NotNull(command, nameof(command));
Validate.It(() => "Cannot add role.", e =>
{
if (string.IsNullOrWhiteSpace(command.Name))
{
e("Name is required.", nameof(command.Name));
}
else if (roles.ContainsKey(command.Name))
{
e("A role with the same name already exists.");
}
});
}
public static void CanDelete(Roles roles, DeleteRole command, AppContributors contributors, AppClients clients)
{
Guard.NotNull(command, nameof(command));
GetRoleOrThrow(roles, command.Name);
Validate.It(() => "Cannot delete role.", e =>
{
if (string.IsNullOrWhiteSpace(command.Name))
{
e("Name is required.", nameof(command.Name));
}
else if (Role.IsDefaultRole(command.Name))
{
e("Cannot delete a default role.");
}
if (clients.Values.Any(x => string.Equals(x.Role, command.Name, StringComparison.OrdinalIgnoreCase)))
{
e("Cannot remove a role when a client is assigned.");
}
if (contributors.Values.Any(x => string.Equals(x, command.Name, StringComparison.OrdinalIgnoreCase)))
{
e("Cannot remove a role when a contributor is assigned.");
}
});
}
public static void CanUpdate(Roles roles, UpdateRole command)
{
Guard.NotNull(command, nameof(command));
GetRoleOrThrow(roles, command.Name);
Validate.It(() => "Cannot delete role.", e =>
{
if (string.IsNullOrWhiteSpace(command.Name))
{
e("Name is required.", nameof(command.Name));
}
else if (Role.IsDefaultRole(command.Name))
{
e("Cannot update a default role.");
}
if (command.Permissions == null)
{
e("Permissions is required.", nameof(command.Permissions));
}
});
}
private static Role GetRoleOrThrow(Roles roles, string name)
{
if (string.IsNullOrWhiteSpace(name))
{
return null;
}
if (!roles.TryGetValue(name, out var role))
{
throw new DomainObjectNotFoundException(name, "Roles", typeof(IAppEntity));
}
return role;
}
}
}

2
src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs

@ -17,6 +17,8 @@ namespace Squidex.Domain.Apps.Entities.Apps
{
string Name { get; }
Roles Roles { get; }
AppPlan Plan { get; }
AppClients Clients { get; }

15
src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs

@ -104,6 +104,21 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
return persistence.WriteSnapshotAsync(state);
}
public Task<List<Guid>> GetAppIdsAsync(params string[] names)
{
var appIds = new List<Guid>();
foreach (var appName in names)
{
if (state.Apps.TryGetValue(appName, out var appId))
{
appIds.Add(appId);
}
}
return Task.FromResult(appIds);
}
public Task<Guid> GetAppIdAsync(string appName)
{
state.Apps.TryGetValue(appName, out var appId);

6
src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndex.cs

@ -24,8 +24,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
Task RemoveReservationAsync(Guid appId, string name);
Task<Guid> GetAppIdAsync(string name);
Task<List<Guid>> GetAppIdsAsync();
Task<List<Guid>> GetAppIdsAsync(string[] names);
Task<Guid> GetAppIdAsync(string name);
}
}

61
src/Squidex.Domain.Apps.Entities/Apps/RoleExtensions.cs

@ -0,0 +1,61 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Linq;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
namespace Squidex.Domain.Apps.Entities.Apps
{
public static class RoleExtensions
{
public static string[] Prefix(this string[] permissions, string name)
{
var result = new string[permissions.Length + 1];
result[0] = Permissions.ForApp(Permissions.AppCommon, name).Id;
if (permissions.Length > 0)
{
var prefix = Permissions.ForApp(Permissions.App, name).Id;
for (var i = 0; i < permissions.Length; i++)
{
result[i + 1] = string.Concat(prefix, ".", permissions[i]);
}
}
permissions = result;
return permissions;
}
public static PermissionSet WithoutApp(this PermissionSet set, string name)
{
var prefix = Permissions.ForApp(Permissions.App, name).Id;
return new PermissionSet(set.Select(x =>
{
var id = x.Id;
if (string.Equals(id, prefix, StringComparison.OrdinalIgnoreCase))
{
return Permission.Any;
}
else if (id.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
return id.Substring(prefix.Length + 1);
}
else
{
return id;
}
}));
}
}
}

74
src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs

@ -0,0 +1,74 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
namespace Squidex.Domain.Apps.Entities.Apps
{
public sealed class RolePermissionsProvider
{
private readonly IAppProvider appProvider;
public RolePermissionsProvider(IAppProvider appProvider)
{
Guard.NotNull(appProvider, nameof(appProvider));
this.appProvider = appProvider;
}
public async Task<List<string>> GetPermissionsAsync(IAppEntity app)
{
var schemaNames = await GetSchemaNamesAsync(app);
var result = new List<string> { Permission.Any };
foreach (var permission in Permissions.ForAppsNonSchema)
{
if (permission.Length > Permissions.App.Length + 1)
{
var trimmed = permission.Substring(Permissions.App.Length + 1);
if (trimmed.Length > 0)
{
result.Add(trimmed);
}
}
}
foreach (var permission in Permissions.ForAppsSchema)
{
var trimmed = permission.Substring(Permissions.App.Length + 1);
foreach (var schema in schemaNames)
{
var replaced = trimmed.Replace("{name}", schema);
result.Add(replaced);
}
}
return result;
}
private async Task<List<string>> GetSchemaNamesAsync(IAppEntity app)
{
var schemas = await appProvider.GetSchemasAsync(app.Id);
var schemaNames = new List<string>(); ;
schemaNames.Add(Permission.Any);
schemaNames.AddRange(schemas.Select(x => x.Name));
return schemaNames;
}
}
}

24
src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs

@ -22,6 +22,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
[JsonProperty]
public string Name { get; set; }
[JsonProperty]
public Roles Roles { get; set; } = Roles.Empty;
[JsonProperty]
public AppPlan Plan { get; set; }
@ -42,6 +45,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
protected void On(AppCreated @event)
{
Roles = Roles.CreateDefaults(@event.Name);
SimpleMapper.Map(@event, this);
}
@ -52,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
protected void On(AppContributorAssigned @event)
{
Contributors = Contributors.Assign(@event.ContributorId, @event.Permission);
Contributors = Contributors.Assign(@event.ContributorId, @event.Role);
}
protected void On(AppContributorRemoved @event)
@ -67,7 +72,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
protected void On(AppClientUpdated @event)
{
Clients = Clients.Update(@event.Id, @event.Permission);
Clients = Clients.Update(@event.Id, @event.Role);
}
protected void On(AppClientRenamed @event)
@ -95,6 +100,21 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
Patterns = Patterns.Update(@event.PatternId, @event.Name, @event.Pattern, @event.Message);
}
protected void On(AppRoleAdded @event)
{
Roles = Roles.Add(@event.Name);
}
protected void On(AppRoleDeleted @event)
{
Roles = Roles.Remove(@event.Name);
}
protected void On(AppRoleUpdated @event)
{
Roles = Roles.Update(@event.Name, @event.Permissions.Prefix(Name));
}
protected void On(AppLanguageAdded @event)
{
LanguagesConfig = LanguagesConfig.Set(new LanguageConfig(@event.Language));

2
src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs

@ -252,7 +252,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
AppId = CurrentJob.AppId,
ContributorId = actor.Identifier,
FromRestore = true,
Permission = AppContributorPermission.Developer
Role = Role.Developer
});
}

21
src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs

@ -8,6 +8,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.OData;
using Squidex.Domain.Apps.Core.Contents;
@ -21,6 +22,9 @@ using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Queries.OData;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Shared.Identity;
using Squidex.Shared.Users;
#pragma warning disable RECS0147
@ -73,6 +77,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
var schema = await GetSchemaAsync(context);
CheckPermission(schema, context.Base.User);
using (Profiler.TraceMethod<ContentQueryService>())
{
var isVersioned = version > EtagVersion.Empty;
@ -86,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (content == null || (content.Status != Status.Published && !context.Base.IsFrontendClient) || content.SchemaId.Id != schema.Id)
{
throw new DomainObjectNotFoundException(id.ToString(), typeof(ISchemaEntity));
throw new DomainObjectNotFoundException(id.ToString(), typeof(IContentEntity));
}
return Transform(context.Base, schema, true, content);
@ -99,6 +105,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
var schema = await GetSchemaAsync(context);
CheckPermission(schema, context.Base.User);
using (Profiler.TraceMethod<ContentQueryService>())
{
var status = GetQueryStatus(context.Base);
@ -257,6 +265,17 @@ namespace Squidex.Domain.Apps.Entities.Contents
return schema;
}
private void CheckPermission(ISchemaEntity schema, ClaimsPrincipal user)
{
var permissions = user.Permissions();
var permission = Permissions.ForApp(Permissions.AppContentsRead, schema.AppId.Name, schema.Name);
if (!permissions.Allows(permission))
{
throw new DomainForbiddenException("You do not have permission for this schema.");
}
}
private static Status[] GetFindStatus(QueryContext context)
{
if (context.IsFrontendClient)

7
src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs

@ -36,6 +36,13 @@ namespace Squidex.Domain.Apps.Entities.History
texts[typeNameRegistry.GetName<TEvent>()] = message;
}
protected void AddEventMessage(string type, string message)
{
Guard.NotNullOrEmpty(message, nameof(message));
texts[type] = message;
}
protected bool HasEventText(IEvent @event)
{
var message = typeNameRegistry.GetName(@event.GetType());

3
src/Squidex.Domain.Apps.Entities/IAppProvider.cs

@ -11,6 +11,7 @@ using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Rules;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure.Security;
namespace Squidex.Domain.Apps.Entities
{
@ -28,6 +29,6 @@ namespace Squidex.Domain.Apps.Entities
Task<List<IRuleEntity>> GetRulesAsync(Guid appId);
Task<List<IAppEntity>> GetUserApps(string userId);
Task<List<IAppEntity>> GetUserApps(string userId, PermissionSet permissions);
}
}

5
src/Squidex.Domain.Apps.Events/Apps/AppClientUpdated.cs

@ -5,16 +5,15 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Apps
{
[EventType(nameof(AppClientUpdated))]
[EventType(nameof(AppClientUpdated), 2)]
public sealed class AppClientUpdated : AppEvent
{
public string Id { get; set; }
public AppClientPermission Permission { get; set; }
public string Role { get; set; }
}
}

5
src/Squidex.Domain.Apps.Events/Apps/AppContributorAssigned.cs

@ -5,16 +5,15 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Apps
{
[EventType(nameof(AppContributorAssigned))]
[EventType(nameof(AppContributorAssigned), 2)]
public sealed class AppContributorAssigned : AppEvent
{
public string ContributorId { get; set; }
public AppContributorPermission Permission { get; set; }
public string Role { get; set; }
}
}

17
src/Squidex.Domain.Apps.Events/Apps/AppRoleAdded.cs

@ -0,0 +1,17 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Apps
{
[EventType(nameof(AppRoleAdded))]
public sealed class AppRoleAdded : AppEvent
{
public string Name { get; set; }
}
}

17
src/Squidex.Domain.Apps.Events/Apps/AppRoleDeleted.cs

@ -0,0 +1,17 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Apps
{
[EventType(nameof(AppRoleDeleted))]
public sealed class AppRoleDeleted : AppEvent
{
public string Name { get; set; }
}
}

16
src/Squidex/Pipeline/MustBeAppDeveloperAttribute.cs → src/Squidex.Domain.Apps.Events/Apps/AppRoleUpdated.cs

@ -1,19 +1,19 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Pipeline
namespace Squidex.Domain.Apps.Events.Apps
{
public sealed class MustBeAppDeveloperAttribute : AppPermissionAttribute
[EventType(nameof(AppRoleUpdated))]
public sealed class AppRoleUpdated : AppEvent
{
public MustBeAppDeveloperAttribute()
: base(AppPermission.Developer)
{
}
public string Name { get; set; }
public string[] Permissions { get; set; }
}
}

7
src/Squidex.Domain.Users.MongoDb/MongoUser.cs

@ -136,6 +136,11 @@ namespace Squidex.Domain.Users.MongoDb
Logins.RemoveAll(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey);
}
public void RemoveClaims(string type)
{
Claims.RemoveAll(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase));
}
public void AddClaim(Claim claim)
{
Claims.Add(claim);
@ -173,7 +178,7 @@ namespace Squidex.Domain.Users.MongoDb
public void SetClaim(string type, string value)
{
Claims.RemoveAll(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase));
RemoveClaims(type);
AddClaim(new Claim(type, value));
}

15
src/Squidex.Domain.Users/UserManagerExtensions.cs

@ -11,6 +11,7 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
using Squidex.Shared.Users;
namespace Squidex.Domain.Users
@ -45,7 +46,7 @@ namespace Squidex.Domain.Users
return result;
}
public static async Task<IUser> CreateAsync(this UserManager<IUser> userManager, IUserFactory factory, string email, string displayName, string password)
public static async Task<IUser> CreateAsync(this UserManager<IUser> userManager, IUserFactory factory, string email, string displayName, string password, PermissionSet permissions = null)
{
var user = factory.Create(email);
@ -54,6 +55,11 @@ namespace Squidex.Domain.Users
user.SetDisplayName(displayName);
user.SetPictureUrlFromGravatar(email);
if (permissions != null)
{
user.SetPermissions(permissions);
}
await DoChecked(() => userManager.CreateAsync(user), "Cannot create user.");
if (!string.IsNullOrWhiteSpace(password))
@ -80,7 +86,7 @@ namespace Squidex.Domain.Users
return userManager.UpdateAsync(user);
}
public static async Task UpdateAsync(this UserManager<IUser> userManager, string id, string email, string displayName, string password)
public static async Task UpdateAsync(this UserManager<IUser> userManager, string id, string email, string displayName, string password, PermissionSet permissions = null)
{
var user = await userManager.FindByIdAsync(id);
@ -100,6 +106,11 @@ namespace Squidex.Domain.Users
user.SetDisplayName(displayName);
}
if (permissions != null)
{
user.SetPermissions(permissions);
}
await DoChecked(() => userManager.UpdateAsync(user), "Cannot update user.");
if (!string.IsNullOrWhiteSpace(password))

22
src/Squidex.Infrastructure/Commands/CommandContext.cs

@ -11,25 +11,13 @@ namespace Squidex.Infrastructure.Commands
{
public sealed class CommandContext
{
private readonly ICommand command;
private readonly ICommandBus commandBus;
private readonly Guid contextId = Guid.NewGuid();
private Tuple<object> result;
public ICommand Command
{
get { return command; }
}
public Guid ContextId { get; } = Guid.NewGuid();
public ICommandBus CommandBus
{
get { return commandBus; }
}
public ICommand Command { get; }
public Guid ContextId
{
get { return contextId; }
}
public ICommandBus CommandBus { get; }
public bool IsCompleted
{
@ -41,8 +29,8 @@ namespace Squidex.Infrastructure.Commands
Guard.NotNull(command, nameof(command));
Guard.NotNull(commandBus, nameof(commandBus));
this.command = command;
this.commandBus = commandBus;
Command = command;
CommandBus = commandBus;
}
public CommandContext Complete(object resultValue = null)

16
src/Squidex.Infrastructure/Language.cs

@ -15,8 +15,6 @@ namespace Squidex.Infrastructure
{
private static readonly Regex CultureRegex = new Regex("^([a-z]{2})(\\-[a-z]{2})?$", RegexOptions.IgnoreCase);
private static readonly Dictionary<string, Language> AllLanguagesField = new Dictionary<string, Language>(StringComparer.OrdinalIgnoreCase);
private readonly string iso2Code;
private readonly string englishName;
private static Language AddLanguage(string iso2Code, string englishName)
{
@ -42,21 +40,15 @@ namespace Squidex.Infrastructure
get { return AllLanguagesField.Values; }
}
public string EnglishName
{
get { return englishName; }
}
public string EnglishName { get; }
public string Iso2Code
{
get { return iso2Code; }
}
public string Iso2Code { get; }
private Language(string iso2Code, string englishName)
{
this.iso2Code = iso2Code;
Iso2Code = iso2Code;
this.englishName = englishName;
EnglishName = englishName;
}
public static bool IsValidLanguage(string iso2Code)

13
src/Squidex.Infrastructure/PubSubExtensions.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System;
using System.Threading;
using System.Threading.Tasks;
#pragma warning disable 4014
@ -46,7 +47,7 @@ namespace Squidex.Infrastructure
IDisposable subscription = null;
try
{
var receiveTask = new TaskCompletionSource<TResponse>();
var receiveTask = new TaskCompletionSource<TResponse>(TaskCreationOptions.RunContinuationsAsynchronously);
subscription = pubsub.Subscribe<Response<TResponse>>(response =>
{
@ -58,17 +59,23 @@ namespace Squidex.Infrastructure
Task.Run(() => pubsub.Publish(request, self));
var firstTask = await Task.WhenAny(receiveTask.Task, Task.Delay(timeout));
using (var cts = new CancellationTokenSource())
{
var delayTask = Task.Delay(timeout, cts.Token);
if (firstTask.Id != receiveTask.Task.Id)
var resultTask = await Task.WhenAny(receiveTask.Task, delayTask);
if (resultTask == delayTask)
{
throw new TaskCanceledException();
}
else
{
cts.Cancel();
return await receiveTask.Task;
}
}
}
finally
{
subscription?.Dispose();

1
src/Squidex.Infrastructure/Queries/FilterValue.cs

@ -20,6 +20,7 @@ namespace Squidex.Infrastructure.Queries
public object Value { get; }
public FilterValueType ValueType { get; }
public bool IsList { get; }
public FilterValue(Guid value)

133
src/Squidex.Infrastructure/Security/Permission.cs

@ -0,0 +1,133 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
namespace Squidex.Infrastructure.Security
{
public sealed class Permission : IComparable<Permission>, IEquatable<Permission>
{
public const string Any = "*";
private static readonly char[] MainSeparators = { '.' };
private static readonly char[] AlternativeSeparators = { '|' };
private readonly string id;
private readonly Lazy<HashSet<string>[]> idParts;
public string Id
{
get { return id; }
}
public Permission(string id)
{
Guard.NotNullOrEmpty(id, nameof(id));
this.id = id;
idParts = new Lazy<HashSet<string>[]>(() => id
.Split(MainSeparators, StringSplitOptions.RemoveEmptyEntries)
.Select(x =>
{
if (x == Any)
{
return null;
}
var alternatives = x.Split(AlternativeSeparators, StringSplitOptions.RemoveEmptyEntries);
return new HashSet<string>(alternatives, StringComparer.OrdinalIgnoreCase);
})
.ToArray());
}
public bool Allows(Permission permission)
{
if (permission == null)
{
return false;
}
var lhs = idParts.Value;
var rhs = permission.idParts.Value;
if (lhs.Length > rhs.Length)
{
return false;
}
for (var i = 0; i < lhs.Length; i++)
{
var l = lhs[i];
var r = rhs[i];
if (l != null && (r == null || !l.Intersect(r).Any()))
{
return false;
}
}
return true;
}
public bool Includes(Permission permission)
{
if (permission == null)
{
return false;
}
var lhs = idParts.Value;
var rhs = permission.idParts.Value;
for (var i = 0; i < Math.Min(lhs.Length, rhs.Length); i++)
{
var l = lhs[i];
var r = rhs[i];
if (l != null && r != null && !l.Intersect(r).Any())
{
return false;
}
}
return true;
}
public bool StartsWith(string id)
{
return id.StartsWith(id, StringComparison.OrdinalIgnoreCase);
}
public override bool Equals(object obj)
{
return Equals(obj as Permission);
}
public bool Equals(Permission other)
{
return other != null && string.Equals(id, other.id, StringComparison.OrdinalIgnoreCase);
}
public override int GetHashCode()
{
return id.GetHashCode();
}
public override string ToString()
{
return id;
}
public int CompareTo(Permission other)
{
return other == null ? -1 : string.Compare(id, other.id, StringComparison.Ordinal);
}
}
}

91
src/Squidex.Infrastructure/Security/PermissionSet.cs

@ -0,0 +1,91 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace Squidex.Infrastructure.Security
{
public sealed class PermissionSet : IReadOnlyCollection<Permission>
{
public static readonly PermissionSet Empty = new PermissionSet(new string[0]);
private readonly List<Permission> permissions;
private readonly Lazy<string> display;
public int Count
{
get { return permissions.Count; }
}
public PermissionSet(params Permission[] permissions)
: this((IEnumerable<Permission>)permissions)
{
}
public PermissionSet(params string[] permissions)
: this(permissions?.Select(x => new Permission(x)))
{
}
public PermissionSet(IEnumerable<string> permissions)
: this(permissions?.Select(x => new Permission(x)))
{
}
public PermissionSet(IEnumerable<Permission> permissions)
{
Guard.NotNull(permissions, nameof(permissions));
this.permissions = permissions.ToList();
display = new Lazy<string>(() => string.Join(";", this.permissions));
}
public bool Allows(Permission other)
{
if (other == null)
{
return false;
}
return permissions.Any(x => x.Allows(other));
}
public bool Includes(Permission other)
{
if (other == null)
{
return false;
}
return permissions.Any(x => x.Includes(other));
}
public override string ToString()
{
return display.Value;
}
public IEnumerable<string> ToIds()
{
return permissions.Select(x => x.Id);
}
public IEnumerator<Permission> GetEnumerator()
{
return permissions.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return permissions.GetEnumerator();
}
}
}

10
src/Squidex.Shared/Identity/ClaimsPrincipalExtensions.cs

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using Squidex.Infrastructure.Security;
namespace Squidex.Shared.Identity
{
@ -16,12 +17,17 @@ namespace Squidex.Shared.Identity
{
public static void SetDisplayName(this ClaimsIdentity identity, string displayName)
{
identity.AddClaim(new Claim(SquidexClaimTypes.SquidexDisplayName, displayName));
identity.AddClaim(new Claim(SquidexClaimTypes.DisplayName, displayName));
}
public static void SetPictureUrl(this ClaimsIdentity identity, string pictureUrl)
{
identity.AddClaim(new Claim(SquidexClaimTypes.SquidexPictureUrl, pictureUrl));
identity.AddClaim(new Claim(SquidexClaimTypes.PictureUrl, pictureUrl));
}
public static PermissionSet Permissions(this ClaimsPrincipal principal)
{
return new PermissionSet(principal.Claims.Where(x => x.Type == SquidexClaimTypes.Permissions).Select(x => new Permission(x.Value)));
}
public static IEnumerable<Claim> GetSquidexClaims(this ClaimsPrincipal principal)

12
src/Squidex.Shared/Identity/SquidexClaimTypes.cs

@ -9,15 +9,17 @@ namespace Squidex.Shared.Identity
{
public static class SquidexClaimTypes
{
public static readonly string SquidexDisplayName = "urn:squidex:name";
public static readonly string DisplayName = "urn:squidex:name";
public static readonly string SquidexPictureUrl = "urn:squidex:picture";
public static readonly string PictureUrl = "urn:squidex:picture";
public static readonly string SquidexConsent = "urn:squidex:consent";
public static readonly string Consent = "urn:squidex:consent";
public static readonly string SquidexConsentForEmails = "urn:squidex:consent:emails";
public static readonly string ConsentForEmails = "urn:squidex:consent:emails";
public static readonly string SquidexHidden = "urn:squidex:hidden";
public static readonly string Hidden = "urn:squidex:hidden";
public static readonly string Permissions = "urn:squidex:permissions";
public static readonly string Prefix = "urn:squidex:";
}

22
src/Squidex.Shared/Identity/SquidexRoles.cs

@ -1,22 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Shared.Identity
{
public static class SquidexRoles
{
public static readonly string Administrator = "ADMINISTRATOR";
public static readonly string AppOwner = "app:owner";
public static readonly string AppEditor = "app:editor";
public static readonly string AppReader = "app:reader";
public static readonly string AppDeveloper = "app:dev";
}
}

180
src/Squidex.Shared/Permissions.cs

@ -0,0 +1,180 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
namespace Squidex.Shared
{
public static class Permissions
{
private static readonly List<string> ForAppsNonSchemaList = new List<string>();
private static readonly List<string> ForAppsSchemaList = new List<string>();
public static IReadOnlyList<string> ForAppsNonSchema
{
get { return ForAppsNonSchemaList; }
}
public static IReadOnlyList<string> ForAppsSchema
{
get { return ForAppsSchemaList; }
}
public const string All = "squidex.*";
public const string Admin = "squidex.admin.*";
public const string AdminOrleans = "squidex.admin.orleans";
public const string AdminRestore = "squidex.admin.restore";
public const string AdminRestoreRead = "squidex.admin.restore.read";
public const string AdminRestoreCreate = "squidex.admin.restore.create";
public const string AdminEvents = "squidex.admin.events";
public const string AdminEventsRead = "squidex.admin.events.read";
public const string AdminEventsManage = "squidex.admin.events.manage";
public const string AdminUsers = "squidex.admin.users";
public const string AdminUsersRead = "squidex.admin.users.read";
public const string AdminUsersCreate = "squidex.admin.users.create";
public const string AdminUsersUpdate = "squidex.admin.users.update";
public const string AdminUsersUnlock = "squidex.admin.users.unlock";
public const string AdminUsersLock = "squidex.admin.users.lock";
public const string App = "squidex.apps.{app}";
public const string AppCommon = "squidex.apps.{app}.common";
public const string AppDelete = "squidex.apps.{app}.delete";
public const string AppClients = "squidex.apps.{app}.clients";
public const string AppClientsRead = "squidex.apps.{app}.clients.read";
public const string AppClientsCreate = "squidex.apps.{app}.clients.create";
public const string AppClientsUpdate = "squidex.apps.{app}.clients.update";
public const string AppClientsDelete = "squidex.apps.{app}.clients.delete";
public const string AppContributors = "squidex.apps.{app}.contributors";
public const string AppContributorsRead = "squidex.apps.{app}.contributors.read";
public const string AppContributorsAssign = "squidex.apps.{app}.contributors.assign";
public const string AppContributorsRevoke = "squidex.apps.{app}.contributors.revoke";
public const string AppLanguages = "squidex.apps.{app}.languages";
public const string AppLanguagesCreate = "squidex.apps.{app}.languages.create";
public const string AppLanguagesUpdate = "squidex.apps.{app}.languages.update";
public const string AppLanguagesDelete = "squidex.apps.{app}.languages.delete";
public const string AppRoles = "squidex.apps.{app}.roles";
public const string AppRolesRead = "squidex.apps.{app}.roles.read";
public const string AppRolesCreate = "squidex.apps.{app}.roles.create";
public const string AppRolesUpdate = "squidex.apps.{app}.roles.update";
public const string AppRolesDelete = "squidex.apps.{app}.roles.delete";
public const string AppPatterns = "squidex.apps.{app}.patterns";
public const string AppPatternsRead = "squidex.apps.{app}.patterns.read";
public const string AppPatternsCreate = "squidex.apps.{app}.patterns.create";
public const string AppPatternsUpdate = "squidex.apps.{app}.patterns.update";
public const string AppPatternsDelete = "squidex.apps.{app}.patterns.delete";
public const string AppBackups = "squidex.apps.{app}.backups";
public const string AppBackupsRead = "squidex.apps.{app}.backups.read";
public const string AppBackupsCreate = "squidex.apps.{app}.backups.create";
public const string AppBackupsDelete = "squidex.apps.{app}.backups.delete";
public const string AppPlans = "squidex.apps.{app}.plans";
public const string AppPlansRead = "squidex.apps.{app}.plans.read";
public const string AppPlansChange = "squidex.apps.{app}.plans.change";
public const string AppAssets = "squidex.apps.{app}.assets";
public const string AppAssetsRead = "squidex.apps.{app}.assets.read";
public const string AppAssetsCreate = "squidex.apps.{app}.assets.create";
public const string AppAssetsUpdate = "squidex.apps.{app}.assets.update";
public const string AppAssetsDelete = "squidex.apps.{app}.assets.delete";
public const string AppRules = "squidex.apps.{app}.rules";
public const string AppRulesRead = "squidex.apps.{app}.rules.read";
public const string AppRulesCreate = "squidex.apps.{app}.rules.create";
public const string AppRulesUpdate = "squidex.apps.{app}.rules.update";
public const string AppRulesDisable = "squidex.apps.{app}.rules.disable";
public const string AppRulesDelete = "squidex.apps.{app}.rules.delete";
public const string AppSchemas = "squidex.apps.{app}.schemas.{name}";
public const string AppSchemasRead = "squidex.apps.{app}.schemas.{name}.read";
public const string AppSchemasCreate = "squidex.apps.{app}.schemas.{name}.create";
public const string AppSchemasUpdate = "squidex.apps.{app}.schemas.{name}.update";
public const string AppSchemasScripts = "squidex.apps.{app}.schemas.{name}.scripts";
public const string AppSchemasPublish = "squidex.apps.{app}.schemas.{name}.publish";
public const string AppSchemasDelete = "squidex.apps.{app}.schemas.{name}.delete";
public const string AppContents = "squidex.apps.{app}.contents.{name}";
public const string AppContentsRead = "squidex.apps.{app}.contents.{name}.read";
public const string AppContentsGraphQL = "squidex.apps.{app}.contents.{name}.graphql";
public const string AppContentsCreate = "squidex.apps.{app}.contents.{name}.create";
public const string AppContentsUpdate = "squidex.apps.{app}.contents.{name}.update";
public const string AppContentsDiscard = "squidex.apps.{app}.contents.{name}.discard";
public const string AppContentsArchive = "squidex.apps.{app}.contents.{name}.archive";
public const string AppContentsRestore = "squidex.apps.{app}.contents.{name}.restore";
public const string AppContentsPublish = "squidex.apps.{app}.contents.{name}.publish";
public const string AppContentsUnpublish = "squidex.apps.{app}.contents.{name}.unpublish";
public const string AppContentsDelete = "squidex.apps.{app}.contents.{name}.delete";
public const string AppApi = "squidex.apps.{app}.api";
static Permissions()
{
foreach (var field in typeof(Permissions).GetFields(BindingFlags.Public | BindingFlags.Static))
{
if (field.IsLiteral && !field.IsInitOnly)
{
var value = (string)field.GetValue(null);
if (value.StartsWith(App, StringComparison.OrdinalIgnoreCase))
{
if (value.IndexOf("{name}", App.Length, StringComparison.OrdinalIgnoreCase) >= 0)
{
ForAppsSchemaList.Add(value);
}
else
{
ForAppsNonSchemaList.Add(value);
}
}
}
}
}
public static Permission ForApp(string id, string app = "*", string schema = "*")
{
Guard.NotNull(id, nameof(id));
return new Permission(id.Replace("{app}", app ?? "*").Replace("{name}", schema ?? "*"));
}
public static PermissionSet ToAppPermissions(this PermissionSet permissions, string app)
{
var matching = permissions.Where(x => x.StartsWith($"squidex.apps.{app}"));
return new PermissionSet(matching);
}
public static string[] ToAppNames(this PermissionSet permissions)
{
var matching = permissions.Where(x => x.StartsWith($"squidex.apps."));
var result =
matching
.Select(x => x.Id.Split('.'))
.Select(x => x[2])
.Distinct()
.ToArray();
return result;
}
}
}

2
src/Squidex.Shared/Users/IUser.cs

@ -24,6 +24,8 @@ namespace Squidex.Shared.Users
IReadOnlyList<ExternalLogin> Logins { get; }
void RemoveClaims(string type);
void SetEmail(string email);
void SetClaim(string type, string value);

66
src/Squidex.Shared/Users/UserExtensions.cs

@ -7,7 +7,9 @@
using System;
using System.Linq;
using System.Security.Claims;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
using Squidex.Shared.Identity;
namespace Squidex.Shared.Users
@ -16,97 +18,117 @@ namespace Squidex.Shared.Users
{
public static void SetDisplayName(this IUser user, string displayName)
{
user.SetClaim(SquidexClaimTypes.SquidexDisplayName, displayName);
user.SetClaim(SquidexClaimTypes.DisplayName, displayName);
}
public static void SetPictureUrl(this IUser user, string pictureUrl)
{
user.SetClaim(SquidexClaimTypes.SquidexPictureUrl, pictureUrl);
user.SetClaim(SquidexClaimTypes.PictureUrl, pictureUrl);
}
public static void SetPictureUrlToStore(this IUser user)
{
user.SetClaim(SquidexClaimTypes.SquidexPictureUrl, "store");
user.SetClaim(SquidexClaimTypes.PictureUrl, "store");
}
public static void SetPictureUrlFromGravatar(this IUser user, string email)
{
user.SetClaim(SquidexClaimTypes.SquidexPictureUrl, GravatarHelper.CreatePictureUrl(email));
user.SetClaim(SquidexClaimTypes.PictureUrl, GravatarHelper.CreatePictureUrl(email));
}
public static void SetHidden(this IUser user, bool value)
{
user.SetClaim(SquidexClaimTypes.SquidexHidden, value.ToString());
user.SetClaim(SquidexClaimTypes.Hidden, value.ToString());
}
public static void SetConsent(this IUser user)
{
user.SetClaim(SquidexClaimTypes.SquidexConsent, "true");
user.SetClaim(SquidexClaimTypes.Consent, "true");
}
public static void SetConsentForEmails(this IUser user, bool value)
{
user.SetClaim(SquidexClaimTypes.SquidexConsentForEmails, value.ToString());
user.SetClaim(SquidexClaimTypes.ConsentForEmails, value.ToString());
}
public static void SetPermissions(this IUser user, PermissionSet permissions)
{
user.RemoveClaims(SquidexClaimTypes.Permissions);
foreach (var permission in permissions)
{
user.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission.Id));
}
}
public static bool IsHidden(this IUser user)
{
return user.HasClaimValue(SquidexClaimTypes.SquidexHidden, "true");
return user.HasClaimValue(SquidexClaimTypes.Hidden, "true");
}
public static bool HasConsent(this IUser user)
{
return user.HasClaimValue(SquidexClaimTypes.SquidexConsent, "true");
return user.HasClaimValue(SquidexClaimTypes.Consent, "true");
}
public static bool HasConsentForEmails(this IUser user)
{
return user.HasClaimValue(SquidexClaimTypes.SquidexConsentForEmails, "true");
return user.HasClaimValue(SquidexClaimTypes.ConsentForEmails, "true");
}
public static bool HasDisplayName(this IUser user)
{
return user.HasClaim(SquidexClaimTypes.SquidexDisplayName);
return user.HasClaim(SquidexClaimTypes.DisplayName);
}
public static bool HasPictureUrl(this IUser user)
{
return user.HasClaim(SquidexClaimTypes.SquidexPictureUrl);
return user.HasClaim(SquidexClaimTypes.PictureUrl);
}
public static bool IsPictureUrlStored(this IUser user)
{
return user.HasClaimValue(SquidexClaimTypes.SquidexPictureUrl, "store");
return user.HasClaimValue(SquidexClaimTypes.PictureUrl, "store");
}
public static string PictureUrl(this IUser user)
{
return user.GetClaimValue(SquidexClaimTypes.SquidexPictureUrl);
return user.GetClaimValue(SquidexClaimTypes.PictureUrl);
}
public static string DisplayName(this IUser user)
{
return user.GetClaimValue(SquidexClaimTypes.SquidexDisplayName);
return user.GetClaimValue(SquidexClaimTypes.DisplayName);
}
public static PermissionSet Permissions(this IUser user)
{
return new PermissionSet(user.GetClaimValues(SquidexClaimTypes.Permissions).Select(x => new Permission(x)));
}
public static string GetClaimValue(this IUser user, string type)
{
return user.Claims.FirstOrDefault(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase))?.Value;
}
public static string GetClaimValue(this IUser user, string claim)
public static string[] GetClaimValues(this IUser user, string type)
{
return user.Claims.FirstOrDefault(x => string.Equals(x.Type, claim, StringComparison.OrdinalIgnoreCase))?.Value;
return user.Claims.Where(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase)).Select(x => x.Value).ToArray();
}
public static bool HasClaim(this IUser user, string claim)
public static bool HasClaim(this IUser user, string type)
{
return user.Claims.Any(x => string.Equals(x.Type, claim, StringComparison.OrdinalIgnoreCase));
return user.Claims.Any(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase));
}
public static bool HasClaimValue(this IUser user, string claim, string value)
public static bool HasClaimValue(this IUser user, string type, string value)
{
return user.Claims.Any(x => string.Equals(x.Type, claim, StringComparison.OrdinalIgnoreCase) && string.Equals(x.Value, value, StringComparison.OrdinalIgnoreCase));
return user.Claims.Any(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase) && string.Equals(x.Value, value, StringComparison.OrdinalIgnoreCase));
}
public static string PictureNormalizedUrl(this IUser user)
{
var url = user.Claims.FirstOrDefault(x => x.Type == SquidexClaimTypes.SquidexPictureUrl)?.Value;
var url = user.Claims.FirstOrDefault(x => x.Type == SquidexClaimTypes.PictureUrl)?.Value;
if (!string.IsNullOrWhiteSpace(url) && Uri.IsWellFormedUriString(url, UriKind.Absolute) && url.Contains("gravatar"))
{

1
src/Squidex/.vscode/settings.json

@ -23,6 +23,7 @@
"**/*.user": true,
"**/*.xproj": true,
"**/*.gitattributes": true,
".awcache": true,
".vs:": true,
".vscode:": true
}

1
src/Squidex/Areas/Api/Controllers/ApiController.cs

@ -16,6 +16,7 @@ using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers
{
[Area("Api")]
[ApiExceptionFilter]
[ApiModelValidation(false)]
public abstract class ApiController : Controller
{

13
src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs

@ -12,16 +12,13 @@ using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Apps
{
/// <summary>
/// Manages and configures apps.
/// </summary>
[ApiAuthorize]
[ApiExceptionFilter]
[AppApi]
[MustBeAppEditor]
[ApiExplorerSettings(GroupName = nameof(Apps))]
public sealed class AppClientsController : ApiController
{
@ -44,6 +41,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
[HttpGet]
[Route("apps/{app}/clients/")]
[ProducesResponseType(typeof(ClientDto[]), 200)]
[ApiPermission(Permissions.AppClientsRead)]
[ApiCosts(0)]
public IActionResult GetClients(string app)
{
@ -70,8 +68,9 @@ namespace Squidex.Areas.Api.Controllers.Apps
[HttpPost]
[Route("apps/{app}/clients/")]
[ProducesResponseType(typeof(ClientDto), 201)]
[ApiPermission(Permissions.AppClientsCreate)]
[ApiCosts(1)]
public async Task<IActionResult> PostClient(string app, [FromBody] CreateAppClientDto request)
public async Task<IActionResult> PostClient(string app, [FromBody] CreateClientDto request)
{
var command = request.ToCommand();
@ -98,8 +97,9 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </remarks>
[HttpPut]
[Route("apps/{app}/clients/{clientId}/")]
[ApiPermission(Permissions.AppClientsUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutClient(string app, string clientId, [FromBody] UpdateAppClientDto request)
public async Task<IActionResult> PutClient(string app, string clientId, [FromBody] UpdateClientDto request)
{
await CommandBus.PublishAsync(request.ToCommand(clientId));
@ -120,6 +120,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </remarks>
[HttpDelete]
[Route("apps/{app}/clients/{clientId}/")]
[ApiPermission(Permissions.AppClientsDelete)]
[ApiCosts(1)]
public async Task<IActionResult> DeleteClient(string app, string clientId)
{

10
src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs

@ -12,16 +12,13 @@ using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Apps
{
/// <summary>
/// Manages and configures apps.
/// </summary>
[ApiAuthorize]
[ApiExceptionFilter]
[AppApi]
[MustBeAppOwner]
[ApiExplorerSettings(GroupName = nameof(Apps))]
public sealed class AppContributorsController : ApiController
{
@ -44,6 +41,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
[HttpGet]
[Route("apps/{app}/contributors/")]
[ProducesResponseType(typeof(ContributorsDto), 200)]
[ApiPermission(Permissions.AppContributorsRead)]
[ApiCosts(0)]
public IActionResult GetContributors(string app)
{
@ -68,8 +66,9 @@ namespace Squidex.Areas.Api.Controllers.Apps
[Route("apps/{app}/contributors/")]
[ProducesResponseType(typeof(ContributorAssignedDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppContributorsAssign)]
[ApiCosts(1)]
public async Task<IActionResult> PostContributor(string app, [FromBody] AssignAppContributorDto request)
public async Task<IActionResult> PostContributor(string app, [FromBody] AssignContributorDto request)
{
var command = request.ToCommand();
var context = await CommandBus.PublishAsync(command);
@ -93,6 +92,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
[HttpDelete]
[Route("apps/{app}/contributors/{id}/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppContributorsRevoke)]
[ApiCosts(1)]
public async Task<IActionResult> DeleteContributor(string app, string id)
{

16
src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs

@ -13,15 +13,13 @@ using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Apps
{
/// <summary>
/// Manages and configures apps.
/// </summary>
[ApiAuthorize]
[ApiExceptionFilter]
[AppApi]
[ApiExplorerSettings(GroupName = nameof(Apps))]
public sealed class AppLanguagesController : ApiController
{
@ -38,10 +36,10 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// 200 => Language configuration returned.
/// 404 => App not found.
/// </returns>
[MustBeAppReader]
[HttpGet]
[Route("apps/{app}/languages/")]
[ProducesResponseType(typeof(AppLanguageDto[]), 200)]
[ApiPermission(Permissions.AppCommon)]
[ApiCosts(0)]
public IActionResult GetLanguages(string app)
{
@ -62,13 +60,13 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// 400 => Language request not valid.
/// 404 => App not found.
/// </returns>
[MustBeAppEditor]
[HttpPost]
[Route("apps/{app}/languages/")]
[ProducesResponseType(typeof(AppLanguageDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppLanguagesCreate)]
[ApiCosts(1)]
public async Task<IActionResult> PostLanguage(string app, [FromBody] AddAppLanguageDto request)
public async Task<IActionResult> PostLanguage(string app, [FromBody] AddLanguageDto request)
{
var command = request.ToCommand();
@ -90,11 +88,11 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// 400 => Language request not valid.
/// 404 => Language or app not found.
/// </returns>
[MustBeAppEditor]
[HttpPut]
[Route("apps/{app}/languages/{language}/")]
[ApiPermission(Permissions.AppLanguagesUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> Update(string app, string language, [FromBody] UpdateAppLanguageDto request)
public async Task<IActionResult> Update(string app, string language, [FromBody] UpdateLanguageDto request)
{
await CommandBus.PublishAsync(request.ToCommand(ParseLanguage(language)));
@ -110,9 +108,9 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// 204 => Language deleted.
/// 404 => Language or app not found.
/// </returns>
[MustBeAppEditor]
[HttpDelete]
[Route("apps/{app}/languages/{language}/")]
[ApiPermission(Permissions.AppLanguagesDelete)]
[ApiCosts(1)]
public async Task<IActionResult> DeleteLanguage(string app, string language)
{

13
src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs

@ -13,16 +13,13 @@ using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Apps
{
/// <summary>
/// Manages and configures app patterns.
/// </summary>
[ApiAuthorize]
[MustBeAppDeveloper]
[ApiExceptionFilter]
[AppApi]
[ApiExplorerSettings(GroupName = nameof(Apps))]
public sealed class AppPatternsController : ApiController
{
@ -45,6 +42,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
[HttpGet]
[Route("apps/{app}/patterns/")]
[ProducesResponseType(typeof(AppPatternDto[]), 200)]
[ApiPermission(Permissions.AppPatternsRead)]
[ApiCosts(0)]
public IActionResult GetPatterns(string app)
{
@ -56,7 +54,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
}
/// <summary>
/// Create a new app patterm.
/// Create a new app pattern.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="request">Pattern to be added to the app.</param>
@ -68,6 +66,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
[HttpPost]
[Route("apps/{app}/patterns/")]
[ProducesResponseType(typeof(AppPatternDto), 201)]
[ApiPermission(Permissions.AppPatternsCreate)]
[ApiCosts(1)]
public async Task<IActionResult> PostPattern(string app, [FromBody] UpdatePatternDto request)
{
@ -81,7 +80,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
}
/// <summary>
/// Update an existing app patterm.
/// Update an existing app pattern.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The id of the pattern to be updated.</param>
@ -94,6 +93,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
[HttpPut]
[Route("apps/{app}/patterns/{id}/")]
[ProducesResponseType(typeof(AppPatternDto), 201)]
[ApiPermission(Permissions.AppPatternsUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> UpdatePattern(string app, Guid id, [FromBody] UpdatePatternDto request)
{
@ -116,6 +116,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </remarks>
[HttpDelete]
[Route("apps/{app}/patterns/{id}/")]
[ApiPermission(Permissions.AppPatternsDelete)]
[ApiCosts(1)]
public async Task<IActionResult> DeletePattern(string app, Guid id)
{

145
src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs

@ -0,0 +1,145 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Apps
{
/// <summary>
/// Manages and configures apps.
/// </summary>
[ApiExplorerSettings(GroupName = nameof(Apps))]
public sealed class AppRolesController : ApiController
{
private readonly RolePermissionsProvider permissionsProvider;
public AppRolesController(ICommandBus commandBus, RolePermissionsProvider permissionsProvider)
: base(commandBus)
{
this.permissionsProvider = permissionsProvider;
}
/// <summary>
/// Get app roles.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => App roles returned.
/// 404 => App not found.
/// </returns>
[HttpGet]
[Route("apps/{app}/roles/")]
[ProducesResponseType(typeof(RolesDto), 200)]
[ApiPermission(Permissions.AppRolesRead)]
[ApiCosts(0)]
public IActionResult GetRoles(string app)
{
var response = RolesDto.FromApp(App);
Response.Headers["ETag"] = App.Version.ToString();
return Ok(response);
}
/// <summary>
/// Get app permissions.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => App permissions returned.
/// 404 => App not found.
/// </returns>
[HttpGet]
[Route("apps/{app}/roles/permissions")]
[ProducesResponseType(typeof(string[]), 200)]
[ApiPermission(Permissions.AppRolesRead)]
[ApiCosts(0)]
public async Task<IActionResult> GetPermissions(string app)
{
var response = await permissionsProvider.GetPermissionsAsync(App);
Response.Headers["ETag"] = string.Join(";", response).Sha256Base64();
return Ok(response);
}
/// <summary>
/// Add role to app.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="request">Role object that needs to be added to the app.</param>
/// <returns>
/// 200 => User assigned to app.
/// 400 => Role name already in use.
/// 404 => App not found.
/// </returns>
[HttpPost]
[Route("apps/{app}/roles/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppRolesCreate)]
[ApiCosts(1)]
public async Task<IActionResult> PostRole(string app, [FromBody] AddRoleDto request)
{
var command = request.ToCommand();
var context = await CommandBus.PublishAsync(command);
return NoContent();
}
/// <summary>
/// Update an existing app role.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="role">The name of the role to be updated.</param>
/// <param name="request">Role to be updated for the app.</param>
/// <returns>
/// 204 => Role updated.
/// 400 => Role request not valid.
/// 404 => Role or app not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/roles/{role}/")]
[ApiPermission(Permissions.AppRolesUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> UpdateRole(string app, string role, [FromBody] UpdateRoleDto request)
{
await CommandBus.PublishAsync(request.ToCommand(role));
return NoContent();
}
/// <summary>
/// Remove role from app.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="role">The name of the role.</param>
/// <returns>
/// 204 => Role deleted.
/// 400 => Role is in use by contributor or client or default role.
/// 404 => Role or app not found.
/// </returns>
[HttpDelete]
[Route("apps/{app}/roles/{role}/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppRolesDelete)]
[ApiCosts(1)]
public async Task<IActionResult> DeleteRole(string app, string role)
{
await CommandBus.PublishAsync(new DeleteRole { Name = role });
return NoContent();
}
}
}

19
src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs

@ -16,14 +16,15 @@ using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Security;
using Squidex.Pipeline;
using Squidex.Shared;
using Squidex.Shared.Identity;
using Squidex.Shared.Users;
namespace Squidex.Areas.Api.Controllers.Apps
{
/// <summary>
/// Manages and configures apps.
/// </summary>
[ApiAuthorize]
[ApiExceptionFilter]
[ApiExplorerSettings(GroupName = nameof(Apps))]
public sealed class AppsController : ApiController
{
@ -52,14 +53,16 @@ namespace Squidex.Areas.Api.Controllers.Apps
[HttpGet]
[Route("apps/")]
[ProducesResponseType(typeof(AppDto[]), 200)]
[ApiPermission]
[ApiCosts(0)]
public async Task<IActionResult> GetApps()
{
var subject = HttpContext.User.OpenIdSubject();
var userId = HttpContext.User.OpenIdSubject();
var userPermissions = HttpContext.User.Permissions();
var entities = await appProvider.GetUserApps(subject);
var entities = await appProvider.GetUserApps(userId, userPermissions);
var response = entities.Select(a => AppDto.FromApp(a, subject, appPlansProvider)).ToList();
var response = entities.Select(a => AppDto.FromApp(a, userId, userPermissions, appPlansProvider)).ToList();
Response.Headers["ETag"] = response.ToManyEtag();
@ -84,13 +87,14 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ProducesResponseType(typeof(AppCreatedDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ProducesResponseType(typeof(ErrorDto), 409)]
[ApiPermission]
[ApiCosts(1)]
public async Task<IActionResult> PostApp([FromBody] CreateAppDto request)
{
var context = await CommandBus.PublishAsync(request.ToCommand());
var result = context.Result<EntityCreatedResult<Guid>>();
var response = AppCreatedDto.FromResult(result, appPlansProvider);
var response = AppCreatedDto.FromResult(request.Name, result, appPlansProvider);
return CreatedAtAction(nameof(GetApps), response);
}
@ -105,9 +109,8 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// </returns>
[HttpDelete]
[Route("apps/{app}/")]
[AppApi]
[ApiPermission(Permissions.AppDelete)]
[ApiCosts(1)]
[MustBeAppOwner]
public async Task<IActionResult> DeleteApp(string app)
{
await CommandBus.PublishAsync(new ArchiveApp());

2
src/Squidex/Areas/Api/Controllers/Apps/Models/AddAppLanguageDto.cs → src/Squidex/Areas/Api/Controllers/Apps/Models/AddLanguageDto.cs

@ -12,7 +12,7 @@ using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class AddAppLanguageDto
public sealed class AddLanguageDto
{
/// <summary>
/// The language to add.

26
src/Squidex/Areas/Api/Controllers/Apps/Models/AddRoleDto.cs

@ -0,0 +1,26 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Entities.Apps.Commands;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class AddRoleDto
{
/// <summary>
/// The role name.
/// </summary>
[Required]
public string Name { get; set; }
public AddRole ToCommand()
{
return new AddRole { Name = Name };
}
}
}

9
src/Squidex/Areas/Api/Controllers/Apps/Models/AppCreatedDto.cs

@ -7,8 +7,8 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Commands;
@ -26,8 +26,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// <summary>
/// The permission level of the user.
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public AppContributorPermission Permission { get; set; }
public string[] Permissions { get; set; }
/// <summary>
/// The new version of the entity.
@ -44,12 +43,12 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary>
public string PlanUpgrade { get; set; }
public static AppCreatedDto FromResult(EntityCreatedResult<Guid> result, IAppPlansProvider apps)
public static AppCreatedDto FromResult(string name, EntityCreatedResult<Guid> result, IAppPlansProvider apps)
{
var response = new AppCreatedDto
{
Id = result.IdOrValue.ToString(),
Permission = AppContributorPermission.Owner,
Permissions = Role.CreateOwner(name).Permissions.ToIds().ToArray(),
PlanName = apps.GetPlan(null)?.Name,
PlanUpgrade = apps.GetPlanUpgrade(null)?.Name,
Version = result.Version

26
src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs

@ -6,15 +6,17 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using NodaTime;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Security;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
@ -50,8 +52,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// <summary>
/// The permission level of the user.
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public AppContributorPermission Permission { get; set; }
public string[] Permissions { get; set; }
/// <summary>
/// Gets the current plan name.
@ -63,12 +64,23 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary>
public string PlanUpgrade { get; set; }
public static AppDto FromApp(IAppEntity app, string subject, IAppPlansProvider plans)
public static AppDto FromApp(IAppEntity app, string userId, PermissionSet userPermissions, IAppPlansProvider plans)
{
var response = SimpleMapper.Map(app, new AppDto());
var permissions = new List<Permission>();
if (app.Contributors.TryGetValue(userId, out var roleName) && app.Roles.TryGetValue(roleName, out var role))
{
permissions.AddRange(role.Permissions);
}
response.Permission = app.Contributors[subject];
if (userPermissions != null)
{
permissions.AddRange(userPermissions.ToAppPermissions(app.Name));
}
var response = SimpleMapper.Map(app, new AppDto());
response.Permissions = permissions.Select(x => x.Id).ToArray();
response.PlanName = plans.GetPlanForApp(app)?.Name;
response.PlanUpgrade = plans.GetPlanUpgradeForApp(app)?.Name;

9
src/Squidex/Areas/Api/Controllers/Apps/Models/AssignAppContributorDto.cs → src/Squidex/Areas/Api/Controllers/Apps/Models/AssignContributorDto.cs

@ -7,14 +7,12 @@
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class AssignAppContributorDto
public sealed class AssignContributorDto
{
/// <summary>
/// The id or email of the user to add to the app.
@ -23,10 +21,9 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
public string ContributorId { get; set; }
/// <summary>
/// The permission level as a contributor.
/// The role of the contributor.
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public AppContributorPermission Permission { get; set; }
public string Role { get; set; }
public AssignContributor ToCommand()
{

10
src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs

@ -8,10 +8,10 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Reflection;
using Roles = Squidex.Domain.Apps.Core.Apps.Role;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
@ -36,11 +36,9 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
public string Name { get; set; }
/// <summary>
/// The permissions of the client.
/// The role of the client.
/// </summary>
[Required]
[JsonConverter(typeof(StringEnumConverter))]
public AppClientPermission Permission { get; set; }
public string Role { get; set; }
public static ClientDto FromKvp(KeyValuePair<string, AppClient> kvp)
{
@ -49,7 +47,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
public static ClientDto FromCommand(AttachClient command)
{
return SimpleMapper.Map(command, new ClientDto { Name = command.Id, Permission = AppClientPermission.Editor });
return SimpleMapper.Map(command, new ClientDto { Name = command.Id, Role = Roles.Editor });
}
}
}

7
src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorDto.cs

@ -7,8 +7,6 @@
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Squidex.Domain.Apps.Core.Apps;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
@ -21,9 +19,8 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
public string ContributorId { get; set; }
/// <summary>
/// The permission level as a contributor.
/// The role of the contributor.
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public AppContributorPermission Permission { get; set; }
public string Role { get; set; }
}
}

2
src/Squidex/Areas/Api/Controllers/Apps/Models/ContributorsDto.cs

@ -29,7 +29,7 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
{
var plan = plans.GetPlanForApp(app);
var contributors = app.Contributors.Select(x => new ContributorDto { ContributorId = x.Key, Permission = x.Value }).ToArray();
var contributors = app.Contributors.Select(x => new ContributorDto { ContributorId = x.Key, Role = x.Value }).ToArray();
return new ContributorsDto { Contributors = contributors, MaxContributors = plan.MaxContributors };
}

2
src/Squidex/Areas/Api/Controllers/Apps/Models/CreateAppClientDto.cs → src/Squidex/Areas/Api/Controllers/Apps/Models/CreateClientDto.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class CreateAppClientDto
public sealed class CreateClientDto
{
/// <summary>
/// The id of the client.

53
src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs

@ -0,0 +1,53 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class RoleDto
{
/// <summary>
/// The role name.
/// </summary>
[Required]
public string Name { get; set; }
/// <summary>
/// The number of clients with this role.
/// </summary>
public int NumClients { get; set; }
/// <summary>
/// The number of contributors with this role.
/// </summary>
public int NumContributors { get; set; }
/// <summary>
/// Associated list of permissions.
/// </summary>
[Required]
public string[] Permissions { get; set; }
public static RoleDto FromRole(Role role, IAppEntity app)
{
var permissions = role.Permissions.WithoutApp(app.Name);
return new RoleDto
{
Name = role.Name,
NumClients = app.Clients.Count(x => string.Equals(x.Value.Role, role.Name, StringComparison.OrdinalIgnoreCase)),
NumContributors = app.Contributors.Count(x => string.Equals(x.Value, role.Name, StringComparison.OrdinalIgnoreCase)),
Permissions = permissions.ToIds().ToArray()
};
}
}
}

29
src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs

@ -0,0 +1,29 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Entities.Apps;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class RolesDto
{
/// <summary>
/// The app roles.
/// </summary>
[Required]
public RoleDto[] Roles { get; set; }
public static RolesDto FromApp(IAppEntity app)
{
var roles = app.Roles.Values.Select(x => RoleDto.FromRole(x, app)).ToArray();
return new RolesDto { Roles = roles };
}
}
}

10
src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppClientDto.cs → src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateClientDto.cs

@ -6,15 +6,12 @@
// ==========================================================================
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class UpdateAppClientDto
public sealed class UpdateClientDto
{
/// <summary>
/// The new display name of the client.
@ -23,10 +20,9 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
public string Name { get; set; }
/// <summary>
/// The permissions of the client.
/// The role of the client.
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public AppClientPermission? Permission { get; set; }
public string Role { get; set; }
public UpdateClient ToCommand(string clientId)
{

2
src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateAppLanguageDto.cs → src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateLanguageDto.cs

@ -12,7 +12,7 @@ using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class UpdateAppLanguageDto
public sealed class UpdateLanguageDto
{
/// <summary>
/// Set the value to true to make the language the master.

26
src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateRoleDto.cs

@ -0,0 +1,26 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Entities.Apps.Commands;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class UpdateRoleDto
{
/// <summary>
/// Associated list of permissions.
/// </summary>
[Required]
public string[] Permissions { get; set; }
public UpdateRole ToCommand(string name)
{
return new UpdateRole { Name = name, Permissions = Permissions };
}
}
}

2
src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs

@ -22,8 +22,6 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// <summary>
/// Uploads and retrieves assets.
/// </summary>
[ApiExceptionFilter]
[AppApi]
[ApiExplorerSettings(GroupName = nameof(Assets))]
public sealed class AssetContentController : ApiController
{

19
src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -24,15 +24,13 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Assets
{
/// <summary>
/// Uploads and retrieves assets.
/// </summary>
[ApiAuthorize]
[ApiExceptionFilter]
[AppApi]
[ApiExplorerSettings(GroupName = nameof(Assets))]
public sealed class AssetsController : ApiController
{
@ -72,10 +70,10 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// <remarks>
/// Get all tags for assets.
/// </remarks>
[MustBeAppReader]
[HttpGet]
[Route("apps/{app}/assets/tags")]
[ProducesResponseType(typeof(Dictionary<string, int>), 200)]
[ApiPermission(Permissions.AppAssetsRead)]
[ApiCosts(1)]
public async Task<IActionResult> GetTags(string app)
{
@ -96,10 +94,10 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// <remarks>
/// Get all assets for the app.
/// </remarks>
[MustBeAppReader]
[HttpGet]
[Route("apps/{app}/assets/")]
[ProducesResponseType(typeof(AssetsDto), 200)]
[ApiPermission(Permissions.AppAssetsRead)]
[ApiCosts(1)]
public async Task<IActionResult> GetAssets(string app, [FromQuery] string ids = null)
{
@ -128,10 +126,10 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// 200 => Asset found.
/// 404 => Asset or app not found.
/// </returns>
[MustBeAppReader]
[HttpGet]
[Route("apps/{app}/assets/{id}/")]
[ProducesResponseType(typeof(AssetsDto), 200)]
[ApiPermission(Permissions.AppAssetsRead)]
[ApiCosts(1)]
public async Task<IActionResult> GetAsset(string app, Guid id)
{
@ -169,11 +167,12 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// <remarks>
/// You can only upload one file at a time. The mime type of the file is not calculated by Squidex and is required correctly.
/// </remarks>
[MustBeAppEditor]
[HttpPost]
[Route("apps/{app}/assets/")]
[ProducesResponseType(typeof(AssetCreatedDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppAssetsCreate)]
[ApiCosts(1)]
public async Task<IActionResult> PostAsset(string app, [SwaggerIgnore] List<IFormFile> file)
{
var assetFile = await CheckAssetFileAsync(file);
@ -201,11 +200,11 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// <remarks>
/// Use multipart request to upload an asset.
/// </remarks>
[MustBeAppEditor]
[HttpPut]
[Route("apps/{app}/assets/{id}/content/")]
[ProducesResponseType(typeof(AssetReplacedDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppAssetsUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutAssetContent(string app, Guid id, [SwaggerIgnore] List<IFormFile> file)
{
@ -231,10 +230,10 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// 400 => Asset name not valid.
/// 404 => Asset or app not found.
/// </returns>
[MustBeAppReader]
[HttpPut]
[Route("apps/{app}/assets/{id}/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppAssetsUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutAsset(string app, Guid id, [FromBody] UpdateAssetDto request)
{
@ -252,9 +251,9 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// 204 => Asset has been deleted.
/// 404 => Asset or app not found.
/// </returns>
[MustBeAppEditor]
[HttpDelete]
[Route("apps/{app}/assets/{id}/")]
[ApiPermission(Permissions.AppAssetsDelete)]
[ApiCosts(1)]
public async Task<IActionResult> DeleteAsset(string app, Guid id)
{

2
src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs

@ -16,8 +16,6 @@ namespace Squidex.Areas.Api.Controllers.Backups
/// <summary>
/// Manages backups for app.
/// </summary>
[ApiExceptionFilter]
[AppApi]
[ApiExplorerSettings(GroupName = nameof(Backups))]
public class BackupContentController : ApiController
{

8
src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs

@ -16,16 +16,13 @@ using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Tasks;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Backups
{
/// <summary>
/// Manages backups for app.
/// </summary>
[ApiAuthorize]
[ApiExceptionFilter]
[AppApi]
[MustBeAppOwner]
[ApiExplorerSettings(GroupName = nameof(Backups))]
public class BackupsController : ApiController
{
@ -48,6 +45,7 @@ namespace Squidex.Areas.Api.Controllers.Backups
[HttpGet]
[Route("apps/{app}/backups/")]
[ProducesResponseType(typeof(List<BackupJobDto>), 200)]
[ApiPermission(Permissions.AppBackupsRead)]
[ApiCosts(0)]
public async Task<IActionResult> GetJobs(string app)
{
@ -71,6 +69,7 @@ namespace Squidex.Areas.Api.Controllers.Backups
[HttpPost]
[Route("apps/{app}/backups/")]
[ProducesResponseType(typeof(List<BackupJobDto>), 200)]
[ApiPermission(Permissions.AppBackupsCreate)]
[ApiCosts(0)]
public IActionResult PostBackup(string app)
{
@ -93,6 +92,7 @@ namespace Squidex.Areas.Api.Controllers.Backups
[HttpDelete]
[Route("apps/{app}/backups/{id}")]
[ProducesResponseType(typeof(List<BackupJobDto>), 200)]
[ApiPermission(Permissions.AppBackupsDelete)]
[ApiCosts(0)]
public async Task<IActionResult> DeleteBackup(string app, Guid id)
{

10
src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs

@ -7,24 +7,20 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Orleans;
using Squidex.Areas.Api.Controllers.Backups.Models;
using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Security;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Backups
{
/// <summary>
/// Restores backups.
/// </summary>
[ApiAuthorize]
[ApiExceptionFilter]
[ApiModelValidation(true)]
[MustBeAdministrator]
[SwaggerIgnore]
public class RestoreController : ApiController
{
private readonly IGrainFactory grainFactory;
@ -37,7 +33,7 @@ namespace Squidex.Areas.Api.Controllers.Backups
[HttpGet]
[Route("apps/restore/")]
[ApiCosts(0)]
[ApiPermission(Permissions.AdminRestoreRead)]
public async Task<IActionResult> GetJob()
{
var restoreGrain = grainFactory.GetGrain<IRestoreGrain>(User.OpenIdSubject());
@ -56,7 +52,7 @@ namespace Squidex.Areas.Api.Controllers.Backups
[HttpPost]
[Route("apps/restore/")]
[ApiCosts(0)]
[ApiPermission(Permissions.AdminRestoreCreate)]
public async Task<IActionResult> PostRestore([FromBody] RestoreRequest request)
{
var restoreGrain = grainFactory.GetGrain<IRestoreGrain>(User.OpenIdSubject());

9
src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs

@ -15,15 +15,13 @@ using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Comments
{
/// <summary>
/// Manages comments for any kind of resource.
/// </summary>
[ApiAuthorize]
[ApiExceptionFilter]
[AppApi]
[ApiExplorerSettings(GroupName = nameof(Comments))]
public sealed class CommentsController : ApiController
{
@ -51,6 +49,7 @@ namespace Squidex.Areas.Api.Controllers.Comments
[HttpGet]
[Route("apps/{app}/comments/{commentsId}")]
[ProducesResponseType(typeof(CommentsDto), 200)]
[ApiPermission(Permissions.AppCommon)]
[ApiCosts(0)]
public async Task<IActionResult> GetComments(string app, Guid commentsId, [FromQuery] long version = EtagVersion.Any)
{
@ -77,6 +76,7 @@ namespace Squidex.Areas.Api.Controllers.Comments
[Route("apps/{app}/comments/{commentsId}")]
[ProducesResponseType(typeof(EntityCreatedDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppCommon)]
[ApiCosts(0)]
public async Task<IActionResult> PostComment(string app, Guid commentsId, [FromBody] UpsertCommentDto request)
{
@ -100,10 +100,10 @@ namespace Squidex.Areas.Api.Controllers.Comments
/// 400 => Comment text not valid.
/// 404 => Comment or app not found.
/// </returns>
[MustBeAppReader]
[HttpPut]
[Route("apps/{app}/comments/{commentsId}/{commentId}")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppCommon)]
[ApiCosts(0)]
public async Task<IActionResult> PutComment(string app, Guid commentsId, Guid commentId, [FromBody] UpsertCommentDto request)
{
@ -125,6 +125,7 @@ namespace Squidex.Areas.Api.Controllers.Comments
[HttpDelete]
[Route("apps/{app}/comments/{commentsId}/{commentId}")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppCommon)]
[ApiCosts(0)]
public async Task<IActionResult> DeleteComment(string app, Guid commentsId, Guid commentId)
{

3
src/Squidex/Areas/Api/Controllers/Contents/ContentSwaggerController.cs

@ -7,7 +7,6 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Contents.Generator;
using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure.Commands;
@ -16,8 +15,6 @@ using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Contents
{
[ApiExceptionFilter]
[AppApi]
[SwaggerIgnore]
public sealed class ContentSwaggerController : ApiController
{
private readonly IAppProvider appProvider;

34
src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -12,7 +12,6 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using NodaTime;
using NodaTime.Text;
using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Contents.Models;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities;
@ -21,13 +20,10 @@ using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.GraphQL;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Contents
{
[ApiAuthorize]
[ApiExceptionFilter]
[AppApi]
[SwaggerIgnore]
public sealed class ContentsController : ApiController
{
private readonly IOptions<MyContentsControllerOptions> controllerOptions;
@ -58,10 +54,10 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs
/// </remarks>
[MustBeAppReader]
[HttpGet]
[HttpPost]
[Route("content/{app}/graphql/")]
[ApiPermission]
[ApiCosts(2)]
public async Task<IActionResult> PostGraphQL(string app, [FromBody] GraphQLQuery query)
{
@ -89,10 +85,10 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs
/// </remarks>
[MustBeAppReader]
[HttpGet]
[HttpPost]
[Route("content/{app}/graphql/batch")]
[ApiPermission]
[ApiCosts(2)]
public async Task<IActionResult> PostGraphQLBatch(string app, [FromBody] GraphQLQuery[] batch)
{
@ -122,9 +118,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs
/// </remarks>
[MustBeAppReader]
[HttpGet]
[Route("content/{app}/{name}/")]
[ApiPermission]
[ApiCosts(2)]
public async Task<IActionResult> GetContents(string app, string name, [FromQuery] bool archived = false, [FromQuery] string ids = null)
{
@ -161,9 +157,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs
/// </remarks>
[MustBeAppReader]
[HttpGet]
[Route("content/{app}/{name}/{id}/")]
[ApiPermission]
[ApiCosts(1)]
public async Task<IActionResult> GetContent(string app, string name, Guid id)
{
@ -197,9 +193,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs
/// </remarks>
[MustBeAppReader]
[HttpGet]
[Route("content/{app}/{name}/{id}/{version}/")]
[ApiPermission(Permissions.AppContentsRead)]
[ApiCosts(1)]
public async Task<IActionResult> GetContentVersion(string app, string name, Guid id, int version)
{
@ -233,9 +229,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs
/// </remarks>
[MustBeAppEditor]
[HttpPost]
[Route("content/{app}/{name}/")]
[ApiPermission(Permissions.AppContentsCreate)]
[ApiCosts(1)]
public async Task<IActionResult> PostContent(string app, string name, [FromBody] NamedContentData request, [FromQuery] bool publish = false)
{
@ -267,9 +263,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs
/// </remarks>
[MustBeAppEditor]
[HttpPut]
[Route("content/{app}/{name}/{id}/")]
[ApiPermission(Permissions.AppContentsUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false)
{
@ -300,9 +296,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs
/// </remarks>
[MustBeAppEditor]
[HttpPatch]
[Route("content/{app}/{name}/{id}/")]
[ApiPermission(Permissions.AppContentsUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PatchContent(string app, string name, Guid id, [FromBody] NamedContentData request, [FromQuery] bool asDraft = false)
{
@ -332,9 +328,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs
/// </remarks>
[MustBeAppEditor]
[HttpPut]
[Route("content/{app}/{name}/{id}/publish/")]
[ApiPermission(Permissions.AppContentsPublish)]
[ApiCosts(1)]
public async Task<IActionResult> PublishContent(string app, string name, Guid id, string dueTime = null)
{
@ -362,9 +358,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs
/// </remarks>
[MustBeAppEditor]
[HttpPut]
[Route("content/{app}/{name}/{id}/unpublish/")]
[ApiPermission(Permissions.AppContentsUnpublish)]
[ApiCosts(1)]
public async Task<IActionResult> UnpublishContent(string app, string name, Guid id, string dueTime = null)
{
@ -392,9 +388,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs
/// </remarks>
[MustBeAppEditor]
[HttpPut]
[Route("content/{app}/{name}/{id}/archive/")]
[ApiPermission(Permissions.AppContentsArchive)]
[ApiCosts(1)]
public async Task<IActionResult> ArchiveContent(string app, string name, Guid id, string dueTime = null)
{
@ -422,9 +418,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs
/// </remarks>
[MustBeAppEditor]
[HttpPut]
[Route("content/{app}/{name}/{id}/restore/")]
[ApiPermission(Permissions.AppContentsRestore)]
[ApiCosts(1)]
public async Task<IActionResult> RestoreContent(string app, string name, Guid id, string dueTime = null)
{
@ -451,9 +447,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs
/// </remarks>
[MustBeAppEditor]
[HttpPut]
[Route("content/{app}/{name}/{id}/discard/")]
[ApiPermission(Permissions.AppContentsDiscard)]
[ApiCosts(1)]
public async Task<IActionResult> DiscardChanges(string app, string name, Guid id)
{
@ -479,9 +475,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// <remarks>
/// You can create an generated documentation for your app at /api/content/{appName}/docs
/// </remarks>
[MustBeAppEditor]
[HttpDelete]
[Route("content/{app}/{name}/{id}/")]
[ApiPermission(Permissions.AppContentsDelete)]
[ApiCosts(1)]
public async Task<IActionResult> DeleteContent(string app, string name, Guid id)
{

34
src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs

@ -10,14 +10,12 @@ using System.Collections.Generic;
using System.Linq;
using NJsonSchema;
using NSwag;
using Squidex.Config;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.GenerateJsonSchema;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Pipeline.Swagger;
using Squidex.Shared.Identity;
namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
@ -25,8 +23,6 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
private static readonly string SchemaQueryDescription;
private static readonly string SchemaBodyDescription;
private static readonly List<SwaggerSecurityRequirement> EditorSecurity;
private static readonly List<SwaggerSecurityRequirement> ReaderSecurity;
private readonly ContentSchemaBuilder schemaBuilder = new ContentSchemaBuilder();
private readonly SwaggerDocument document;
private readonly JsonSchema4 contentSchema;
@ -40,26 +36,6 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
SchemaBodyDescription = SwaggerHelper.LoadDocs("schemabody");
SchemaQueryDescription = SwaggerHelper.LoadDocs("schemaquery");
ReaderSecurity = new List<SwaggerSecurityRequirement>
{
new SwaggerSecurityRequirement
{
{
Constants.SecurityDefinition, new[] { SquidexRoles.AppReader }
}
}
};
EditorSecurity = new List<SwaggerSecurityRequirement>
{
new SwaggerSecurityRequirement
{
{
Constants.SecurityDefinition, new[] { SquidexRoles.AppEditor }
}
}
};
}
public SchemaSwaggerGenerator(SwaggerDocument document, string path, Schema schema, Func<string, JsonSchema4, JsonSchema4> schemaResolver, PartitionResolver partitionResolver)
@ -111,7 +87,6 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
operation.OperationId = $"Query{schemaType}Contents";
operation.Summary = $"Queries {schemaName} contents.";
operation.Security = ReaderSecurity;
operation.Description = SchemaQueryDescription;
@ -131,7 +106,6 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
operation.OperationId = $"Get{schemaType}Content";
operation.Summary = $"Get a {schemaName} content.";
operation.Security = ReaderSecurity;
operation.AddResponse("200", $"{schemaName} content found.", contentSchema);
});
@ -143,7 +117,6 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
operation.OperationId = $"Create{schemaType}Content";
operation.Summary = $"Create a {schemaName} content.";
operation.Security = EditorSecurity;
operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription);
operation.AddQueryParameter("publish", JsonObjectType.Boolean, "Set to true to autopublish content.");
@ -158,7 +131,6 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
operation.OperationId = $"Update{schemaType}Content";
operation.Summary = $"Update a {schemaName} content.";
operation.Security = EditorSecurity;
operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription);
@ -172,7 +144,6 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
operation.OperationId = $"Path{schemaType}Content";
operation.Summary = $"Patch a {schemaName} content.";
operation.Security = EditorSecurity;
operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription);
@ -186,7 +157,6 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
operation.OperationId = $"Publish{schemaType}Content";
operation.Summary = $"Publish a {schemaName} content.";
operation.Security = EditorSecurity;
operation.AddResponse("204", $"{schemaName} content published.");
});
@ -198,7 +168,6 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
operation.OperationId = $"Unpublish{schemaType}Content";
operation.Summary = $"Unpublish a {schemaName} content.";
operation.Security = EditorSecurity;
operation.AddResponse("204", $"{schemaName} content unpublished.");
});
@ -210,7 +179,6 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
operation.OperationId = $"Archive{schemaType}Content";
operation.Summary = $"Archive a {schemaName} content.";
operation.Security = EditorSecurity;
operation.AddResponse("204", $"{schemaName} content restored.");
});
@ -222,7 +190,6 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
operation.OperationId = $"Restore{schemaType}Content";
operation.Summary = $"Restore a {schemaName} content.";
operation.Security = EditorSecurity;
operation.AddResponse("204", $"{schemaName} content restored.");
});
@ -234,7 +201,6 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
operation.OperationId = $"Delete{schemaType}Content";
operation.Summary = $"Delete a {schemaName} content.";
operation.Security = EditorSecurity;
operation.AddResponse("204", $"{schemaName} content deleted.");
});

4
src/Squidex/Areas/Api/Controllers/Docs/DocsController.cs

@ -6,13 +6,10 @@
// ==========================================================================
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Docs
{
[SwaggerIgnore]
public sealed class DocsController : ApiController
{
public DocsController(ICommandBus commandBus)
@ -22,7 +19,6 @@ namespace Squidex.Areas.Api.Controllers.Docs
[HttpGet]
[Route("docs/")]
[ApiCosts(0)]
public IActionResult Docs()
{
var vm = new DocsVM { Specification = "~/swagger/v1/swagger.json" };

14
src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs

@ -8,20 +8,16 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Orleans;
using Squidex.Areas.Api.Controllers.EventConsumers.Models;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing.Grains;
using Squidex.Infrastructure.Orleans;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.EventConsumers
{
[ApiAuthorize]
[ApiExceptionFilter]
[MustBeAdministrator]
[SwaggerIgnore]
public sealed class EventConsumersController : ApiController
{
private readonly IEventConsumerManagerGrain eventConsumerManagerGrain;
@ -34,7 +30,7 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers
[HttpGet]
[Route("event-consumers/")]
[ApiCosts(0)]
[ApiPermission(Permissions.AdminEventsRead)]
public async Task<IActionResult> GetEventConsumers()
{
var entities = await eventConsumerManagerGrain.GetConsumersAsync();
@ -46,7 +42,7 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers
[HttpPut]
[Route("event-consumers/{name}/start/")]
[ApiCosts(0)]
[ApiPermission(Permissions.AdminEventsManage)]
public async Task<IActionResult> Start(string name)
{
await eventConsumerManagerGrain.StartAsync(name);
@ -56,7 +52,7 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers
[HttpPut]
[Route("event-consumers/{name}/stop/")]
[ApiCosts(0)]
[ApiPermission(Permissions.AdminEventsManage)]
public async Task<IActionResult> Stop(string name)
{
await eventConsumerManagerGrain.StopAsync(name);
@ -66,7 +62,7 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers
[HttpPut]
[Route("event-consumers/{name}/reset/")]
[ApiCosts(0)]
[ApiPermission(Permissions.AdminEventsManage)]
public async Task<IActionResult> Reset(string name)
{
await eventConsumerManagerGrain.ResetAsync(name);

6
src/Squidex/Areas/Api/Controllers/History/HistoryController.cs

@ -12,16 +12,13 @@ using Squidex.Areas.Api.Controllers.History.Models;
using Squidex.Domain.Apps.Entities.History.Repositories;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.History
{
/// <summary>
/// Readonly API to get an event stream.
/// </summary>
[ApiAuthorize]
[ApiExceptionFilter]
[AppApi]
[MustBeAppEditor]
[ApiExplorerSettings(GroupName = nameof(History))]
public sealed class HistoryController : ApiController
{
@ -45,6 +42,7 @@ namespace Squidex.Areas.Api.Controllers.History
[HttpGet]
[Route("apps/{app}/history/")]
[ProducesResponseType(typeof(HistoryEventDto), 200)]
[ApiPermission(Permissions.AppCommon)]
[ApiCosts(0.1)]
public async Task<IActionResult> GetHistory(string app, string channel)
{

4
src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs

@ -16,8 +16,6 @@ namespace Squidex.Areas.Api.Controllers.Languages
/// <summary>
/// Readonly API to the supported langauges.
/// </summary>
[ApiAuthorize]
[ApiExceptionFilter]
[ApiExplorerSettings(GroupName = nameof(Languages))]
public sealed class LanguagesController : ApiController
{
@ -38,7 +36,7 @@ namespace Squidex.Areas.Api.Controllers.Languages
[HttpGet]
[Route("languages/")]
[ProducesResponseType(typeof(string[]), 200)]
[ApiCosts(0)]
[ApiPermission]
public IActionResult GetLanguages()
{
var response = Language.AllLanguages.Select(LanguageDto.FromLanguage).ToList();

8
src/Squidex/Areas/Api/Controllers/Ping/PingController.cs

@ -8,13 +8,13 @@
using Microsoft.AspNetCore.Mvc;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Ping
{
/// <summary>
/// Makes a ping request.
/// </summary>
[ApiExceptionFilter]
[ApiExplorerSettings(GroupName = nameof(Ping))]
public sealed class PingController : ApiController
{
@ -49,12 +49,10 @@ namespace Squidex.Areas.Api.Controllers.Ping
/// <remarks>
/// Can be used to test, if the Squidex API is alive and responding.
/// </remarks>
[ApiAuthorize]
[ApiCosts(0)]
[AppApi]
[MustBeAppReader]
[HttpGet]
[Route("ping/{app}/")]
[ApiPermission(Permissions.AppCommon)]
[ApiCosts(0)]
public IActionResult GetPing(string app)
{
return NoContent();

8
src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs

@ -11,15 +11,13 @@ using Squidex.Areas.Api.Controllers.Plans.Models;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Plans
{
/// <summary>
/// Manages and configures plans.
/// </summary>
[ApiAuthorize]
[ApiExceptionFilter]
[AppApi]
[ApiExplorerSettings(GroupName = nameof(Plans))]
public sealed class AppPlansController : ApiController
{
@ -43,10 +41,10 @@ namespace Squidex.Areas.Api.Controllers.Plans
/// 200 => App plan information returned.
/// 404 => App not found.
/// </returns>
[MustBeAppOwner]
[HttpGet]
[Route("apps/{app}/plans/")]
[ProducesResponseType(typeof(AppPlansDto), 200)]
[ApiPermission(Permissions.AppPlansRead)]
[ApiCosts(0)]
public IActionResult GetPlans(string app)
{
@ -69,11 +67,11 @@ namespace Squidex.Areas.Api.Controllers.Plans
/// 400 => Plan not owned by user.
/// 404 => App not found.
/// </returns>
[MustBeAppOwner]
[HttpPut]
[Route("apps/{app}/plan/")]
[ProducesResponseType(typeof(PlanChangedDto), 200)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppPlansChange)]
[ApiCosts(0)]
public async Task<IActionResult> ChangePlanAsync(string app, [FromBody] ChangePlanDto request)
{

18
src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs

@ -17,24 +17,22 @@ using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Rules.Commands;
using Squidex.Domain.Apps.Entities.Rules.Repositories;
using Squidex.Extensions.Actions;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Rules
{
/// <summary>
/// Manages and retrieves information about schemas.
/// </summary>
[ApiAuthorize]
[ApiExceptionFilter]
[AppApi]
[ApiExplorerSettings(GroupName = nameof(Rules))]
[MustBeAppDeveloper]
public sealed class RulesController : ApiController
{
private static readonly string RuleActionsEtag = string.Join(";", RuleElementRegistry.Actions.Select(x => x.Key)).Sha256();
private static readonly string RuleTriggersEtag = string.Join(";", RuleElementRegistry.Triggers.Select(x => x.Key)).Sha256();
private static readonly string RuleActionsEtag = string.Join(";", RuleElementRegistry.Actions.Select(x => x.Key)).Sha256Base64();
private static readonly string RuleTriggersEtag = string.Join(";", RuleElementRegistry.Triggers.Select(x => x.Key)).Sha256Base64();
private readonly IAppProvider appProvider;
private readonly IRuleEventRepository ruleEventsRepository;
@ -56,6 +54,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
[HttpGet]
[Route("rules/actions/")]
[ProducesResponseType(typeof(Dictionary<string, RuleElementDto>), 200)]
[ApiPermission]
[ApiCosts(0)]
public IActionResult GetActions()
{
@ -75,6 +74,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
[HttpGet]
[Route("rules/triggers/")]
[ProducesResponseType(typeof(Dictionary<string, RuleElementDto>), 200)]
[ApiPermission]
[ApiCosts(0)]
public IActionResult GetTriggers()
{
@ -96,6 +96,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
[HttpGet]
[Route("apps/{app}/rules/")]
[ProducesResponseType(typeof(RuleDto[]), 200)]
[ApiPermission(Permissions.AppRulesRead)]
[ApiCosts(1)]
public async Task<IActionResult> GetRules(string app)
{
@ -122,6 +123,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
[Route("apps/{app}/rules/")]
[ProducesResponseType(typeof(EntityCreatedDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppRulesCreate)]
[ApiCosts(1)]
public async Task<IActionResult> PostRule(string app, [FromBody] CreateRuleDto request)
{
@ -150,6 +152,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
[HttpPut]
[Route("apps/{app}/rules/{id}/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppRulesUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutRule(string app, Guid id, [FromBody] UpdateRuleDto request)
{
@ -170,6 +173,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
/// </returns>
[HttpPut]
[Route("apps/{app}/rules/{id}/enable/")]
[ApiPermission(Permissions.AppRulesDisable)]
[ApiCosts(1)]
public async Task<IActionResult> EnableRule(string app, Guid id)
{
@ -190,6 +194,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
/// </returns>
[HttpPut]
[Route("apps/{app}/rules/{id}/disable/")]
[ApiPermission(Permissions.AppRulesDisable)]
[ApiCosts(1)]
public async Task<IActionResult> DisableRule(string app, Guid id)
{
@ -209,6 +214,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
/// </returns>
[HttpDelete]
[Route("apps/{app}/rules/{id}/")]
[ApiPermission(Permissions.AppRulesDelete)]
[ApiCosts(1)]
public async Task<IActionResult> DeleteRule(string app, Guid id)
{

20
src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs

@ -11,16 +11,13 @@ using Squidex.Areas.Api.Controllers.Schemas.Models;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Schemas
{
/// <summary>
/// Manages and retrieves information about schemas.
/// </summary>
[ApiAuthorize]
[ApiExceptionFilter]
[AppApi]
[MustBeAppDeveloper]
[ApiExplorerSettings(GroupName = nameof(Schemas))]
public sealed class SchemaFieldsController : ApiController
{
@ -46,6 +43,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[ProducesResponseType(typeof(EntityCreatedDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 409)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PostField(string app, string name, [FromBody] AddFieldDto request)
{
@ -75,6 +73,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[ProducesResponseType(typeof(EntityCreatedDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 409)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PostNestedField(string app, string name, long parentId, [FromBody] AddFieldDto request)
{
@ -100,6 +99,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/ordering/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutSchemaFieldOrdering(string app, string name, [FromBody] ReorderFieldsDto request)
{
@ -123,6 +123,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/ordering/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutNestedFieldOrdering(string app, string name, long parentId, [FromBody] ReorderFieldsDto request)
{
@ -147,6 +148,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[Route("apps/{app}/schemas/{name}/fields/{id:long}/")]
[ProducesResponseType(typeof(ErrorDto), 409)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutField(string app, string name, long id, [FromBody] UpdateFieldDto request)
{
@ -172,6 +174,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/")]
[ProducesResponseType(typeof(ErrorDto), 409)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutNestedField(string app, string name, long parentId, long id, [FromBody] UpdateFieldDto request)
{
@ -197,6 +200,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{id:long}/lock/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> LockField(string app, string name, long id)
{
@ -223,6 +227,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/lock/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> LockNestedField(string app, string name, long parentId, long id)
{
@ -248,6 +253,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{id:long}/hide/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> HideField(string app, string name, long id)
{
@ -274,6 +280,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/hide/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> HideNestedField(string app, string name, long parentId, long id)
{
@ -299,6 +306,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{id:long}/show/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> ShowField(string app, string name, long id)
{
@ -325,6 +333,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/show/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> ShowNestedField(string app, string name, long parentId, long id)
{
@ -350,6 +359,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{id:long}/enable/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> EnableField(string app, string name, long id)
{
@ -376,6 +386,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/enable/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> EnableNestedField(string app, string name, long parentId, long id)
{
@ -401,6 +412,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{id:long}/disable/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> DisableField(string app, string name, long id)
{

22
src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs

@ -15,15 +15,13 @@ using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Schemas
{
/// <summary>
/// Manages and retrieves information about schemas.
/// </summary>
[ApiAuthorize]
[ApiExceptionFilter]
[AppApi]
[ApiExplorerSettings(GroupName = nameof(Schemas))]
public sealed class SchemasController : ApiController
{
@ -43,10 +41,10 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// 200 => Schemas returned.
/// 404 => App not found.
/// </returns>
[MustBeAppEditor]
[HttpGet]
[Route("apps/{app}/schemas/")]
[ProducesResponseType(typeof(SchemaDto[]), 200)]
[ApiPermission(Permissions.AppCommon)]
[ApiCosts(0)]
public async Task<IActionResult> GetSchemas(string app)
{
@ -68,10 +66,10 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// 200 => Schema found.
/// 404 => Schema or app not found.
/// </returns>
[MustBeAppEditor]
[HttpGet]
[Route("apps/{app}/schemas/{name}/")]
[ProducesResponseType(typeof(SchemaDetailsDto[]), 200)]
[ApiPermission(Permissions.AppCommon)]
[ApiCosts(0)]
public async Task<IActionResult> GetSchema(string app, string name)
{
@ -108,12 +106,12 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// 400 => Schema name or properties are not valid.
/// 409 => Schema name already in use.
/// </returns>
[MustBeAppDeveloper]
[HttpPost]
[Route("apps/{app}/schemas/")]
[ProducesResponseType(typeof(EntityCreatedDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ProducesResponseType(typeof(ErrorDto), 409)]
[ApiPermission(Permissions.AppSchemasCreate)]
[ApiCosts(1)]
public async Task<IActionResult> PostSchema(string app, [FromBody] CreateSchemaDto request)
{
@ -137,9 +135,9 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// 400 => Schema properties are not valid.
/// 404 => Schema or app not found.
/// </returns>
[MustBeAppDeveloper]
[HttpPut]
[Route("apps/{app}/schemas/{name}/")]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutSchema(string app, string name, [FromBody] UpdateSchemaDto request)
{
@ -159,9 +157,9 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// 400 => Schema properties are not valid.
/// 404 => Schema or app not found.
/// </returns>
[MustBeAppDeveloper]
[HttpPut]
[Route("apps/{app}/schemas/{name}/category")]
[ApiPermission(Permissions.AppSchemasUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutCategory(string app, string name, [FromBody] ChangeCategoryDto request)
{
@ -181,9 +179,9 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// 400 => Schema properties are not valid.
/// 404 => Schema or app not found.
/// </returns>
[MustBeAppDeveloper]
[HttpPut]
[Route("apps/{app}/schemas/{name}/scripts/")]
[ApiPermission(Permissions.AppSchemasScripts)]
[ApiCosts(1)]
public async Task<IActionResult> PutSchemaScripts(string app, string name, [FromBody] ConfigureScriptsDto request)
{
@ -202,10 +200,10 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// 400 => Schema is already published.
/// 404 => Schema or app not found.
/// </returns>
[MustBeAppDeveloper]
[HttpPut]
[Route("apps/{app}/schemas/{name}/publish/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasPublish)]
[ApiCosts(1)]
public async Task<IActionResult> PublishSchema(string app, string name)
{
@ -224,10 +222,10 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// 400 => Schema is not published.
/// 404 => Schema or app not found.
/// </returns>
[MustBeAppDeveloper]
[HttpPut]
[Route("apps/{app}/schemas/{name}/unpublish/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[ApiPermission(Permissions.AppSchemasPublish)]
[ApiCosts(1)]
public async Task<IActionResult> UnpublishSchema(string app, string name)
{
@ -245,9 +243,9 @@ namespace Squidex.Areas.Api.Controllers.Schemas
/// 204 => Schema has been deleted.
/// 404 => Schema or app not found.
/// </returns>
[MustBeAppDeveloper]
[HttpDelete]
[Route("apps/{app}/schemas/{name}/")]
[ApiPermission(Permissions.AppSchemasDelete)]
[ApiCosts(1)]
public async Task<IActionResult> DeleteSchema(string app, string name)
{

9
src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs

@ -16,16 +16,13 @@ using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.UsageTracking;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Statistics
{
/// <summary>
/// Retrieves usage information for apps.
/// </summary>
[ApiAuthorize]
[ApiExceptionFilter]
[AppApi]
[MustBeAppEditor]
[ApiExplorerSettings(GroupName = nameof(Statistics))]
public sealed class UsagesController : ApiController
{
@ -57,6 +54,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics
[HttpGet]
[Route("apps/{app}/usages/calls/month/")]
[ProducesResponseType(typeof(CurrentCallsDto), 200)]
[ApiPermission(Permissions.AppCommon)]
[ApiCosts(0)]
public async Task<IActionResult> GetMonthlyCalls(string app)
{
@ -83,6 +81,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics
[HttpGet]
[Route("apps/{app}/usages/calls/{fromDate}/{toDate}/")]
[ProducesResponseType(typeof(Dictionary<string, CallsUsageDto[]>), 200)]
[ApiPermission(Permissions.AppCommon)]
[ApiCosts(0)]
public async Task<IActionResult> GetUsages(string app, DateTime fromDate, DateTime toDate)
{
@ -109,6 +108,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics
[HttpGet]
[Route("apps/{app}/usages/storage/today/")]
[ProducesResponseType(typeof(CurrentStorageDto), 200)]
[ApiPermission(Permissions.AppCommon)]
[ApiCosts(0)]
public async Task<IActionResult> GetCurrentStorageSize(string app)
{
@ -135,6 +135,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics
[HttpGet]
[Route("apps/{app}/usages/storage/{fromDate}/{toDate}/")]
[ProducesResponseType(typeof(StorageUsageDto[]), 200)]
[ApiPermission(Permissions.AppCommon)]
[ApiCosts(0)]
public async Task<IActionResult> GetStorageSizes(string app, DateTime fromDate, DateTime toDate)
{

9
src/Squidex/Areas/Api/Controllers/UI/UIController.cs

@ -21,9 +21,6 @@ namespace Squidex.Areas.Api.Controllers.UI
/// <summary>
/// Manages ui settings and configs.
/// </summary>
[ApiAuthorize]
[ApiExceptionFilter]
[AppApi]
[ApiExplorerSettings(GroupName = nameof(UI))]
public sealed class UIController : ApiController
{
@ -53,7 +50,7 @@ namespace Squidex.Areas.Api.Controllers.UI
[HttpGet]
[Route("apps/{app}/ui/settings/")]
[ProducesResponseType(typeof(UISettingsDto), 200)]
[ApiCosts(0)]
[ApiPermission]
public async Task<IActionResult> GetSettings(string app)
{
var result = await grainFactory.GetGrain<IAppUISettingsGrain>(App.Id).GetAsync();
@ -77,7 +74,7 @@ namespace Squidex.Areas.Api.Controllers.UI
/// </returns>
[HttpPut]
[Route("apps/{app}/ui/settings/{key}")]
[ApiCosts(0)]
[ApiPermission]
public async Task<IActionResult> PutSetting(string app, string key, [FromBody] UpdateSettingDto request)
{
await grainFactory.GetGrain<IAppUISettingsGrain>(App.Id).SetAsync(key, request.Value);
@ -96,7 +93,7 @@ namespace Squidex.Areas.Api.Controllers.UI
/// </returns>
[HttpDelete]
[Route("apps/{app}/ui/settings/{key}")]
[ApiCosts(0)]
[ApiPermission]
public async Task<IActionResult> DeleteSetting(string app, string key)
{
await grainFactory.GetGrain<IAppUISettingsGrain>(App.Id).RemoveAsync(key);

6
src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs

@ -29,5 +29,11 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
/// </summary>
[Required]
public string Password { get; set; }
/// <summary>
/// Additional permissions for the user.
/// </summary>
[Required]
public string[] Permissions { get; set; }
}
}

6
src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs

@ -28,5 +28,11 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
/// The password of the user.
/// </summary>
public string Password { get; set; }
/// <summary>
/// Additional permissions for the user.
/// </summary>
[Required]
public string[] Permissions { get; set; }
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save