diff --git a/.github/workflows/dotnet-test.yml b/.github/workflows/dotnet-test.yml new file mode 100644 index 0000000..8fb8fb0 --- /dev/null +++ b/.github/workflows/dotnet-test.yml @@ -0,0 +1,26 @@ +name: dotnet test + +on: + pull_request: + branches: + - develop + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: bufbuild/buf-setup-action@v1.11.0 + + - name: Generate Protobuf files + run: buf generate --include-imports + working-directory: src/Permify.AspNetCore/Protos + + - name: Restore dependencies + working-directory: test/Permify.AspNetCore.Tests + run: dotnet restore + + - name: Run tests + working-directory: test/Permify.AspNetCore.Tests + run: dotnet test diff --git a/Implementations/PermifyAuthorizationService.cs b/Implementations/PermifyAuthorizationService.cs deleted file mode 100644 index c26ea6e..0000000 --- a/Implementations/PermifyAuthorizationService.cs +++ /dev/null @@ -1,71 +0,0 @@ -using Grpc.Net.Client; -using Permify.AspNetCore.Extensions; -using Permify.AspNetCore.Interfaces; -using Permify.AspNetCore.Model; - -namespace Permify.AspNetCore.Implementations; - -public class PermifyAuthorizationService : IPermifyAuthorizationService -{ - private Base.V1.Permission.PermissionClient _permissionClient; - private Base.V1.Relationship.RelationshipClient _relationshipClient; - - public PermifyAuthorizationService(PermifyOptions options) - { - var channel = GrpcChannel.ForAddress(options?.Host ?? ""); - - _permissionClient = new Base.V1.Permission.PermissionClient(channel); - _relationshipClient = new Base.V1.Relationship.RelationshipClient(channel); - } - - public async Task CreateRelationship(Entity entity, string relation, Subject subject) - { - var tupleList = new Google.Protobuf.Collections.RepeatedField(); - tupleList.Add( - new Base.V1.Tuple - { - Entity = new Base.V1.Entity { Id = entity.Id, Type = entity.Type }, - Relation = relation, - Subject = new Base.V1.Subject { Id = subject.Id, Type = subject.Type, Relation = subject.Relation }, - }); - - var req = new Base.V1.RelationshipWriteRequest - { - Metadata = new Base.V1.RelationshipWriteRequestMetadata - { - SchemaVersion = "", - }, - }; - req.Tuples.AddRange(tupleList); - - var response = await _relationshipClient.WriteAsync(req); - return response.SnapToken; - } - - public async Task Can(Subject subject, string action, Entity entity) - { - var response = await _permissionClient.CheckAsync( - new Base.V1.PermissionCheckRequest - { - Metadata = new Base.V1.PermissionCheckRequestMetadata - { - SchemaVersion = "", - Depth = 3, - }, - Subject = new Base.V1.Subject - { - Id = subject.Id, - Type = subject.Type, - }, - Permission = action, - Entity = new Base.V1.Entity - { - Id = entity.Id, - Type = entity.Type, - } - } - ); - - return response.Can == Base.V1.PermissionCheckResponse.Types.Result.Allowed; - } -} \ No newline at end of file diff --git a/Interfaces/IPermifyAuthorizationService.cs b/Interfaces/IPermifyAuthorizationService.cs deleted file mode 100644 index 182fc44..0000000 --- a/Interfaces/IPermifyAuthorizationService.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Permify.AspNetCore.Model; - -namespace Permify.AspNetCore.Interfaces; - -public interface IPermifyAuthorizationService -{ - /// - /// Creates a new relationship between the given and - /// - /// The of the relationship - /// The name of the relation within the - /// The of the relationship - /// A string representing the snap token returned by Permify - Task CreateRelationship(Entity entity, string relation, Subject subject); - - /// - /// Check whether or not the given is allowed to execute the given on the given - /// - /// The that wants to execute the action - /// The action itself - /// The on which the subject want to execute the action - /// True if the subject is allowed, false otherwise - Task Can(Subject subject, string action, Entity entity); -} diff --git a/Protos/base/v1/service.proto b/Protos/base/v1/service.proto deleted file mode 100644 index a70b14b..0000000 --- a/Protos/base/v1/service.proto +++ /dev/null @@ -1,423 +0,0 @@ -syntax = "proto3"; -package base.v1; - -import "google/protobuf/empty.proto"; - -option go_package = "github.com/Permify/permify/pkg/pb/base/v1"; - -import "base/v1/tuple.proto"; -import "base/v1/schema.proto"; -import "validate/validate.proto"; -import "google/api/annotations.proto"; -import "protoc-gen-openapiv2/options/annotations.proto"; - -// ** PERMISSION SERVICE ** - -// Permission -service Permission { - rpc Check(PermissionCheckRequest) returns (PermissionCheckResponse) { - option (google.api.http) = { - post: "/v1/permissions/check" - body: "*" - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "This method returns a decision about whether user can perform an action on a certain resource. For example, Can the user 1 push to repository 1?" - tags: [ - "Permission" - ] - operation_id: "permissions.check" - }; - } - - rpc Expand(PermissionExpandRequest) returns (PermissionExpandResponse) { - option (google.api.http) = { - post: "/v1/permissions/expand" - body: "*" - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "expand relationships according to schema" - tags: [ - "Permission" - ] - operation_id: "permissions.expand" - }; - } - - rpc LookupSchema(PermissionLookupSchemaRequest) returns (PermissionLookupSchemaResponse) { - option (google.api.http) = { - post: "/v1/permissions/lookup-schema" - body: "*" - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "" - tags: [ - "Permission" - ] - operation_id: "permissions.lookupSchema" - }; - } - - rpc LookupEntity(PermissionLookupEntityRequest) returns (PermissionLookupEntityResponse) { - option (google.api.http) = { - post: "/v1/permissions/lookup-entity" - body: "*" - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "" - tags: [ - "Permission" - ] - operation_id: "permissions.lookupEntity" - }; - } - - rpc LookupEntityStream(PermissionLookupEntityRequest) returns (stream PermissionLookupEntityStreamResponse) { - option (google.api.http) = { - post: "/v1/permissions/lookup-entity-stream" - body: "*" - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "" - tags: [ - "Permission" - ] - operation_id: "permissions.lookupEntityStream" - }; - } -} - -// CHECK - -// CheckRequest -message PermissionCheckRequest { - PermissionCheckRequestMetadata metadata = 1 [json_name = "metadata", (validate.rules).message.required = true]; - - Entity entity = 2 [json_name = "entity", (validate.rules).message.required = true]; - - // its can be action or relation - string permission = 3 [json_name = "permission", (validate.rules).string = { - pattern : "^([a-z][a-z0-9_]{1,62}[a-z0-9])$", - max_bytes : 64, - ignore_empty: false, - }]; - - Subject subject = 4 [json_name = "subject", (validate.rules).message.required = true]; -} - -// PermissionCheckRequestMetadata -message PermissionCheckRequestMetadata { - string schema_version = 1 [json_name = "schema_version"]; - string snap_token = 2 [json_name = "snap_token"]; - bool exclusion = 3 [json_name = "exclusion"]; - int32 depth = 4 [json_name = "depth", (validate.rules).int32.gte = 3]; -} - -// PermissionCheckResponse -message PermissionCheckResponse { - // Result - enum Result { - RESULT_UNKNOWN = 0; - RESULT_ALLOWED = 1; - RESULT_DENIED = 2; - } - - Result can = 1 [json_name = "can"]; - PermissionCheckResponseMetadata metadata = 2 [json_name = "metadata"]; -} - -// CheckResponseMetadata -message PermissionCheckResponseMetadata { - int32 check_count = 1 [json_name = "check_count"]; -} - -// EXPAND - -// PermissionExpandRequest -message PermissionExpandRequest { - PermissionExpandRequestMetadata metadata = 1 [json_name = "metadata", (validate.rules).message.required = true]; - - Entity entity = 2 [json_name = "entity", (validate.rules).message.required = true]; - - string permission = 3 [json_name = "permission", (validate.rules).string = { - pattern : "^([a-z][a-z0-9_]{1,62}[a-z0-9])$", - max_bytes : 64, - ignore_empty: true, - }]; -} - -// ExpandRequestMetadata -message PermissionExpandRequestMetadata { - string schema_version = 1 [json_name = "schema_version"]; - string snap_token = 2 [json_name = "snap_token"]; -} - -// PermissionExpandResponse -message PermissionExpandResponse { - Expand tree = 1 [json_name = "tree"]; -} - -//LOOKUP - -// PermissionLookupSchemaRequest -message PermissionLookupSchemaRequest { - PermissionLookupSchemaRequestMetadata metadata = 1 [json_name = "metadata", (validate.rules).message.required = true]; - - string entity_type = 2 [json_name = "entity_type", (validate.rules).string = { - pattern : "^([a-z][a-z0-9_]{1,62}[a-z0-9])$", - max_bytes : 64, - ignore_empty: false, - }]; - - repeated string relation_names = 3 [json_name = "relation_names"]; -} - -// LookupSchemaRequestMetadata -message PermissionLookupSchemaRequestMetadata { - string schema_version = 1 [json_name = "schema_version"]; -} - -// PermissionLookupSchemaResponse -message PermissionLookupSchemaResponse { - repeated string action_names = 1 [json_name = "action_names"]; -} - -// PermissionLookupEntityRequest -message PermissionLookupEntityRequest { - PermissionLookupEntityRequestMetadata metadata = 1 [json_name = "metadata", (validate.rules).message.required = true]; - - string entity_type = 2 [json_name = "entity_type", (validate.rules).string = { - pattern : "^([a-z][a-z0-9_]{1,62}[a-z0-9])$", - max_bytes : 64, - ignore_empty: false, - }]; - - string permission = 3 [json_name = "permission", (validate.rules).string = { - pattern : "^([a-z][a-z0-9_]{1,62}[a-z0-9])$", - max_bytes : 64, - ignore_empty: false, - }]; - - Subject subject = 4 [json_name = "subject", (validate.rules).message.required = true]; -} - -// LookupEntityRequestMetadata -message PermissionLookupEntityRequestMetadata { - string schema_version = 1 [json_name = "schema_version"]; - string snap_token = 2 [json_name = "snap_token"]; - int32 depth = 3 [json_name = "depth", (validate.rules).int32.gte = 3]; -} - -// PermissionLookupEntityResponse -message PermissionLookupEntityResponse { - repeated string entity_ids = 1 [json_name = "entity_ids"]; -} - -// PermissionLookupEntityStreamResponse -message PermissionLookupEntityStreamResponse { - string entity_id = 1 [json_name = "entity_id"]; -} - -// ** SCHEMA SERVICE ** - -// Schema -service Schema { - rpc Write(SchemaWriteRequest) returns (SchemaWriteResponse) { - option (google.api.http) = { - post: "/v1/schemas/write" - body: "*" - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "write your authorization model" - tags: [ - "Schema" - ] - operation_id: "schemas.write" - }; - } - - rpc Read(SchemaReadRequest) returns (SchemaReadResponse) { - option (google.api.http) = { - post: "/v1/schemas/read" - body: "*" - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "read your authorization model" - tags: [ - "Schema" - ] - operation_id: "schemas.read" - }; - } -} - -// WRITE - -// SchemaWriteRequest -message SchemaWriteRequest { - string schema = 1 [json_name = "schema"]; -} - -// SchemaWriteResponse -message SchemaWriteResponse { - string schema_version = 1 [json_name = "schema_version"]; -} - -// READ - -// SchemaReadRequest -message SchemaReadRequest { - SchemaReadRequestMetadata metadata = 1 [json_name = "metadata", (validate.rules).message.required = true]; -} - -// LookupSchemaRequestMetadata -message SchemaReadRequestMetadata { - string schema_version = 1 [json_name = "schema_version"]; -} - -// SchemaReadRequest -message SchemaReadResponse { - IndexedSchema schema = 1 [json_name = "schema"]; -} - -// ** RELATIONSHIP SERVICE ** - -// Schema -service Relationship { - rpc Write(RelationshipWriteRequest) returns (RelationshipWriteResponse) { - option (google.api.http) = { - post: "/v1/relationships/write" - body: "*" - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "create new relation tuple" - tags: [ - "Relationship" - ] - operation_id: "relationships.write" - }; - } - - rpc Read(RelationshipReadRequest) returns (RelationshipReadResponse) { - option (google.api.http) = { - post: "/v1/relationships/read" - body: "*" - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "read relation tuple(s)" - tags: [ - "Relationship" - ] - operation_id: "relationships.read" - }; - } - - rpc Delete(RelationshipDeleteRequest) returns (RelationshipDeleteResponse) { - option (google.api.http) = { - post: "/v1/relationships/delete" - body: "*" - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "delete relation tuple" - tags: [ - "Relationship" - ] - operation_id: "relationships.delete" - }; - } -} - -// RelationshipWriteRequest -message RelationshipWriteRequest { - RelationshipWriteRequestMetadata metadata = 1 [json_name = "metadata", (validate.rules).message.required = true]; - - repeated Tuple tuples = 2 [json_name = "tuples", (validate.rules).repeated = { - min_items : 1, - max_items : 100, - items : { - message : { - required : true, - }, - }, - }]; -} - -// RelationshipWriteRequestMetadata -message RelationshipWriteRequestMetadata { - string schema_version = 1 [json_name = "schema_version"]; -} - -// RelationshipWriteResponse -message RelationshipWriteResponse { - string snap_token = 1 [json_name = "snap_token"]; -} - -// RelationshipReadRequest -message RelationshipReadRequest { - RelationshipReadRequestMetadata metadata = 1 [json_name = "metadata", (validate.rules).message.required = true]; - - TupleFilter filter = 2 [json_name = "filter"]; -} - -// RelationshipWriteRequestMetadata -message RelationshipReadRequestMetadata { - string snap_token = 1 [json_name = "snap_token"]; -} - -// RelationshipReadResponse -message RelationshipReadResponse { - repeated Tuple tuples = 1 [json_name = "tuples"]; -} - -// RelationshipDeleteRequest -message RelationshipDeleteRequest { - TupleFilter filter = 1 [json_name = "filter"]; -} - -// RelationshipDeleteResponse -message RelationshipDeleteResponse { - string snap_token = 1 [json_name = "snap_token"]; -} - -// ** WELCOME SERVICE ** -service Welcome { - rpc Hello(google.protobuf.Empty) returns (welcomeResponse) { - option (google.api.http) = { - get: "/" - }; - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "welcome to permify" - tags: [ - "Welcome" - ] - operation_id: "welcome.hello" - }; - } -} - -// WelcomeResponse -message welcomeResponse { - message Sources { - string docs = 1; - string gitHub = 2; - string blog = 3; - } - message Socials { - string discord = 1; - string twitter = 2; - string linkedin = 3; - } - - string permify = 1; - Sources sources = 2; - Socials socials = 3; -} \ No newline at end of file diff --git a/README.md b/README.md index 64e58ec..3219838 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,9 @@ builder.Services.AddPermify(opts => opts.Host = "http://localhost:3478"; }); +// From now on, the IPermifyAuthorizationService singleton is available for DI + + // MyController.cs [HttpGet] public Task GetData() @@ -29,13 +32,13 @@ public Task GetData() bool isUserAllowed = await _authorizationService.Can ( // Extract from the HTTP request - new Subject { Id = "1", Type = "user" }, + new Subject("1", "user"), // Depending on the action this request is representing "get_data", // Extracted from request parameters - new Entity { Id = "10", Type = "datarepo" } + new Entity("10", "datarepo") ); if (!isUserAllowed) @@ -57,7 +60,7 @@ Once all the tools have been installed, clone the repository, generate the proto ``` git clone https://github.com/luco5826/Permify.AspNetCore.git -cd Permify.AspNetCore/Protos +cd Permify.AspNetCore/src/Permify.AspNetCore/Protos buf generate --include-imports cd .. dotnet build diff --git a/Extensions/PermifyOptions.cs b/src/Permify.AspNetCore/Extensions/PermifyOptions.cs similarity index 100% rename from Extensions/PermifyOptions.cs rename to src/Permify.AspNetCore/Extensions/PermifyOptions.cs diff --git a/Extensions/PermifyServiceCollectionExtensions.cs b/src/Permify.AspNetCore/Extensions/PermifyServiceCollectionExtensions.cs similarity index 100% rename from Extensions/PermifyServiceCollectionExtensions.cs rename to src/Permify.AspNetCore/Extensions/PermifyServiceCollectionExtensions.cs diff --git a/src/Permify.AspNetCore/Implementations/PermifyAuthorizationService.cs b/src/Permify.AspNetCore/Implementations/PermifyAuthorizationService.cs new file mode 100644 index 0000000..c5aa56f --- /dev/null +++ b/src/Permify.AspNetCore/Implementations/PermifyAuthorizationService.cs @@ -0,0 +1,139 @@ +using Google.Protobuf.Collections; +using Grpc.Net.Client; +using Permify.AspNetCore.Extensions; +using Permify.AspNetCore.Interfaces; +using Permify.AspNetCore.Model; + +namespace Permify.AspNetCore.Implementations; + +public class PermifyAuthorizationService : IPermifyAuthorizationService +{ + private Base.V1.Permission.PermissionClient _permissionClient; + private Base.V1.Relationship.RelationshipClient _relationshipClient; + + public PermifyAuthorizationService(PermifyOptions options) + { + var channel = GrpcChannel.ForAddress(options?.Host ?? ""); + + _permissionClient = new Base.V1.Permission.PermissionClient(channel); + _relationshipClient = new Base.V1.Relationship.RelationshipClient(channel); + } + + public async Task CreateRelationship(Entity entity, string relation, Subject subject, String tenant) + { + var tupleList = new Google.Protobuf.Collections.RepeatedField(); + tupleList.Add( + new Base.V1.Tuple + { + Entity = new Base.V1.Entity { Id = entity.Id, Type = entity.Type }, + Relation = relation, + Subject = new Base.V1.Subject { Id = subject.Id, Type = subject.Type, Relation = subject.Relation }, + }); + + var req = new Base.V1.RelationshipWriteRequest + { + TenantId = tenant, + Metadata = new Base.V1.RelationshipWriteRequestMetadata + { + SchemaVersion = "", + }, + }; + req.Tuples.AddRange(tupleList); + + var response = await _relationshipClient.WriteAsync(req); + return response.SnapToken; + } + + public async Task DeleteRelationship(Entity entity, string relation, Subject subject, String tenant) + { + // Build the EntityFilter + var entityFilter = new Base.V1.EntityFilter { Type = entity.Type }; + entityFilter.Ids.Add(entity.Id); + + // Build the SubjectFilter + var subjectFilter = new Base.V1.SubjectFilter { Type = subject.Type, Relation = subject.Relation }; + subjectFilter.Ids.Add(subject.Id); + + // Perform the action + var response = await _relationshipClient.DeleteAsync(new Base.V1.RelationshipDeleteRequest + { + TenantId = tenant, + Filter = new Base.V1.TupleFilter + { + Entity = entityFilter, + Relation = relation, + Subject = subjectFilter + }, + }); + + return response.SnapToken; + } + + public async Task> SchemaLookup(string entityType, IEnumerable relationNames, String tenant) + { + var request = new Base.V1.PermissionLookupSchemaRequest + { + TenantId = tenant, + Metadata = new Base.V1.PermissionLookupSchemaRequestMetadata + { + SchemaVersion = "", + }, + EntityType = entityType, + }; + request.RelationNames.AddRange(relationNames); + + var response = await _permissionClient.LookupSchemaAsync(request); + + return response.PermissionNames.AsEnumerable(); + } + + public async Task Can(Subject subject, string action, Entity entity, String tenant) + { + var response = await _permissionClient.CheckAsync( + new Base.V1.PermissionCheckRequest + { + TenantId = tenant, + Metadata = new Base.V1.PermissionCheckRequestMetadata + { + SchemaVersion = "", + Depth = 3, + }, + Subject = new Base.V1.Subject + { + Id = subject.Id, + Type = subject.Type, + }, + Permission = action, + Entity = new Base.V1.Entity + { + Id = entity.Id, + Type = entity.Type, + } + } + ); + + return response.Can == Base.V1.PermissionCheckResponse.Types.Result.Allowed; + } + + public async Task> ReadRelationship(Entity entity, string relation, String tenant) + { + var entityFilter = new Base.V1.EntityFilter + { + Type = entity.Type + }; + entityFilter.Ids.Add(entity.Id); + + var response = await _relationshipClient.ReadAsync(new Base.V1.RelationshipReadRequest + { + TenantId = tenant, + Metadata = new Base.V1.RelationshipReadRequestMetadata { SnapToken = "" }, + Filter = new Base.V1.TupleFilter + { + Entity = entityFilter, + Relation = relation + } + }); + + return response.Tuples.Select(tuple => new Subject(tuple.Subject.Id, tuple.Subject.Type)); + } +} \ No newline at end of file diff --git a/src/Permify.AspNetCore/Interfaces/IPermifyAuthorizationService.cs b/src/Permify.AspNetCore/Interfaces/IPermifyAuthorizationService.cs new file mode 100644 index 0000000..ca3d2b4 --- /dev/null +++ b/src/Permify.AspNetCore/Interfaces/IPermifyAuthorizationService.cs @@ -0,0 +1,54 @@ +using Permify.AspNetCore.Model; + +namespace Permify.AspNetCore.Interfaces; + +public interface IPermifyAuthorizationService +{ + /// + /// Creates a new relationship between the given and + /// + /// The of the relationship + /// The name of the relation within the + /// The of the relationship + /// The tenant in which to apply the operation (defaults to "t1") + /// A string representing the snap token returned by Permify + Task CreateRelationship(Entity entity, string relation, Subject subject, String tenant = "t1"); + + /// + /// Deletes an existing relationship between the given and + /// + /// The of the relationship + /// The name of the relation within the + /// The of the relationship + /// The tenant in which to apply the operation (defaults to "t1") + /// A string representing the snap token returned by Permify + Task DeleteRelationship(Entity entity, string relation, Subject subject, String tenant = "t1"); + + /// + /// Retrieve the collection of that are connected to the given through + /// + /// The of the relationship + /// The name of the relation within the + /// The tenant in which to apply the operation (defaults to "t1") + /// The collection of connected to the entity + Task> ReadRelationship(Entity entity, string relation, String tenant = "t1"); + + /// + /// Lookup the schema retrieving the collection of actions that a given relation can perform + /// + /// The to lookup + /// The collection of relations to lookup + /// The tenant in which to apply the operation (defaults to "t1") + /// The collection of action that the given relations can perform + Task> SchemaLookup(string entityType, IEnumerable relationNames, String tenant = "t1"); + + /// + /// Check whether or not the given is allowed to execute the given on the given + /// + /// The that wants to execute the action + /// The action itself + /// The on which the subject want to execute the action + /// The tenant in which to apply the operation (defaults to "t1") + /// True if the subject is allowed, false otherwise + Task Can(Subject subject, string action, Entity entity, String tenant = "t1"); +} diff --git a/Model/Entity.cs b/src/Permify.AspNetCore/Model/Entity.cs similarity index 100% rename from Model/Entity.cs rename to src/Permify.AspNetCore/Model/Entity.cs diff --git a/Model/Subject.cs b/src/Permify.AspNetCore/Model/Subject.cs similarity index 79% rename from Model/Subject.cs rename to src/Permify.AspNetCore/Model/Subject.cs index 50ac69d..01443d8 100644 --- a/Model/Subject.cs +++ b/src/Permify.AspNetCore/Model/Subject.cs @@ -2,7 +2,7 @@ namespace Permify.AspNetCore.Model; public class Subject { - public Subject(string id, string type, string? relation = null) + public Subject(string id, string type, string relation = "") { Id = id; Type = type; diff --git a/Permify.AspNetCore.csproj b/src/Permify.AspNetCore/Permify.AspNetCore.csproj similarity index 84% rename from Permify.AspNetCore.csproj rename to src/Permify.AspNetCore/Permify.AspNetCore.csproj index 3a4440f..5621132 100644 --- a/Permify.AspNetCore.csproj +++ b/src/Permify.AspNetCore/Permify.AspNetCore.csproj @@ -4,7 +4,7 @@ enable enable - 0.0.1 + 0.1.1 Luca Errani https://github.com/luco5826/Permify.AspNetCore https://github.com/luco5826/Permify.AspNetCore @@ -21,7 +21,7 @@ - - + + \ No newline at end of file diff --git a/Protos/README.md b/src/Permify.AspNetCore/Protos/README.md similarity index 100% rename from Protos/README.md rename to src/Permify.AspNetCore/Protos/README.md diff --git a/Protos/base/v1/errors.proto b/src/Permify.AspNetCore/Protos/base/v1/errors.proto similarity index 87% rename from Protos/base/v1/errors.proto rename to src/Permify.AspNetCore/Protos/base/v1/errors.proto index 895598b..21d4696 100644 --- a/Protos/base/v1/errors.proto +++ b/src/Permify.AspNetCore/Protos/base/v1/errors.proto @@ -9,6 +9,8 @@ enum ErrorCode { // authn ERROR_CODE_MISSING_BEARER_TOKEN = 1001; ERROR_CODE_UNAUTHENTICATED = 1002; + ERROR_CODE_MISSING_TENANT_ID = 1003; + // validation ERROR_CODE_VALIDATION = 2000; @@ -22,7 +24,7 @@ enum ErrorCode { ERROR_CODE_RELATION_REFERENCE_MUST_HAVE_ONE_ENTITY_REFERENCE = 2011; ERROR_CODE_DUPLICATED_ENTITY_REFERENCE = 2012; ERROR_CODE_DUPLICATED_RELATION_REFERENCE = 2013; - ERROR_CODE_DUPLICATED_ACTION_REFERENCE = 2014; + ERROR_CODE_DUPLICATED_PERMISSION_REFERENCE = 2014; ERROR_CODE_SCHEMA_PARSE = 2015; ERROR_CODE_SCHEMA_COMPILE = 2016; ERROR_CODE_SUBJECT_RELATION_MUST_BE_EMPTY = 2017; @@ -33,13 +35,15 @@ enum ErrorCode { // not found ERROR_CODE_NOT_FOUND = 4000; ERROR_CODE_ENTITY_TYPE_NOT_FOUND = 4001; - ERROR_CODE_ACTION_NOT_FOUND = 4002; + ERROR_CODE_PERMISSION_NOT_FOUND = 4002; ERROR_CODE_SCHEMA_NOT_FOUND = 4003; ERROR_CODE_SUBJECT_TYPE_NOT_FOUND = 4004; ERROR_CODE_ENTITY_DEFINITION_NOT_FOUND = 4005; - ERROR_CODE_ACTION_DEFINITION_NOT_FOUND = 4006; + ERROR_CODE_PERMISSION_DEFINITION_NOT_FOUND = 4006; ERROR_CODE_RELATION_DEFINITION_NOT_FOUND = 4007; ERROR_CODE_RECORD_NOT_FOUND = 4008; + ERROR_CODE_TENANT_NOT_FOUND = 4009; + ERROR_CODE_INVALID_CONTINUOUS_TOKEN = 4010; // internal ERROR_CODE_INTERNAL = 5000; diff --git a/Protos/base/v1/openapi.proto b/src/Permify.AspNetCore/Protos/base/v1/openapi.proto similarity index 98% rename from Protos/base/v1/openapi.proto rename to src/Permify.AspNetCore/Protos/base/v1/openapi.proto index dc96ee7..0a11f2c 100644 --- a/Protos/base/v1/openapi.proto +++ b/src/Permify.AspNetCore/Protos/base/v1/openapi.proto @@ -9,7 +9,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { title: "Permify API"; description: "Permify is an open-source authorization service for creating and maintaining fine-grained authorizations across your individual applications and services. Permify converts authorization data as relational tuples into a database you point at. We called that database a Write Database (WriteDB) and it behaves as a centralized data source for your authorization system. You can model of your authorization with Permify's DSL - Permify Schema - and perform access checks with a single API call anywhere on your stack. Access decisions made according to stored relational tuples."; - version: "v0.2.0"; + version: "v0.3.9"; contact: { name: "API Support"; url: "https://github.com/Permify/permify/issues"; diff --git a/Protos/base/v1/schema.proto b/src/Permify.AspNetCore/Protos/base/v1/schema.proto similarity index 74% rename from Protos/base/v1/schema.proto rename to src/Permify.AspNetCore/Protos/base/v1/schema.proto index 31f722e..5e21874 100644 --- a/Protos/base/v1/schema.proto +++ b/src/Permify.AspNetCore/Protos/base/v1/schema.proto @@ -38,15 +38,9 @@ message Rewrite { repeated Child children = 2; } -// IndexedSchema -message IndexedSchema { +// Definition +message SchemaDefinition { map entity_definitions = 1; - - // ["entity_name#relation_name"] => RelationDefinition - map relation_definitions = 2; - - // ["entity_name#action_name"] => ActionDefinition - map action_definitions = 3; } // EntityDefinition @@ -55,7 +49,7 @@ message EntityDefinition { enum RelationalReference { RELATIONAL_REFERENCE_UNSPECIFIED = 0; RELATIONAL_REFERENCE_RELATION = 1; - RELATIONAL_REFERENCE_ACTION = 2; + RELATIONAL_REFERENCE_PERMISSION = 2; } string name = 1 [(validate.rules).string = { @@ -66,13 +60,11 @@ message EntityDefinition { // ["relation_name"] => RelationDefinition map relations = 2; - // ["action_name"] => ActionDefinition - map actions = 3; + // ["permission_name"] => PermissionDefinition + map permissions = 3; - // ["relation_name or action_name"] => RelationalReference + // ["relation_name or permission_name"] => RelationalReference map references = 4; - - map option = 5; } // RelationDefinition @@ -82,16 +74,11 @@ message RelationDefinition { max_bytes : 64, }]; - RelationReference entity_reference = 2; - - // relation reference includes entity reference - repeated RelationReference relation_references = 3; - - map option = 4; + repeated RelationReference relation_references = 2; } -// ActionDefinition -message ActionDefinition { +// PermissionDefinition +message PermissionDefinition { string name = 1 [(validate.rules).string = { pattern : "^([a-z][a-z0-9_]{1,62}[a-z0-9])$", max_bytes : 64, @@ -102,10 +89,15 @@ message ActionDefinition { // RelationReference message RelationReference { - string name = 1 [(validate.rules).string = { + string type = 1 [(validate.rules).string = { pattern : "^([a-z][a-z0-9_]{1,62}[a-z0-9])$", max_bytes : 64, }]; + + string relation = 2 [(validate.rules).string = { + pattern : "^[a-z][a-z0-9_]{1,62}[a-z0-9]$", + max_bytes : 64, + }]; } // ComputedUserSet diff --git a/src/Permify.AspNetCore/Protos/base/v1/service.proto b/src/Permify.AspNetCore/Protos/base/v1/service.proto new file mode 100644 index 0000000..a118b60 --- /dev/null +++ b/src/Permify.AspNetCore/Protos/base/v1/service.proto @@ -0,0 +1,659 @@ +syntax = "proto3"; +package base.v1; + +import "google/protobuf/empty.proto"; + +option go_package = "github.com/Permify/permify/pkg/pb/base/v1"; + +import "base/v1/tuple.proto"; +import "base/v1/schema.proto"; +import "validate/validate.proto"; +import "google/api/annotations.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; + +// ** PERMISSION SERVICE ** + +// Permission +service Permission { + rpc Check(PermissionCheckRequest) returns (PermissionCheckResponse) { + option (google.api.http) = { + post: "/v1/tenants/{tenant_id}/permissions/check" + body: "*" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "This method returns a decision about whether user can perform an permission on a certain resource. For example, Can the user 1 push to repository 1?" + tags: [ + "Permission" + ] + operation_id: "permissions.check" + }; + } + + rpc Expand(PermissionExpandRequest) returns (PermissionExpandResponse) { + option (google.api.http) = { + post: "/v1/tenants/{tenant_id}/permissions/expand" + body: "*" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "expand relationships according to schema" + tags: [ + "Permission" + ] + operation_id: "permissions.expand" + }; + } + + rpc LookupSchema(PermissionLookupSchemaRequest) returns (PermissionLookupSchemaResponse) { + option (google.api.http) = { + post: "/v1/tenants/{tenant_id}/permissions/lookup-schema" + body: "*" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "" + tags: [ + "Permission" + ] + operation_id: "permissions.lookupSchema" + }; + } + + rpc LookupEntity(PermissionLookupEntityRequest) returns (PermissionLookupEntityResponse) { + option (google.api.http) = { + post: "/v1/tenants/{tenant_id}/permissions/lookup-entity" + body: "*" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "" + tags: [ + "Permission" + ] + operation_id: "permissions.lookupEntity" + }; + } + + rpc LookupEntityStream(PermissionLookupEntityRequest) returns (stream PermissionLookupEntityStreamResponse) { + option (google.api.http) = { + post: "/v1/tenants/{tenant_id}/permissions/lookup-entity-stream" + body: "*" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "" + tags: [ + "Permission" + ] + operation_id: "permissions.lookupEntityStream" + }; + } +} + +// CHECK + +// PermissionCheckRequest +message PermissionCheckRequest { + string tenant_id = 1 [json_name = "tenant_id", (validate.rules).string = { + pattern : "[a-zA-Z0-9-,]+", + max_bytes : 64, + ignore_empty: false, + }]; + + PermissionCheckRequestMetadata metadata = 2 [json_name = "metadata", (validate.rules).message.required = true]; + + Entity entity = 3 [json_name = "entity", (validate.rules).message.required = true]; + + // its can be permission or relation + string permission = 4 [json_name = "permission", (validate.rules).string = { + pattern : "^([a-zA-Z][a-zA-Z0-9_]{1,62}[a-zA-Z0-9])$", + max_bytes : 64, + ignore_empty: false, + }]; + + Subject subject = 5 [json_name = "subject", (validate.rules).message.required = true]; +} + +// PermissionCheckRequestMetadata +message PermissionCheckRequestMetadata { + string schema_version = 1 [json_name = "schema_version"]; + string snap_token = 2 [json_name = "snap_token"]; + bool exclusion = 3 [json_name = "exclusion"]; + int32 depth = 4 [json_name = "depth", (validate.rules).int32.gte = 3]; +} + +// PermissionCheckResponse +message PermissionCheckResponse { + // Result + enum Result { + RESULT_UNKNOWN = 0; + RESULT_ALLOWED = 1; + RESULT_DENIED = 2; + } + + Result can = 1 [json_name = "can"]; + PermissionCheckResponseMetadata metadata = 2 [json_name = "metadata"]; +} + +// CheckResponseMetadata +message PermissionCheckResponseMetadata { + int32 check_count = 1 [json_name = "check_count"]; +} + +// EXPAND + +// PermissionExpandRequest +message PermissionExpandRequest { + string tenant_id = 1 [json_name = "tenant_id", (validate.rules).string = { + pattern : "[a-zA-Z0-9-,]+", + max_bytes : 64, + ignore_empty: false, + }]; + + PermissionExpandRequestMetadata metadata = 2 [json_name = "metadata", (validate.rules).message.required = true]; + + Entity entity = 3 [json_name = "entity", (validate.rules).message.required = true]; + + string permission = 4 [json_name = "permission", (validate.rules).string = { + pattern : "^([a-zA-Z][a-zA-Z0-9_]{1,62}[a-zA-Z0-9])$", + max_bytes : 64, + ignore_empty: true, + }]; +} + +// PermissionExpandRequestMetadata +message PermissionExpandRequestMetadata { + string schema_version = 1 [json_name = "schema_version"]; + string snap_token = 2 [json_name = "snap_token"]; +} + +// PermissionExpandResponse +message PermissionExpandResponse { + Expand tree = 1 [json_name = "tree"]; +} + +//LOOKUP + +// PermissionLookupSchemaRequest +message PermissionLookupSchemaRequest { + string tenant_id = 1 [json_name = "tenant_id", (validate.rules).string = { + pattern : "[a-zA-Z0-9-,]+", + max_bytes : 64, + ignore_empty: false, + }]; + + PermissionLookupSchemaRequestMetadata metadata = 2 [json_name = "metadata", (validate.rules).message.required = true]; + + string entity_type = 3 [json_name = "entity_type", (validate.rules).string = { + pattern : "^([a-z][a-z0-9_]{1,62}[a-z0-9])$", + max_bytes : 64, + ignore_empty: false, + }]; + + repeated string relation_names = 4 [json_name = "relation_names"]; +} + +// PermissionLookupSchemaRequestMetadata +message PermissionLookupSchemaRequestMetadata { + string schema_version = 1 [json_name = "schema_version"]; +} + +// PermissionLookupSchemaResponse +message PermissionLookupSchemaResponse { + repeated string permission_names = 1 [json_name = "permission_names"]; +} + +// PermissionLookupEntityRequest +message PermissionLookupEntityRequest { + string tenant_id = 1 [json_name = "tenant_id", (validate.rules).string = { + pattern : "[a-zA-Z0-9-,]+", + max_bytes : 64, + ignore_empty: false, + }]; + + PermissionLookupEntityRequestMetadata metadata = 2 [json_name = "metadata", (validate.rules).message.required = true]; + + string entity_type = 3 [json_name = "entity_type", (validate.rules).string = { + pattern : "^([a-zA-Z][a-zA-Z0-9_]{1,62}[a-zA-Z0-9])$", + max_bytes : 64, + ignore_empty: false, + }]; + + string permission = 4 [json_name = "permission", (validate.rules).string = { + pattern : "^([a-zA-Z][a-zA-Z0-9_]{1,62}[a-zA-Z0-9])$", + max_bytes : 64, + ignore_empty: false, + }]; + + Subject subject = 5 [json_name = "subject", (validate.rules).message.required = true]; +} + +// PermissionLookupEntityRequestMetadata +message PermissionLookupEntityRequestMetadata { + string schema_version = 1 [json_name = "schema_version"]; + string snap_token = 2 [json_name = "snap_token"]; + int32 depth = 3 [json_name = "depth", (validate.rules).int32.gte = 3]; +} + +// PermissionLookupEntityResponse +message PermissionLookupEntityResponse { + repeated string entity_ids = 1 [json_name = "entity_ids"]; +} + +// PermissionLookupEntityStreamResponse +message PermissionLookupEntityStreamResponse { + string entity_id = 1 [json_name = "entity_id"]; +} + +// PermissionLinkedEntityRequest +message PermissionLinkedEntityRequest { + string tenant_id = 1 [json_name = "tenant_id", (validate.rules).string = { + pattern : "[a-zA-Z0-9-,]+", + max_bytes : 64, + ignore_empty: false, + }]; + + PermissionLinkedEntityRequestMetadata metadata = 2 [json_name = "metadata", (validate.rules).message.required = true]; + + RelationReference entity_reference = 3 [json_name = "entity_reference"]; + + Subject subject = 4 [json_name = "subject"]; +} + +// PermissionLookupEntityRequestMetadata +message PermissionLinkedEntityRequestMetadata { + string schema_version = 1 [json_name = "schema_version"]; + string snap_token = 2 [json_name = "snap_token"]; + int32 depth = 3 [json_name = "depth", (validate.rules).int32.gte = 3]; +} + + +// ** SCHEMA SERVICE ** + +// Schema +service Schema { + rpc Write(SchemaWriteRequest) returns (SchemaWriteResponse) { + option (google.api.http) = { + post: "/v1/tenants/{tenant_id}/schemas/write" + body: "*" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "write your authorization model" + tags: [ + "Schema" + ] + operation_id: "schemas.write" + }; + } + + rpc Read(SchemaReadRequest) returns (SchemaReadResponse) { + option (google.api.http) = { + post: "/v1/tenants/{tenant_id}/schemas/read" + body: "*" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "read your authorization model" + tags: [ + "Schema" + ] + operation_id: "schemas.read" + }; + } +} + +// WRITE + +// SchemaWriteRequest +message SchemaWriteRequest { + string tenant_id = 1 [json_name = "tenant_id", (validate.rules).string = { + pattern : "[a-zA-Z0-9-,]+", + max_bytes : 64, + ignore_empty: false, + }]; + + string schema = 2 [json_name = "schema"]; +} + +// SchemaWriteResponse +message SchemaWriteResponse { + string schema_version = 1 [json_name = "schema_version"]; +} + +// READ + +// SchemaReadRequest +message SchemaReadRequest { + string tenant_id = 1 [json_name = "tenant_id", (validate.rules).string = { + pattern : "[a-zA-Z0-9-,]+", + max_bytes : 64, + ignore_empty: false, + }]; + + SchemaReadRequestMetadata metadata = 2 [json_name = "metadata", (validate.rules).message.required = true]; +} + +// SchemaReadRequestMetadata +message SchemaReadRequestMetadata { + string schema_version = 1 [json_name = "schema_version"]; +} + +// SchemaReadRequest +message SchemaReadResponse { + SchemaDefinition schema = 1 [json_name = "schema"]; +} + +// ** RELATIONSHIP SERVICE ** + +// Schema +service Relationship { + rpc Write(RelationshipWriteRequest) returns (RelationshipWriteResponse) { + option (google.api.http) = { + post: "/v1/tenants/{tenant_id}/relationships/write" + body: "*" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "create new relation tuple" + tags: [ + "Relationship" + ] + operation_id: "relationships.write" + }; + } + + rpc Read(RelationshipReadRequest) returns (RelationshipReadResponse) { + option (google.api.http) = { + post: "/v1/tenants/{tenant_id}/relationships/read" + body: "*" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "read relation tuple(s)" + tags: [ + "Relationship" + ] + operation_id: "relationships.read" + }; + } + + rpc Delete(RelationshipDeleteRequest) returns (RelationshipDeleteResponse) { + option (google.api.http) = { + post: "/v1/tenants/{tenant_id}/relationships/delete" + body: "*" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "delete relation tuple" + tags: [ + "Relationship" + ] + operation_id: "relationships.delete" + }; + } +} + +// RelationshipWriteRequest +message RelationshipWriteRequest { + string tenant_id = 1 [json_name = "tenant_id", (validate.rules).string = { + pattern : "[a-zA-Z0-9-,]+", + max_bytes : 64, + ignore_empty: false, + }]; + + RelationshipWriteRequestMetadata metadata = 2 [json_name = "metadata", (validate.rules).message.required = true]; + + repeated Tuple tuples = 3 [json_name = "tuples", (validate.rules).repeated = { + min_items : 1, + max_items : 100, + items : { + message : { + required : true, + }, + }, + }]; +} + +// RelationshipWriteRequestMetadata +message RelationshipWriteRequestMetadata { + string schema_version = 1 [json_name = "schema_version"]; +} + +// RelationshipWriteResponse +message RelationshipWriteResponse { + string snap_token = 1 [json_name = "snap_token"]; +} + +// RelationshipReadRequest +message RelationshipReadRequest { + string tenant_id = 1 [json_name = "tenant_id", (validate.rules).string = { + pattern : "[a-zA-Z0-9-,]+", + max_bytes : 64, + ignore_empty: false, + }]; + + RelationshipReadRequestMetadata metadata = 2 [json_name = "metadata", (validate.rules).message.required = true]; + + TupleFilter filter = 3 [json_name = "filter"]; + + uint32 page_size = 4 [ + json_name = "page_size", + (validate.rules).uint32 = {gte: 1, lte: 100, ignore_empty: true} + ]; + + string continuous_token = 5 [json_name = "continuous_token", (validate.rules).string = {ignore_empty: true}]; +} + +// RelationshipWriteRequestMetadata +message RelationshipReadRequestMetadata { + string snap_token = 1 [json_name = "snap_token"]; +} + +// RelationshipReadResponse +message RelationshipReadResponse { + repeated Tuple tuples = 1 [json_name = "tuples"]; + string continuous_token = 2 [json_name = "continuous_token"]; +} + +// RelationshipDeleteRequest +message RelationshipDeleteRequest { + string tenant_id = 1 [json_name = "tenant_id", (validate.rules).string = { + pattern : "[a-zA-Z0-9-,]+", + max_bytes : 64, + ignore_empty: false, + }]; + + TupleFilter filter = 2 [json_name = "filter"]; +} + +// RelationshipDeleteResponse +message RelationshipDeleteResponse { + string snap_token = 1 [json_name = "snap_token"]; +} + +// ** TENANCY SERVICE ** + +service Tenancy { + rpc Create(TenantCreateRequest) returns (TenantCreateResponse) { + option (google.api.http) = { + post: "/v1/tenants/create" + body: "*" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "create new tenant" + tags: [ + "Tenancy" + ] + operation_id: "tenants.create" + }; + } + + rpc Delete(TenantDeleteRequest) returns (TenantDeleteResponse) { + option (google.api.http) = { + delete: "/v1/tenants/{id}" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "delete tenant" + tags: [ + "Tenancy" + ] + operation_id: "tenants.delete" + }; + } + + rpc List(TenantListRequest) returns (TenantListResponse) { + option (google.api.http) = { + post: "/v1/tenants/list" + body: "*" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "list tenants" + tags: [ + "Tenancy" + ] + operation_id: "tenants.list" + }; + } +} + +// TenantCreateRequest +message TenantCreateRequest { + string id = 1 [json_name = "id", (validate.rules).string = { + pattern : "[a-zA-Z0-9-,]+", + max_bytes : 64, + ignore_empty: false, + }]; + + string name = 2 [json_name = "name", (validate.rules).string = { + max_bytes : 64, + ignore_empty: false, + }]; +} + +// TenantCreateResponse +message TenantCreateResponse { + Tenant tenant = 1 [json_name = "tenant"]; +} + +// TenantDeleteRequest +message TenantDeleteRequest { + string id = 1 [json_name = "id", (validate.rules).string = { + ignore_empty: false, + }]; +} + +// TenantDeleteResponse +message TenantDeleteResponse { + Tenant tenant = 1 [json_name = "tenant"]; +} + +// TenantListRequest +message TenantListRequest { + uint32 page_size = 4 [ + json_name = "page_size", + (validate.rules).uint32 = {gte: 1, lte: 100, ignore_empty: true} + ]; + + string continuous_token = 5 [json_name = "continuous_token", (validate.rules).string = {ignore_empty: true}]; +} + +// TenantListResponse +message TenantListResponse { + repeated Tenant tenants = 1 [json_name = "tenants"]; + string continuous_token = 2 [json_name = "continuous_token"]; +} + +// ** WELCOME SERVICE ** +service Welcome { + rpc Hello(google.protobuf.Empty) returns (welcomeResponse) { + option (google.api.http) = { + get: "/" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "welcome to permify" + tags: [ + "Welcome" + ] + operation_id: "welcome.hello" + }; + } +} + +// WelcomeResponse +message welcomeResponse { + message Sources { + string docs = 1; + string gitHub = 2; + string blog = 3; + } + message Socials { + string discord = 1; + string twitter = 2; + string linkedin = 3; + } + + string permify = 1; + Sources sources = 2; + Socials socials = 3; +} + +// ** Consistent SERVICE ** + +service Consistent { + rpc Get(ConsistentGetRequest) returns (ConsistentGetResponse) { + option (google.api.http) = { + post: "/v1/consistent/get" + body: "*" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "get consistent key" + tags: [ + "Consistent" + ] + operation_id: "consistent.get" + }; + } + + rpc Set(ConsistentSetRequest) returns (ConsistentSetResponse) { + option (google.api.http) = { + post: "/v1/consistent/set" + body: "*" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Set consistent key" + tags: [ + "Consistent" + ] + operation_id: "consistent.set" + }; + } +} + +// ConsistentGetRequest +message ConsistentGetRequest { + PermissionCheckRequest permission_check_request = 1 [json_name = "permission_check_request", (validate.rules).message.required = false]; +} + +// ConsistentGetResponse +message ConsistentGetResponse { + PermissionCheckRequest permission_check_request = 1 [json_name = "permission_check_request", (validate.rules).message.required = true]; + PermissionCheckResponse permission_check_response = 2 [json_name = "permission_check_response", (validate.rules).message.required = true]; +} + +// ConsistentGetRequest +message ConsistentSetRequest { + PermissionCheckRequest permission_check_request = 1 [json_name = "permission_check_request", (validate.rules).message.required = true]; + PermissionCheckResponse permission_check_response = 2 [json_name = "permission_check_response", (validate.rules).message.required = false]; +} + +// ConsistentGetResponse +message ConsistentSetResponse { + string value = 1 [json_name = "value"]; +} \ No newline at end of file diff --git a/Protos/base/v1/tuple.proto b/src/Permify.AspNetCore/Protos/base/v1/tuple.proto similarity index 91% rename from Protos/base/v1/tuple.proto rename to src/Permify.AspNetCore/Protos/base/v1/tuple.proto index 777c522..5970f17 100644 --- a/Protos/base/v1/tuple.proto +++ b/src/Permify.AspNetCore/Protos/base/v1/tuple.proto @@ -4,6 +4,7 @@ package base.v1; option go_package = "github.com/Permify/permify/pkg/pb/base/v1"; import "validate/validate.proto"; +import "google/protobuf/timestamp.proto"; // Tuple message Tuple { @@ -103,7 +104,7 @@ message SubjectFilter { repeated string ids = 2 [json_name = "ids"]; string relation = 3 [(validate.rules).string = { - pattern : "^([a-z][a-z0-9_]{1,62}[a-z0-9])$", + pattern : "^([.&a-z][.&a-z0-9_]{1,62}[.&a-z0-9])$", max_bytes : 64, ignore_empty: true, }]; @@ -136,4 +137,11 @@ message Result { EntityAndRelation target = 1; bool exclusion = 2; repeated Subject subjects = 3; +} + +// Tenant +message Tenant { + string id = 1 [json_name = "id"]; + string name = 2 [json_name = "name"]; + google.protobuf.Timestamp created_at = 3 [json_name = "created_at"]; } \ No newline at end of file diff --git a/Protos/buf.gen.yaml b/src/Permify.AspNetCore/Protos/buf.gen.yaml similarity index 74% rename from Protos/buf.gen.yaml rename to src/Permify.AspNetCore/Protos/buf.gen.yaml index 5b2d692..82be32d 100644 --- a/Protos/buf.gen.yaml +++ b/src/Permify.AspNetCore/Protos/buf.gen.yaml @@ -2,7 +2,7 @@ version: v1 managed: enabled: true plugins: - - plugin: buf.build/protocolbuffers/csharp + - plugin: buf.build/protocolbuffers/csharp:v21.7 out: ../generated/ opt: base_namespace= - plugin: buf.build/grpc/csharp diff --git a/Protos/buf.lock b/src/Permify.AspNetCore/Protos/buf.lock similarity index 100% rename from Protos/buf.lock rename to src/Permify.AspNetCore/Protos/buf.lock diff --git a/Protos/buf.yaml b/src/Permify.AspNetCore/Protos/buf.yaml similarity index 100% rename from Protos/buf.yaml rename to src/Permify.AspNetCore/Protos/buf.yaml diff --git a/test/Permify.AspNetCore.Tests/Permify.AspNetCore.Tests.csproj b/test/Permify.AspNetCore.Tests/Permify.AspNetCore.Tests.csproj new file mode 100644 index 0000000..e1d4669 --- /dev/null +++ b/test/Permify.AspNetCore.Tests/Permify.AspNetCore.Tests.csproj @@ -0,0 +1,21 @@ + + + + net6.0 + false + + enable + enable + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Permify.AspNetCore.Tests/PermifySerivceTests.cs b/test/Permify.AspNetCore.Tests/PermifySerivceTests.cs new file mode 100644 index 0000000..ebb6dbc --- /dev/null +++ b/test/Permify.AspNetCore.Tests/PermifySerivceTests.cs @@ -0,0 +1,171 @@ +using System.Text; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; +using NUnit.Framework; +using Permify.AspNetCore.Extensions; +using Permify.AspNetCore.Implementations; +using Permify.AspNetCore.Interfaces; +using Permify.AspNetCore.Model; + +namespace Permify.AspNetCore.Tests; + +[TestFixture] +public class PermifySerivceTests +{ + private IPermifyAuthorizationService _service; + private HttpClient _httpClient = new HttpClient(); + + private string DEFAULT_SCHEMA = @"{""schema"":""entity user {} entity organization { relation admin @user\n relation member @user } entity repository { relation parent @organization \n relation owner @user \n action push = owner \n action read = owner or parent.admin or parent.member \n action delete = parent.admin or owner }""}"; + + [SetUp] + public async Task SetUpTests() + { + var permifyContainer = new TestcontainersBuilder() + .WithName(Guid.NewGuid().ToString("D")) + .WithImage("ghcr.io/permify/permify:latest") + .WithCommand("serve") + .WithPortBinding(3478, true) // gRPC port mapping + .WithPortBinding(3476, true) // REST port mapping + .Build(); + + await permifyContainer.StartAsync().ConfigureAwait(false); + + int grpcMappedPort = permifyContainer.GetMappedPublicPort(3478); + int restMappedPort = permifyContainer.GetMappedPublicPort(3476); + + // TODO: Wait for a specific event instead of a fixed timeout + await Task.Delay(1000); + + var requestContent = new StringContent(DEFAULT_SCHEMA, Encoding.UTF8, "application/json"); + await _httpClient.PostAsync($"http://localhost:{restMappedPort}/v1/tenants/t1/schemas/write", requestContent); + + _service = new PermifyAuthorizationService(new PermifyOptions + { + Host = $"http://localhost:{grpcMappedPort}", + }); + } + + [Test] + [Parallelizable] + [TestCase("organization", "admin", "user", "")] + [TestCase("organization", "member", "user", "")] + [TestCase("repository", "parent", "organization", "...")] + [TestCase("repository", "owner", "user", "")] + public async Task CreateRelationship(string entityType, string relation, string subjectType, string subjectRelation) + { + // Arrange + + // Act + _ = await _service.CreateRelationship( + new Entity("1", entityType), + relation, + new Subject("1", subjectType, subjectRelation) + ); + + var foundSubjects = await _service.ReadRelationship( + new Entity("1", entityType), relation + ); + + // Assert + Assert.AreEqual(1, foundSubjects.Count()); + Assert.AreEqual(subjectType, foundSubjects.First().Type); + } + + [Test] + public async Task Check() + { + // Arrange + await _service.CreateRelationship(new Entity("10", "organization"), "admin", new Subject("20", "user")); + await _service.CreateRelationship(new Entity("10", "organization"), "member", new Subject("21", "user")); + await _service.CreateRelationship(new Entity("10", "organization"), "member", new Subject("22", "user")); + await _service.CreateRelationship(new Entity("30", "repository"), "parent", new Subject("10", "organization", "...")); + await _service.CreateRelationship(new Entity("30", "repository"), "owner", new Subject("21", "user")); + + // Act + bool adminCanPush = await _service.Can(new Subject("20", "user"), "push", new Entity("30", "repository")); + bool ownerCanPush = await _service.Can(new Subject("21", "user"), "push", new Entity("30", "repository")); + bool memberCanPush = await _service.Can(new Subject("22", "user"), "push", new Entity("30", "repository")); + + bool adminCanRead = await _service.Can(new Subject("20", "user"), "read", new Entity("30", "repository")); + bool ownerCanRead = await _service.Can(new Subject("21", "user"), "read", new Entity("30", "repository")); + bool memberCanRead = await _service.Can(new Subject("22", "user"), "read", new Entity("30", "repository")); + + bool adminCanDelete = await _service.Can(new Subject("20", "user"), "delete", new Entity("30", "repository")); + bool ownerCanDelete = await _service.Can(new Subject("21", "user"), "delete", new Entity("30", "repository")); + bool memberCanDelete = await _service.Can(new Subject("22", "user"), "delete", new Entity("30", "repository")); + + // Assert + Assert.IsFalse(adminCanPush); + Assert.IsTrue(ownerCanPush); + Assert.IsFalse(memberCanPush); + + Assert.IsTrue(adminCanRead); + Assert.IsTrue(ownerCanRead); + Assert.IsTrue(memberCanRead); + + Assert.IsTrue(adminCanDelete); + Assert.IsTrue(ownerCanDelete); + Assert.IsFalse(memberCanDelete); + } + + [Test] + public async Task SchemaLookup_RepositoryParentAdmin() + { + // Arrange + + // Act + var actions = await _service.SchemaLookup("repository", new[] { "parent.admin" }); + + // Assert + Assert.AreEqual(2, actions.Count()); + Assert.IsTrue(actions.Where(a => a == "delete").Count() == 1); + Assert.IsTrue(actions.Where(a => a == "read").Count() == 1); + } + + [Test] + public async Task SchemaLookup_RepositoryOwner() + { + // Arrange + + // Act + var actions = await _service.SchemaLookup("repository", new[] { "owner" }); + + // Assert + Assert.AreEqual(3, actions.Count()); + Assert.IsTrue(actions.Where(a => a == "push").Count() == 1); + Assert.IsTrue(actions.Where(a => a == "delete").Count() == 1); + Assert.IsTrue(actions.Where(a => a == "read").Count() == 1); + } + + [Test] + public async Task DeleteRelationship() + { + // Arrange + await _service.CreateRelationship(new Entity("30", "repository"), "owner", new Subject("21", "user")); + + // Act + bool ownerCanPushBefore = await _service.Can(new Subject("21", "user"), "push", new Entity("30", "repository")); + _ = await _service.DeleteRelationship(new Entity("30", "repository"), "owner", new Subject("21", "user")); + bool ownerCanPushAfter = await _service.Can(new Subject("21", "user"), "push", new Entity("30", "repository")); + + // Assert + Assert.IsTrue(ownerCanPushBefore); + Assert.IsFalse(ownerCanPushAfter); + } + + [Test] + public async Task ReadRelationship() + { + // Arrange + await _service.CreateRelationship(new Entity("10", "organization"), "member", new Subject("20", "user")); + await _service.CreateRelationship(new Entity("10", "organization"), "member", new Subject("21", "user")); + + // Act + var subjects = await _service.ReadRelationship(new Entity("10", "organization"), "member"); + + // Assert + Assert.AreEqual(2, subjects.Count()); + Assert.IsTrue(subjects.Where(s => s.Id == "20").Count() == 1); + Assert.IsTrue(subjects.Where(s => s.Id == "21").Count() == 1); + } +}