From c5702602118ed3550a13fdb8031f8c0117c7baf9 Mon Sep 17 00:00:00 2001 From: nsojic Date: Fri, 30 Oct 2026 17:27:18 +0100 Subject: [PATCH 1/7] LAB-01: kostur modula Laboratories (AddLaboratories, MapLaboratories) --- .../Modules/Laboratories/LaboratoriesModule.cs | 14 ++++++++++++++ backend/Platform.Api/Program.cs | 5 +++++ 2 files changed, 19 insertions(+) create mode 100644 backend/Platform.Api/Modules/Laboratories/LaboratoriesModule.cs diff --git a/backend/Platform.Api/Modules/Laboratories/LaboratoriesModule.cs b/backend/Platform.Api/Modules/Laboratories/LaboratoriesModule.cs new file mode 100644 index 0000000..02c7a3b --- /dev/null +++ b/backend/Platform.Api/Modules/Laboratories/LaboratoriesModule.cs @@ -0,0 +1,14 @@ +namespace Platform.Api.Modules.Laboratories; + +public static class LaboratoriesModule +{ + public static IServiceCollection AddLaboratories(this IServiceCollection services) + { + return services; + } + + public static IEndpointRouteBuilder MapLaboratories(this IEndpointRouteBuilder app) + { + return app; + } +} \ No newline at end of file diff --git a/backend/Platform.Api/Program.cs b/backend/Platform.Api/Program.cs index 28dd706..ff1d804 100644 --- a/backend/Platform.Api/Program.cs +++ b/backend/Platform.Api/Program.cs @@ -1,4 +1,7 @@ +using Platform.Api.Modules.Laboratories; + var builder = WebApplication.CreateBuilder(args); +builder.Services.AddLaboratories(); var app = builder.Build(); // Starter endpoint only. Add domain APIs together with server-side authorization. @@ -9,6 +12,8 @@ note = "Initial scaffold; not a production-ready laboratory platform" })); +app.MapLaboratories(); + app.Run(); public partial class Program { } From d3a7d9b4c46ea5f228b8a3e2443f49af1ca5cbdf Mon Sep 17 00:00:00 2001 From: nsojic Date: Fri, 30 Oct 2026 17:45:42 +0100 Subject: [PATCH 2/7] LAB-01: ugovori IIdentityContext i IAccessDecision --- .../Application/IAccessDecision.cs | 23 +++++++++++++++++++ .../Application/IIdentityContext.cs | 17 ++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 backend/Platform.Api/Modules/Laboratories/Application/IAccessDecision.cs create mode 100644 backend/Platform.Api/Modules/Laboratories/Application/IIdentityContext.cs diff --git a/backend/Platform.Api/Modules/Laboratories/Application/IAccessDecision.cs b/backend/Platform.Api/Modules/Laboratories/Application/IAccessDecision.cs new file mode 100644 index 0000000..e07707c --- /dev/null +++ b/backend/Platform.Api/Modules/Laboratories/Application/IAccessDecision.cs @@ -0,0 +1,23 @@ +namespace Platform.Api.Modules.Laboratories.Application; + +// Ugovor iz specifikacije. Podrazumevano odbijanje. +// KT1: FakeAccessDecision, kasnije ACC modul (tim 3). + +public interface IAccessDecision +{ + AccessResult Decide(IIdentityContext subject, string action, string? resourceId = null); +} + +public enum DenyReason +{ + Unauthenticated, + UnknownAction, + NotPermitted +} + +public sealed record AccessResult(bool Allowed, DenyReason? Reason) +{ + public static AccessResult Allow() => new(true, null); + + public static AccessResult Deny(DenyReason reason) => new(false, reason); +} \ No newline at end of file diff --git a/backend/Platform.Api/Modules/Laboratories/Application/IIdentityContext.cs b/backend/Platform.Api/Modules/Laboratories/Application/IIdentityContext.cs new file mode 100644 index 0000000..d242c63 --- /dev/null +++ b/backend/Platform.Api/Modules/Laboratories/Application/IIdentityContext.cs @@ -0,0 +1,17 @@ +namespace Platform.Api.Modules.Laboratories.Application; + +// Ugovor iz specifikacije. KT1: fake, kasnije IDN modul (tim 18). +public interface IIdentityContext +{ + bool IsAuthenticated { get; } + string? UserId { get; } + PlatformRole? Role { get; } +} + +public enum PlatformRole +{ + Student, + Teacher, + Assistant, + Administrator +} \ No newline at end of file From 726010ada5c79965c94006f5731bedffbfe38acf Mon Sep 17 00:00:00 2001 From: nsojic Date: Fri, 30 Oct 2026 18:10:15 +0100 Subject: [PATCH 3/7] LAB-01: fake identitet i podrazumevano odbijanje pristupa --- .../Laboratories/Application/LabActions.cs | 7 +++ .../Infrastructure/FakeAccessDecision.cs | 35 +++++++++++ .../Infrastructure/FakeIdentityContext.cs | 61 +++++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 backend/Platform.Api/Modules/Laboratories/Application/LabActions.cs create mode 100644 backend/Platform.Api/Modules/Laboratories/Infrastructure/FakeAccessDecision.cs create mode 100644 backend/Platform.Api/Modules/Laboratories/Infrastructure/FakeIdentityContext.cs diff --git a/backend/Platform.Api/Modules/Laboratories/Application/LabActions.cs b/backend/Platform.Api/Modules/Laboratories/Application/LabActions.cs new file mode 100644 index 0000000..b6d6c3d --- /dev/null +++ b/backend/Platform.Api/Modules/Laboratories/Application/LabActions.cs @@ -0,0 +1,7 @@ +namespace Platform.Api.Modules.Laboratories.Application; + +public static class LabActions +{ + public const string List = "laboratory.list"; + public const string Create = "laboratory.create"; +} \ No newline at end of file diff --git a/backend/Platform.Api/Modules/Laboratories/Infrastructure/FakeAccessDecision.cs b/backend/Platform.Api/Modules/Laboratories/Infrastructure/FakeAccessDecision.cs new file mode 100644 index 0000000..c921696 --- /dev/null +++ b/backend/Platform.Api/Modules/Laboratories/Infrastructure/FakeAccessDecision.cs @@ -0,0 +1,35 @@ +using Platform.Api.Modules.Laboratories.Application; + +namespace Platform.Api.Modules.Laboratories.Infrastructure; + +public sealed class FakeAccessDecision : IAccessDecision +{ + private static readonly Dictionary AllowedRoles = new() + { + [LabActions.List] = [PlatformRole.Student, PlatformRole.Teacher, PlatformRole.Assistant, PlatformRole.Administrator], + [LabActions.Create] = [PlatformRole.Teacher], + }; + + public AccessResult Decide(IIdentityContext subject, string action, string? resourceId = null) + { + // 1. Da li je korisnik poznat? + if (!subject.IsAuthenticated || subject.Role is null) + { + return AccessResult.Deny(DenyReason.Unauthenticated); + } + + // 2. Da li je radnja uopste definisana? + if (string.IsNullOrEmpty(action) || !AllowedRoles.TryGetValue(action, out PlatformRole[]? roles)) + { + return AccessResult.Deny(DenyReason.UnknownAction); + } + + // 3. Da li njegova uloga sme tu radnju? + if (!roles.Contains(subject.Role.Value)) + { + return AccessResult.Deny(DenyReason.NotPermitted); + } + + return AccessResult.Allow(); + } +} \ No newline at end of file diff --git a/backend/Platform.Api/Modules/Laboratories/Infrastructure/FakeIdentityContext.cs b/backend/Platform.Api/Modules/Laboratories/Infrastructure/FakeIdentityContext.cs new file mode 100644 index 0000000..3443df5 --- /dev/null +++ b/backend/Platform.Api/Modules/Laboratories/Infrastructure/FakeIdentityContext.cs @@ -0,0 +1,61 @@ +using Platform.Api.Modules.Laboratories.Application; +using Microsoft.Extensions.Primitives; + +namespace Platform.Api.Modules.Laboratories.Infrastructure; + +public sealed class FakeIdentityContext : IIdentityContext +{ + public const string HeaderName = "X-Test-User"; + + private static readonly Dictionary KnownRoles = new() + { + ["Student"] = PlatformRole.Student, + ["Teacher"] = PlatformRole.Teacher, + ["Assistant"] = PlatformRole.Assistant, + ["Administrator"] = PlatformRole.Administrator + }; + + public FakeIdentityContext(IHttpContextAccessor httpContextAccessor) + { + IHeaderDictionary? headers = httpContextAccessor.HttpContext?.Request.Headers; + + if (headers is null || !headers.TryGetValue(HeaderName, out StringValues values) || values.Count != 1) + { + return; + } + + string? raw = values[0]; + if (string.IsNullOrWhiteSpace(raw)) + { + return; + } + + string[] parts = raw.Split(':'); + if (parts.Length != 2) + { + return; + } + + string userId = parts[0].Trim(); + string roleName = parts[1].Trim(); + + if (!IsValidUserId(userId) || !KnownRoles.TryGetValue(roleName, out PlatformRole role)) + { + return; + } + + IsAuthenticated = true; + UserId = userId; + Role = role; + } + + public bool IsAuthenticated { get; } + + public string? UserId { get; } + + public PlatformRole? Role { get; } + + private static bool IsValidUserId(string userId) => + userId.Length is >= 1 and <= 64 + && userId.All(c => char.IsAsciiLetterOrDigit(c) || c is '.' or '_' or '-'); +} \ No newline at end of file From 12d4ae74442162a1a6702d9d85b46fb818cc484a Mon Sep 17 00:00:00 2001 From: nsojic Date: Fri, 30 Oct 2026 18:25:18 +0100 Subject: [PATCH 4/7] LAB-01: fake samo u Development rezimu, inace sve odbijeno --- .../AnonymousIdentityContext.cs | 14 +++++++++++ .../Infrastructure/DenyAllAccessDecision.cs | 18 +++++++++++++++ .../Laboratories/LaboratoriesModule.cs | 23 ++++++++++++++++++- backend/Platform.Api/Program.cs | 2 +- 4 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 backend/Platform.Api/Modules/Laboratories/Infrastructure/AnonymousIdentityContext.cs create mode 100644 backend/Platform.Api/Modules/Laboratories/Infrastructure/DenyAllAccessDecision.cs diff --git a/backend/Platform.Api/Modules/Laboratories/Infrastructure/AnonymousIdentityContext.cs b/backend/Platform.Api/Modules/Laboratories/Infrastructure/AnonymousIdentityContext.cs new file mode 100644 index 0000000..e243e2c --- /dev/null +++ b/backend/Platform.Api/Modules/Laboratories/Infrastructure/AnonymousIdentityContext.cs @@ -0,0 +1,14 @@ +using Platform.Api.Modules.Laboratories.Application; + +namespace Platform.Api.Modules.Laboratories.Infrastructure; + +// Koristi se VAN Development rezima dok ne stigne IDN modul: +// niko nije prepoznat (fail closed). +public sealed class AnonymousIdentityContext : IIdentityContext +{ + public bool IsAuthenticated => false; + + public string? UserId => null; + + public PlatformRole? Role => null; +} \ No newline at end of file diff --git a/backend/Platform.Api/Modules/Laboratories/Infrastructure/DenyAllAccessDecision.cs b/backend/Platform.Api/Modules/Laboratories/Infrastructure/DenyAllAccessDecision.cs new file mode 100644 index 0000000..beab344 --- /dev/null +++ b/backend/Platform.Api/Modules/Laboratories/Infrastructure/DenyAllAccessDecision.cs @@ -0,0 +1,18 @@ +using Platform.Api.Modules.Laboratories.Application; + +namespace Platform.Api.Modules.Laboratories.Infrastructure; + +// Koristi se VAN Development rezima dok ne stigne ACC modul: +// sve je odbijeno (fail closed). +public sealed class DenyAllAccessDecision : IAccessDecision +{ + public AccessResult Decide(IIdentityContext subject, string action, string? resourceId = null) + { + if (!subject.IsAuthenticated) + { + return AccessResult.Deny(DenyReason.Unauthenticated); + } + + return AccessResult.Deny(DenyReason.NotPermitted); + } +} \ No newline at end of file diff --git a/backend/Platform.Api/Modules/Laboratories/LaboratoriesModule.cs b/backend/Platform.Api/Modules/Laboratories/LaboratoriesModule.cs index 02c7a3b..60d0c39 100644 --- a/backend/Platform.Api/Modules/Laboratories/LaboratoriesModule.cs +++ b/backend/Platform.Api/Modules/Laboratories/LaboratoriesModule.cs @@ -1,9 +1,30 @@ +using Platform.Api.Modules.Laboratories.Application; +using Platform.Api.Modules.Laboratories.Infrastructure; + namespace Platform.Api.Modules.Laboratories; public static class LaboratoriesModule { - public static IServiceCollection AddLaboratories(this IServiceCollection services) + // Registracija servisa modula + public static IServiceCollection AddLaboratories(this IServiceCollection services, IHostEnvironment environment) { + services.AddHttpContextAccessor(); + + if (environment.IsDevelopment()) + { + // FAKE implementacije - samo u Development rezimu. + // FakeIdentityContext - IDN modul (tim 18) + // FakeAccessDecision - ACC modul (tim 3) + services.AddScoped(); + services.AddSingleton(); + } + else + { + // Niko nije prepoznat, sve je odbijeno. + services.AddScoped(); + services.AddSingleton(); + } + return services; } diff --git a/backend/Platform.Api/Program.cs b/backend/Platform.Api/Program.cs index ff1d804..1979df0 100644 --- a/backend/Platform.Api/Program.cs +++ b/backend/Platform.Api/Program.cs @@ -1,7 +1,7 @@ using Platform.Api.Modules.Laboratories; var builder = WebApplication.CreateBuilder(args); -builder.Services.AddLaboratories(); +builder.Services.AddLaboratories(builder.Environment); var app = builder.Build(); // Starter endpoint only. Add domain APIs together with server-side authorization. From 55810fd485cf070012b426c1caf1a2b47e36e508 Mon Sep 17 00:00:00 2001 From: nsojic Date: Fri, 30 Oct 2026 18:53:03 +0100 Subject: [PATCH 5/7] LAB-01: endpointi laboratorija sa serverskom proverom prava --- .../Laboratories/Api/AccessCheckFilter.cs | 37 +++++++++++++++++++ .../Laboratories/Api/LaboratoriesEndpoints.cs | 19 ++++++++++ .../Laboratories/LaboratoriesModule.cs | 2 + 3 files changed, 58 insertions(+) create mode 100644 backend/Platform.Api/Modules/Laboratories/Api/AccessCheckFilter.cs create mode 100644 backend/Platform.Api/Modules/Laboratories/Api/LaboratoriesEndpoints.cs diff --git a/backend/Platform.Api/Modules/Laboratories/Api/AccessCheckFilter.cs b/backend/Platform.Api/Modules/Laboratories/Api/AccessCheckFilter.cs new file mode 100644 index 0000000..f23caf3 --- /dev/null +++ b/backend/Platform.Api/Modules/Laboratories/Api/AccessCheckFilter.cs @@ -0,0 +1,37 @@ +using Platform.Api.Modules.Laboratories.Application; + +namespace Platform.Api.Modules.Laboratories.Api; + +// zahtev -> ko je? (IIdentityContext) -> sme li? (IAccessDecision) -> dozvoli / odbij +public static class AccessCheckFilter +{ + public static RouteHandlerBuilder RequireLabAccess(this RouteHandlerBuilder builder, string action) + { + return builder.AddEndpointFilter(async (context, next) => + { + HttpContext http = context.HttpContext; + IIdentityContext identity = http.RequestServices.GetRequiredService(); + IAccessDecision accessDecision = http.RequestServices.GetRequiredService(); + + AccessResult result = accessDecision.Decide(identity, action); + + // Dozvoljeno + if (result.Allowed) + { + return await next(context); + } + + // Odbijeno -> zapisi u log i vrati gresku + ILogger logger = http.RequestServices.GetRequiredService().CreateLogger("Laboratories.Access"); + logger.LogWarning("Pristup odbijen: radnja={Action} korisnik={UserId} razlog={Reason}", + action, identity.UserId ?? "(nepoznat)", result.Reason); + + if (result.Reason == DenyReason.Unauthenticated) + { + return Results.Problem(statusCode: 401, title: "Unauthenticated"); + } + + return Results.Problem(statusCode: 403, title: "Access denied"); + }); + } +} \ No newline at end of file diff --git a/backend/Platform.Api/Modules/Laboratories/Api/LaboratoriesEndpoints.cs b/backend/Platform.Api/Modules/Laboratories/Api/LaboratoriesEndpoints.cs new file mode 100644 index 0000000..2e637cb --- /dev/null +++ b/backend/Platform.Api/Modules/Laboratories/Api/LaboratoriesEndpoints.cs @@ -0,0 +1,19 @@ +using Platform.Api.Modules.Laboratories.Application; + +namespace Platform.Api.Modules.Laboratories.Api; + +public static class LaboratoriesEndpoints +{ + public static IEndpointRouteBuilder MapLaboratoriesEndpoints(this IEndpointRouteBuilder app) + { + RouteGroupBuilder group = app.MapGroup("/api/laboratories"); + + group.MapGet("/", () => Results.Ok(Array.Empty())) + .RequireLabAccess(LabActions.List); + + group.MapPost("/", () => Results.StatusCode(501)) + .RequireLabAccess(LabActions.Create); + + return app; + } +} \ No newline at end of file diff --git a/backend/Platform.Api/Modules/Laboratories/LaboratoriesModule.cs b/backend/Platform.Api/Modules/Laboratories/LaboratoriesModule.cs index 60d0c39..4f4ba6f 100644 --- a/backend/Platform.Api/Modules/Laboratories/LaboratoriesModule.cs +++ b/backend/Platform.Api/Modules/Laboratories/LaboratoriesModule.cs @@ -1,3 +1,4 @@ +using Platform.Api.Modules.Laboratories.Api; using Platform.Api.Modules.Laboratories.Application; using Platform.Api.Modules.Laboratories.Infrastructure; @@ -30,6 +31,7 @@ public static IServiceCollection AddLaboratories(this IServiceCollection service public static IEndpointRouteBuilder MapLaboratories(this IEndpointRouteBuilder app) { + app.MapLaboratoriesEndpoints(); return app; } } \ No newline at end of file From 4b62cbb0b33f547f89baae37ca08edcca8f9cd0c Mon Sep 17 00:00:00 2001 From: nsojic Date: Fri, 30 Oct 2026 19:09:19 +0100 Subject: [PATCH 6/7] LAB-01: NUnit testovi za nepoznatog korisnika i nepoznatu radnju --- .../FakeAccessDecisionTests.cs | 69 +++++++++++++++++++ .../Laboratories.Tests.csproj | 27 ++++++++ 2 files changed, 96 insertions(+) create mode 100644 backend/Laboratories.Tests/FakeAccessDecisionTests.cs create mode 100644 backend/Laboratories.Tests/Laboratories.Tests.csproj diff --git a/backend/Laboratories.Tests/FakeAccessDecisionTests.cs b/backend/Laboratories.Tests/FakeAccessDecisionTests.cs new file mode 100644 index 0000000..673bc27 --- /dev/null +++ b/backend/Laboratories.Tests/FakeAccessDecisionTests.cs @@ -0,0 +1,69 @@ +using Platform.Api.Modules.Laboratories.Application; +using Platform.Api.Modules.Laboratories.Infrastructure; + +namespace Laboratories.Tests; + +// Testovi politike pristupa: sve sto nije izricito dozvoljeno -> odbijeno. +public class FakeAccessDecisionTests +{ + private readonly FakeAccessDecision Policy = new FakeAccessDecision(); + + [Test] + public void Decide_WhenUserIsUnknown_Denies() + { + IIdentityContext unknownUser = new AnonymousIdentityContext(); + + AccessResult result = Policy.Decide(unknownUser, LabActions.List); + + Assert.That(result.Allowed, Is.False); + Assert.That(result.Reason, Is.EqualTo(DenyReason.Unauthenticated)); + } + + [Test] + public void Decide_WhenActionIsUnknown_Denies() + { + IIdentityContext teacher = new TestUser("marko", PlatformRole.Teacher); + + AccessResult result = Policy.Decide(teacher, "laboratory.delete-everything"); + + Assert.That(result.Allowed, Is.False); + Assert.That(result.Reason, Is.EqualTo(DenyReason.UnknownAction)); + } + + [Test] + public void Decide_WhenStudentCreatesLaboratory_Denies() + { + IIdentityContext student = new TestUser("ana", PlatformRole.Student); + + AccessResult result = Policy.Decide(student, LabActions.Create); + + Assert.That(result.Allowed, Is.False); + Assert.That(result.Reason, Is.EqualTo(DenyReason.NotPermitted)); + } + + [Test] + public void Decide_WhenTeacherCreatesLaboratory_Allows() + { + IIdentityContext teacher = new TestUser("marko", PlatformRole.Teacher); + + AccessResult result = Policy.Decide(teacher, LabActions.Create); + + Assert.That(result.Allowed, Is.True); + } + + //lazni korisnik za potrebe testova + private sealed class TestUser : IIdentityContext + { + public TestUser(string userId, PlatformRole role) + { + UserId = userId; + Role = role; + } + + public bool IsAuthenticated => true; + + public string? UserId { get; } + + public PlatformRole? Role { get; } + } +} \ No newline at end of file diff --git a/backend/Laboratories.Tests/Laboratories.Tests.csproj b/backend/Laboratories.Tests/Laboratories.Tests.csproj new file mode 100644 index 0000000..6296d37 --- /dev/null +++ b/backend/Laboratories.Tests/Laboratories.Tests.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + latest + enable + enable + false + + + + + + + + + + + + + + + + + + + From 94dfd049c38ff5481e50917677636f421e052e2e Mon Sep 17 00:00:00 2001 From: nsojic Date: Fri, 30 Oct 2026 19:21:11 +0100 Subject: [PATCH 7/7] LAB-01: domenski model Laboratory sa trajnim Id --- backend/Laboratories.Tests/LaboratoryTests.cs | 18 ++++++++++++++++++ .../Modules/Laboratories/Domain/Laboratory.cs | 0 2 files changed, 18 insertions(+) create mode 100644 backend/Laboratories.Tests/LaboratoryTests.cs create mode 100644 backend/Platform.Api/Modules/Laboratories/Domain/Laboratory.cs diff --git a/backend/Laboratories.Tests/LaboratoryTests.cs b/backend/Laboratories.Tests/LaboratoryTests.cs new file mode 100644 index 0000000..5fbe54a --- /dev/null +++ b/backend/Laboratories.Tests/LaboratoryTests.cs @@ -0,0 +1,18 @@ +using Platform.Api.Modules.Laboratories.Domain; + +namespace Laboratories.Tests; + +public class LaboratoryTests +{ + [Test] + public void Rename_KeepsSameId() + { + Guid id = Guid.NewGuid(); + Laboratory lab = new Laboratory(id, "SQL injection"); + + lab.Rename("SQL injection - napredno"); + + Assert.That(lab.Id, Is.EqualTo(id)); + Assert.That(lab.Title, Is.EqualTo("SQL injection - napredno")); + } +} \ No newline at end of file diff --git a/backend/Platform.Api/Modules/Laboratories/Domain/Laboratory.cs b/backend/Platform.Api/Modules/Laboratories/Domain/Laboratory.cs new file mode 100644 index 0000000..e69de29