diff --git a/.github/workflows/build-chart.yml b/.github/workflows/build-chart.yml new file mode 100644 index 00000000..b10fc844 --- /dev/null +++ b/.github/workflows/build-chart.yml @@ -0,0 +1,32 @@ +name: build and release chart + +on: + push: + branches: [ "main", "github-packages" ] + +jobs: + build: + runs-on: ubuntu-latest + + permissions: + contents: write + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + + - name: Install Helm + uses: azure/setup-helm@v3 + + - name: Run chart-releaser + uses: helm/chart-releaser-action@v1.6.0 + env: + CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build-container.yml similarity index 100% rename from .github/workflows/build.yml rename to .github/workflows/build-container.yml diff --git a/IguideME.Web/Controllers/DataController.cs b/IguideME.Web/Controllers/DataController.cs index 3a47fa8c..1c17c0e9 100644 --- a/IguideME.Web/Controllers/DataController.cs +++ b/IguideME.Web/Controllers/DataController.cs @@ -70,6 +70,7 @@ protected int GetCourseID() // tmp for brightspace until we have an environment // return 83337; // returns the ID of course in which the IguideME instance is loaded + _logger.LogInformation("User identity: {}", User.Identity); return int.Parse((User.Identity as ClaimsIdentity).FindFirst("courseid").Value); } diff --git a/IguideME.Web/Program.cs b/IguideME.Web/Program.cs index 5f1156e8..bd4fc2d9 100644 --- a/IguideME.Web/Program.cs +++ b/IguideME.Web/Program.cs @@ -1,192 +1,203 @@ -using System; -using UvA.LTI; -using System.Text; -using System.Linq; -using StackExchange.Redis; -using System.Security.Claims; -using IguideME.Web.Services; -using IguideME.Web.Services.Data; -using System.Collections.Generic; -using Microsoft.Extensions.Hosting; -using Microsoft.AspNetCore.Builder; -using Microsoft.IdentityModel.Tokens; -using Microsoft.AspNetCore.HttpOverrides; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.AspNetCore.Server.Kestrel.Core; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using IguideME.Web.Services.LMSHandlers; - - -// //======================== Builder configuration =========================// - - -// /------------------------- Create builder --------------------------/ - -// Create a new WebApplicationBuilder for setting up the application. -WebApplicationBuilder builder = WebApplication.CreateBuilder( - new WebApplicationOptions{ - Args = args, // command-line arguments passed to the app - WebRootPath = "wwwroot/build" // specifies the path to the web root directory - } -); - - -// /------------------------ Read appsettings.json -------------------------/ - -Backends lms; -if (!Backends.TryParse(builder.Configuration.GetSection("LMS:Backend").Value, out lms)) -{ - // Console.WriteLine(); - throw new Exception("Incorrect settings for LMS:Backend"); -} - -// "UnsecureApplicationSettings:UseRedisCache" - indicates whether to use Redis cache or not. -bool useRedisCache = bool.Parse(builder.Configuration.GetSection( - "UnsecureApplicationSettings:UseRedisCache").Value); - -// "UnsecureApplicationSettings:RedisCacheConnectionString" - contains the Redis cache connection string. -string redisCacheConnectionString = builder.Configuration.GetSection( -"UnsecureApplicationSettings:RedisCacheConnectionString").Value; - -// "LTI:SigningKey" - is the key used for LTI authentication and to generate own key. -string key = builder.Configuration.GetSection("LTI:SigningKey").Value; - -// Use the specified key to create a symmetric security key for own authorization using JWTs. -SymmetricSecurityKey signingKey = new(Encoding.ASCII.GetBytes(key)); - - -// /----------------------- Configure services ------------------------/ - -// work object, where the computations are done. -builder.Services.AddTransient(); - -// QueuedBackgroundService is a dual-purpose service -builder.Services.AddHostedService(); -builder.Services.AddTransient(); - -builder.Services.AddHostedService(); - -// Manages jobs -builder.Services.AddTransient(); - -if (useRedisCache && !string.IsNullOrWhiteSpace(redisCacheConnectionString)) -{ - // setup redis cache for horizontally scaled services - builder.Services.AddSingleton( - ConnectionMultiplexer.Connect(redisCacheConnectionString)); - - // job status service, CRUD operations on jobs stored in redis cache. - builder.Services.AddTransient(); -} -else -{ - // strictly for testing purposes - builder.Services.AddTransient(); -} - -// Add authorization (validating the JWT) -builder.Services - .AddAuthorization() - .AddAuthentication(opt => opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme) - .AddJwtBearer(opt => - { - opt.TokenValidationParameters.ValidateAudience = false; - opt.TokenValidationParameters.ValidIssuer = "lti"; - opt.TokenValidationParameters.IssuerSigningKey = signingKey; - }); - -// Add a policy that checks whether a user is an admin. -builder.Services.AddAuthorization(options => - options.AddPolicy("IsInstructor", - policy => policy.RequireRole("Teacher"))); - -builder.Services.Configure(opt => -{ - opt.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; - opt.KnownNetworks.Clear(); - opt.KnownProxies.Clear(); -}); - -builder.Services.Configure(options => options.AllowSynchronousIO = true); - -// If using IIS: -builder.Services.Configure(options => options.AllowSynchronousIO = true); - -builder.Services.AddControllersWithViews(); -switch (lms) -{ - case Backends.Brightspace: - builder.Services.AddSingleton(); - break; - case Backends.Canvas: - builder.Services.AddSingleton(); - break; -} -builder.Services.AddHttpClient(); - -builder.Services.AddControllers() - .AddJsonOptions(options => - options.JsonSerializerOptions.PropertyNamingPolicy = null); - - -builder.Services.AddControllers().AddNewtonsoftJson(); - - -// //========================== App configuration ===========================// - - -WebApplication app = builder.Build(); - -var ltiConfig = builder.Configuration.GetSection("LTI"); -app.UseForwardedHeaders(); - -app.UseLti(new LtiOptions -{ - ClientId = ltiConfig["ClientId"] ?? throw new Exception("Client id not set"), - AuthenticateUrl = ltiConfig["AuthenticateUrl"] ?? throw new Exception("Authenticate url not set"), - JwksUrl = ltiConfig["JwksUrl"] ?? throw new Exception("Jwks url not set"), - SigningKey = key, - ClaimsMapping = p => new Dictionary - { - [ClaimTypes.NameIdentifier] = p.NameIdentifier!.Split("_").Last(), - ["contextLabel"] = p.Context.Label, - ["courseName"] = p.Context.Title, - ["user_name"] = p.Name, - ["courseid"] = p.CustomClaims?.GetProperty("courseid").ToString(), - ["userid"] = p.CustomClaims?.GetProperty("userid").ToString(), - [ClaimTypes.Role] = p.Roles.Any(e => e.Contains("http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor")) - ? "Teacher" : "Student", - [ClaimTypes.Email] = p.Email, - } -}); - - -if (app.Environment.IsDevelopment()) -{ - Console.WriteLine("In Development."); - app.UseDeveloperExceptionPage(); -} -else -{ - Console.WriteLine("In Production."); - app.UseExceptionHandler("/Error"); -} - -DatabaseManager.Initialize(app.Environment.IsDevelopment()); - -app.UseDefaultFiles(); -app.UseStaticFiles(); - -app.UseRouting(); - -app.UseAuthentication(); -app.UseAuthorization(); - -app.MapControllerRoute( - name: "default", - pattern: "{controller}/{action=Index}/{id?}"); - - -// //=============================== Run app ================================// - -app.Run(); +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text; +using IguideME.Web.Services; +using IguideME.Web.Services.Data; +using IguideME.Web.Services.LMSHandlers; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.IdentityModel.Tokens; +using StackExchange.Redis; +using UvA.LTI; + +// //======================== Builder configuration =========================// + + +// /------------------------- Create builder --------------------------/ + +// Create a new WebApplicationBuilder for setting up the application. +WebApplicationBuilder builder = WebApplication.CreateBuilder( + new WebApplicationOptions + { + Args = args, // command-line arguments passed to the app + WebRootPath = "wwwroot/build" // specifies the path to the web root directory + } +); + +// /------------------------ Read appsettings.json -------------------------/ + +Backends lms; +if (!Backends.TryParse(builder.Configuration.GetSection("LMS:Backend").Value, out lms)) +{ + // Console.WriteLine(); + throw new Exception("Incorrect settings for LMS:Backend"); +} + +// "UnsecureApplicationSettings:UseRedisCache" - indicates whether to use Redis cache or not. +bool useRedisCache = bool.Parse( + builder.Configuration.GetSection("UnsecureApplicationSettings:UseRedisCache").Value +); + +// "UnsecureApplicationSettings:RedisCacheConnectionString" - contains the Redis cache connection string. +string redisCacheConnectionString = builder + .Configuration.GetSection("UnsecureApplicationSettings:RedisCacheConnectionString") + .Value; + +// "LTI:SigningKey" - is the key used for LTI authentication and to generate own key. +string key = builder.Configuration.GetSection("LTI:SigningKey").Value; + +// Use the specified key to create a symmetric security key for own authorization using JWTs. +SymmetricSecurityKey signingKey = new(Encoding.ASCII.GetBytes(key)); + +// /----------------------- Configure services ------------------------/ + +// work object, where the computations are done. +builder.Services.AddTransient(); + +// QueuedBackgroundService is a dual-purpose service +builder.Services.AddHostedService(); +builder.Services.AddTransient(); + +builder.Services.AddHostedService(); + +// Manages jobs +builder.Services.AddTransient(); + +if (useRedisCache && !string.IsNullOrWhiteSpace(redisCacheConnectionString)) +{ + // setup redis cache for horizontally scaled services + builder.Services.AddSingleton( + ConnectionMultiplexer.Connect(redisCacheConnectionString) + ); + + // job status service, CRUD operations on jobs stored in redis cache. + builder.Services.AddTransient(); +} +else +{ + // strictly for testing purposes + builder.Services.AddTransient(); +} + +// Add authorization (validating the JWT) +builder + .Services.AddAuthorization() + .AddAuthentication(opt => + opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme + ) + .AddJwtBearer(opt => + { + opt.TokenValidationParameters.ValidateAudience = false; + opt.TokenValidationParameters.ValidIssuer = "lti"; + opt.TokenValidationParameters.IssuerSigningKey = signingKey; + }); + +// Add a policy that checks whether a user is an admin. +builder.Services.AddAuthorization(options => + options.AddPolicy("IsInstructor", policy => policy.RequireRole("Teacher")) +); + +builder.Services.Configure(opt => +{ + opt.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + opt.KnownNetworks.Clear(); + opt.KnownProxies.Clear(); +}); + +builder.Services.Configure(options => options.AllowSynchronousIO = true); + +// If using IIS: +builder.Services.Configure(options => options.AllowSynchronousIO = true); + +builder.Services.AddControllersWithViews(); +switch (lms) +{ + case Backends.Brightspace: + builder.Services.AddSingleton(); + break; + case Backends.Canvas: + builder.Services.AddSingleton(); + break; +} +builder.Services.AddHttpClient(); + +builder + .Services.AddControllers() + .AddJsonOptions(options => options.JsonSerializerOptions.PropertyNamingPolicy = null); + +builder.Services.AddControllers().AddNewtonsoftJson(); + +// //========================== App configuration ===========================// + + +WebApplication app = builder.Build(); + +var ltiConfig = builder.Configuration.GetSection("LTI"); +app.UseForwardedHeaders(); + +app.UseLti( + new LtiOptions + { + ClientId = ltiConfig["ClientId"] ?? throw new Exception("Client id not set"), + AuthenticateUrl = + ltiConfig["AuthenticateUrl"] ?? throw new Exception("Authenticate url not set"), + JwksUrl = ltiConfig["JwksUrl"] ?? throw new Exception("Jwks url not set"), + SigningKey = key, + ClaimsMapping = p => + { + string courseID = p.CustomClaims?.GetProperty("courseid").ToString(); + if (courseID.IsNullOrEmpty()) + { + courseID = p.Context.Id; + } + return new Dictionary + { + [ClaimTypes.NameIdentifier] = p.NameIdentifier!.Split("_").Last(), + ["contextLabel"] = p.Context.Label, + ["courseName"] = p.Context.Title, + ["user_name"] = p.Name, + ["courseid"] = courseID, + ["userid"] = p.CustomClaims?.GetProperty("userid").ToString(), + [ClaimTypes.Role] = p.Roles.Any(e => + e.Contains("http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor") + ) + ? "Teacher" + : "Student", + [ClaimTypes.Email] = p.Email, + }; + } + } +); + +if (app.Environment.IsDevelopment()) +{ + Console.WriteLine("In Development."); + app.UseDeveloperExceptionPage(); +} +else +{ + Console.WriteLine("In Production."); + app.UseExceptionHandler("/Error"); +} + +DatabaseManager.Initialize(app.Environment.IsDevelopment()); + +app.UseDefaultFiles(); +app.UseStaticFiles(); + +app.UseRouting(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllerRoute(name: "default", pattern: "{controller}/{action=Index}/{id?}"); + +// //=============================== Run app ================================// + +app.Run(); diff --git a/IguideME.Web/Services/LMSHandlers/BrightspaceHandler.cs b/IguideME.Web/Services/LMSHandlers/BrightspaceHandler.cs index 3b5df5ed..66d3a7c2 100644 --- a/IguideME.Web/Services/LMSHandlers/BrightspaceHandler.cs +++ b/IguideME.Web/Services/LMSHandlers/BrightspaceHandler.cs @@ -28,11 +28,11 @@ public BrightspaceHandler(IConfiguration config, ILogger logger) + ";Database = " + config["LMS:Brightspace:Connection:Database"] + ";User ID = " - + config["LMS:Brightspace:Connection:User ID"] + + config["LMS:Brightspace:Connection:UserID"] + ";Password = " + config["LMS:Brightspace:Connection:Password"] + ";Search Path = " - + config["LMS:Brightspace:Connection:Search Path"] + + config["LMS:Brightspace:Connection:SearchPath"] + ";" + config["LMS:Brightspace:Connection:Rest"]; _logger = logger; @@ -129,10 +129,7 @@ FROM users INNER JOIN user_enrollments ON users.user_id = user_enrollments.user_id WHERE user_enrollments.org_unit_id = @courseID - AND users.username= @userID - AND (user_enrollments.role_id = 110 - OR user_enrollments.role_id = 130 - OR user_enrollments.role_id = 134)", + AND users.user_id= @userID", new NpgsqlParameter("userID", int.Parse(userID)), new NpgsqlParameter("courseID", courseID) ) @@ -355,17 +352,18 @@ IEnumerable users using ( NpgsqlDataReader r = Query( - @$"SELECT grade_object_id, - user_id, - points_numerator, - points_denominator, - grade_text, - grade_released_date - FROM grade_results - WHERE org_unit_id = @courseID - AND is_released = TRUE - AND user_id - IN ({string.Join(",", users.Select((_, index) => $"@userID{index}"))})", + @$"SELECT grade_results.grade_object_id, + users.username, + grade_results.points_numerator, + grade_results.points_denominator, + grade_results.grade_text, + grade_results.grade_released_date + FROM grade_results + INNER JOIN users + ON users.user_id = grade_results.user_id + WHERE grade_results.org_unit_id = @courseID + AND grade_results.user_id + IN ({string.Join(",", users.Select((_, index) => $"@userID{index}"))})", parameters ) ) diff --git a/IguideME.Web/Services/Workers/AssignmentWorker.cs b/IguideME.Web/Services/Workers/AssignmentWorker.cs index c3f926b8..33c995a1 100644 --- a/IguideME.Web/Services/Workers/AssignmentWorker.cs +++ b/IguideME.Web/Services/Workers/AssignmentWorker.cs @@ -5,6 +5,7 @@ using IguideME.Web.Models.App; using IguideME.Web.Models.Impl; using IguideME.Web.Services.LMSHandlers; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.Extensions.Logging; namespace IguideME.Web.Services.Workers @@ -75,6 +76,8 @@ private void RegisterSubmissions( Dictionary gradingTypes ) { + _logger.LogInformation("Starting submission registry..."); + double max; AppGradingType type; (double, AppGradingType) elem; @@ -158,6 +161,7 @@ public void Start() users ); Dictionary gradingTypes = new(); + List assignmentSubmissionsWithTiles = new (); foreach (AppAssignment assignment in assignments) { @@ -174,8 +178,18 @@ public void Start() assignment.AssignmentID, (assignment.PointsPossible, assignment.GradingType) ); + + // We find all submissions for this assignment, and save them with the corresponding entryID that we found + foreach (AssignmentSubmission sub in submissions) { + if(sub.AssignmentID == assignment.AssignmentID){ + sub.EntryID = entry.ID; + assignmentSubmissionsWithTiles.Add(sub); + } + } } - this.RegisterSubmissions(submissions, gradingTypes); + + // Finally we register the submissions + this.RegisterSubmissions(assignmentSubmissionsWithTiles, gradingTypes); } } } diff --git a/IguideME.Web/appsettings.json b/IguideME.Web/appsettings.json index 73e29e7d..8e366c46 100644 --- a/IguideME.Web/appsettings.json +++ b/IguideME.Web/appsettings.json @@ -17,7 +17,7 @@ "SigningKey": "blawlaekltjwelkrjtwlkejlekwjrklwejr32423" }, "LMS": { - "Backend": "Canvas", + "Backend": "Brightspace", "Canvas": { "Url": "https://uvadlo-tes.instructure.com/", "AccessToken": "10441~hNIJcVXzv25ibhKMqYROQ06GiBlUkpBEs29h25OBUjGUNZfNtvf4MeVgeC3B1O0p" @@ -27,9 +27,9 @@ "Host": "178.21.117.23", "Port": "5433;", "Database": "bds", - "User ID": "xxxxxxxxxxxxxxx", + "UserID": "xxxxxxxxxxxxxxx", "Password": "xxxxxxxxxxxxxxx", - "Search Path": "bds", + "SearchPath": "bds", "Rest": "Persist Security Info=true;" } } diff --git a/charts/iguideme/Chart.yaml b/charts/iguideme/Chart.yaml index 6f7195ca..1e70bcab 100644 --- a/charts/iguideme/Chart.yaml +++ b/charts/iguideme/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: iguideme description: IguideME type: application -version: 0.1.0 -appVersion: "0.1.0" +version: 0.2.1 +appVersion: "0.2.1" diff --git a/charts/iguideme/templates/azureidentity.yaml b/charts/iguideme/templates/azureidentity.yaml deleted file mode 100644 index 70b95b49..00000000 --- a/charts/iguideme/templates/azureidentity.yaml +++ /dev/null @@ -1,11 +0,0 @@ -{{- if .Values.azure }} -apiVersion: aadpodidentity.k8s.io/v1 -kind: AzureIdentity -metadata: - name: iguideme-identity-aks-kv - namespace: "{{ .Release.Namespace }}" -spec: - type: 0 - resourceID: "/subscriptions/{{ .Values.azure.subscriptionId }}/resourcegroups/{{ .Values.azure.resourceGroup }}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{{ .Values.azure.identityName }}" - clientID: {{ .Values.azure.clientId }} -{{- end }} diff --git a/charts/iguideme/templates/azureidentitybinding.yaml b/charts/iguideme/templates/azureidentitybinding.yaml deleted file mode 100644 index 193467aa..00000000 --- a/charts/iguideme/templates/azureidentitybinding.yaml +++ /dev/null @@ -1,10 +0,0 @@ -{{- if .Values.azure }} -apiVersion: aadpodidentity.k8s.io/v1 -kind: AzureIdentityBinding -metadata: - name: iguideme-identity-aks-kv-binding - namespace: "{{ .Release.Namespace }}" -spec: - azureIdentity: iguideme-identity-aks-kv - selector: iguideme-azure-kv -{{- end }} diff --git a/charts/iguideme/templates/deployment.yaml b/charts/iguideme/templates/deployment.yaml index 714c0e61..d0e9c5d0 100644 --- a/charts/iguideme/templates/deployment.yaml +++ b/charts/iguideme/templates/deployment.yaml @@ -30,6 +30,16 @@ spec: value: "{{ .Values.lmsBackend }}" - name: LMS__Canvas__Url value: "{{ .Values.canvasUrl }}" + - name: LMS__Brightspace__Host + value: "{{ .Values.brightspaceHost}}" + - name: LMS__Brigtspace__Port + value: "{{ .Values.brightspacePort }}" + - name: LMS__Brightspace__Database + value: "{{ .Values.brightspaceDatabase}}" + - name: LMS__Brightspace__SearchPath + value: "{{ .Values.brightspaceSearchPath}}" + - name: LMS__Canvas__Brightspace__Rest + value: "{{ .Values.brightspaceRest}}" - name: LTI__ClientId value: "{{ .Values.clientId }}" - name: LTI__AuthenticateUrl diff --git a/charts/iguideme/templates/secretproviderclass.yaml b/charts/iguideme/templates/secretproviderclass.yaml index 34762d6d..43e342fd 100644 --- a/charts/iguideme/templates/secretproviderclass.yaml +++ b/charts/iguideme/templates/secretproviderclass.yaml @@ -15,9 +15,9 @@ spec: key: {{ .key }} {{- end }} parameters: - usePodIdentity: "true" - useVMManagedIdentity: "false" - userAssignedIdentityID: "" + usePodIdentity: "false" + useVMManagedIdentity: "true" + userAssignedIdentityID: "{{ .Values.azure.clientId }}" keyvaultName: "{{ .Values.azure.keyvaultName }}" cloudName: AzurePublicCloud objects: | diff --git a/charts/iguideme/values.yaml b/charts/iguideme/values.yaml index 2044e9eb..f6b6b496 100644 --- a/charts/iguideme/values.yaml +++ b/charts/iguideme/values.yaml @@ -8,6 +8,11 @@ clientId: 104410000000000150 authenticateUrl: https://uvadlo-dev.instructure.com/api/lti/authorize_redirect jwksUrl: https://canvas.instructure.com/api/lti/security/jwks lmsBackend: Canvas +brightspaceHost: 178.21.117.23, +brightspacePort: 5433;, +brightspaceDatabase: bds, +brightspaceSearchPath: bds, +brightspaceRest: Persist Security Info=true; azure: keyvaultName: keyvaultName @@ -23,3 +28,7 @@ azure: key: LTI__SigningKey - name: iguideme-canvas-token key: LMS__Canvas__AccessToken + - name: iguideme-brightspace-user + key: LMS__Brightspace__UserID + - name: iguideme-brightspace-pass + key: LMS__Brightspace__Password