Finally implemented automatic IAM role and key generation and renewal

This commit is contained in:
Kim 2023-10-23 13:08:09 +02:00
parent 01f1def61b
commit a94116a584
6 changed files with 277 additions and 86 deletions

View File

@ -16,13 +16,17 @@ public class Installation : TreeNode
public Double Lat { get; set; }
public Double Long { get; set; }
public String S3Region { get; set; } = "";
public String S3Provider { get; set; } = "";
public String S3Region { get; set; } = "sos-ch-dk-2";
public String S3Provider { get; set; } = "exo.io";
public String S3WriteKey { get; set; } = "";
public String S3Key { get; set; } = "";
public String S3WriteSecret { get; set; } = "";
public String S3Secret { get; set; } = "";
public String ReadRoleId { get; set; } = "";
public String WriteRoleId { get; set; } = "";
[Ignore]
public IReadOnlyList<String>? OrderNumbers { get; set; }
public String OrderNumbers { get; set; }
}

View File

@ -2,76 +2,246 @@ using System.Text.Json;
using InnovEnergy.Lib.S3Utils;
using InnovEnergy.Lib.S3Utils.DataTypes;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json.Nodes;
using InnovEnergy.App.Backend.Database;
namespace InnovEnergy.App.Backend.DataTypes.Methods;
public static class ExoCmd
{
private const String Key = "EXOea18f5a82bd358896154c783";
private const String Secret = "lYtzU7R5e0L6XKOgBaLVPFr41nEBDxDdXU47zBAEI6M";
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
public static readonly S3Credentials? S3Creds = JsonSerializer.Deserialize<S3Credentials>(File.OpenRead("./Resources/exoscaleS3.json"));
private static Byte[] HmacSha256Digest(String message, String secret)
{
var encoding = new UTF8Encoding();
var keyBytes = encoding.GetBytes(secret);
var messageBytes = encoding.GetBytes(message);
var cryptographer = new System.Security.Cryptography.HMACSHA256(keyBytes);
var bytes = cryptographer.ComputeHash(messageBytes);
return bytes;
}
private static String BuildSignature(String method, String path, String? data, Int64 time)
{
var messageToSign = "";
messageToSign += method + " /v2/" + path + "\n";
messageToSign += data + "\n";
// query strings
messageToSign += "\n";
// headers
messageToSign += "\n";
messageToSign += time;
Console.WriteLine("Message to sign:\n" + messageToSign);
var hmac = HmacSha256Digest(messageToSign, Secret);
return Convert.ToBase64String(hmac);
}
private static String BuildSignature(String method, String path, Int64 time)
{
return BuildSignature(method, path, null, time);
}
public static async Task<(String,String)> CreateReadKey(this Installation installation)
{
var url = $"https://{installation.S3Region}-2.exoscale.com/v2/access-key";
var readRoleId = installation.ReadRoleId;
var content = new HttpMessageContent(new HttpRequestMessage(HttpMethod.Post, requestUri: $$"""
if (String.IsNullOrEmpty(readRoleId)
||! await CheckRoleExists(readRoleId))
{
"name" : {{installation.Name}},
"operations": [
"list-objects",
"get-object"
],
"resources": {
"resource-name": "{{installation.BucketName()}}"
readRoleId = await installation.CreateReadRole();
Thread.Sleep(4000); // Exoscale is to slow for us the role might not be there yet
}
return await CreateKey(installation, readRoleId);
}
"""));
// await Iam.CreateRoleAsync(iamService, $"READ{installation.BucketName()}");
// await Iam.PutRolePolicyAsync(iamService, $"READ{installation.BucketName()}", $"READ{installation.BucketName()}",readOnlyPolicy);
private static async Task<Boolean> CheckRoleExists(String roleId)
{
const String url = "https://api-ch-dk-2.exoscale.com/v2/iam-role";
const String method = "iam-role";
var unixtime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()+60;
var authheader = "credential="+Key+",signed-query-args="+",expires="+unixtime+",signature="+BuildSignature("GET", method, unixtime);
var client = new HttpClient();
var postRequestResponse = await client.PostAsync(url, content);
// var keySecret = await Iam.CreateAccessKeyAsync(iamService, $"READ{installation.BucketName()}");
return (postRequestResponse.Content.ToString(), postRequestResponse.Content.ToString());
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authheader);
var response = await client.GetAsync(url);
var responseString = await response.Content.ReadAsStringAsync();
return responseString.Contains(roleId);
}
private static async Task<(String,String)> CreateKey(Installation installation, String roleName)
{
var url = "https://api-ch-dk-2.exoscale.com/v2/api-key";
var method = "api-key";
var contentString = $$"""{"role-id": "{{roleName}}", "name":"{{installation.BucketName()}}v2"}""";
var unixtime = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 60;
var authheader = "credential=" + Key + ",expires=" + unixtime + ",signature=" +
BuildSignature("POST", method, contentString, unixtime);
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authheader);
var content = new StringContent(contentString, Encoding.UTF8, "application/json");
var response = await client.PostAsync(url, content);
if (response.StatusCode != HttpStatusCode.OK){
Console.WriteLine("Fuck");
}
Console.WriteLine($"Created Key for {installation.Name}");
var responseString = await response.Content.ReadAsStringAsync();
var responseJson = JsonNode.Parse(responseString) ;
return (responseJson!["key"]!.GetValue<String>(), responseJson!["secret"]!.GetValue<String>());
}
public static async Task<String> CreateReadRole(this Installation installation)
{
const String url = "https://api-ch-dk-2.exoscale.com/v2/iam-role";
const String method = "iam-role";
var contentString = $$"""
{
"name" : "{{installation.Id + installation.Name}}",
"policy" : {
"default-service-strategy": "deny",
"services": {
"sos": {
"type": "rules",
"rules": [
{
"expression": "operation == 'get-object' && resources.bucket.startsWith('{{installation.BucketName()}}')",
"action": "allow"
}
]
}
}
}
}
""";
var unixtime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()+60;
var authheader = "credential="+Key+",signed-query-args="+",expires="+unixtime+",signature="+BuildSignature("POST", method, contentString, unixtime);
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authheader);
var content = new StringContent(contentString, Encoding.UTF8, "application/json");
var response = await client.PostAsync(url, content);
var responseString = await response.Content.ReadAsStringAsync();
Console.WriteLine(responseString);
//Put Role ID into database
var id = JsonNode.Parse(responseString)!["reference"]!["id"]!.GetValue<String>();
installation.ReadRoleId = id;
Db.Update(installation);
return id;
}
public static async Task<Boolean> RevokeReadKey(this Installation installation)
{
var iamService = new S3Region($"https://{installation.S3Region}.{installation.S3Provider}", S3Creds!).GetIamClient();
if (!await Iam.RoleExists(iamService, $"READ{installation.BucketName()}"))
var url = $"https://api-ch-dk-2.exoscale.com/v2/access-key/{installation.S3Key}";
var method = $"access-key/{installation.S3Key}";
var unixtime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()+60;
var authheader = "credential="+Key+",expires="+unixtime+",signature="+BuildSignature("DELETE", method, unixtime);
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authheader);
var response = await client.DeleteAsync(url);
return response.IsSuccessStatusCode;
}
public static async Task<(String, String)> CreateWriteKey(this Installation installation)
{
return true;
}
var writeRoleId = installation.WriteRoleId;
return await Iam.RevokeAccessKey(iamService, $"READ{installation.BucketName()}");
}
public static async Task<(String key, String secret)> CreateWriteKey(this Installation installation)
if (String.IsNullOrEmpty(writeRoleId)
|| !await CheckRoleExists(writeRoleId))
{
var iamService = new S3Region($"https://{installation.S3Region}.{installation.S3Provider}", S3Creds!).GetIamClient();
if (!await Iam.RoleExists(iamService, $"READWRITE{installation.BucketName()}"))
writeRoleId = await installation.CreateWriteRole();
Thread.Sleep(4000); // Exoscale is to slow for us the role might not be there yet
}
return await CreateKey(installation, writeRoleId);
}
public static async Task<String> CreateWriteRole(this Installation installation)
{
var readWritePolicy = @"{
""default-service-strategy"": ""deny"",
""services"": {
""sos"": {
""type"": ""allow"",
""resource"": " + $@"{installation.BucketName()}
}}
}}
}}";
await Iam.CreateRoleAsync(iamService, $"READWRITE{installation.BucketName()}");
await Iam.PutRolePolicyAsync(iamService, $"READWRITE{installation.BucketName()}", $"READWRITE{installation.BucketName()}",readWritePolicy);
const String url = "https://api-ch-dk-2.exoscale.com/v2/iam-role";
const String method = "iam-role";
var contentString = $$"""
{
"name" : "WRITE{{installation.Id + installation.Name}}",
"policy" : {
"default-service-strategy": "deny",
"services": {
"sos": {
"type": "rules",
"rules":[{
"action" : "allow",
"expression": "resources.bucket.startsWith('{{installation.BucketName()}}')"
}]
}
var keySecret = await Iam.CreateAccessKeyAsync(iamService, $"READWRITE{installation.BucketName()}");
return (keySecret.AccessKeyId, keySecret.SecretAccessKey);
}
}
}
""";
var unixtime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()+60;
var authheader = "credential="+Key+",signed-query-args="+",expires="+unixtime+",signature="+BuildSignature("POST", method, contentString, unixtime);
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authheader);
var content = new StringContent(contentString, Encoding.UTF8, "application/json");
var response = await client.PostAsync(url, content);
var responseString = await response.Content.ReadAsStringAsync();
Console.WriteLine(responseString);
//Put Role ID into database
var id = JsonNode.Parse(responseString)!["reference"]!["id"]!.GetValue<String>();
installation.WriteRoleId = id;
;
Db.Update(installation);
return id;
}
public static async Task<Boolean> CreateBucket(this Installation installation)
{

View File

@ -11,7 +11,10 @@ namespace InnovEnergy.App.Backend.DataTypes.Methods;
public static class InstallationMethods
{
private const String BucketNameSalt = "3e5b3069-214a-43ee-8d85-57d72000c19d";
private static readonly String BucketNameSalt =
Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == ""
? "stage"
:"3e5b3069-214a-43ee-8d85-57d72000c19d";
public static String BucketName(this Installation installation)
{
@ -24,6 +27,9 @@ public static class InstallationMethods
var (key,secret) = await installation.CreateReadKey();
installation.S3Key = key;
installation.S3Secret = secret;
if (installation.S3WriteKey == "" || installation.S3WriteSecret == "")
{
var (writeKey,writeSecret) = await installation.CreateWriteKey();
@ -31,9 +37,6 @@ public static class InstallationMethods
installation.S3WriteKey = writeKey;
}
installation.S3Key = key;
installation.S3Secret = secret;
return Db.Update(installation);
}
@ -141,12 +144,12 @@ public static class InstallationMethods
return Db.Installations.Any(i => i.Id == installation.Id);
}
public static IReadOnlyList<String>? GetOrderNumbers(this Installation installation)
public static String GetOrderNumbers(this Installation installation)
{
return Db.OrderNumber2Installation
.Where(i => i.InstallationId == installation.Id)
.Select(i => i.OrderNumber)
.ToReadOnlyList();
.ToReadOnlyList().JoinWith(",");
}
public static Installation FillOrderNumbers(this Installation installation)
@ -155,4 +158,27 @@ public static class InstallationMethods
return installation;
}
public static Boolean SetOrderNumbers(this Installation installation)
{
var relations = Db.OrderNumber2Installation.Where(i => i.InstallationId == installation.Id).ToList();
foreach (var orderNumber in installation.OrderNumbers.Split(","))
{
var rel = relations.FirstOrDefault(i => i.OrderNumber == orderNumber);
if ( rel != null) relations.Remove(rel);
var o2I = new OrderNumber2Installation
{
OrderNumber = orderNumber,
InstallationId = installation.Id
};
Db.Create(o2I);
}
foreach (var rel in relations)
{
Db.Delete(rel);
}
return true;
}
}

