Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ feat: crud access tokens #268

Merged
merged 49 commits into from
Mar 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
4d4501d
init
cosmos-explorer Mar 8, 2023
0b7c37f
in progress
cosmos-explorer Mar 9, 2023
23091ec
in progress
cosmos-explorer Mar 11, 2023
6b0edae
added AccessTokenController
cosmos-explorer Mar 11, 2023
669330e
in progress
cosmos-explorer Mar 11, 2023
b6f3f43
in progress
cosmos-explorer Mar 11, 2023
dc9e4b8
in progress
cosmos-explorer Mar 11, 2023
b238dcf
in progress
cosmos-explorer Mar 11, 2023
5a98e85
in progress
cosmos-explorer Mar 12, 2023
660feaa
added translations
cosmos-explorer Mar 12, 2023
e9525bf
in progress
cosmos-explorer Mar 12, 2023
da2a2b9
in progress
cosmos-explorer Mar 12, 2023
1f299f9
in progress
cosmos-explorer Mar 12, 2023
76fcb95
in progress
cosmos-explorer Mar 12, 2023
010fac9
in progress
cosmos-explorer Mar 12, 2023
7a04461
in progress
cosmos-explorer Mar 12, 2023
cf38cbc
Update init.js
cosmos-explorer Mar 12, 2023
484b06e
fix
cosmos-explorer Mar 12, 2023
ab7906e
in progress
cosmos-explorer Mar 12, 2023
a97f23c
in progress
cosmos-explorer Mar 12, 2023
02b14a4
Merge branch 'main' into feat/access-tokens
cosmos-explorer Mar 12, 2023
ea91797
refactor
cosmos-explorer Mar 13, 2023
14dd12a
refactor
cosmos-explorer Mar 13, 2023
6141809
fixed error
cosmos-explorer Mar 13, 2023
5bdec8e
fixed bug
cosmos-explorer Mar 13, 2023
4af48b7
in progress
cosmos-explorer Mar 13, 2023
7419094
🧹refactor iam (#269)
cosmos-explorer Mar 14, 2023
e8715a4
Merge branch 'feat/access-tokens' of https://github.com/featbit/FeatB…
cosmos-explorer Mar 14, 2023
83c2e73
Merge branch 'main' into feat/access-tokens
cosmos-explorer Mar 15, 2023
7c27092
translation fix
cosmos-explorer Mar 15, 2023
a00d40e
more work
cosmos-explorer Mar 16, 2023
9098ebb
style
cosmos-explorer Mar 16, 2023
67f9b6c
refactor
cosmos-explorer Mar 16, 2023
2984fac
refactor
cosmos-explorer Mar 16, 2023
ee542fe
check permissions when creating access token
cosmos-explorer Mar 16, 2023
115e7f5
prevent creating access tokens with the same name && ignore case when…
cosmos-explorer Mar 16, 2023
81297a4
fixed bug
cosmos-explorer Mar 16, 2023
3e8ca1f
Merge branch 'main' into feat/access-tokens
cosmos-explorer Mar 17, 2023
9d975a1
environment ajustement
cosmos-explorer Mar 17, 2023
6d9228e
refactor
deleteLater Mar 17, 2023
fe5471c
fix
deleteLater Mar 17, 2023
b6c6f9f
rename
deleteLater Mar 17, 2023
4fbea53
refactor ResourceService
deleteLater Mar 17, 2023
d44f3c3
fixed bugs
cosmos-explorer Mar 17, 2023
86adc30
translations
cosmos-explorer Mar 17, 2023
a4d862d
code format
deleteLater Mar 18, 2023
b15f343
fix
deleteLater Mar 18, 2023
67adddb
format fix
deleteLater Mar 18, 2023
8a8c229
Merge branch 'main' into feat/access-tokens
deleteLater Mar 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
62 changes: 50 additions & 12 deletions infra/mongodb/docker-entrypoint-initdb.d/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,37 +96,56 @@ db.Policies.insertOne(
statements: [
{
_id: getUUIDString(),
resourceType: "general",
resourceType: "account",
effect: "allow",
actions: ["UpdateOrgName"],
resources: ["account/*"]
},
{
_id: getUUIDString(),
resourceType: "iam",
effect: "allow",
actions: ["CanManageIAM"],
resources: ["iam"]
resources: ["iam/*"]
},
{
_id: getUUIDString(),
resourceType: "general",
resourceType: "access-token",
effect: "allow",
actions: ["UpdateOrgName"],
resources: ["account"]
actions: [
"ManageServiceAccessTokens",
"ManagePersonalAccessTokens",
"ListAccessTokens"
],
resources: ["access-token/*"]
},
{
_id: getUUIDString(),
resourceType: "general",
resourceType: "project",
effect: "allow",
actions: [
"ListProjects",
"CreateProject",
"DeleteProject",
"AccessEnvs",
"UpdateProjectSettings",
"ListEnvs",
"CreateEnv",
"CreateEnv"
],
resources: ["project/*"]
},
{
_id: getUUIDString(),
resourceType: "env",
effect: "allow",
actions: [
"AccessEnvs",
"DeleteEnv",
"UpdateEnvSettings",
"CreateEnvSecret",
"DeleteEnvSecret",
"UpdateEnvSecret"
],
resources: ["project"]
resources: ["project/*:env/*"]
}
],
createdAt: new Date(),
Expand All @@ -143,14 +162,33 @@ db.Policies.insertOne(
statements: [
{
_id: getUUIDString(),
resourceType: "general",
resourceType: "access-token",
effect: "allow",
actions: [
"ManageServiceAccessTokens",
"ManagePersonalAccessTokens",
"ListAccessTokens"
],
resources: ["access-token/*"]
},
{
_id: getUUIDString(),
resourceType: "project",
effect: "allow",
actions: [
"AccessEnvs",
"ListProjects",
"ListEnvs"
],
resources: ["project"]
resources: ["project/*"]
},
{
_id: getUUIDString(),
resourceType: "env",
effect: "allow",
actions: [
"AccessEnvs"
],
resources: ["project/*:env/*"]
}
],
createdAt: new Date(),
Expand Down
79 changes: 79 additions & 0 deletions modules/back-end/src/Api/Controllers/AccessTokenController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using Application.AccessTokens;
using Application.Bases.Models;

namespace Api.Controllers;

[Route("api/v{version:apiVersion}/organizations/{organizationId:guid}/access-tokens")]
public class AccessTokenController : ApiControllerBase
{
[HttpGet]
public async Task<ApiResponse<PagedResult<AccessTokenVm>>> GetListAsync(
Guid organizationId,
[FromQuery] AccessTokenFilter filter)
{
var request = new GetAccessTokenList
{
OrganizationId = organizationId,
Filter = filter
};

var accessTokens = await Mediator.Send(request);
return Ok(accessTokens);
}

[HttpGet("is-name-used")]
public async Task<ApiResponse<bool>> IsNameUsedAsync(Guid organizationId, string name)
{
var request = new IsAccessTokenNameUsed
{
OrganizationId = organizationId,
Name = name
};

var isNameUsed = await Mediator.Send(request);
return Ok(isNameUsed);
}

[HttpPost]
public async Task<ApiResponse<AccessTokenVm>> CreateAsync(Guid organizationId, CreateAccessToken request)
{
request.OrganizationId = organizationId;

var accessToken = await Mediator.Send(request);
return Ok(accessToken);
}

[HttpPut("{id:guid}/toggle")]
public async Task<ApiResponse<bool>> ToggleAsync(Guid id)
{
var request = new ToggleAccessTokenStatus
{
Id = id
};

var success = await Mediator.Send(request);
return Ok(success);
}

[HttpDelete("{id:guid}")]
public async Task<ApiResponse<bool>> DeleteAsync(Guid id)
{
var request = new DeleteAccessToken
{
Id = id
};

var success = await Mediator.Send(request);
return Ok(success);
}

[HttpPut("{id:guid}")]
public async Task<ApiResponse<bool>> UpdateAsync(Guid id, UpdateAccessToken request)
{
request.Id = id;

var accessTokenVm = await Mediator.Send(request);

return Ok(accessTokenVm);
}
}
12 changes: 12 additions & 0 deletions modules/back-end/src/Application/AccessTokens/AccessTokenFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Application.Bases.Models;

namespace Application.AccessTokens;

public class AccessTokenFilter : PagedRequest
{
public string Name { get; set; }

public Guid? CreatorId { get; set; }

public string Type { get; set; }
}
23 changes: 23 additions & 0 deletions modules/back-end/src/Application/AccessTokens/AccessTokenVm.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Application.Users;
using Domain.Policies;

namespace Application.AccessTokens;

public class AccessTokenVm
{
public Guid Id { get; set; }

public string Name { get; set; }

public string Type { get; set; }

public string Status { get; set; }

public string Token { get; set; }

public IEnumerable<PolicyStatement> Permissions { get; set; }

public DateTime? LastUsedAt { get; set; }

public UserVm Creator { get; set; }
}
116 changes: 116 additions & 0 deletions modules/back-end/src/Application/AccessTokens/CreateAccessToken.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
using System.Text.RegularExpressions;
using Application.Bases;
using Application.Bases.Exceptions;
using Application.Users;
using Domain.AccessTokens;
using Domain.Policies;

namespace Application.AccessTokens;

public class CreateAccessToken : IRequest<AccessTokenVm>
{
public Guid OrganizationId { get; set; }

public string Name { get; set; }

public string Type { get; set; }

public IEnumerable<PolicyStatement> Permissions { get; set; }
}

public class CreateAccessTokenValidator : AbstractValidator<CreateAccessToken>
{
public CreateAccessTokenValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithErrorCode(ErrorCodes.NameIsRequired);

RuleFor(x => x.Type)
.Must(AccessTokenTypes.IsDefined).WithErrorCode(ErrorCodes.InvalidAccessTokenType);

RuleFor(x => x.Permissions)
.Must(permissions => permissions.Any())
.Unless(x => x.Type == AccessTokenTypes.Personal)
.WithErrorCode(ErrorCodes.ServiceAccessTokenMustDefinePolicies);
}
}

public class CreateAccessTokenHandler : IRequestHandler<CreateAccessToken, AccessTokenVm>
{
private readonly IMemberService _memberService;
private readonly ICurrentUser _currentUser;
private readonly IAccessTokenService _service;
private readonly IMapper _mapper;

public CreateAccessTokenHandler(
IAccessTokenService service,
IMemberService memberService,
ICurrentUser currentUser,
IMapper mapper)
{
_service = service;
_currentUser = currentUser;
_memberService = memberService;
_mapper = mapper;
}

public async Task<AccessTokenVm> Handle(CreateAccessToken request, CancellationToken cancellationToken)
{
if (request.Type == AccessTokenTypes.Service)
{
var authorizedPolices =
await _memberService.GetPoliciesAsync(request.OrganizationId, _currentUser.Id);

var haveUnauthorizedPermissions = request.Permissions.Any(x =>
{
var matchedPolicy = authorizedPolices.FirstOrDefault(policy =>
policy.Statements.Any(statement =>
(statement.ResourceType == x.ResourceType || statement.ResourceType == "*") &&
statement.Effect == "allow" &&
x.Resources.All(rsc => statement.Resources.Any(resource => MatchRule(rsc, resource))) &&
x.Actions.All(act => statement.Actions.Any(action => MatchRule(act, action))))
);

return matchedPolicy == null;
});

if (haveUnauthorizedPermissions)
{
throw new BusinessException(ErrorCodes.Forbidden);
}
}

var existed =
await _service.FindOneAsync(at => string.Equals(at.Name, request.Name, StringComparison.OrdinalIgnoreCase));
if (existed != null)
{
throw new BusinessException(ErrorCodes.EntityExistsAlready);
}

var accessToken =
new AccessToken(request.OrganizationId, _currentUser.Id, request.Name, request.Type, request.Permissions);

await _service.AddOneAsync(accessToken);

return _mapper.Map<AccessTokenVm>(accessToken);
}

// use "*" (star) as a wildcard for example:
// "a*b" => everything that starts with "a" and ends with "b"
// "a*" => everything that starts with "a"
// "*b" => everything that ends with "b"
// "*a*" => everything that has an "a" in it
// "*a*b*"=> everything that has an "a" in it, followed by anything, followed by a "b", followed by anything
private static bool MatchRule(string str, string rule)
{
string EscapeRegex(string s) => Regex.Replace(s, "([.*+?^=!:${}()|\\[\\]\\\\/])", "\\$1");

var matchPattern = rule
.Split('*')
.Select(EscapeRegex)
.Aggregate((x, y) => $"{x}.*{y}");

var regex = new Regex($"^{matchPattern}$");
return regex.IsMatch(str);
}
}
23 changes: 23 additions & 0 deletions modules/back-end/src/Application/AccessTokens/DeleteAccessToken.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Application.AccessTokens;

public class DeleteAccessToken : IRequest<bool>
{
public Guid Id { get; set; }
}

public class DeleteAccessTokenHandler : IRequestHandler<DeleteAccessToken, bool>
{
private readonly IAccessTokenService _service;

public DeleteAccessTokenHandler(IAccessTokenService service)
{
_service = service;
}

public async Task<bool> Handle(DeleteAccessToken request, CancellationToken cancellationToken)
{
await _service.DeleteAsync(request.Id);

return true;
}
}