BitBetter icon indicating copy to clipboard operation
BitBetter copied to clipboard

Cannot create Projects for SecretsManager because PlanType "Custom" does not have inherent SecretsManager access

Open Ayitaka opened this issue 1 year ago • 7 comments

Currently, for the SecretsManager feature, you can add Secrets and you can add ServiceAccounts, but you cannot create new Projects.

Trying to add a new Project to SecretsManager gives an error of "Existing plan not found" because the PlanType we use for Org licenses is "Plan: Custom", with "PlanType: 6". The type CustomPlan does not have inherent access to "SecretsManager" like the EnterprisePlan does.

Unless I am missing some way to set this with a Custom plan, the only way I see to allow creation of Projects is to change the license plan type to Enterprise (Annually), with PlanType of 15 but I am unsure of the overall implications of that change would be or why Custom was chosen to begin with. (Having tried it on my own server, the expiration date still says 2123, so I don't believe it would expire annually).

Should we add an additional option to org license creation for something like "SMProjects" as true/false? True would set "Plan": "Enterprise (Annually)" and "PlanType": 15, while false (or empty) would leave it as "Plan": "Custom" and "PlanType": 6.

Or should we just change to Enterprise (Annually) as the plan type and ditch Custom?

Edit: Or should we add an option to allow people to pick what plan type to use? Family, Teams, Enterprise, Custom

p.s. Simply changing "Plan" and "PlanType" in current licenses does not work - the license must be generated with those options set to begin with, not after-the-fact.

Thoughts?

Ayitaka avatar Dec 01 '23 06:12 Ayitaka

Hello, funny thing is that you can create project using "Enterprise" license then switch back to "Custom" and everything will work normally apart project creation.

psychodracon avatar Dec 02 '23 16:12 psychodracon

p.s. Simply changing "Plan" and "PlanType" in current licenses does not work - the license must be generated with those options set to begin with, not after-the-fact.

Yes, that's always the case, you can't just modify the license file, they always have to regenerated.

Someone would need to modify licenseGen and perform some tests, and then submit a PR.

captainhook avatar Dec 02 '23 18:12 captainhook

I am still curious to know the initial reasoning for using "Custom" instead of "Enterprise". I have already tested changing it to Enterprise on my server and, so far, it works the same as Custom with the exception of being able to create Projects. But I do wish to avoid unintended consequences if there was a reason for not using Enterprise.

Ayitaka avatar Dec 04 '23 03:12 Ayitaka

I think there is no restriction against changing the CustomPlan as we are currently injecting the DLL for certificate replacement.

Anyway I am using this code successfully. Feel free to use it as you want.

(based on unified, 2023.10.1 / 2023.12.0) src/bitBetter/Program.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using dnlib.DotNet;
using dnlib.DotNet.Emit;
using dnlib.DotNet.Writer;
using dnlib.IO;

namespace bitBetter
{
    internal class Program
    {
        private static int Main(string[] args)
        {
            const string certFile = "/app/cert.cert";
            string[] files = Directory.GetFiles("/app/mount", "Core.dll", SearchOption.AllDirectories);

            foreach (string file in files)
            {
                Console.WriteLine(file);
                ModuleDefMD moduleDefMd = ModuleDefMD.Load(file);
                byte[] cert = File.ReadAllBytes(certFile);

                EmbeddedResource embeddedResourceToRemove = moduleDefMd.Resources
                    .OfType<EmbeddedResource>()
                    .First(r => r.Name.Equals("Bit.Core.licensing.cer"));

                Console.WriteLine(embeddedResourceToRemove.Name);

                EmbeddedResource embeddedResourceToAdd = new("Bit.Core.licensing.cer", cert)
                {
                    Attributes = embeddedResourceToRemove.Attributes
                };
                moduleDefMd.Resources.Add(embeddedResourceToAdd);
                moduleDefMd.Resources.Remove(embeddedResourceToRemove);

                DataReader reader = embeddedResourceToRemove.CreateReader();
                X509Certificate2 existingCert = new(reader.ReadRemainingBytes());

                Console.WriteLine($"Existing Cert Thumbprint: {existingCert.Thumbprint}");
                X509Certificate2 certificate = new(cert);

                Console.WriteLine($"New Cert Thumbprint: {certificate.Thumbprint}");

                IEnumerable<TypeDef> services = moduleDefMd.Types.Where(t => t.Namespace == "Bit.Core.Services");
                TypeDef type = services.First(t => t.Name == "LicensingService");
                MethodDef constructor = type.FindConstructors().First();

                Instruction instructionToPatch =
                    constructor.Body.Instructions
                        .FirstOrDefault(i => i.OpCode == OpCodes.Ldstr
                                             && string.Equals((string)i.Operand, existingCert.Thumbprint, StringComparison.InvariantCultureIgnoreCase));

                if (instructionToPatch != null)
                {
                    instructionToPatch.Operand = certificate.Thumbprint;
                }
                else
                {
                    Console.WriteLine("Can't find constructor to patch");
                }

                // CustomPlan modify start
                Console.WriteLine("Start to modify CustomPlan");
                Console.WriteLine("Find the set method for the SecretsManager property in the Plan type");
                TypeDef planType = moduleDefMd.Types.First(t => t.FullName == "Bit.Core.Models.StaticStore.Plan");
                PropertyDef planSecretsManagerProperty = planType?.Properties.First(p => p.Name == "SecretsManager");
                TypeDef customPlanType = moduleDefMd.Types.First(t => t.FullName == "Bit.Core.Models.StaticStore.Plans.CustomPlan");
                MethodDef customPlanCtor = customPlanType?.FindConstructors().First();
                TypeDef secretsManagerPlanFeaturesType = planType?.NestedTypes.First(t => t.Name == "SecretsManagerPlanFeatures");
                PropertyDef secretsManagerPlanFeaturesAllowSeatAutoscaleProperty = secretsManagerPlanFeaturesType?.Properties.First(p => p.Name == "AllowSeatAutoscale");
                PropertyDef secretsManagerPlanFeaturesAllowServiceAccountsAutoscaleProperty = secretsManagerPlanFeaturesType?.Properties.First(p => p.Name == "AllowServiceAccountsAutoscale");
                if (customPlanType?.NestedTypes.FirstOrDefault(t => t.Name == "CustomSecretsManagerFeatures") != null)
                {
                    Console.WriteLine("The content you are trying to modify is already defined.");
                }
                else if (customPlanCtor == null)
                {
                    Console.WriteLine("Cannot find target constructor");
                }
                else if (planType != null
                         && planSecretsManagerProperty != null
                         && secretsManagerPlanFeaturesType != null
                         && secretsManagerPlanFeaturesAllowSeatAutoscaleProperty != null
                         && secretsManagerPlanFeaturesAllowServiceAccountsAutoscaleProperty != null)
                {
                    Console.WriteLine("Create CustomSecretsManagerFeatures as a nested class within CustomPlan");
                    TypeDef customSecretsManagerFeaturesType = new TypeDefUser(customPlanType.Namespace, "CustomSecretsManagerPlanFeatures", secretsManagerPlanFeaturesType);
                    customSecretsManagerFeaturesType.Attributes = TypeAttributes.NestedPrivate | TypeAttributes.Class;
                    MethodDef customSecretsManagerFeaturesCtor = new MethodDefUser(".ctor",
                        MethodSig.CreateInstance(moduleDefMd.CorLibTypes.Void),
                        MethodImplAttributes.IL | MethodImplAttributes.Managed,
                        MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName);
                    customSecretsManagerFeaturesCtor.Body = new CilBody();
                    customSecretsManagerFeaturesCtor.Body.Instructions.Add(OpCodes.Ldarg_0.ToInstruction());
                    customSecretsManagerFeaturesCtor.Body.Instructions.Add(OpCodes.Ldc_I4_1.ToInstruction());
                    customSecretsManagerFeaturesCtor.Body.Instructions.Add(OpCodes.Call.ToInstruction(secretsManagerPlanFeaturesAllowSeatAutoscaleProperty.SetMethod));
                    customSecretsManagerFeaturesCtor.Body.Instructions.Add(OpCodes.Ldarg_0.ToInstruction());
                    customSecretsManagerFeaturesCtor.Body.Instructions.Add(OpCodes.Ldc_I4_1.ToInstruction());
                    customSecretsManagerFeaturesCtor.Body.Instructions.Add(OpCodes.Call.ToInstruction(secretsManagerPlanFeaturesAllowServiceAccountsAutoscaleProperty.SetMethod));
                    customSecretsManagerFeaturesCtor.Body.Instructions.Add(OpCodes.Ret.ToInstruction());
                    customSecretsManagerFeaturesType.Methods.Add(customSecretsManagerFeaturesCtor);
                    customPlanType.NestedTypes.Add(customSecretsManagerFeaturesType);

                    Console.WriteLine("Modify the CustomPlan class constructor");
                    Instruction[] newInstructions = new[]
                    {
                        OpCodes.Ldarg_0.ToInstruction(),
                        OpCodes.Newobj.ToInstruction(customSecretsManagerFeaturesCtor),
                        OpCodes.Call.ToInstruction(planSecretsManagerProperty.SetMethod),
                    };
                    foreach (var instruction in newInstructions.Reverse())
                    {
                        customPlanCtor.Body.Instructions.Insert(0, instruction);
                    }
                    customPlanCtor.Body.MaxStack += 2;
                }
                else
                {
                    Console.WriteLine("WARNING: Cannot modifying CustomPlan");
                }
                // CustomPlan modify end

                ModuleWriterOptions moduleWriterOptions = new(moduleDefMd);
                moduleWriterOptions.MetadataOptions.Flags |= MetadataFlags.KeepOldMaxStack;
                moduleWriterOptions.MetadataOptions.Flags |= MetadataFlags.PreserveAll;
                moduleWriterOptions.MetadataOptions.Flags |= MetadataFlags.PreserveRids;

                moduleDefMd.Write(file + ".new");
                moduleDefMd.Dispose();
                File.Delete(file);
                File.Move(file + ".new", file);
            }

            return 0;
        }
    }
}

blood72 avatar Dec 31 '23 04:12 blood72

PR #182 contains the same changes I made prior to posting this issue and they have been working fine. Though, forcing the SM class into the Custom plan is interesting, I still think its better to opt for keeping BW as-is as much as possible and using the Enterprise plan unless there is a specific reason CustomPlan type was used to begin with.

@raksta01 might want to add the new AllowAdminAccessToAllCollectionItems license option to the PR to save an extra PR later, but its fine either way.

set("AllowAdminAccessToAllCollectionItems", true);

Ayitaka avatar Dec 31 '23 05:12 Ayitaka

PR #182 contains the same changes I made prior to posting this issue and they have been working fine. Though, forcing the SM class into the Custom plan is interesting, I still think its better to opt for keeping BW as-is as much as possible and using the Enterprise plan unless there is a specific reason CustomPlan type was used to begin with.

@raksta01 might want to add the new AllowAdminAccessToAllCollectionItems license option to the PR to save an extra PR later, but its fine either way.

set("AllowAdminAccessToAllCollectionItems", true);

I will add it in to the PR so a new PR doesn't need to be created however I will make it a question asked when generating the license wheter it should be true or false by default will set it to true

raksta01 avatar Dec 31 '23 05:12 raksta01

hi,

run into the same problem. What is the current status of the push request?

ruben-herold avatar Jan 02 '24 09:01 ruben-herold