View File

@ -100,12 +100,12 @@ public static class SessionMethods
&& user.HasWriteAccess
&& user.HasAccessToParentOf(installation)
&& Db.Create(installation) // TODO: these two in a transaction
// && installation.SetOrderNumbers()
&& installation.SetOrderNumbers()
&& Db.Create(new InstallationAccess { UserId = user.Id, InstallationId = installation.Id })
&& await installation.CreateBucket()
&& await installation.RenewS3Credentials(); // generation of access _after_ generation of
// bucket to prevent "zombie" access-rights.
// This might fuck us over if the creation of access rights fails,
// This might ** us over if the creation of access rights fails,
// as bucket-names are unique and bound to the installation id... -K
}
@ -114,30 +114,6 @@ public static class SessionMethods
var user = session?.User;
var original = Db.GetInstallationById(installation?.Id);
var originalOrderNumbers = original!.GetOrderNumbers();
if (!Equals(originalOrderNumbers, installation?.OrderNumbers))
{
foreach (var orderNumber in installation!.OrderNumbers!)
{
if (originalOrderNumbers!.Contains(orderNumber)) continue;
var o2I = new OrderNumber2Installation
{
OrderNumber = orderNumber,
InstallationId = installation.Id
};
Db.Create(o2I);
}
foreach (var orderNumberOld in originalOrderNumbers!)
{
if (!installation.OrderNumbers.Contains(orderNumberOld))
{
Db.OrderNumber2Installation.Delete(i =>
i.InstallationId == installation.Id && i.OrderNumber == orderNumberOld);
}
}
}
return user is not null
&& installation is not null

View File

@ -78,4 +78,9 @@ public static partial class Db
BackupDatabase();
return delete;
}
public static void Delete(OrderNumber2Installation relation)
{
OrderNumber2Installation.Delete(s => s.InstallationId == relation.InstallationId && s.OrderNumber == relation.OrderNumber);
}
}

View File

@ -97,6 +97,16 @@ public static class S3
return response.ResponseStream;
}
// public static async Task<Boolean> CheckRoleExists(this S3Region region, String roleId)
// {
//
//
// var response = await region
// .GetIamClient()
// .GetRoleAsync(new GetRoleRequest(){RoleName = roleId});
// return response.HttpStatusCode != HttpStatusCode.NotFound;
// }
public static async Task<IReadOnlyList<Byte>> GetObject(this S3Url url)
{
// beautiful await using stream soup...