diff --git a/adsys.pb.go b/adsys.pb.go index 1b84188d6..9a5ddae6e 100644 --- a/adsys.pb.go +++ b/adsys.pb.go @@ -596,7 +596,7 @@ var file_adsys_proto_rawDesc = []byte{ 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x68, 0x61, 0x70, 0x74, 0x65, 0x72, 0x22, 0x22, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x6f, 0x63, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x61, 0x77, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x72, - 0x61, 0x77, 0x32, 0x96, 0x04, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x20, + 0x61, 0x77, 0x32, 0xc9, 0x04, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x03, 0x43, 0x61, 0x74, 0x12, 0x06, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0f, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x24, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x2e, 0x45, 0x6d, @@ -629,9 +629,13 @@ var file_adsys_proto_rawDesc = []byte{ 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x2a, 0x0a, 0x0d, 0x47, 0x50, 0x4f, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x12, 0x06, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0f, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, - 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x42, 0x19, 0x5a, 0x17, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x75, 0x62, 0x75, 0x6e, 0x74, 0x75, - 0x2f, 0x61, 0x64, 0x73, 0x79, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x31, 0x0a, 0x14, 0x43, + 0x65, 0x72, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x53, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x12, 0x06, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0f, 0x2e, 0x53, 0x74, + 0x72, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x42, 0x19, + 0x5a, 0x17, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x75, 0x62, 0x75, + 0x6e, 0x74, 0x75, 0x2f, 0x61, 0x64, 0x73, 0x79, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, } var ( @@ -671,19 +675,21 @@ var file_adsys_proto_depIdxs = []int32{ 9, // 8: service.ListDoc:input_type -> ListDocRequest 1, // 9: service.ListUsers:input_type -> ListUsersRequest 0, // 10: service.GPOListScript:input_type -> Empty - 3, // 11: service.Cat:output_type -> StringResponse - 3, // 12: service.Version:output_type -> StringResponse - 3, // 13: service.Status:output_type -> StringResponse - 0, // 14: service.Stop:output_type -> Empty - 0, // 15: service.UpdatePolicy:output_type -> Empty - 3, // 16: service.DumpPolicies:output_type -> StringResponse - 7, // 17: service.DumpPoliciesDefinitions:output_type -> DumpPolicyDefinitionsResponse - 3, // 18: service.GetDoc:output_type -> StringResponse - 3, // 19: service.ListDoc:output_type -> StringResponse - 3, // 20: service.ListUsers:output_type -> StringResponse - 3, // 21: service.GPOListScript:output_type -> StringResponse - 11, // [11:22] is the sub-list for method output_type - 0, // [0:11] is the sub-list for method input_type + 0, // 11: service.CertAutoEnrollScript:input_type -> Empty + 3, // 12: service.Cat:output_type -> StringResponse + 3, // 13: service.Version:output_type -> StringResponse + 3, // 14: service.Status:output_type -> StringResponse + 0, // 15: service.Stop:output_type -> Empty + 0, // 16: service.UpdatePolicy:output_type -> Empty + 3, // 17: service.DumpPolicies:output_type -> StringResponse + 7, // 18: service.DumpPoliciesDefinitions:output_type -> DumpPolicyDefinitionsResponse + 3, // 19: service.GetDoc:output_type -> StringResponse + 3, // 20: service.ListDoc:output_type -> StringResponse + 3, // 21: service.ListUsers:output_type -> StringResponse + 3, // 22: service.GPOListScript:output_type -> StringResponse + 3, // 23: service.CertAutoEnrollScript:output_type -> StringResponse + 12, // [12:24] is the sub-list for method output_type + 0, // [0:12] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name diff --git a/adsys.proto b/adsys.proto index cc95a16a2..307b28ca5 100644 --- a/adsys.proto +++ b/adsys.proto @@ -14,6 +14,7 @@ service service { rpc ListDoc(ListDocRequest) returns (stream StringResponse); rpc ListUsers(ListUsersRequest) returns (stream StringResponse); rpc GPOListScript(Empty) returns (stream StringResponse); + rpc CertAutoEnrollScript(Empty) returns (stream StringResponse); } message Empty {} diff --git a/adsys_grpc.pb.go b/adsys_grpc.pb.go index 787185740..1bc7b2b16 100644 --- a/adsys_grpc.pb.go +++ b/adsys_grpc.pb.go @@ -30,6 +30,7 @@ const ( Service_ListDoc_FullMethodName = "/service/ListDoc" Service_ListUsers_FullMethodName = "/service/ListUsers" Service_GPOListScript_FullMethodName = "/service/GPOListScript" + Service_CertAutoEnrollScript_FullMethodName = "/service/CertAutoEnrollScript" ) // ServiceClient is the client API for Service service. @@ -47,6 +48,7 @@ type ServiceClient interface { ListDoc(ctx context.Context, in *ListDocRequest, opts ...grpc.CallOption) (Service_ListDocClient, error) ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (Service_ListUsersClient, error) GPOListScript(ctx context.Context, in *Empty, opts ...grpc.CallOption) (Service_GPOListScriptClient, error) + CertAutoEnrollScript(ctx context.Context, in *Empty, opts ...grpc.CallOption) (Service_CertAutoEnrollScriptClient, error) } type serviceClient struct { @@ -409,6 +411,38 @@ func (x *serviceGPOListScriptClient) Recv() (*StringResponse, error) { return m, nil } +func (c *serviceClient) CertAutoEnrollScript(ctx context.Context, in *Empty, opts ...grpc.CallOption) (Service_CertAutoEnrollScriptClient, error) { + stream, err := c.cc.NewStream(ctx, &Service_ServiceDesc.Streams[11], Service_CertAutoEnrollScript_FullMethodName, opts...) + if err != nil { + return nil, err + } + x := &serviceCertAutoEnrollScriptClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type Service_CertAutoEnrollScriptClient interface { + Recv() (*StringResponse, error) + grpc.ClientStream +} + +type serviceCertAutoEnrollScriptClient struct { + grpc.ClientStream +} + +func (x *serviceCertAutoEnrollScriptClient) Recv() (*StringResponse, error) { + m := new(StringResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + // ServiceServer is the server API for Service service. // All implementations must embed UnimplementedServiceServer // for forward compatibility @@ -424,6 +458,7 @@ type ServiceServer interface { ListDoc(*ListDocRequest, Service_ListDocServer) error ListUsers(*ListUsersRequest, Service_ListUsersServer) error GPOListScript(*Empty, Service_GPOListScriptServer) error + CertAutoEnrollScript(*Empty, Service_CertAutoEnrollScriptServer) error mustEmbedUnimplementedServiceServer() } @@ -464,6 +499,9 @@ func (UnimplementedServiceServer) ListUsers(*ListUsersRequest, Service_ListUsers func (UnimplementedServiceServer) GPOListScript(*Empty, Service_GPOListScriptServer) error { return status.Errorf(codes.Unimplemented, "method GPOListScript not implemented") } +func (UnimplementedServiceServer) CertAutoEnrollScript(*Empty, Service_CertAutoEnrollScriptServer) error { + return status.Errorf(codes.Unimplemented, "method CertAutoEnrollScript not implemented") +} func (UnimplementedServiceServer) mustEmbedUnimplementedServiceServer() {} // UnsafeServiceServer may be embedded to opt out of forward compatibility for this service. @@ -708,6 +746,27 @@ func (x *serviceGPOListScriptServer) Send(m *StringResponse) error { return x.ServerStream.SendMsg(m) } +func _Service_CertAutoEnrollScript_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(Empty) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(ServiceServer).CertAutoEnrollScript(m, &serviceCertAutoEnrollScriptServer{stream}) +} + +type Service_CertAutoEnrollScriptServer interface { + Send(*StringResponse) error + grpc.ServerStream +} + +type serviceCertAutoEnrollScriptServer struct { + grpc.ServerStream +} + +func (x *serviceCertAutoEnrollScriptServer) Send(m *StringResponse) error { + return x.ServerStream.SendMsg(m) +} + // Service_ServiceDesc is the grpc.ServiceDesc for Service service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -771,6 +830,11 @@ var Service_ServiceDesc = grpc.ServiceDesc{ Handler: _Service_GPOListScript_Handler, ServerStreams: true, }, + { + StreamName: "CertAutoEnrollScript", + Handler: _Service_CertAutoEnrollScript_Handler, + ServerStreams: true, + }, }, Metadata: "adsys.proto", } diff --git a/cmd/adsysd/client/policy.go b/cmd/adsysd/client/policy.go index 0d90ad3db..db8ef39e4 100644 --- a/cmd/adsysd/client/policy.go +++ b/cmd/adsysd/client/policy.go @@ -86,6 +86,14 @@ func (a *App) installPolicy() { RunE: func(cmd *cobra.Command, args []string) error { return a.dumpGPOListScript() }, } debugCmd.AddCommand(gpoListCmd) + certEnrollCmd := &cobra.Command{ + Use: "cert-autoenroll-script", + Short: gotext.Get("Write certificate autoenrollment python embedded script in current directory"), + Args: cobra.NoArgs, + ValidArgsFunction: cmdhandler.NoValidArgs, + RunE: func(cmd *cobra.Command, args []string) error { return a.dumpCertEnrollScript() }, + } + debugCmd.AddCommand(certEnrollCmd) var updateMachine, updateAll *bool updateCmd := &cobra.Command{ @@ -271,6 +279,26 @@ func (a *App) dumpGPOListScript() error { return os.WriteFile("adsys-gpolist", []byte(script), 0600) } +func (a *App) dumpCertEnrollScript() error { + client, err := adsysservice.NewClient(a.config.Socket, a.getTimeout()) + if err != nil { + return err + } + defer client.Close() + + stream, err := client.CertAutoEnrollScript(a.ctx, &adsys.Empty{}) + if err != nil { + return err + } + + script, err := singleMsg(stream) + if err != nil { + return err + } + + return os.WriteFile("cert-autoenroll", []byte(script), 0600) +} + func colorizePolicies(policies string) (string, error) { first := true var out stringsBuilderWithError diff --git a/cmd/adsysd/integration_tests/adsysctl_policy_test.go b/cmd/adsysd/integration_tests/adsysctl_policy_test.go index f2b2fe958..aa5908006 100644 --- a/cmd/adsysd/integration_tests/adsysctl_policy_test.go +++ b/cmd/adsysd/integration_tests/adsysctl_policy_test.go @@ -1088,26 +1088,33 @@ func TestPolicyUpdate(t *testing.T) { } } -func TestPolicyDebugGPOListScript(t *testing.T) { - gpolistSrc, err := os.ReadFile(filepath.Join(rootProjectDir, "internal/ad/adsys-gpolist")) - require.NoError(t, err, "Setup: failed to load source of adsys-gpolist") - +func TestPolicyDebugScriptDump(t *testing.T) { tests := map[string]struct { + script string + cmdName string + path string + systemAnswer string daemonNotStarted bool wantErr bool }{ - "Get adsys-gpolist script": {systemAnswer: "polkit_yes"}, - "Version is always authorized": {systemAnswer: "polkit_no"}, + "Get adsys-gpolist script": {script: "adsys-gpolist", cmdName: "gpolist-script", path: "internal/ad", systemAnswer: "polkit_yes"}, + "Get cert-autoenroll script": {script: "cert-autoenroll", cmdName: "cert-autoenroll-script", path: "internal/policies/certificate", systemAnswer: "polkit_yes"}, + "adsys-gpolist is always authorized": {script: "adsys-gpolist", cmdName: "gpolist-script", path: "internal/ad", systemAnswer: "polkit_no"}, + "cert-autoenroll is always authorized": {script: "cert-autoenroll", cmdName: "cert-autoenroll-script", path: "internal/policies/certificate", systemAnswer: "polkit_no"}, - "Error on daemon not responding": {daemonNotStarted: true, wantErr: true}, + "Error on daemon not responding for adsys-gpolist": {script: "adsys-gpolist", cmdName: "gpolist-script", path: "internal/ad", daemonNotStarted: true, wantErr: true}, + "Error on daemon not responding for cert-autoenroll": {script: "cert-autoenroll", cmdName: "cert-autoenroll-script", path: "internal/policies/certificate", daemonNotStarted: true, wantErr: true}, } for name, tc := range tests { tc := tc t.Run(name, func(t *testing.T) { dbusAnswer(t, tc.systemAnswer) + scriptSrc, err := os.ReadFile(filepath.Join(rootProjectDir, tc.path, tc.script)) + require.NoError(t, err, "Setup: failed to load source of %s script", tc.script) + conf := createConf(t) if !tc.daemonNotStarted { defer runDaemon(t, conf)() @@ -1115,21 +1122,21 @@ func TestPolicyDebugGPOListScript(t *testing.T) { testutils.Chdir(t, os.TempDir()) - _, err := runClient(t, conf, "policy", "debug", "gpolist-script") + _, err = runClient(t, conf, "policy", "debug", tc.cmdName) if tc.wantErr { require.Error(t, err, "client should exit with an error") return } - f, err := os.Stat("adsys-gpolist") - require.NoError(t, err, "gpo list script should exists") + f, err := os.Stat(tc.script) + require.NoError(t, err, "%s script should exists", tc.script) require.NotEqual(t, 0, f.Mode()&0111, "Script should be executable") - got, err := os.ReadFile("adsys-gpolist") - require.NoError(t, err, "gpo list script is not readable") + got, err := os.ReadFile(tc.script) + require.NoError(t, err, "%s script is not readable", tc.script) - require.Equal(t, string(gpolistSrc), string(got), "Script content should match source") + require.Equal(t, string(scriptSrc), string(got), "Script content should match source") }) } } diff --git a/cmd/adsysd/integration_tests/testdata/TestServiceStatus/golden/ubuntu_pro_subscription_is_not_active b/cmd/adsysd/integration_tests/testdata/TestServiceStatus/golden/ubuntu_pro_subscription_is_not_active index e522b2cc7..747e1fae3 100644 --- a/cmd/adsysd/integration_tests/testdata/TestServiceStatus/golden/ubuntu_pro_subscription_is_not_active +++ b/cmd/adsysd/integration_tests/testdata/TestServiceStatus/golden/ubuntu_pro_subscription_is_not_active @@ -6,6 +6,7 @@ Next Refresh: Tue May 25 14:55 Ubuntu Pro subscription is not active on this machine. Rules belonging to the following policy types will not be applied: - apparmor + - certificate - mount - privilege - proxy diff --git a/internal/ad/ad.go b/internal/ad/ad.go index 9f55fa18b..e8b21e9bc 100644 --- a/internal/ad/ad.go +++ b/internal/ad/ad.go @@ -41,6 +41,13 @@ const ( UserObject ObjectClass = "user" // ComputerObject is a computer representation in AD. ComputerObject ObjectClass = "computer" + + // certAutoEnrollKey is the GPO entry that configures certificate autoenrollment. + certAutoEnrollKey string = "Software/Policies/Microsoft/Cryptography/AutoEnrollment/AEPolicy" + + // policyServerPrefix is the GPO prefix containing keys that configure + // policy servers for certificate enrollment. + policyServersPrefix string = "Software/Policies/Microsoft/Cryptography/PolicyServers/" ) type gpo downloadable @@ -522,6 +529,16 @@ func (ad *AD) parseGPOs(ctx context.Context, gpos []gpo, objectClass ObjectClass var currentKey string var overrideEnabled bool for _, pol := range pols { + // Rewrite the certificate autoenrollment key so we can easily + // use it in the policy manager + if pol.Key == certAutoEnrollKey { + pol.Key = fmt.Sprintf("%scertificate/autoenroll/all", keyFilterPrefix) + } + + if strings.HasPrefix(pol.Key, policyServersPrefix) { + pol.Key = fmt.Sprintf("%scertificate/%s/all", keyFilterPrefix, pol.Key) + } + // Only consider supported policies for this distro if !strings.HasPrefix(pol.Key, keyFilterPrefix) { continue diff --git a/internal/ad/ad_test.go b/internal/ad/ad_test.go index fb34f3379..2d0a07a6b 100644 --- a/internal/ad/ad_test.go +++ b/internal/ad/ad_test.go @@ -396,6 +396,24 @@ func TestGetPolicies(t *testing.T) { }}}, }}, }, + "Include non Ubuntu keys used to configure certificate autoenrollment": { + objectName: hostname, + objectClass: ad.ComputerObject, + gpoListArgs: []string{"gpoonly.com", hostname + ":filtered-with-certificate-autoenrollment"}, + want: policies.Policies{GPOs: []policies.GPO{ + {ID: "filtered-with-certificate-autoenrollment", Name: "filtered-with-certificate-autoenrollment-name", Rules: map[string][]entry.Entry{ + "certificate": { + {Key: "autoenroll", Value: "1"}, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/Flags", Value: "0"}, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/37c9dc30f207f27f61a2f7c3aed598a6e2920b54/URL", Value: "LDAP:"}, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/37c9dc30f207f27f61a2f7c3aed598a6e2920b54/PolicyID", Value: "{A5E9BF57-71C6-443A-B7FC-79EFA6F73EBD}"}, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/37c9dc30f207f27f61a2f7c3aed598a6e2920b54/FriendlyName", Value: "Active Directory Enrollment Policy"}, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/37c9dc30f207f27f61a2f7c3aed598a6e2920b54/Flags", Value: "20"}, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/37c9dc30f207f27f61a2f7c3aed598a6e2920b54/AuthFlags", Value: "2"}, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/37c9dc30f207f27f61a2f7c3aed598a6e2920b54/Cost", Value: "2147483645"}, + }}}, + }}, + }, "Ignore errors on non Ubuntu keys": { gpoListArgs: []string{"gpoonly.com", "bob:unsupported-with-errors"}, want: policies.Policies{GPOs: []policies.GPO{ diff --git a/internal/ad/testdata/AD/SYSVOL/gpoonly.com/Policies/filtered-with-certificate-autoenrollment/GPT.INI b/internal/ad/testdata/AD/SYSVOL/gpoonly.com/Policies/filtered-with-certificate-autoenrollment/GPT.INI new file mode 100644 index 000000000..c5498a917 --- /dev/null +++ b/internal/ad/testdata/AD/SYSVOL/gpoonly.com/Policies/filtered-with-certificate-autoenrollment/GPT.INI @@ -0,0 +1,3 @@ +[General] +Version=1000 +displayName=New Group Policy Object diff --git a/internal/ad/testdata/AD/SYSVOL/gpoonly.com/Policies/filtered-with-certificate-autoenrollment/Machine/Registry.pol b/internal/ad/testdata/AD/SYSVOL/gpoonly.com/Policies/filtered-with-certificate-autoenrollment/Machine/Registry.pol new file mode 100644 index 000000000..94c6f4bb9 Binary files /dev/null and b/internal/ad/testdata/AD/SYSVOL/gpoonly.com/Policies/filtered-with-certificate-autoenrollment/Machine/Registry.pol differ diff --git a/internal/adsysservice/adsysservice.go b/internal/adsysservice/adsysservice.go index 34f95f6a3..76926ac4c 100644 --- a/internal/adsysservice/adsysservice.go +++ b/internal/adsysservice/adsysservice.go @@ -282,7 +282,7 @@ func New(ctx context.Context, opts ...option) (s *Service, err error) { if args.systemUnitDir != "" { policyOptions = append(policyOptions, policies.WithSystemUnitDir(args.systemUnitDir)) } - m, err := policies.NewManager(bus, hostname, policyOptions...) + m, err := policies.NewManager(bus, hostname, adBackend, policyOptions...) if err != nil { return nil, err } diff --git a/internal/adsysservice/policy.go b/internal/adsysservice/policy.go index ab498e398..f30e0ca46 100644 --- a/internal/adsysservice/policy.go +++ b/internal/adsysservice/policy.go @@ -11,6 +11,7 @@ import ( log "github.com/ubuntu/adsys/internal/grpc/logstreamer" "github.com/ubuntu/adsys/internal/i18n" "github.com/ubuntu/adsys/internal/policies" + "github.com/ubuntu/adsys/internal/policies/certificate" "github.com/ubuntu/decorate" "golang.org/x/sync/errgroup" ) @@ -156,4 +157,21 @@ func (s *Service) GPOListScript(_ *adsys.Empty, stream adsys.Service_GPOListScri return nil } +// CertAutoEnrollScript returns the embedded certificate autoenrollment python script. +func (s *Service) CertAutoEnrollScript(_ *adsys.Empty, stream adsys.Service_CertAutoEnrollScriptServer) (err error) { + defer decorate.OnError(&err, i18n.G("error while getting certificate autoenrollment script")) + + if err := s.authorizer.IsAllowedFromContext(stream.Context(), authorizer.ActionAlwaysAllowed); err != nil { + return err + } + + if err := stream.Send(&adsys.StringResponse{ + Msg: certificate.CertEnrollCode, + }); err != nil { + log.Warningf(stream.Context(), "couldn't send certificate autoenrollment script to client: %v", err) + } + + return nil +} + // FIXME: check cache file permission diff --git a/internal/consts/consts.go b/internal/consts/consts.go index c44f66d56..e53cb0017 100644 --- a/internal/consts/consts.go +++ b/internal/consts/consts.go @@ -21,9 +21,15 @@ const ( // DefaultCacheDir is the default path for adsys system cache directory. DefaultCacheDir = "/var/cache/adsys" + // DefaultStateDir is the default path for adsys system state directory. + DefaultStateDir = "/var/lib/adsys" + // DefaultRunDir is the default path for adsys run directory. DefaultRunDir = "/run/adsys" + // DefaultShareDir is the default path for adsys share directory. + DefaultShareDir = "/usr/share/adsys" + // DefaultClientTimeout is the maximum default time in seconds between 2 server activities before the client returns and abort the request. DefaultClientTimeout = 30 diff --git a/internal/policies/certificate/cert-autoenroll b/internal/policies/certificate/cert-autoenroll new file mode 100755 index 000000000..4c2429a6d --- /dev/null +++ b/internal/policies/certificate/cert-autoenroll @@ -0,0 +1,131 @@ +#!/usr/bin/python3 + +import argparse +import json +import os +import sys +import tempfile + +from samba import param +from samba.credentials import MUST_USE_KERBEROS, Credentials +from samba.dcerpc import preg + +from vendor_samba.gp.gpclass import GPOStorage +from vendor_samba.gp import gp_cert_auto_enroll_ext as cae + +class adsys_cert_auto_enroll(cae.gp_cert_auto_enroll_ext): + def enroll(self, guid, entries, trust_dir, private_dir, global_trust_dir): + self._gp_cert_auto_enroll_ext__enroll(guid, entries, trust_dir, private_dir, global_trust_dir) + + def unenroll(self, guid): + ca_attrs = self.cache_get_all_attribute_values(guid) + self.clean(guid, remove=list(ca_attrs.keys())) + +def smb_config(realm, enable_debug): + config = "[global]\nrealm = %s\n" % realm + if enable_debug: + config += "log level = 10\n" + return config + +def main(): + parser = argparse.ArgumentParser(description='Certificate autoenrollment via Samba') + parser.add_argument('action', type=str, + help='Action to perform (one of: enroll, unenroll)', + choices=['enroll', 'unenroll']) + parser.add_argument('object_name', type=str, + help='The computer name to enroll/unenroll, e.g. keypress') + parser.add_argument('realm', type=str, + help='The realm of the domain, e.g. example.com') + + parser.add_argument('--policy_servers_json', type=str, + help='GPO entries for advanced configuration of the policy servers. \ + Must be in JSON format.') + parser.add_argument('--samba_cache_dir', type=str, + default='/var/lib/adsys/samba', + help='Directory to store GPO Samba cache in.') + parser.add_argument('--private_dir', type=str, + default='/var/lib/adsys/private/certs', + help='Directory to store private keys in.') + parser.add_argument('--trust_dir', type=str, + default='/var/lib/adsys/certs', + help='Directory to store trusted certificates in.') + parser.add_argument('--global_trust_dir', type=str, + default='/usr/local/share/ca-certificates', + help='Directory to symlink root CA certificates to.') + parser.add_argument('--debug', action='store_true', + help='Enable samba debug output.') + + args = parser.parse_args() + + samba_cache_dir = args.samba_cache_dir + trust_dir = args.trust_dir + private_dir = args.private_dir + global_trust_dir = args.global_trust_dir + + # Create needed directories if they don't exist + for directory in [samba_cache_dir, trust_dir, private_dir, global_trust_dir]: + if not os.path.exists(directory): + perms = 0o700 if directory == private_dir else 0o755 + os.makedirs(directory, mode=perms) + + with tempfile.NamedTemporaryFile(prefix='smb_conf') as smb_conf: + smb_conf.write(smb_config(args.realm, args.debug).encode('utf-8')) + smb_conf.flush() + + lp = param.LoadParm(smb_conf.name) + c = Credentials() + c.set_kerberos_state(MUST_USE_KERBEROS) + c.guess(lp) + username = c.get_username() + store = GPOStorage(os.path.join(samba_cache_dir, f'cert_gpo_state_{args.object_name}.tdb')) + + ext = adsys_cert_auto_enroll(lp, c, username, store) + guid = f'adsys-cert-autoenroll-{args.object_name}' + if args.action == 'enroll': + entries = gpo_entries(args.policy_servers_json) + ext.enroll(guid, entries, trust_dir, private_dir, global_trust_dir) + else: + ext.unenroll(guid) + os.removedirs(samba_cache_dir) + +def gpo_entries(entries_json): + """ + Convert JSON string to list of GPO entries + + JSON must be an array of objects with the following keys: + keyname (str): Registry key name + valuename (str): Registry value name + type (int): Registry value type + data (any): Registry value data + + Parameters: + entries_json (str): JSON string of GPO entries + Returns: + list: List of GPO entries, or empty list if entries_json is empty + """ + + if not entries_json: + return [] + + entries_dict = json.loads(entries_json) + if not entries_dict: + return [] + + entries = [] + for entry in entries_dict: + try: + e = preg.entry() + e.keyname = entry['keyname'] + e.valuename = entry['valuename'] + e.type = entry['type'] + e.data = entry['data'] + entries.append(e) + except KeyError as exc: + raise ValueError(f'Could not find key {exc} in GPO entry') from exc + except TypeError as exc: + raise ValueError(f'GPO data must be a JSON array of objects') from exc + return entries + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/internal/policies/certificate/cert-autoenroll_test.go b/internal/policies/certificate/cert-autoenroll_test.go new file mode 100644 index 000000000..8f7bbc94a --- /dev/null +++ b/internal/policies/certificate/cert-autoenroll_test.go @@ -0,0 +1,143 @@ +package certificate_test + +import ( + "bytes" + "encoding/json" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "github.com/ubuntu/adsys/internal/testutils" + "golang.org/x/exp/slices" +) + +const advancedConfigurationJSON = `[ + { + "keyname": "Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54", + "valuename": "AuthFlags", + "data": 2, + "type": 4 + }, + { + "keyname": "Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54", + "valuename": "Cost", + "data": 2147483645, + "type": 4 + }, + { + "keyname": "Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54", + "valuename": "Flags", + "data": 20, + "type": 4 + }, + { + "keyname": "Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54", + "valuename": "FriendlyName", + "data": "ActiveDirectoryEnrollmentPolicy", + "type": 1 + }, + { + "keyname": "Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54", + "valuename": "PolicyID", + "data": "{A5E9BF57-71C6-443A-B7FC-79EFA6F73EBD}", + "type": 1 + }, + { + "keyname": "Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54", + "valuename": "URL", + "data": "LDAP:", + "type": 1 + }, + { + "keyname": "Software\\Policies\\Microsoft\\Cryptography\\PolicyServers", + "valuename": "Flags", + "data": 0, + "type": 4 + } +]` + +func TestCertAutoenrollScript(t *testing.T) { + coverageOn := testutils.PythonCoverageToGoFormat(t, "cert-autoenroll", false) + certAutoenrollCmd := "./cert-autoenroll" + if coverageOn { + certAutoenrollCmd = "cert-autoenroll" + } + + compactedJSON := &bytes.Buffer{} + err := json.Compact(compactedJSON, []byte(advancedConfigurationJSON)) + require.NoError(t, err, "Failed to compact JSON") + + // Setup samba mock + pythonPath, err := filepath.Abs("../../testutils/admock") + require.NoError(t, err, "Setup: Failed to get current absolute path for mock") + + tests := map[string]struct { + args []string + + readOnlyPath bool + autoenrollError bool + + wantErr bool + }{ + "Enroll with simple configuration": {args: []string{"enroll", "keypress", "example.com"}}, + "Enroll with simple configuration and debug enabled": {args: []string{"enroll", "keypress", "example.com", "--debug"}}, + "Enroll with empty advanced configuration": {args: []string{"enroll", "keypress", "example.com", "--policy_servers_json", "null"}}, + "Enroll with valid advanced configuration": {args: []string{"enroll", "keypress", "example.com", "--policy_servers_json", compactedJSON.String()}}, + + "Unenroll": {args: []string{"unenroll", "keypress", "example.com"}}, + + // Error cases + "Error on missing arguments": {args: []string{"enroll"}, wantErr: true}, + "Error on invalid flags": {args: []string{"enroll", "keypress", "example.com", "--invalid_flag"}, wantErr: true}, + "Error on invalid JSON": {args: []string{"enroll", "keypress", "example.com", "--policy_servers_json", "invalid_json"}, wantErr: true}, + "Error on invalid JSON keys": { + args: []string{"enroll", "keypress", "example.com", "--policy_servers_json", `[{"key":"Software\\Policies\\Microsoft","value":"MyValue"}]`}, wantErr: true}, + "Error on invalid JSON structure": { + args: []string{"enroll", "keypress", "example.com", "--policy_servers_json", `{"key":"Software\\Policies\\Microsoft","value":"MyValue"}`}, wantErr: true}, + "Error on read-only path": {readOnlyPath: true, args: []string{"enroll", "keypress", "example.com"}, wantErr: true}, + "Error on enroll failure": {autoenrollError: true, args: []string{"enroll", "keypress", "example.com"}, wantErr: true}, + "Error on unenroll failure": {autoenrollError: true, args: []string{"unenroll", "keypress", "example.com"}, wantErr: true}, + } + + for name, tc := range tests { + tc := tc + name := name + t.Run(name, func(t *testing.T) { + tmpdir := t.TempDir() + stateDir := filepath.Join(tmpdir, "state") + privateDir := filepath.Join(tmpdir, "private") + trustDir := filepath.Join(tmpdir, "trust") + globalTrustDir := filepath.Join(tmpdir, "ca-certificates") + + if tc.readOnlyPath { + testutils.MakeReadOnly(t, tmpdir) + } + + args := append(tc.args, "--samba_cache_dir", stateDir, "--private_dir", privateDir, "--trust_dir", trustDir, "--global_trust_dir", globalTrustDir) + + // #nosec G204: we control the command line name and only change it for tests + cmd := exec.Command(certAutoenrollCmd, args...) + cmd.Env = append(os.Environ(), "PYTHONPATH="+pythonPath) + if tc.autoenrollError { + cmd.Env = append(os.Environ(), "ADSYS_WANT_AUTOENROLL_ERROR=1") + } + out, err := cmd.CombinedOutput() + if tc.wantErr { + require.Error(t, err, "cert-autoenroll should have failed but didn’t") + return + } + require.NoErrorf(t, err, "cert-autoenroll should have exited successfully: %s", string(out)) + + got := strings.ReplaceAll(string(out), tmpdir, "#TMPDIR#") + want := testutils.LoadWithUpdateFromGolden(t, got) + require.Equal(t, want, got, "Unexpected output from cert-autoenroll script") + + if slices.Contains(tc.args, "unenroll") { + require.NoDirExists(t, filepath.Join(stateDir, "samba"), "Samba cache directory should have been removed on unenroll") + } + }) + } +} diff --git a/internal/policies/certificate/certificate.go b/internal/policies/certificate/certificate.go new file mode 100644 index 000000000..27241509e --- /dev/null +++ b/internal/policies/certificate/certificate.go @@ -0,0 +1,271 @@ +// Package certificate provides a manager that handles certificate +// autoenrollment. +// +// This manager only applies to computer objects. +// +// Provided that the AD backend is online and AD CS is set up, the manager will +// parse the relevant GPOs and delegate to an external Python script that will +// request Samba to enroll or un-enroll the machine for certificates. +// +// If the GPO is disabled/not configured, the policy manager will attempt to +// unenroll the machine only if traces of Samba cache are found on the disk. +// If the enroll flag is unchecked, the machine will be unenrolled, namely the +// certificates will be removed and monitoring will stop. +// If any errors occur during the enrollment process, the manager will log them +// prior to failing. +package certificate + +import ( + "bytes" + "context" + _ "embed" // embed cert enroll python script + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "sync" + "time" + + "github.com/ubuntu/adsys/internal/consts" + log "github.com/ubuntu/adsys/internal/grpc/logstreamer" + "github.com/ubuntu/adsys/internal/i18n" + "github.com/ubuntu/adsys/internal/policies/entry" + "github.com/ubuntu/adsys/internal/smbsafe" + "github.com/ubuntu/decorate" + "golang.org/x/exp/slices" +) + +// Manager prevents running multiple Python scripts in parallel while parsing +// the policy in ApplyPolicy. +type Manager struct { + domain string + sambaCacheDir string + krb5CacheDir string + vendorPythonDir string + certEnrollCmd []string + + mu sync.Mutex // Prevents multiple instances of the certificate manager from running in parallel +} + +// gpoEntry is a single GPO registry entry to be serialised to JSON in a format +// Samba expects. +type gpoEntry struct { + KeyName string `json:"keyname"` + ValueName string `json:"valuename"` + Data any `json:"data"` + Type int `json:"type"` +} + +// integerGPOValues is a list of GPO registry values that contain integer data. +var integerGPOValues = []string{"AuthFlags", "Cost", "Flags"} + +const ( + gpoTypeString int = 1 // REG_SZ + gpoTypeInteger int = 4 // REG_DWORD + + // See [MS-CAESO] 4.4.5.1. + enrollFlag int = 0x1 + disabledFlag int = 0x8000 +) + +// CertEnrollCode is the embedded Python script which requests +// Samba to autoenroll for certificates using the given GPOs. +// +//go:embed cert-autoenroll +var CertEnrollCode string + +type options struct { + stateDir string + runDir string + shareDir string + certAutoenrollCmd []string +} + +// Option reprents an optional function to change the certificate manager. +type Option func(*options) + +// WithStateDir overrides the default state directory. +func WithStateDir(p string) func(*options) { + return func(a *options) { + a.stateDir = p + } +} + +// WithRunDir overrides the default run directory. +func WithRunDir(p string) func(*options) { + return func(a *options) { + a.runDir = p + } +} + +// WithShareDir overrides the default share directory. +func WithShareDir(p string) func(*options) { + return func(a *options) { + a.shareDir = p + } +} + +// WithCertAutoenrollCmd overrides the default certificate autoenroll command. +func WithCertAutoenrollCmd(cmd []string) func(*options) { + return func(a *options) { + a.certAutoenrollCmd = cmd + } +} + +// New returns a new manager for the certificate policy. +func New(domain string, opts ...Option) *Manager { + // defaults + args := options{ + stateDir: consts.DefaultStateDir, + runDir: consts.DefaultRunDir, + shareDir: consts.DefaultShareDir, + certAutoenrollCmd: []string{"python3", "-c", CertEnrollCode}, + } + // applied options + for _, o := range opts { + o(&args) + } + + return &Manager{ + domain: domain, + sambaCacheDir: filepath.Join(args.stateDir, "samba"), + krb5CacheDir: filepath.Join(args.runDir, "krb5cc"), + vendorPythonDir: filepath.Join(args.shareDir, "python"), + certEnrollCmd: args.certAutoenrollCmd, + } +} + +// ApplyPolicy runs the certificate autoenrollment script to enroll or un-enroll the machine. +func (m *Manager) ApplyPolicy(ctx context.Context, objectName string, isComputer, isOnline bool, entries []entry.Entry) (err error) { + defer decorate.OnError(&err, i18n.G("can't apply certificate policy")) + + m.mu.Lock() + defer m.mu.Unlock() + + if !isComputer { + log.Debug(ctx, "Certificate policy is only supported for computers, skipping...") + return nil + } + + if !isOnline { + log.Debug(ctx, i18n.G("AD backend is offline, skipping certificate policy")) + return nil + } + + idx := slices.IndexFunc(entries, func(e entry.Entry) bool { return e.Key == "autoenroll" }) + if idx == -1 { + // If the Samba cache directory doesn't exist, we don't have anything to unenroll + if _, err := os.Stat(m.sambaCacheDir); err != nil && os.IsNotExist(err) { + return nil + } + + log.Debug(ctx, "Certificate autoenrollment is not configured, unenrolling machine") + if err := m.runScript(ctx, "unenroll", objectName, "--samba_cache_dir", m.sambaCacheDir); err != nil { + return err + } + + return nil + } + + log.Debug(ctx, "ApplyPolicy certificate policy") + + entry := entries[idx] + value, err := strconv.Atoi(entry.Value) + if err != nil { + return fmt.Errorf(i18n.G("failed to parse certificate policy entry value: %w"), err) + } + + if value&disabledFlag == disabledFlag { + log.Debug(ctx, "Certificate policy is disabled, skipping...") + return nil + } + + var polSrvRegistryEntries []gpoEntry + for _, entry := range entries { + // We already handled the autoenroll entry + if entry.Key == "autoenroll" { + continue + } + + // Samba expects the key parts to be joined by backslashes + keyparts := strings.Split(entry.Key, "/") + keyname := strings.Join(keyparts[:len(keyparts)-1], `\`) + valuename := keyparts[len(keyparts)-1] + gpoData, err := gpoData(entry.Value, valuename) + if err != nil { + return fmt.Errorf(i18n.G("failed to parse policy entry value: %w"), err) + } + polSrvRegistryEntries = append(polSrvRegistryEntries, gpoEntry{keyname, valuename, gpoData, gpoType(valuename)}) + + log.Debugf(ctx, "Certificate policy entry: %#v", entry) + } + + var action string + log.Debugf(ctx, "Certificate policy value: %d", value) + action = "unenroll" + if value&enrollFlag == enrollFlag { + action = "enroll" + } + + jsonGPOData, err := json.Marshal(polSrvRegistryEntries) + if err != nil { + return fmt.Errorf(i18n.G("failed to marshal policy server registry entries: %v"), err) + } + + if err := m.runScript(ctx, action, objectName, + "--policy_servers_json", string(jsonGPOData), + "--samba_cache_dir", m.sambaCacheDir, + ); err != nil { + return err + } + + return nil +} + +// runScript runs the certificate autoenrollment script with the given arguments. +func (m *Manager) runScript(ctx context.Context, action, objectName string, extraArgs ...string) error { + scriptArgs := []string{action, objectName, m.domain} + scriptArgs = append(scriptArgs, extraArgs...) + cmdArgs := append(m.certEnrollCmd, scriptArgs...) + cmdCtx, cancel := context.WithTimeout(ctx, time.Second*10) + defer cancel() + log.Debugf(ctx, "Running cert autoenroll script with arguments: %q", strings.Join(scriptArgs, " ")) + // #nosec G204 - cmdArgs is under our control (python embedded script or mock for tests) + cmd := exec.CommandContext(cmdCtx, cmdArgs[0], cmdArgs[1:]...) + cmd.Env = append(os.Environ(), + fmt.Sprintf("KRB5CCNAME=%s", filepath.Join(m.krb5CacheDir, objectName)), + fmt.Sprintf("PYTHONPATH=%s", m.vendorPythonDir), + ) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + smbsafe.WaitExec() + defer smbsafe.DoneExec() + + if err := cmd.Run(); err != nil { + return fmt.Errorf(i18n.G("failed to run certificate autoenrollment script (exited with %d): %v\n%s"), cmd.ProcessState.ExitCode(), err, stderr.String()) + } + log.Infof(ctx, i18n.G("Certificate autoenrollment script ran successfully\n%s"), stdout.String()) + return nil +} + +// gpoData returns the data for a GPO entry. +func gpoData(data, value string) (any, error) { + if slices.Contains(integerGPOValues, value) { + return strconv.Atoi(data) + } + + return data, nil +} + +// gpoType returns the type for a GPO entry. +func gpoType(value string) int { + if slices.Contains(integerGPOValues, value) { + return gpoTypeInteger + } + + return gpoTypeString +} diff --git a/internal/policies/certificate/certificate_test.go b/internal/policies/certificate/certificate_test.go new file mode 100644 index 000000000..872a3efd2 --- /dev/null +++ b/internal/policies/certificate/certificate_test.go @@ -0,0 +1,171 @@ +package certificate_test + +import ( + "context" + "flag" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "github.com/ubuntu/adsys/internal/policies/certificate" + "github.com/ubuntu/adsys/internal/policies/entry" + "github.com/ubuntu/adsys/internal/testutils" +) + +const ( + enrollValue = "7" // string representation of 0b111 + unenrollValue = "6" // string representation of 0b110 + disabledValue = "32768" // string representation of 0x8000 +) + +var enrollEntry = entry.Entry{Key: "autoenroll", Value: enrollValue} +var advancedConfigurationEntries = []entry.Entry{ + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/37c9dc30f207f27f61a2f7c3aed598a6e2920b54/AuthFlags", Value: "2"}, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/37c9dc30f207f27f61a2f7c3aed598a6e2920b54/Cost", Value: "2147483645"}, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/37c9dc30f207f27f61a2f7c3aed598a6e2920b54/Flags", Value: "20"}, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/37c9dc30f207f27f61a2f7c3aed598a6e2920b54/FriendlyName", Value: "ActiveDirectoryEnrollmentPolicy"}, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/37c9dc30f207f27f61a2f7c3aed598a6e2920b54/PolicyID", Value: "{A5E9BF57-71C6-443A-B7FC-79EFA6F73EBD}"}, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/37c9dc30f207f27f61a2f7c3aed598a6e2920b54/URL", Value: "LDAP:"}, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/Flags", Value: "0"}, +} + +func TestPolicyApply(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + entries []entry.Entry + + isUser bool + isOffline bool + + autoenrollScriptError bool + runScript bool + sambaDirExists bool + + wantErr bool + }{ + // No-op cases + "Computer, no entries": {}, + "Computer, autoenroll disabled": {entries: []entry.Entry{{Key: "autoenroll", Value: disabledValue}}}, + "Computer, domain is offline": {entries: []entry.Entry{enrollEntry}, isOffline: true}, + + // Enroll cases + "Computer, configured to enroll": {entries: []entry.Entry{enrollEntry}, runScript: true}, + "Computer, configured to enroll, advanced configuration": {entries: append(advancedConfigurationEntries, enrollEntry), runScript: true}, + + // Unenroll cases + "Computer, configured to unenroll": {entries: []entry.Entry{{Key: "autoenroll", Value: unenrollValue}}, runScript: true}, + "Computer, no entries, Samba cache present": {sambaDirExists: true, runScript: true}, + + "User, autoenroll not supported": {isUser: true, entries: []entry.Entry{enrollEntry}}, + + // Error cases + "Error on autoenroll script failure": {autoenrollScriptError: true, entries: []entry.Entry{enrollEntry}, wantErr: true}, + "Error on invalid autoenroll value": {entries: []entry.Entry{{Key: "autoenroll", Value: "notanumber"}}, wantErr: true}, + "Error on invalid advanced configuration value": { + entries: []entry.Entry{ + enrollEntry, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/37c9dc30f207f27f61a2f7c3aed598a6e2920b54/Flags", Value: "NotANumber"}, + }, wantErr: true}, + } + + for name, tc := range tests { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + + tmpdir := t.TempDir() + sambaCacheDir := filepath.Join(tmpdir, "statedir", "samba") + if tc.sambaDirExists { + require.NoError(t, os.MkdirAll(sambaCacheDir, 0750), "Setup: Samba cache dir should be created") + } + + autoenrollCmdOutputFile := filepath.Join(tmpdir, "autoenroll-output") + autoenrollCmd := mockAutoenrollScript(t, autoenrollCmdOutputFile, tc.autoenrollScriptError) + + m := certificate.New( + "example.com", + certificate.WithStateDir(filepath.Join(tmpdir, "statedir")), + certificate.WithRunDir(filepath.Join(tmpdir, "rundir")), + certificate.WithShareDir(filepath.Join(tmpdir, "sharedir")), + certificate.WithCertAutoenrollCmd(autoenrollCmd), + ) + + err := m.ApplyPolicy(context.Background(), "keypress", !tc.isUser, !tc.isOffline, tc.entries) + if tc.wantErr { + require.Error(t, err, "ApplyPolicy should fail") + return + } + require.NoError(t, err, "ApplyPolicy should succeed") + + // Check that the autoenroll script was called with the expected arguments + // and that the output file was created + if !tc.runScript { + return + } + + got, err := os.ReadFile(autoenrollCmdOutputFile) + require.NoError(t, err, "Setup: Autoenroll mock output should be readable") + + want := testutils.LoadWithUpdateFromGolden(t, string(got)) + require.Equal(t, want, string(got), "Unexpected output from autoenroll mock") + }) + } +} + +func mockAutoenrollScript(t *testing.T, scriptOutputFile string, autoenrollScriptError bool) []string { + t.Helper() + + cmdArgs := []string{"env", "GO_WANT_HELPER_PROCESS=1", os.Args[0], "-test.run=TestMockAutoenrollScript", "--", scriptOutputFile} + if autoenrollScriptError { + cmdArgs = append(cmdArgs, "-Exit1-") + } + + return cmdArgs +} + +func TestMockAutoenrollScript(t *testing.T) { + if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { + return + } + defer os.Exit(0) + + var outputFile string + + args := os.Args + for len(args) > 0 { + if args[0] == "--" { + outputFile = args[1] + args = args[2:] + break + } + args = args[1:] + } + + if args[0] == "-Exit1-" { + fmt.Fprintf(os.Stderr, "EXIT 1 requested in mock") + os.Exit(1) + } + + dataToWrite := strings.Join(args, " ") + "\n" + dataToWrite += "KRB5CCNAME=" + os.Getenv("KRB5CCNAME") + "\n" + dataToWrite += "PYTHONPATH=" + os.Getenv("PYTHONPATH") + "\n" + + // Replace tmpdir with a placeholder to avoid non-deterministic test failures + tmpdir := filepath.Dir(outputFile) + dataToWrite = strings.ReplaceAll(dataToWrite, tmpdir, "#TMPDIR#") + + err := os.WriteFile(outputFile, []byte(dataToWrite), 0600) + require.NoError(t, err, "Setup: Can't write script args to output file") +} + +func TestMain(m *testing.M) { + testutils.InstallUpdateFlag() + flag.Parse() + + m.Run() + testutils.MergeCoverages() +} diff --git a/internal/policies/certificate/python/vendor_samba/gp/gp_cert_auto_enroll_ext.py b/internal/policies/certificate/python/vendor_samba/gp/gp_cert_auto_enroll_ext.py new file mode 100644 index 000000000..54be3bc28 --- /dev/null +++ b/internal/policies/certificate/python/vendor_samba/gp/gp_cert_auto_enroll_ext.py @@ -0,0 +1,536 @@ +# gp_cert_auto_enroll_ext samba group policy +# Copyright (C) David Mulder 2021 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import operator +import requests +from vendor_samba.gp.gpclass import gp_pol_ext, gp_applier, GPOSTATE +from samba import Ldb +from ldb import SCOPE_SUBTREE, SCOPE_BASE +from samba.auth import system_session +from vendor_samba.gp.gpclass import get_dc_hostname +import base64 +from shutil import which +from subprocess import Popen, PIPE +import re +import json +from vendor_samba.gp.util.logging import log +import struct +try: + from cryptography.hazmat.primitives.serialization.pkcs7 import \ + load_der_pkcs7_certificates +except ModuleNotFoundError: + def load_der_pkcs7_certificates(x): return [] + log.error('python cryptography missing pkcs7 support. ' + 'Certificate chain parsing will fail') +from cryptography.hazmat.primitives.serialization import Encoding +from cryptography.x509 import load_der_x509_certificate +from cryptography.hazmat.backends import default_backend +from samba.common import get_string + +cert_wrap = b""" +-----BEGIN CERTIFICATE----- +%s +-----END CERTIFICATE-----""" +endpoint_re = '(https|HTTPS)://(?P[a-zA-Z0-9.-]+)/ADPolicyProvider' + \ + '_CEP_(?P[a-zA-Z]+)/service.svc/CEP' + + +def octet_string_to_objectGUID(data): + """Convert an octet string to an objectGUID.""" + return '%s-%s-%s-%s-%s' % ('%02x' % struct.unpack('H', data[8:10])[0], + '%02x%02x' % struct.unpack('>HL', data[10:])) + + +def group_and_sort_end_point_information(end_point_information): + """Group and Sort End Point Information. + + [MS-CAESO] 4.4.5.3.2.3 + In this step autoenrollment processes the end point information by grouping + it by CEP ID and sorting in the order with which it will use the end point + to access the CEP information. + """ + # Create groups of the CertificateEnrollmentPolicyEndPoint instances that + # have the same value of the EndPoint.PolicyID datum. + end_point_groups = {} + for e in end_point_information: + if e['PolicyID'] not in end_point_groups.keys(): + end_point_groups[e['PolicyID']] = [] + end_point_groups[e['PolicyID']].append(e) + + # Sort each group by following these rules: + for end_point_group in end_point_groups.values(): + # Sort the CertificateEnrollmentPolicyEndPoint instances in ascending + # order based on the EndPoint.Cost value. + end_point_group.sort(key=lambda e: e['Cost']) + + # For instances that have the same EndPoint.Cost: + cost_list = [e['Cost'] for e in end_point_group] + costs = set(cost_list) + for cost in costs: + i = cost_list.index(cost) + j = len(cost_list)-operator.indexOf(reversed(cost_list), cost)-1 + if i == j: + continue + + # Sort those that have EndPoint.Authentication equal to Kerberos + # first. Then sort those that have EndPoint.Authentication equal to + # Anonymous. The rest of the CertificateEnrollmentPolicyEndPoint + # instances follow in an arbitrary order. + def sort_auth(e): + # 0x2 - Kerberos + if e['AuthFlags'] == 0x2: + return 0 + # 0x1 - Anonymous + elif e['AuthFlags'] == 0x1: + return 1 + else: + return 2 + end_point_group[i:j+1] = sorted(end_point_group[i:j+1], + key=sort_auth) + return list(end_point_groups.values()) + +def obtain_end_point_information(entries): + """Obtain End Point Information. + + [MS-CAESO] 4.4.5.3.2.2 + In this step autoenrollment initializes the + CertificateEnrollmentPolicyEndPoints table. + """ + end_point_information = {} + section = 'Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\' + for e in entries: + if not e.keyname.startswith(section): + continue + name = e.keyname.replace(section, '') + if name not in end_point_information.keys(): + end_point_information[name] = {} + end_point_information[name][e.valuename] = e.data + for ca in end_point_information.values(): + m = re.match(endpoint_re, ca['URL']) + if m: + name = '%s-CA' % m.group('server').replace('.', '-') + ca['name'] = name + ca['hostname'] = m.group('server') + ca['auth'] = m.group('auth') + elif ca['URL'].lower() != 'ldap:': + edata = { 'endpoint': ca['URL'] } + log.error('Failed to parse the endpoint', edata) + return {} + end_point_information = \ + group_and_sort_end_point_information(end_point_information.values()) + return end_point_information + +def fetch_certification_authorities(ldb): + """Initialize CAs. + + [MS-CAESO] 4.4.5.3.1.2 + """ + result = [] + basedn = ldb.get_default_basedn() + # Autoenrollment MUST do an LDAP search for the CA information + # (pKIEnrollmentService) objects under the following container: + dn = 'CN=Enrollment Services,CN=Public Key Services,CN=Services,CN=Configuration,%s' % basedn + attrs = ['cACertificate', 'cn', 'dNSHostName'] + expr = '(objectClass=pKIEnrollmentService)' + res = ldb.search(dn, SCOPE_SUBTREE, expr, attrs) + if len(res) == 0: + return result + for es in res: + data = { 'name': get_string(es['cn'][0]), + 'hostname': get_string(es['dNSHostName'][0]), + 'cACertificate': get_string(base64.b64encode(es['cACertificate'][0])) + } + result.append(data) + return result + +def fetch_template_attrs(ldb, name, attrs=None): + if attrs is None: + attrs = ['msPKI-Minimal-Key-Size'] + basedn = ldb.get_default_basedn() + dn = 'CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,%s' % basedn + expr = '(cn=%s)' % name + res = ldb.search(dn, SCOPE_SUBTREE, expr, attrs) + if len(res) == 1 and 'msPKI-Minimal-Key-Size' in res[0]: + return dict(res[0]) + else: + return {'msPKI-Minimal-Key-Size': ['2048']} + +def format_root_cert(cert): + return cert_wrap % re.sub(b"(.{64})", b"\\1\n", cert.encode(), 0, re.DOTALL) + +def find_cepces_submit(): + certmonger_dirs = [os.environ.get("PATH"), '/usr/lib/certmonger', + '/usr/libexec/certmonger'] + return which('cepces-submit', path=':'.join(certmonger_dirs)) + +def get_supported_templates(server): + cepces_submit = find_cepces_submit() + if os.path.exists(cepces_submit): + env = os.environ + env['CERTMONGER_OPERATION'] = 'GET-SUPPORTED-TEMPLATES' + p = Popen([cepces_submit, '--server=%s' % server, '--auth=Kerberos'], + env=env, stdout=PIPE, stderr=PIPE) + out, err = p.communicate() + if p.returncode != 0: + data = { 'Error': err.decode() } + log.error('Failed to fetch the list of supported templates.', data) + return out.strip().split() + return [] + + +def getca(ca, url, trust_dir): + """Fetch Certificate Chain from the CA.""" + root_cert = os.path.join(trust_dir, '%s.crt' % ca['name']) + root_certs = [] + + try: + r = requests.get(url=url, params={'operation': 'GetCACert', + 'message': 'CAIdentifier'}) + except requests.exceptions.ConnectionError: + log.warn('Failed to establish a new connection') + r = None + if r is None or r.content == b'' or r.headers['Content-Type'] == 'text/html': + log.warn('Failed to fetch the root certificate chain.') + log.warn('The Network Device Enrollment Service is either not' + + ' installed or not configured.') + if 'cACertificate' in ca: + log.warn('Installing the server certificate only.') + try: + cert = load_der_x509_certificate(ca['cACertificate']) + except TypeError: + cert = load_der_x509_certificate(ca['cACertificate'], + default_backend()) + cert_data = cert.public_bytes(Encoding.PEM) + with open(root_cert, 'wb') as w: + w.write(cert_data) + root_certs.append(root_cert) + return root_certs + + if r.headers['Content-Type'] == 'application/x-x509-ca-cert': + # Older versions of load_der_x509_certificate require a backend param + try: + cert = load_der_x509_certificate(r.content) + except TypeError: + cert = load_der_x509_certificate(r.content, default_backend()) + cert_data = cert.public_bytes(Encoding.PEM) + with open(root_cert, 'wb') as w: + w.write(cert_data) + root_certs.append(root_cert) + elif r.headers['Content-Type'] == 'application/x-x509-ca-ra-cert': + certs = load_der_pkcs7_certificates(r.content) + for i in range(0, len(certs)): + cert = certs[i].public_bytes(Encoding.PEM) + filename, extension = root_cert.rsplit('.', 1) + dest = '%s.%d.%s' % (filename, i, extension) + with open(dest, 'wb') as w: + w.write(cert) + root_certs.append(dest) + else: + log.warn('getca: Wrong (or missing) MIME content type') + + return root_certs + + +def cert_enroll(ca, ldb, trust_dir, private_dir, global_trust_dir, auth='Kerberos'): + """Install the root certificate chain.""" + data = dict({'files': [], 'templates': []}, **ca) + url = 'http://%s/CertSrv/mscep/mscep.dll/pkiclient.exe?' % ca['hostname'] + root_certs = getca(ca, url, trust_dir) + data['files'].extend(root_certs) + for src in root_certs: + # Symlink the certs to global trust dir + dst = os.path.join(global_trust_dir, os.path.basename(src)) + try: + os.symlink(src, dst) + data['files'].append(dst) + except PermissionError: + log.warn('Failed to symlink root certificate to the' + ' admin trust anchors') + except FileNotFoundError: + log.warn('Failed to symlink root certificate to the' + ' admin trust anchors.' + ' The directory was not found', global_trust_dir) + except FileExistsError: + # If we're simply downloading a renewed cert, the symlink + # already exists. Ignore the FileExistsError. Preserve the + # existing symlink in the unapply data. + data['files'].append(dst) + update = which('update-ca-certificates') + if update is not None: + Popen([update]).wait() + # Setup Certificate Auto Enrollment + getcert = which('getcert') + cepces_submit = find_cepces_submit() + if getcert is not None and os.path.exists(cepces_submit): + p = Popen([getcert, 'add-ca', '-c', ca['name'], '-e', + '%s --server=%s --auth=%s' % (cepces_submit, + ca['hostname'], auth)], + stdout=PIPE, stderr=PIPE) + out, err = p.communicate() + log.debug(out.decode()) + if p.returncode != 0: + data = { 'Error': err.decode(), 'CA': ca['name'] } + log.error('Failed to add Certificate Authority', data) + supported_templates = get_supported_templates(ca['hostname']) + for template in supported_templates: + attrs = fetch_template_attrs(ldb, template) + nickname = '%s.%s' % (ca['name'], template.decode()) + keyfile = os.path.join(private_dir, '%s.key' % nickname) + certfile = os.path.join(trust_dir, '%s.crt' % nickname) + p = Popen([getcert, 'request', '-c', ca['name'], + '-T', template.decode(), + '-I', nickname, '-k', keyfile, '-f', certfile, + '-g', attrs['msPKI-Minimal-Key-Size'][0]], + stdout=PIPE, stderr=PIPE) + out, err = p.communicate() + log.debug(out.decode()) + if p.returncode != 0: + data = { 'Error': err.decode(), 'Certificate': nickname } + log.error('Failed to request certificate', data) + data['files'].extend([keyfile, certfile]) + data['templates'].append(nickname) + if update is not None: + Popen([update]).wait() + else: + log.warn('certmonger and cepces must be installed for ' + + 'certificate auto enrollment to work') + return json.dumps(data) + +class gp_cert_auto_enroll_ext(gp_pol_ext, gp_applier): + def __str__(self): + return 'Cryptography\AutoEnrollment' + + def unapply(self, guid, attribute, value): + ca_cn = base64.b64decode(attribute) + data = json.loads(value) + getcert = which('getcert') + if getcert is not None: + Popen([getcert, 'remove-ca', '-c', ca_cn]).wait() + for nickname in data['templates']: + Popen([getcert, 'stop-tracking', '-i', nickname]).wait() + for f in data['files']: + if os.path.exists(f): + if os.path.exists(f): + os.unlink(f) + self.cache_remove_attribute(guid, attribute) + + def apply(self, guid, ca, applier_func, *args, **kwargs): + attribute = base64.b64encode(ca['name'].encode()).decode() + # If the policy has changed, unapply, then apply new policy + old_val = self.cache_get_attribute_value(guid, attribute) + old_data = json.loads(old_val) if old_val is not None else {} + templates = ['%s.%s' % (ca['name'], t.decode()) for t in get_supported_templates(ca['hostname'])] + new_data = { 'templates': templates, **ca } + if any((new_data[k] != old_data[k] if k in old_data else False) \ + for k in new_data.keys()) or \ + self.cache_get_apply_state() == GPOSTATE.ENFORCE: + self.unapply(guid, attribute, old_val) + # If policy is already applied, skip application + if old_val is not None and \ + self.cache_get_apply_state() != GPOSTATE.ENFORCE: + return + + # Apply the policy and log the changes + data = applier_func(*args, **kwargs) + self.cache_add_attribute(guid, attribute, data) + + def process_group_policy(self, deleted_gpo_list, changed_gpo_list, + trust_dir=None, private_dir=None, global_trust_dir=None): + if trust_dir is None: + trust_dir = self.lp.cache_path('certs') + if private_dir is None: + private_dir = self.lp.private_path('certs') + if global_trust_dir is None: + global_trust_dir = '/etc/pki/trust/anchors' + if not os.path.exists(trust_dir): + os.mkdir(trust_dir, mode=0o755) + if not os.path.exists(private_dir): + os.mkdir(private_dir, mode=0o700) + + for guid, settings in deleted_gpo_list: + if str(self) in settings: + for ca_cn_enc, data in settings[str(self)].items(): + self.unapply(guid, ca_cn_enc, data) + + for gpo in changed_gpo_list: + if gpo.file_sys_path: + section = 'Software\Policies\Microsoft\Cryptography\AutoEnrollment' + pol_file = 'MACHINE/Registry.pol' + path = os.path.join(gpo.file_sys_path, pol_file) + pol_conf = self.parse(path) + if not pol_conf: + continue + for e in pol_conf.entries: + if e.keyname == section and e.valuename == 'AEPolicy': + # This policy applies as specified in [MS-CAESO] 4.4.5.1 + if e.data & 0x8000: + continue # The policy is disabled + enroll = e.data & 0x1 == 0x1 + manage = e.data & 0x2 == 0x2 + retrive_pending = e.data & 0x4 == 0x4 + if enroll: + ca_names = self.__enroll(gpo.name, + pol_conf.entries, + trust_dir, private_dir, + global_trust_dir) + + # Cleanup any old CAs that have been removed + ca_attrs = [base64.b64encode(n.encode()).decode() \ + for n in ca_names] + self.clean(gpo.name, keep=ca_attrs) + else: + # If enrollment has been disabled for this GPO, + # remove any existing policy + ca_attrs = \ + self.cache_get_all_attribute_values(gpo.name) + self.clean(gpo.name, remove=list(ca_attrs.keys())) + + def __read_cep_data(self, guid, ldb, end_point_information, + trust_dir, private_dir, global_trust_dir): + """Read CEP Data. + + [MS-CAESO] 4.4.5.3.2.4 + In this step autoenrollment initializes instances of the + CertificateEnrollmentPolicy by accessing end points associated with CEP + groups created in the previous step. + """ + # For each group created in the previous step: + for end_point_group in end_point_information: + # Pick an arbitrary instance of the + # CertificateEnrollmentPolicyEndPoint from the group + e = end_point_group[0] + + # If this instance does not have the AutoEnrollmentEnabled flag set + # in the EndPoint.Flags, continue with the next group. + if not e['Flags'] & 0x10: + continue + + # If the current group contains a + # CertificateEnrollmentPolicyEndPoint instance with EndPoint.URI + # equal to "LDAP": + if any([e['URL'] == 'LDAP:' for e in end_point_group]): + # Perform an LDAP search to read the value of the objectGuid + # attribute of the root object of the forest root domain NC. If + # any errors are encountered, continue with the next group. + res = ldb.search('', SCOPE_BASE, '(objectClass=*)', + ['rootDomainNamingContext']) + if len(res) != 1: + continue + res2 = ldb.search(res[0]['rootDomainNamingContext'][0], + SCOPE_BASE, '(objectClass=*)', + ['objectGUID']) + if len(res2) != 1: + continue + + # Compare the value read in the previous step to the + # EndPoint.PolicyId datum CertificateEnrollmentPolicyEndPoint + # instance. If the values do not match, continue with the next + # group. + objectGUID = '{%s}' % \ + octet_string_to_objectGUID(res2[0]['objectGUID'][0]).upper() + if objectGUID != e['PolicyID']: + continue + + # For each CertificateEnrollmentPolicyEndPoint instance for that + # group: + ca_names = [] + for ca in end_point_group: + # If EndPoint.URI equals "LDAP": + if ca['URL'] == 'LDAP:': + # This is a basic configuration. + cas = fetch_certification_authorities(ldb) + for _ca in cas: + self.apply(guid, _ca, cert_enroll, _ca, ldb, trust_dir, + private_dir, global_trust_dir) + ca_names.append(_ca['name']) + # If EndPoint.URI starts with "HTTPS//": + elif ca['URL'].lower().startswith('https://'): + self.apply(guid, ca, cert_enroll, ca, ldb, trust_dir, + private_dir, global_trust_dir, auth=ca['auth']) + ca_names.append(ca['name']) + else: + edata = { 'endpoint': ca['URL'] } + log.error('Unrecognized endpoint', edata) + return ca_names + + def __enroll(self, guid, entries, trust_dir, private_dir, global_trust_dir): + url = 'ldap://%s' % get_dc_hostname(self.creds, self.lp) + ldb = Ldb(url=url, session_info=system_session(), + lp=self.lp, credentials=self.creds) + + ca_names = [] + end_point_information = obtain_end_point_information(entries) + if len(end_point_information) > 0: + ca_names.extend(self.__read_cep_data(guid, ldb, + end_point_information, + trust_dir, private_dir, global_trust_dir)) + else: + cas = fetch_certification_authorities(ldb) + for ca in cas: + self.apply(guid, ca, cert_enroll, ca, ldb, trust_dir, + private_dir, global_trust_dir) + ca_names.append(ca['name']) + return ca_names + + def rsop(self, gpo): + output = {} + pol_file = 'MACHINE/Registry.pol' + section = 'Software\Policies\Microsoft\Cryptography\AutoEnrollment' + if gpo.file_sys_path: + path = os.path.join(gpo.file_sys_path, pol_file) + pol_conf = self.parse(path) + if not pol_conf: + return output + for e in pol_conf.entries: + if e.keyname == section and e.valuename == 'AEPolicy': + enroll = e.data & 0x1 == 0x1 + if e.data & 0x8000 or not enroll: + continue + output['Auto Enrollment Policy'] = {} + url = 'ldap://%s' % get_dc_hostname(self.creds, self.lp) + ldb = Ldb(url=url, session_info=system_session(), + lp=self.lp, credentials=self.creds) + end_point_information = \ + obtain_end_point_information(pol_conf.entries) + cas = fetch_certification_authorities(ldb) + if len(end_point_information) > 0: + cas2 = [ep for sl in end_point_information for ep in sl] + if any([ca['URL'] == 'LDAP:' for ca in cas2]): + cas.extend(cas2) + else: + cas = cas2 + for ca in cas: + if 'URL' in ca and ca['URL'] == 'LDAP:': + continue + policy = 'Auto Enrollment Policy' + cn = ca['name'] + if policy not in output: + output[policy] = {} + output[policy][cn] = {} + if 'cACertificate' in ca: + output[policy][cn]['CA Certificate'] = \ + format_root_cert(ca['cACertificate']).decode() + output[policy][cn]['Auto Enrollment Server'] = \ + ca['hostname'] + supported_templates = \ + get_supported_templates(ca['hostname']) + output[policy][cn]['Templates'] = \ + [t.decode() for t in supported_templates] + return output diff --git a/internal/policies/certificate/python/vendor_samba/gp/gpclass.py b/internal/policies/certificate/python/vendor_samba/gp/gpclass.py new file mode 100644 index 000000000..0ef86576d --- /dev/null +++ b/internal/policies/certificate/python/vendor_samba/gp/gpclass.py @@ -0,0 +1,884 @@ +# Reads important GPO parameters and updates Samba +# Copyright (C) Luke Morrison 2013 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +import sys +import os, shutil +import errno +import tdb +import pwd +sys.path.insert(0, "bin/python") +from samba import NTSTATUSError +from configparser import ConfigParser +from io import StringIO +import traceback +from samba.common import get_bytes +from abc import ABCMeta, abstractmethod +import xml.etree.ElementTree as etree +import re +from samba.net import Net +from samba.dcerpc import nbt +from samba.samba3 import libsmb_samba_internal as libsmb +import samba.gpo as gpo +from samba.param import LoadParm +from uuid import UUID +from tempfile import NamedTemporaryFile +from samba.dcerpc import preg +from samba.dcerpc import misc +from samba.ndr import ndr_pack, ndr_unpack +from samba.credentials import SMB_SIGNING_REQUIRED +from vendor_samba.gp.util.logging import log +from hashlib import blake2b +import numbers +from samba.common import get_string + +try: + from enum import Enum + GPOSTATE = Enum('GPOSTATE', 'APPLY ENFORCE UNAPPLY') +except ImportError: + class GPOSTATE: + APPLY = 1 + ENFORCE = 2 + UNAPPLY = 3 + + +class gp_log: + ''' Log settings overwritten by gpo apply + The gp_log is an xml file that stores a history of gpo changes (and the + original setting value). + + The log is organized like so: + + + + + + + + + -864000000000 + -36288000000000 + 7 + 1 + + + 1d + + 300 + + + + + + Each guid value contains a list of extensions, which contain a list of + attributes. The guid value represents a GPO. The attributes are the values + of those settings prior to the application of the GPO. + The list of guids is enclosed within a user name, which represents the user + the settings were applied to. This user may be the samaccountname of the + local computer, which implies that these are machine policies. + The applylog keeps track of the order in which the GPOs were applied, so + that they can be rolled back in reverse, returning the machine to the state + prior to policy application. + ''' + def __init__(self, user, gpostore, db_log=None): + ''' Initialize the gp_log + param user - the username (or machine name) that policies are + being applied to + param gpostore - the GPOStorage obj which references the tdb which + contains gp_logs + param db_log - (optional) a string to initialize the gp_log + ''' + self._state = GPOSTATE.APPLY + self.gpostore = gpostore + self.username = user + if db_log: + self.gpdb = etree.fromstring(db_log) + else: + self.gpdb = etree.Element('gp') + self.user = user + user_obj = self.gpdb.find('user[@name="%s"]' % user) + if user_obj is None: + user_obj = etree.SubElement(self.gpdb, 'user') + user_obj.attrib['name'] = user + + def state(self, value): + ''' Policy application state + param value - APPLY, ENFORCE, or UNAPPLY + + The behavior of the gp_log depends on whether we are applying policy, + enforcing policy, or unapplying policy. During an apply, old settings + are recorded in the log. During an enforce, settings are being applied + but the gp_log does not change. During an unapply, additions to the log + should be ignored (since function calls to apply settings are actually + reverting policy), but removals from the log are allowed. + ''' + # If we're enforcing, but we've unapplied, apply instead + if value == GPOSTATE.ENFORCE: + user_obj = self.gpdb.find('user[@name="%s"]' % self.user) + apply_log = user_obj.find('applylog') + if apply_log is None or len(apply_log) == 0: + self._state = GPOSTATE.APPLY + else: + self._state = value + else: + self._state = value + + def get_state(self): + '''Check the GPOSTATE + ''' + return self._state + + def set_guid(self, guid): + ''' Log to a different GPO guid + param guid - guid value of the GPO from which we're applying + policy + ''' + self.guid = guid + user_obj = self.gpdb.find('user[@name="%s"]' % self.user) + obj = user_obj.find('guid[@value="%s"]' % guid) + if obj is None: + obj = etree.SubElement(user_obj, 'guid') + obj.attrib['value'] = guid + if self._state == GPOSTATE.APPLY: + apply_log = user_obj.find('applylog') + if apply_log is None: + apply_log = etree.SubElement(user_obj, 'applylog') + prev = apply_log.find('guid[@value="%s"]' % guid) + if prev is None: + item = etree.SubElement(apply_log, 'guid') + item.attrib['count'] = '%d' % (len(apply_log) - 1) + item.attrib['value'] = guid + + def store(self, gp_ext_name, attribute, old_val): + ''' Store an attribute in the gp_log + param gp_ext_name - Name of the extension applying policy + param attribute - The attribute being modified + param old_val - The value of the attribute prior to policy + application + ''' + if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE: + return None + user_obj = self.gpdb.find('user[@name="%s"]' % self.user) + guid_obj = user_obj.find('guid[@value="%s"]' % self.guid) + assert guid_obj is not None, "gpo guid was not set" + ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name) + if ext is None: + ext = etree.SubElement(guid_obj, 'gp_ext') + ext.attrib['name'] = gp_ext_name + attr = ext.find('attribute[@name="%s"]' % attribute) + if attr is None: + attr = etree.SubElement(ext, 'attribute') + attr.attrib['name'] = attribute + attr.text = old_val + + def retrieve(self, gp_ext_name, attribute): + ''' Retrieve a stored attribute from the gp_log + param gp_ext_name - Name of the extension which applied policy + param attribute - The attribute being retrieved + return - The value of the attribute prior to policy + application + ''' + user_obj = self.gpdb.find('user[@name="%s"]' % self.user) + guid_obj = user_obj.find('guid[@value="%s"]' % self.guid) + assert guid_obj is not None, "gpo guid was not set" + ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name) + if ext is not None: + attr = ext.find('attribute[@name="%s"]' % attribute) + if attr is not None: + return attr.text + return None + + def retrieve_all(self, gp_ext_name): + ''' Retrieve all stored attributes for this user, GPO guid, and CSE + param gp_ext_name - Name of the extension which applied policy + return - The values of the attributes prior to policy + application + ''' + user_obj = self.gpdb.find('user[@name="%s"]' % self.user) + guid_obj = user_obj.find('guid[@value="%s"]' % self.guid) + assert guid_obj is not None, "gpo guid was not set" + ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name) + if ext is not None: + attrs = ext.findall('attribute') + return {attr.attrib['name']: attr.text for attr in attrs} + return {} + + def get_applied_guids(self): + ''' Return a list of applied ext guids + return - List of guids for gpos that have applied settings + to the system. + ''' + guids = [] + user_obj = self.gpdb.find('user[@name="%s"]' % self.user) + if user_obj is not None: + apply_log = user_obj.find('applylog') + if apply_log is not None: + guid_objs = apply_log.findall('guid[@count]') + guids_by_count = [(g.get('count'), g.get('value')) + for g in guid_objs] + guids_by_count.sort(reverse=True) + guids.extend(guid for count, guid in guids_by_count) + return guids + + def get_applied_settings(self, guids): + ''' Return a list of applied ext guids + return - List of tuples containing the guid of a gpo, then + a dictionary of policies and their values prior + policy application. These are sorted so that the + most recently applied settings are removed first. + ''' + ret = [] + user_obj = self.gpdb.find('user[@name="%s"]' % self.user) + for guid in guids: + guid_settings = user_obj.find('guid[@value="%s"]' % guid) + exts = guid_settings.findall('gp_ext') + settings = {} + for ext in exts: + attr_dict = {} + attrs = ext.findall('attribute') + for attr in attrs: + attr_dict[attr.attrib['name']] = attr.text + settings[ext.attrib['name']] = attr_dict + ret.append((guid, settings)) + return ret + + def delete(self, gp_ext_name, attribute): + ''' Remove an attribute from the gp_log + param gp_ext_name - name of extension from which to remove the + attribute + param attribute - attribute to remove + ''' + user_obj = self.gpdb.find('user[@name="%s"]' % self.user) + guid_obj = user_obj.find('guid[@value="%s"]' % self.guid) + assert guid_obj is not None, "gpo guid was not set" + ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name) + if ext is not None: + attr = ext.find('attribute[@name="%s"]' % attribute) + if attr is not None: + ext.remove(attr) + if len(ext) == 0: + guid_obj.remove(ext) + + def commit(self): + ''' Write gp_log changes to disk ''' + self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8')) + + +class GPOStorage: + def __init__(self, log_file): + if os.path.isfile(log_file): + self.log = tdb.open(log_file) + else: + self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT | os.O_RDWR) + + def start(self): + self.log.transaction_start() + + def get_int(self, key): + try: + return int(self.log.get(get_bytes(key))) + except TypeError: + return None + + def get(self, key): + return self.log.get(get_bytes(key)) + + def get_gplog(self, user): + return gp_log(user, self, self.log.get(get_bytes(user))) + + def store(self, key, val): + self.log.store(get_bytes(key), get_bytes(val)) + + def cancel(self): + self.log.transaction_cancel() + + def delete(self, key): + self.log.delete(get_bytes(key)) + + def commit(self): + self.log.transaction_commit() + + def __del__(self): + self.log.close() + + +class gp_ext(object): + __metaclass__ = ABCMeta + + def __init__(self, lp, creds, username, store): + self.lp = lp + self.creds = creds + self.username = username + self.gp_db = store.get_gplog(username) + + @abstractmethod + def process_group_policy(self, deleted_gpo_list, changed_gpo_list): + pass + + @abstractmethod + def read(self, policy): + pass + + def parse(self, afile): + local_path = self.lp.cache_path('gpo_cache') + data_file = os.path.join(local_path, check_safe_path(afile).upper()) + if os.path.exists(data_file): + return self.read(data_file) + return None + + @abstractmethod + def __str__(self): + pass + + @abstractmethod + def rsop(self, gpo): + return {} + + +class gp_inf_ext(gp_ext): + def read(self, data_file): + policy = open(data_file, 'rb').read() + inf_conf = ConfigParser(interpolation=None) + inf_conf.optionxform = str + try: + inf_conf.readfp(StringIO(policy.decode())) + except UnicodeDecodeError: + inf_conf.readfp(StringIO(policy.decode('utf-16'))) + return inf_conf + + +class gp_pol_ext(gp_ext): + def read(self, data_file): + raw = open(data_file, 'rb').read() + return ndr_unpack(preg.file, raw) + + +class gp_xml_ext(gp_ext): + def read(self, data_file): + raw = open(data_file, 'rb').read() + try: + return etree.fromstring(raw.decode()) + except UnicodeDecodeError: + return etree.fromstring(raw.decode('utf-16')) + + +class gp_applier(object): + '''Group Policy Applier/Unapplier/Modifier + The applier defines functions for monitoring policy application, + removal, and modification. It must be a multi-derived class paired + with a subclass of gp_ext. + ''' + __metaclass__ = ABCMeta + + def cache_add_attribute(self, guid, attribute, value): + '''Add an attribute and value to the Group Policy cache + guid - The GPO guid which applies this policy + attribute - The attribute name of the policy being applied + value - The value of the policy being applied + + Normally called by the subclass apply() function after applying policy. + ''' + self.gp_db.set_guid(guid) + self.gp_db.store(str(self), attribute, value) + self.gp_db.commit() + + def cache_remove_attribute(self, guid, attribute): + '''Remove an attribute from the Group Policy cache + guid - The GPO guid which applies this policy + attribute - The attribute name of the policy being unapplied + + Normally called by the subclass unapply() function when removing old + policy. + ''' + self.gp_db.set_guid(guid) + self.gp_db.delete(str(self), attribute) + self.gp_db.commit() + + def cache_get_attribute_value(self, guid, attribute): + '''Retrieve the value stored in the cache for the given attribute + guid - The GPO guid which applies this policy + attribute - The attribute name of the policy + ''' + self.gp_db.set_guid(guid) + return self.gp_db.retrieve(str(self), attribute) + + def cache_get_all_attribute_values(self, guid): + '''Retrieve all attribute/values currently stored for this gpo+policy + guid - The GPO guid which applies this policy + ''' + self.gp_db.set_guid(guid) + return self.gp_db.retrieve_all(str(self)) + + def cache_get_apply_state(self): + '''Return the current apply state + return - APPLY|ENFORCE|UNAPPLY + ''' + return self.gp_db.get_state() + + def generate_attribute(self, name, *args): + '''Generate an attribute name from arbitrary data + name - A name to ensure uniqueness + args - Any arbitrary set of args, str or bytes + return - A blake2b digest of the data, the attribute + + The importance here is the digest of the data makes the attribute + reproducible and uniquely identifies it. Hashing the name with + the data ensures we don't falsly identify a match which is the same + text in a different file. Using this attribute generator is optional. + ''' + data = b''.join([get_bytes(arg) for arg in [*args]]) + return blake2b(get_bytes(name)+data).hexdigest() + + def generate_value_hash(self, *args): + '''Generate a unique value which identifies value changes + args - Any arbitrary set of args, str or bytes + return - A blake2b digest of the data, the value represented + ''' + data = b''.join([get_bytes(arg) for arg in [*args]]) + return blake2b(data).hexdigest() + + @abstractmethod + def unapply(self, guid, attribute, value): + '''Group Policy Unapply + guid - The GPO guid which applies this policy + attribute - The attribute name of the policy being unapplied + value - The value of the policy being unapplied + ''' + pass + + @abstractmethod + def apply(self, guid, attribute, applier_func, *args): + '''Group Policy Apply + guid - The GPO guid which applies this policy + attribute - The attribute name of the policy being applied + applier_func - An applier function which takes variable args + args - The variable arguments to pass to applier_func + + The applier_func function MUST return the value of the policy being + applied. It's important that implementations of `apply` check for and + first unapply any changed policy. See for example calls to + `cache_get_all_attribute_values()` which searches for all policies + applied by this GPO for this Client Side Extension (CSE). + ''' + pass + + def clean(self, guid, keep=None, remove=None, **kwargs): + '''Cleanup old removed attributes + keep - A list of attributes to keep + remove - A single attribute to remove, or a list of attributes to + remove + kwargs - Additional keyword args required by the subclass unapply + function + + This is only necessary for CSEs which provide multiple attributes. + ''' + # Clean syntax is, either provide a single remove attribute, + # or a list of either removal attributes or keep attributes. + if keep is None: + keep = [] + if remove is None: + remove = [] + + if type(remove) != list: + value = self.cache_get_attribute_value(guid, remove) + if value is not None: + self.unapply(guid, remove, value, **kwargs) + else: + old_vals = self.cache_get_all_attribute_values(guid) + for attribute, value in old_vals.items(): + if (len(remove) > 0 and attribute in remove) or \ + (len(keep) > 0 and attribute not in keep): + self.unapply(guid, attribute, value, **kwargs) + + +class gp_file_applier(gp_applier): + '''Group Policy File Applier/Unapplier/Modifier + Subclass of abstract class gp_applier for monitoring policy applied + via a file. + ''' + + def __generate_value(self, value_hash, files, sep): + data = [value_hash] + data.extend(files) + return sep.join(data) + + def __parse_value(self, value, sep): + '''Parse a value + return - A unique HASH, followed by the file list + ''' + if value is None: + return None, [] + data = value.split(sep) + if '/' in data[0]: + # The first element is not a hash, but a filename. This is a + # legacy value. + return None, data + else: + return data[0], data[1:] if len(data) > 1 else [] + + def unapply(self, guid, attribute, files, sep=':'): + # If the value isn't a list of files, parse value from the log + if type(files) != list: + _, files = self.__parse_value(files, sep) + for file in files: + if os.path.exists(file): + os.unlink(file) + self.cache_remove_attribute(guid, attribute) + + def apply(self, guid, attribute, value_hash, applier_func, *args, sep=':'): + ''' + applier_func MUST return a list of files created by the applier. + + This applier is for policies which only apply to a single file (with + a couple small exceptions). This applier will remove any policy applied + by this GPO which doesn't match the new policy. + ''' + # If the policy has changed, unapply, then apply new policy + old_val = self.cache_get_attribute_value(guid, attribute) + # Ignore removal if this policy is applied and hasn't changed + old_val_hash, old_val_files = self.__parse_value(old_val, sep) + if (old_val_hash != value_hash or \ + self.cache_get_apply_state() == GPOSTATE.ENFORCE) or \ + not all([os.path.exists(f) for f in old_val_files]): + self.unapply(guid, attribute, old_val_files) + else: + # If policy is already applied, skip application + return + + # Apply the policy and log the changes + files = applier_func(*args) + new_value = self.__generate_value(value_hash, files, sep) + self.cache_add_attribute(guid, attribute, new_value) + + +''' Fetch the hostname of a writable DC ''' + + +def get_dc_hostname(creds, lp): + net = Net(creds=creds, lp=lp) + cldap_ret = net.finddc(domain=lp.get('realm'), flags=(nbt.NBT_SERVER_LDAP | + nbt.NBT_SERVER_DS)) + return cldap_ret.pdc_dns_name + + +''' Fetch a list of GUIDs for applicable GPOs ''' + + +def get_gpo_list(dc_hostname, creds, lp, username): + gpos = [] + ads = gpo.ADS_STRUCT(dc_hostname, lp, creds) + if ads.connect(): + # username is DOM\\SAM, but get_gpo_list expects SAM + gpos = ads.get_gpo_list(username.split('\\')[-1]) + return gpos + + +def cache_gpo_dir(conn, cache, sub_dir): + loc_sub_dir = sub_dir.upper() + local_dir = os.path.join(cache, loc_sub_dir) + try: + os.makedirs(local_dir, mode=0o755) + except OSError as e: + if e.errno != errno.EEXIST: + raise + for fdata in conn.list(sub_dir): + if fdata['attrib'] & libsmb.FILE_ATTRIBUTE_DIRECTORY: + cache_gpo_dir(conn, cache, os.path.join(sub_dir, fdata['name'])) + else: + local_name = fdata['name'].upper() + f = NamedTemporaryFile(delete=False, dir=local_dir) + fname = os.path.join(sub_dir, fdata['name']).replace('/', '\\') + f.write(conn.loadfile(fname)) + f.close() + os.rename(f.name, os.path.join(local_dir, local_name)) + + +def check_safe_path(path): + dirs = re.split('/|\\\\', path) + if 'sysvol' in path.lower(): + ldirs = re.split('/|\\\\', path.lower()) + dirs = dirs[ldirs.index('sysvol') + 1:] + if '..' not in dirs: + return os.path.join(*dirs) + raise OSError(path) + + +def check_refresh_gpo_list(dc_hostname, lp, creds, gpos): + # Force signing for the connection + saved_signing_state = creds.get_smb_signing() + creds.set_smb_signing(SMB_SIGNING_REQUIRED) + conn = libsmb.Conn(dc_hostname, 'sysvol', lp=lp, creds=creds) + # Reset signing state + creds.set_smb_signing(saved_signing_state) + cache_path = lp.cache_path('gpo_cache') + for gpo_obj in gpos: + if not gpo_obj.file_sys_path: + continue + cache_gpo_dir(conn, cache_path, check_safe_path(gpo_obj.file_sys_path)) + + +def get_deleted_gpos_list(gp_db, gpos): + applied_gpos = gp_db.get_applied_guids() + current_guids = set([p.name for p in gpos]) + deleted_gpos = [guid for guid in applied_gpos if guid not in current_guids] + return gp_db.get_applied_settings(deleted_gpos) + +def gpo_version(lp, path): + # gpo.gpo_get_sysvol_gpt_version() reads the GPT.INI from a local file, + # read from the gpo client cache. + gpt_path = lp.cache_path(os.path.join('gpo_cache', path)) + return int(gpo.gpo_get_sysvol_gpt_version(gpt_path)[1]) + + +def apply_gp(lp, creds, store, gp_extensions, username, target, force=False): + gp_db = store.get_gplog(username) + dc_hostname = get_dc_hostname(creds, lp) + gpos = get_gpo_list(dc_hostname, creds, lp, username) + del_gpos = get_deleted_gpos_list(gp_db, gpos) + try: + check_refresh_gpo_list(dc_hostname, lp, creds, gpos) + except: + log.error('Failed downloading gpt cache from \'%s\' using SMB' + % dc_hostname) + return + + if force: + changed_gpos = gpos + gp_db.state(GPOSTATE.ENFORCE) + else: + changed_gpos = [] + for gpo_obj in gpos: + if not gpo_obj.file_sys_path: + continue + guid = gpo_obj.name + path = check_safe_path(gpo_obj.file_sys_path).upper() + version = gpo_version(lp, path) + if version != store.get_int(guid): + log.info('GPO %s has changed' % guid) + changed_gpos.append(gpo_obj) + gp_db.state(GPOSTATE.APPLY) + + store.start() + for ext in gp_extensions: + try: + ext = ext(lp, creds, username, store) + if target == 'Computer': + ext.process_group_policy(del_gpos, changed_gpos) + else: + drop_privileges(creds.get_principal(), ext.process_group_policy, + del_gpos, changed_gpos) + except Exception as e: + log.error('Failed to apply extension %s' % str(ext)) + _, _, tb = sys.exc_info() + filename, line_number, _, _ = traceback.extract_tb(tb)[-1] + log.error('%s:%d: %s: %s' % (filename, line_number, + type(e).__name__, str(e))) + continue + for gpo_obj in gpos: + if not gpo_obj.file_sys_path: + continue + guid = gpo_obj.name + path = check_safe_path(gpo_obj.file_sys_path).upper() + version = gpo_version(lp, path) + store.store(guid, '%i' % version) + store.commit() + + +def unapply_gp(lp, creds, store, gp_extensions, username, target): + gp_db = store.get_gplog(username) + gp_db.state(GPOSTATE.UNAPPLY) + # Treat all applied gpos as deleted + del_gpos = gp_db.get_applied_settings(gp_db.get_applied_guids()) + store.start() + for ext in gp_extensions: + try: + ext = ext(lp, creds, username, store) + if target == 'Computer': + ext.process_group_policy(del_gpos, []) + else: + drop_privileges(username, ext.process_group_policy, + del_gpos, []) + except Exception as e: + log.error('Failed to unapply extension %s' % str(ext)) + log.error('Message was: ' + str(e)) + continue + store.commit() + + +def __rsop_vals(vals, level=4): + if type(vals) == dict: + ret = [' '*level + '[ %s ] = %s' % (k, __rsop_vals(v, level+2)) + for k, v in vals.items()] + return '\n' + '\n'.join(ret) + elif type(vals) == list: + ret = [' '*level + '[ %s ]' % __rsop_vals(v, level+2) for v in vals] + return '\n' + '\n'.join(ret) + else: + if isinstance(vals, numbers.Number): + return ' '*(level+2) + str(vals) + else: + return ' '*(level+2) + get_string(vals) + +def rsop(lp, creds, store, gp_extensions, username, target): + dc_hostname = get_dc_hostname(creds, lp) + gpos = get_gpo_list(dc_hostname, creds, lp, username) + check_refresh_gpo_list(dc_hostname, lp, creds, gpos) + + print('Resultant Set of Policy') + print('%s Policy\n' % target) + term_width = shutil.get_terminal_size(fallback=(120, 50))[0] + for gpo_obj in gpos: + if gpo_obj.display_name.strip() == 'Local Policy': + continue # We never apply local policy + print('GPO: %s' % gpo_obj.display_name) + print('='*term_width) + for ext in gp_extensions: + ext = ext(lp, creds, username, store) + cse_name_m = re.findall(r"'([\w\.]+)'", str(type(ext))) + if len(cse_name_m) > 0: + cse_name = cse_name_m[-1].split('.')[-1] + else: + cse_name = ext.__module__.split('.')[-1] + print(' CSE: %s' % cse_name) + print(' ' + ('-'*int(term_width/2))) + for section, settings in ext.rsop(gpo_obj).items(): + print(' Policy Type: %s' % section) + print(' ' + ('-'*int(term_width/2))) + print(__rsop_vals(settings).lstrip('\n')) + print(' ' + ('-'*int(term_width/2))) + print(' ' + ('-'*int(term_width/2))) + print('%s\n' % ('='*term_width)) + + +def parse_gpext_conf(smb_conf): + from samba.samba3 import param as s3param + lp = s3param.get_context() + if smb_conf is not None: + lp.load(smb_conf) + else: + lp.load_default() + ext_conf = lp.state_path('gpext.conf') + parser = ConfigParser(interpolation=None) + parser.read(ext_conf) + return lp, parser + + +def atomic_write_conf(lp, parser): + ext_conf = lp.state_path('gpext.conf') + with NamedTemporaryFile(mode="w+", delete=False, dir=os.path.dirname(ext_conf)) as f: + parser.write(f) + os.rename(f.name, ext_conf) + + +def check_guid(guid): + # Check for valid guid with curly braces + if guid[0] != '{' or guid[-1] != '}' or len(guid) != 38: + return False + try: + UUID(guid, version=4) + except ValueError: + return False + return True + + +def register_gp_extension(guid, name, path, + smb_conf=None, machine=True, user=True): + # Check that the module exists + if not os.path.exists(path): + return False + if not check_guid(guid): + return False + + lp, parser = parse_gpext_conf(smb_conf) + if guid not in parser.sections(): + parser.add_section(guid) + parser.set(guid, 'DllName', path) + parser.set(guid, 'ProcessGroupPolicy', name) + parser.set(guid, 'NoMachinePolicy', "0" if machine else "1") + parser.set(guid, 'NoUserPolicy', "0" if user else "1") + + atomic_write_conf(lp, parser) + + return True + + +def list_gp_extensions(smb_conf=None): + _, parser = parse_gpext_conf(smb_conf) + results = {} + for guid in parser.sections(): + results[guid] = {} + results[guid]['DllName'] = parser.get(guid, 'DllName') + results[guid]['ProcessGroupPolicy'] = \ + parser.get(guid, 'ProcessGroupPolicy') + results[guid]['MachinePolicy'] = \ + not int(parser.get(guid, 'NoMachinePolicy')) + results[guid]['UserPolicy'] = not int(parser.get(guid, 'NoUserPolicy')) + return results + + +def unregister_gp_extension(guid, smb_conf=None): + if not check_guid(guid): + return False + + lp, parser = parse_gpext_conf(smb_conf) + if guid in parser.sections(): + parser.remove_section(guid) + + atomic_write_conf(lp, parser) + + return True + + +def set_privileges(username, uid, gid): + ''' + Set current process privileges + ''' + + os.setegid(gid) + os.seteuid(uid) + + +def drop_privileges(username, func, *args): + ''' + Run supplied function with privileges for specified username. + ''' + current_uid = os.getuid() + + if not current_uid == 0: + raise Exception('Not enough permissions to drop privileges') + + user_uid = pwd.getpwnam(username).pw_uid + user_gid = pwd.getpwnam(username).pw_gid + + # Drop privileges + set_privileges(username, user_uid, user_gid) + + # We need to catch exception in order to be able to restore + # privileges later in this function + out = None + exc = None + try: + out = func(*args) + except Exception as e: + exc = e + + # Restore privileges + set_privileges('root', current_uid, 0) + + if exc: + raise exc + + return out diff --git a/internal/policies/certificate/python/vendor_samba/gp/util/logging.py b/internal/policies/certificate/python/vendor_samba/gp/util/logging.py new file mode 100644 index 000000000..a74a8707d --- /dev/null +++ b/internal/policies/certificate/python/vendor_samba/gp/util/logging.py @@ -0,0 +1,112 @@ +# +# samba-gpupdate enhanced logging +# +# Copyright (C) 2019-2020 BaseALT Ltd. +# Copyright (C) David Mulder 2022 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import json +import datetime +import logging +import gettext +import random +import sys + +logger = logging.getLogger() +def logger_init(name, log_level): + logger = logging.getLogger(name) + logger.addHandler(logging.StreamHandler(sys.stdout)) + logger.setLevel(logging.CRITICAL) + if log_level == 1: + logger.setLevel(logging.ERROR) + elif log_level == 2: + logger.setLevel(logging.WARNING) + elif log_level == 3: + logger.setLevel(logging.INFO) + elif log_level >= 4: + logger.setLevel(logging.DEBUG) + +class slogm(object): + ''' + Structured log message class + ''' + def __init__(self, message, kwargs=None): + if kwargs is None: + kwargs = {} + self.message = message + self.kwargs = kwargs + if not isinstance(self.kwargs, dict): + self.kwargs = { 'val': self.kwargs } + + def __str__(self): + now = str(datetime.datetime.now().isoformat(sep=' ', timespec='milliseconds')) + args = dict() + args.update(self.kwargs) + result = '{}|{} | {}'.format(now, self.message, args) + + return result + +def message_with_code(mtype, message): + random.seed(message) + code = random.randint(0, 99999) + return '[' + mtype + str(code).rjust(5, '0') + ']| ' + \ + gettext.gettext(message) + +class log(object): + @staticmethod + def info(message, data=None): + if data is None: + data = {} + msg = message_with_code('I', message) + logger.info(slogm(msg, data)) + return msg + + @staticmethod + def warning(message, data=None): + if data is None: + data = {} + msg = message_with_code('W', message) + logger.warning(slogm(msg, data)) + return msg + + @staticmethod + def warn(message, data=None): + if data is None: + data = {} + return log.warning(message, data) + + @staticmethod + def error(message, data=None): + if data is None: + data = {} + msg = message_with_code('E', message) + logger.error(slogm(msg, data)) + return msg + + @staticmethod + def fatal(message, data=None): + if data is None: + data = {} + msg = message_with_code('F', message) + logger.fatal(slogm(msg, data)) + return msg + + @staticmethod + def debug(message, data=None): + if data is None: + data = {} + msg = message_with_code('D', message) + logger.debug(slogm(msg, data)) + return msg diff --git a/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_empty_advanced_configuration b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_empty_advanced_configuration new file mode 100644 index 000000000..d64213e44 --- /dev/null +++ b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_empty_advanced_configuration @@ -0,0 +1,11 @@ +Loading smb.conf +[global] +realm = example.com + +Loading state file: #TMPDIR#/state/cert_gpo_state_keypress.tdb +Enroll called + +guid: adsys-cert-autoenroll-keypress +trust_dir: #TMPDIR#/trust; mode: 0o40755 +private_dir: #TMPDIR#/private; mode: 0o40700 +global_trust_dir: #TMPDIR#/ca-certificates; mode: 0o40755 diff --git a/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_simple_configuration b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_simple_configuration new file mode 100644 index 000000000..d64213e44 --- /dev/null +++ b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_simple_configuration @@ -0,0 +1,11 @@ +Loading smb.conf +[global] +realm = example.com + +Loading state file: #TMPDIR#/state/cert_gpo_state_keypress.tdb +Enroll called + +guid: adsys-cert-autoenroll-keypress +trust_dir: #TMPDIR#/trust; mode: 0o40755 +private_dir: #TMPDIR#/private; mode: 0o40700 +global_trust_dir: #TMPDIR#/ca-certificates; mode: 0o40755 diff --git a/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_simple_configuration_and_debug_enabled b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_simple_configuration_and_debug_enabled new file mode 100644 index 000000000..3876ae0a7 --- /dev/null +++ b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_simple_configuration_and_debug_enabled @@ -0,0 +1,12 @@ +Loading smb.conf +[global] +realm = example.com +log level = 10 + +Loading state file: #TMPDIR#/state/cert_gpo_state_keypress.tdb +Enroll called + +guid: adsys-cert-autoenroll-keypress +trust_dir: #TMPDIR#/trust; mode: 0o40755 +private_dir: #TMPDIR#/private; mode: 0o40700 +global_trust_dir: #TMPDIR#/ca-certificates; mode: 0o40755 diff --git a/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_valid_advanced_configuration b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_valid_advanced_configuration new file mode 100644 index 000000000..f2d1af1de --- /dev/null +++ b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_valid_advanced_configuration @@ -0,0 +1,48 @@ +Loading smb.conf +[global] +realm = example.com + +Loading state file: #TMPDIR#/state/cert_gpo_state_keypress.tdb +Enroll called + +guid: adsys-cert-autoenroll-keypress +trust_dir: #TMPDIR#/trust; mode: 0o40755 +private_dir: #TMPDIR#/private; mode: 0o40700 +global_trust_dir: #TMPDIR#/ca-certificates; mode: 0o40755 + +entries: +keyname: Software\Policies\Microsoft\Cryptography\PolicyServers\37c9dc30f207f27f61a2f7c3aed598a6e2920b54 +valuename: AuthFlags +type: 4 +data: 2 + +keyname: Software\Policies\Microsoft\Cryptography\PolicyServers\37c9dc30f207f27f61a2f7c3aed598a6e2920b54 +valuename: Cost +type: 4 +data: 2147483645 + +keyname: Software\Policies\Microsoft\Cryptography\PolicyServers\37c9dc30f207f27f61a2f7c3aed598a6e2920b54 +valuename: Flags +type: 4 +data: 20 + +keyname: Software\Policies\Microsoft\Cryptography\PolicyServers\37c9dc30f207f27f61a2f7c3aed598a6e2920b54 +valuename: FriendlyName +type: 1 +data: ActiveDirectoryEnrollmentPolicy + +keyname: Software\Policies\Microsoft\Cryptography\PolicyServers\37c9dc30f207f27f61a2f7c3aed598a6e2920b54 +valuename: PolicyID +type: 1 +data: {A5E9BF57-71C6-443A-B7FC-79EFA6F73EBD} + +keyname: Software\Policies\Microsoft\Cryptography\PolicyServers\37c9dc30f207f27f61a2f7c3aed598a6e2920b54 +valuename: URL +type: 1 +data: LDAP: + +keyname: Software\Policies\Microsoft\Cryptography\PolicyServers +valuename: Flags +type: 4 +data: 0 + diff --git a/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/unenroll b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/unenroll new file mode 100644 index 000000000..8b44d2e33 --- /dev/null +++ b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/unenroll @@ -0,0 +1,8 @@ +Loading smb.conf +[global] +realm = example.com + +Loading state file: #TMPDIR#/state/cert_gpo_state_keypress.tdb +Unenroll called +guid: adsys-cert-autoenroll-keypress +remove: ['ZXhhbXBsZS1DQQ=='] diff --git a/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_enroll b/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_enroll new file mode 100644 index 000000000..1dbbbe06a --- /dev/null +++ b/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_enroll @@ -0,0 +1,3 @@ +enroll keypress example.com --policy_servers_json null --samba_cache_dir #TMPDIR#/statedir/samba +KRB5CCNAME=#TMPDIR#/rundir/krb5cc/keypress +PYTHONPATH=#TMPDIR#/sharedir/python diff --git a/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_enroll,_advanced_configuration b/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_enroll,_advanced_configuration new file mode 100644 index 000000000..7af6802c2 --- /dev/null +++ b/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_enroll,_advanced_configuration @@ -0,0 +1,3 @@ +enroll keypress example.com --policy_servers_json [{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54","valuename":"AuthFlags","data":2,"type":4},{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54","valuename":"Cost","data":2147483645,"type":4},{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54","valuename":"Flags","data":20,"type":4},{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54","valuename":"FriendlyName","data":"ActiveDirectoryEnrollmentPolicy","type":1},{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54","valuename":"PolicyID","data":"{A5E9BF57-71C6-443A-B7FC-79EFA6F73EBD}","type":1},{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54","valuename":"URL","data":"LDAP:","type":1},{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers","valuename":"Flags","data":0,"type":4}] --samba_cache_dir #TMPDIR#/statedir/samba +KRB5CCNAME=#TMPDIR#/rundir/krb5cc/keypress +PYTHONPATH=#TMPDIR#/sharedir/python diff --git a/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_unenroll b/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_unenroll new file mode 100644 index 000000000..5d4686e66 --- /dev/null +++ b/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_unenroll @@ -0,0 +1,3 @@ +unenroll keypress example.com --policy_servers_json null --samba_cache_dir #TMPDIR#/statedir/samba +KRB5CCNAME=#TMPDIR#/rundir/krb5cc/keypress +PYTHONPATH=#TMPDIR#/sharedir/python diff --git a/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_no_entries,_samba_cache_present b/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_no_entries,_samba_cache_present new file mode 100644 index 000000000..e877e31f7 --- /dev/null +++ b/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_no_entries,_samba_cache_present @@ -0,0 +1,3 @@ +unenroll keypress example.com --samba_cache_dir #TMPDIR#/statedir/samba +KRB5CCNAME=#TMPDIR#/rundir/krb5cc/keypress +PYTHONPATH=#TMPDIR#/sharedir/python diff --git a/internal/policies/manager.go b/internal/policies/manager.go index 0c77642a1..953ef5f75 100644 --- a/internal/policies/manager.go +++ b/internal/policies/manager.go @@ -33,10 +33,12 @@ import ( "time" "github.com/godbus/dbus/v5" + "github.com/ubuntu/adsys/internal/ad/backends" "github.com/ubuntu/adsys/internal/consts" log "github.com/ubuntu/adsys/internal/grpc/logstreamer" "github.com/ubuntu/adsys/internal/i18n" "github.com/ubuntu/adsys/internal/policies/apparmor" + "github.com/ubuntu/adsys/internal/policies/certificate" "github.com/ubuntu/adsys/internal/policies/dconf" "github.com/ubuntu/adsys/internal/policies/entry" "github.com/ubuntu/adsys/internal/policies/gdm" @@ -52,20 +54,23 @@ import ( // ProOnlyRules are the rules that are only available for Pro subscribers. They // will be filtered otherwise. -var ProOnlyRules = []string{"privilege", "scripts", "mount", "apparmor", "proxy"} +var ProOnlyRules = []string{"privilege", "scripts", "mount", "apparmor", "proxy", "certificate"} // Manager handles all managers for various policy handlers. type Manager struct { policiesCacheDir string hostname string - dconf *dconf.Manager - privilege *privilege.Manager - scripts *scripts.Manager - mount *mount.Manager - gdm *gdm.Manager - apparmor *apparmor.Manager - proxy *proxy.Manager + backend backends.Backend + + dconf *dconf.Manager + privilege *privilege.Manager + scripts *scripts.Manager + mount *mount.Manager + gdm *gdm.Manager + apparmor *apparmor.Manager + proxy *proxy.Manager + certificate *certificate.Manager subscriptionDbus dbus.BusObject @@ -88,10 +93,12 @@ type systemdCaller interface { type options struct { cacheDir string + stateDir string dconfDir string sudoersDir string policyKitDir string runDir string + shareDir string apparmorDir string apparmorFsDir string systemUnitDir string @@ -100,6 +107,7 @@ type options struct { gdm *gdm.Manager apparmorParserCmd []string + certAutoenrollCmd []string } // Option reprents an optional function to change Policies behavior. @@ -113,6 +121,14 @@ func WithCacheDir(p string) Option { } } +// WithStateDir specifies a personalized state directory. +func WithStateDir(p string) Option { + return func(o *options) error { + o.stateDir = p + return nil + } +} + // WithDconfDir specifies a personalized dconf directory. func WithDconfDir(p string) Option { return func(o *options) error { @@ -145,6 +161,14 @@ func WithRunDir(p string) Option { } } +// WithShareDir specifies a personalized share directory. +func WithShareDir(p string) Option { + return func(o *options) error { + o.shareDir = p + return nil + } +} + // WithApparmorDir specifies a personalized apparmor directory. func WithApparmorDir(p string) Option { return func(o *options) error { @@ -194,8 +218,16 @@ func WithSystemdCaller(p systemdCaller) Option { } } +// WithCertAutoenrollCmd specifies a personalized certificate autoenroll command. +func WithCertAutoenrollCmd(cmd []string) Option { + return func(o *options) error { + o.certAutoenrollCmd = cmd + return nil + } +} + // NewManager returns a new manager with all default policy handlers. -func NewManager(bus *dbus.Conn, hostname string, opts ...Option) (m *Manager, err error) { +func NewManager(bus *dbus.Conn, hostname string, backend backends.Backend, opts ...Option) (m *Manager, err error) { defer decorate.OnError(&err, i18n.G("can't create a new policy handlers manager")) defaultSystemdCaller, err := systemd.New(bus) @@ -206,7 +238,9 @@ func NewManager(bus *dbus.Conn, hostname string, opts ...Option) (m *Manager, er // defaults args := options{ cacheDir: consts.DefaultCacheDir, + stateDir: consts.DefaultStateDir, runDir: consts.DefaultRunDir, + shareDir: consts.DefaultShareDir, apparmorDir: consts.DefaultApparmorDir, systemUnitDir: consts.DefaultSystemUnitDir, systemdCaller: defaultSystemdCaller, @@ -256,6 +290,17 @@ func NewManager(bus *dbus.Conn, hostname string, opts ...Option) (m *Manager, er } proxyManager := proxy.New(bus, proxyOptions...) + // certificate manager + certificateOpts := []certificate.Option{ + certificate.WithStateDir(args.stateDir), + certificate.WithRunDir(args.runDir), + certificate.WithShareDir(args.shareDir), + } + if args.certAutoenrollCmd != nil { + certificateOpts = append(certificateOpts, certificate.WithCertAutoenrollCmd(args.certAutoenrollCmd)) + } + certificateManager := certificate.New(backend.Domain(), certificateOpts...) + // inject applied dconf mangager if we need to build a gdm manager if args.gdm == nil { if args.gdm, err = gdm.New(gdm.WithDconf(dconfManager)); err != nil { @@ -272,6 +317,7 @@ func NewManager(bus *dbus.Conn, hostname string, opts ...Option) (m *Manager, er dbus.ObjectPath(consts.SubscriptionDbusObjectPath)) return &Manager{ + backend: backend, policiesCacheDir: policiesCacheDir, hostname: hostname, dconf: dconfManager, @@ -280,6 +326,7 @@ func NewManager(bus *dbus.Conn, hostname string, opts ...Option) (m *Manager, er mount: mountManager, apparmor: apparmorManager, proxy: proxyManager, + certificate: certificateManager, gdm: args.gdm, subscriptionDbus: subscriptionDbus, @@ -337,6 +384,11 @@ func (m *Manager) ApplyPolicies(ctx context.Context, objectName string, isComput g.Go(func() error { return m.proxy.ApplyPolicy(ctx, objectName, isComputer, rules["proxy"]) }) + g.Go(func() error { + // Ignore error as we don't want to fail because of online status this late in the process + isOnline, _ := m.backend.IsOnline() + return m.certificate.ApplyPolicy(ctx, objectName, isComputer, isOnline, rules["certificate"]) + }) if err := g.Wait(); err != nil { return err } diff --git a/internal/policies/manager_test.go b/internal/policies/manager_test.go index 018a68dc9..370589c9c 100644 --- a/internal/policies/manager_test.go +++ b/internal/policies/manager_test.go @@ -41,10 +41,12 @@ func TestApplyPolicies(t *testing.T) { isNotSubscribed bool secondCallWithNoSubscription bool noUbuntuProxyManager bool + backendOfflineError bool wantErr bool }{ "Succeed": {policiesDir: "all_entry_types"}, + "Succeed if checking for backend online status returns an error": {backendOfflineError: true, policiesDir: "all_entry_types"}, "Second call with no rules deletes everything": {policiesDir: "all_entry_types", secondCallWithNoRules: true, scriptSessionEndedForSecondCall: true}, "Second call with no rules don't remove scripts if session hasn’t ended": {policiesDir: "all_entry_types", secondCallWithNoRules: true, scriptSessionEndedForSecondCall: false}, @@ -54,12 +56,13 @@ func TestApplyPolicies(t *testing.T) { "Second call with no subscription don't remove scripts if session hasn’t ended": {policiesDir: "all_entry_types", secondCallWithNoSubscription: true, scriptSessionEndedForSecondCall: false}, // Error cases - "Error when applying dconf policy": {policiesDir: "dconf_failing", wantErr: true}, - "Error when applying privilege policy": {makeDirReadOnly: "etc/sudoers.d", policiesDir: "all_entry_types", wantErr: true}, - "Error when applying scripts policy": {makeDirReadOnly: "run/adsys/machine", policiesDir: "all_entry_types", wantErr: true}, - "Error when applying apparmor policy": {makeDirReadOnly: "etc/apparmor.d/adsys", policiesDir: "all_entry_types", wantErr: true}, - "Error when applying mount policy": {makeDirReadOnly: "etc/systemd/system", policiesDir: "all_entry_types", wantErr: true}, - "Error when applying proxy policy": {noUbuntuProxyManager: true, policiesDir: "all_entry_types", wantErr: true}, + "Error when applying dconf policy": {policiesDir: "dconf_failing", wantErr: true}, + "Error when applying privilege policy": {makeDirReadOnly: "etc/sudoers.d", policiesDir: "all_entry_types", wantErr: true}, + "Error when applying scripts policy": {makeDirReadOnly: "run/adsys/machine", policiesDir: "all_entry_types", wantErr: true}, + "Error when applying apparmor policy": {makeDirReadOnly: "etc/apparmor.d/adsys", policiesDir: "all_entry_types", wantErr: true}, + "Error when applying mount policy": {makeDirReadOnly: "etc/systemd/system", policiesDir: "all_entry_types", wantErr: true}, + "Error when applying proxy policy": {noUbuntuProxyManager: true, policiesDir: "all_entry_types", wantErr: true}, + "Error when applying certificate policy": {policiesDir: "certificate_failing", wantErr: true}, } for name, tc := range tests { tc := tc @@ -80,6 +83,8 @@ func TestApplyPolicies(t *testing.T) { sudoersDir := filepath.Join(fakeRootDir, "etc", "sudoers.d") apparmorDir := filepath.Join(fakeRootDir, "etc", "apparmor.d", "adsys") systemUnitDir := filepath.Join(fakeRootDir, "etc", "systemd", "system") + stateDir := filepath.Join(fakeRootDir, "var", "lib", "adsys") + shareDir := filepath.Join(fakeRootDir, "usr", "share", "adsys") loadedPoliciesFile := filepath.Join(fakeRootDir, "sys", "kernel", "security", "apparmor", "profiles") err = os.MkdirAll(filepath.Dir(loadedPoliciesFile), 0700) @@ -98,14 +103,18 @@ func TestApplyPolicies(t *testing.T) { m, err := policies.NewManager(bus, hostname, + mockBackend{}, policies.WithCacheDir(cacheDir), + policies.WithStateDir(stateDir), policies.WithRunDir(runDir), + policies.WithShareDir(shareDir), policies.WithDconfDir(dconfDir), policies.WithPolicyKitDir(policyKitDir), policies.WithSudoersDir(sudoersDir), policies.WithApparmorDir(apparmorDir), policies.WithApparmorFsDir(filepath.Dir(loadedPoliciesFile)), policies.WithApparmorParserCmd([]string{"/bin/true"}), + policies.WithCertAutoenrollCmd([]string{"/bin/true"}), policies.WithSystemUnitDir(systemUnitDir), policies.WithProxyApplier(&mockProxyApplier{wantApplyError: tc.noUbuntuProxyManager}), policies.WithSystemdCaller(&testutils.MockSystemdCaller{}), @@ -282,7 +291,7 @@ func TestDumpPolicies(t *testing.T) { t.Parallel() cacheDir, runDir := t.TempDir(), t.TempDir() - m, err := policies.NewManager(bus, hostname, policies.WithCacheDir(cacheDir), policies.WithRunDir(runDir)) + m, err := policies.NewManager(bus, hostname, mockBackend{}, policies.WithCacheDir(cacheDir), policies.WithRunDir(runDir)) require.NoError(t, err, "Setup: couldn’t get a new policy manager") err = os.MkdirAll(filepath.Join(cacheDir, policies.PoliciesCacheBaseName), 0750) @@ -349,7 +358,7 @@ func TestLastUpdateFor(t *testing.T) { t.Parallel() cacheDir, runDir := t.TempDir(), t.TempDir() - m, err := policies.NewManager(bus, hostname, policies.WithCacheDir(cacheDir), policies.WithRunDir(runDir)) + m, err := policies.NewManager(bus, hostname, mockBackend{}, policies.WithCacheDir(cacheDir), policies.WithRunDir(runDir)) require.NoError(t, err, "Setup: couldn’t get a new policy manager") err = os.MkdirAll(filepath.Join(cacheDir, policies.PoliciesCacheBaseName), 0750) @@ -411,7 +420,7 @@ func TestGetSubscriptionState(t *testing.T) { }() cacheDir, runDir := t.TempDir(), t.TempDir() - m, err := policies.NewManager(bus, hostname, policies.WithCacheDir(cacheDir), policies.WithRunDir(runDir)) + m, err := policies.NewManager(bus, hostname, mockBackend{}, policies.WithCacheDir(cacheDir), policies.WithRunDir(runDir)) require.NoError(t, err, "Setup: couldn’t get a new policy manager") got := m.GetSubscriptionState(context.Background()) @@ -435,3 +444,20 @@ func (d *mockProxyApplier) Call(_ string, _ dbus.Flags, _ ...interface{}) *dbus. return &dbus.Call{Err: errApply} } + +// mockBackend is a mock for the backend object. +type mockBackend struct { + wantOnlineErr bool +} + +func (m mockBackend) Domain() string { return "example.com" } +func (m mockBackend) ServerURL(context.Context) (string, error) { return "adc.example.com", nil } +func (m mockBackend) HostKrb5CCName() (string, error) { return "/tmp/krb5cc_0", nil } +func (m mockBackend) DefaultDomainSuffix() string { return "example.com" } +func (m mockBackend) IsOnline() (bool, error) { + if m.wantOnlineErr { + return false, errors.New("mock error") + } + return true, nil +} +func (m mockBackend) Config() string { return "mock config" } diff --git a/internal/policies/testdata/TestApplyPolicies/golden/no_subscription_is_only_dconf_content/var/cache/adsys/policies/hostname/policies b/internal/policies/testdata/TestApplyPolicies/golden/no_subscription_is_only_dconf_content/var/cache/adsys/policies/hostname/policies index 65c86435b..0fa3a5d6a 100644 --- a/internal/policies/testdata/TestApplyPolicies/golden/no_subscription_is_only_dconf_content/var/cache/adsys/policies/hostname/policies +++ b/internal/policies/testdata/TestApplyPolicies/golden/no_subscription_is_only_dconf_content/var/cache/adsys/policies/hostname/policies @@ -9,6 +9,10 @@ gpos: usr.bin.bar nested/usr.bin.baz disabled: false + certificate: + - key: autoenroll + value: "7" + disabled: false dconf: - key: path/to/key1 value: ValueOfKey1 diff --git "a/internal/policies/testdata/TestApplyPolicies/golden/second_call_with_no_subscription_don't_remove_scripts_if_session_hasn\342\200\231t_ended/var/cache/adsys/policies/hostname/policies" "b/internal/policies/testdata/TestApplyPolicies/golden/second_call_with_no_subscription_don't_remove_scripts_if_session_hasn\342\200\231t_ended/var/cache/adsys/policies/hostname/policies" index 65c86435b..0fa3a5d6a 100644 --- "a/internal/policies/testdata/TestApplyPolicies/golden/second_call_with_no_subscription_don't_remove_scripts_if_session_hasn\342\200\231t_ended/var/cache/adsys/policies/hostname/policies" +++ "b/internal/policies/testdata/TestApplyPolicies/golden/second_call_with_no_subscription_don't_remove_scripts_if_session_hasn\342\200\231t_ended/var/cache/adsys/policies/hostname/policies" @@ -9,6 +9,10 @@ gpos: usr.bin.bar nested/usr.bin.baz disabled: false + certificate: + - key: autoenroll + value: "7" + disabled: false dconf: - key: path/to/key1 value: ValueOfKey1 diff --git a/internal/policies/testdata/TestApplyPolicies/golden/second_call_with_no_subscription_should_remove_everything_but_dconf_content/var/cache/adsys/policies/hostname/policies b/internal/policies/testdata/TestApplyPolicies/golden/second_call_with_no_subscription_should_remove_everything_but_dconf_content/var/cache/adsys/policies/hostname/policies index 65c86435b..0fa3a5d6a 100644 --- a/internal/policies/testdata/TestApplyPolicies/golden/second_call_with_no_subscription_should_remove_everything_but_dconf_content/var/cache/adsys/policies/hostname/policies +++ b/internal/policies/testdata/TestApplyPolicies/golden/second_call_with_no_subscription_should_remove_everything_but_dconf_content/var/cache/adsys/policies/hostname/policies @@ -9,6 +9,10 @@ gpos: usr.bin.bar nested/usr.bin.baz disabled: false + certificate: + - key: autoenroll + value: "7" + disabled: false dconf: - key: path/to/key1 value: ValueOfKey1 diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed/var/cache/adsys/policies/hostname/policies b/internal/policies/testdata/TestApplyPolicies/golden/succeed/var/cache/adsys/policies/hostname/policies index 65c86435b..0fa3a5d6a 100644 --- a/internal/policies/testdata/TestApplyPolicies/golden/succeed/var/cache/adsys/policies/hostname/policies +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed/var/cache/adsys/policies/hostname/policies @@ -9,6 +9,10 @@ gpos: usr.bin.bar nested/usr.bin.baz disabled: false + certificate: + - key: autoenroll + value: "7" + disabled: false dconf: - key: path/to/key1 value: ValueOfKey1 diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/apparmor.d/adsys/machine/nested/usr.bin.baz b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/apparmor.d/adsys/machine/nested/usr.bin.baz new file mode 100644 index 000000000..c3fdc981e --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/apparmor.d/adsys/machine/nested/usr.bin.baz @@ -0,0 +1 @@ +/usr/bin/baz {} diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/apparmor.d/adsys/machine/usr.bin.bar b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/apparmor.d/adsys/machine/usr.bin.bar new file mode 100644 index 000000000..9fc2774f1 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/apparmor.d/adsys/machine/usr.bin.bar @@ -0,0 +1 @@ +/usr/bin/bar {} diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/apparmor.d/adsys/machine/usr.bin.foo b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/apparmor.d/adsys/machine/usr.bin.foo new file mode 100644 index 000000000..450648222 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/apparmor.d/adsys/machine/usr.bin.foo @@ -0,0 +1 @@ +/usr/bin/foo {} diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/dconf/db/machine.d/adsys b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/dconf/db/machine.d/adsys new file mode 100644 index 000000000..3067d2ff6 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/dconf/db/machine.d/adsys @@ -0,0 +1,5 @@ +[path/to] +key1='ValueOfKey1' +key2='ValueOfKey2 +On +Multilines' diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/dconf/db/machine.d/locks/adsys b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/dconf/db/machine.d/locks/adsys new file mode 100644 index 000000000..82ce2888c --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/dconf/db/machine.d/locks/adsys @@ -0,0 +1,2 @@ +/path/to/key1 +/path/to/key2 diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/polkit-1/localauthority.conf.d/99-adsys-privilege-enforcement.conf b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/polkit-1/localauthority.conf.d/99-adsys-privilege-enforcement.conf new file mode 100644 index 000000000..dbf1b12ee --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/polkit-1/localauthority.conf.d/99-adsys-privilege-enforcement.conf @@ -0,0 +1,6 @@ +# This file is managed by adsys. +# Do not edit this file manually. +# Any changes will be overwritten. + +[Configuration] +AdminIdentities=unix-user:alice@domain;unix-user:bob@domain2;unix-group:mygroup@domain;unix-user:cosmic carole@domain diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/sudoers.d/99-adsys-privilege-enforcement b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/sudoers.d/99-adsys-privilege-enforcement new file mode 100644 index 000000000..e97388f49 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/sudoers.d/99-adsys-privilege-enforcement @@ -0,0 +1,9 @@ +# This file is managed by adsys. +# Do not edit this file manually. +# Any changes will be overwritten. + +"alice@domain" ALL=(ALL:ALL) ALL +"bob@domain2" ALL=(ALL:ALL) ALL +"%mygroup@domain" ALL=(ALL:ALL) ALL +"cosmic carole@domain" ALL=(ALL:ALL) ALL + diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/systemd/system/adsys-cifs-example.com-smb_share.mount b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/systemd/system/adsys-cifs-example.com-smb_share.mount new file mode 100644 index 000000000..74da4d221 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/systemd/system/adsys-cifs-example.com-smb_share.mount @@ -0,0 +1,17 @@ +# This template defines the basic structure of a mount unit generated by ADSys for system mounts. +[Unit] +Description=ADSys mount for smb://example.com/smb_share +After=network-online.target +Requires=network-online.target + +[Mount] +What=//example.com/smb_share +Where=/adsys/cifs/example.com/smb_share +Type=cifs +Options=defaults +# This option prevents hangs on shutdown due to an unreachable network share. +LazyUnmount=true +TimeoutSec=30 + +[Install] +WantedBy=default.target diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/systemd/system/adsys-fuse-example.com-ftp_share.mount b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/systemd/system/adsys-fuse-example.com-ftp_share.mount new file mode 100644 index 000000000..f051513af --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/systemd/system/adsys-fuse-example.com-ftp_share.mount @@ -0,0 +1,17 @@ +# This template defines the basic structure of a mount unit generated by ADSys for system mounts. +[Unit] +Description=ADSys mount for ftp://example.com/ftp_share +After=network-online.target +Requires=network-online.target + +[Mount] +What=curlftpfs#example.com +Where=/adsys/fuse/example.com/ftp_share +Type=fuse +Options=defaults +# This option prevents hangs on shutdown due to an unreachable network share. +LazyUnmount=true +TimeoutSec=30 + +[Install] +WantedBy=default.target diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/systemd/system/adsys-nfs-example.com-nfs_share.mount b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/systemd/system/adsys-nfs-example.com-nfs_share.mount new file mode 100644 index 000000000..bdfa1c268 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/systemd/system/adsys-nfs-example.com-nfs_share.mount @@ -0,0 +1,17 @@ +# This template defines the basic structure of a mount unit generated by ADSys for system mounts. +[Unit] +Description=ADSys mount for nfs://example.com/nfs_share +After=network-online.target +Requires=network-online.target + +[Mount] +What=example.com:/nfs_share +Where=/adsys/nfs/example.com/nfs_share +Type=nfs +Options=defaults +# This option prevents hangs on shutdown due to an unreachable network share. +LazyUnmount=true +TimeoutSec=30 + +[Install] +WantedBy=default.target diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/.ready b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/.ready new file mode 100644 index 000000000..e69de29bb diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/.running b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/.running new file mode 100644 index 000000000..e69de29bb diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/logoff b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/logoff new file mode 100644 index 000000000..f1f55fa88 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/logoff @@ -0,0 +1 @@ +scripts/otherfolder/script-user-logoff diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/logon b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/logon new file mode 100644 index 000000000..0aa0488d4 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/logon @@ -0,0 +1 @@ +scripts/script-user-logon diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/empty-subfolder/.empty b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/empty-subfolder/.empty new file mode 100644 index 000000000..e69de29bb diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/final-machine-script.sh b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/final-machine-script.sh new file mode 100755 index 000000000..ae7b4be6c --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/final-machine-script.sh @@ -0,0 +1 @@ +final machine script diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/otherfolder/script-user-logoff b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/otherfolder/script-user-logoff new file mode 100755 index 000000000..4080816a8 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/otherfolder/script-user-logoff @@ -0,0 +1 @@ +script user logoff diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/script-machine-shutdown b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/script-machine-shutdown new file mode 100755 index 000000000..4dc4c0713 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/script-machine-shutdown @@ -0,0 +1 @@ +script machine shutdown diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/script-machine-startup b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/script-machine-startup new file mode 100755 index 000000000..5adba498f --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/script-machine-startup @@ -0,0 +1 @@ +script machine startup diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/script-user-logon b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/script-user-logon new file mode 100755 index 000000000..e5a483136 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/script-user-logon @@ -0,0 +1 @@ +script user logon diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/subfolder/other-script b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/subfolder/other-script new file mode 100755 index 000000000..47e740068 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/subfolder/other-script @@ -0,0 +1 @@ +subfolder other script diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/unreferenced-data b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/unreferenced-data new file mode 100644 index 000000000..802d880a9 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/unreferenced-data @@ -0,0 +1 @@ +unreferenced data diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/unreferenced-script b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/unreferenced-script new file mode 100644 index 000000000..be58cc792 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/unreferenced-script @@ -0,0 +1 @@ +unreferenced script diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/shutdown b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/shutdown new file mode 100644 index 000000000..58c17cc29 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/shutdown @@ -0,0 +1 @@ +scripts/script-machine-shutdown diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/startup b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/startup new file mode 100644 index 000000000..62ca19d92 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/startup @@ -0,0 +1,3 @@ +scripts/script-machine-startup +scripts/subfolder/other-script +scripts/final-machine-script.sh diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/users/.empty b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/users/.empty new file mode 100644 index 000000000..e69de29bb diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/sys/kernel/security/apparmor/profiles b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/sys/kernel/security/apparmor/profiles new file mode 100644 index 000000000..bb27bcb87 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/sys/kernel/security/apparmor/profiles @@ -0,0 +1 @@ +someprofile (enforce) diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/var/cache/adsys/policies/hostname/assets.db b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/var/cache/adsys/policies/hostname/assets.db new file mode 100644 index 000000000..3b52764c0 Binary files /dev/null and b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/var/cache/adsys/policies/hostname/assets.db differ diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/var/cache/adsys/policies/hostname/policies b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/var/cache/adsys/policies/hostname/policies new file mode 100644 index 000000000..0fa3a5d6a --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/var/cache/adsys/policies/hostname/policies @@ -0,0 +1,74 @@ +gpos: + - id: '{GPOId}' + name: GPOName + rules: + apparmor: + - key: apparmor-machine + value: | + usr.bin.foo + usr.bin.bar + nested/usr.bin.baz + disabled: false + certificate: + - key: autoenroll + value: "7" + disabled: false + dconf: + - key: path/to/key1 + value: ValueOfKey1 + disabled: false + meta: s + - key: path/to/key2 + value: | + ValueOfKey2 + On + Multilines + disabled: false + meta: s + mount: + - key: system-mounts + value: | + nfs://example.com/nfs_share + smb://example.com/smb_share + ftp://example.com/ftp_share + disabled: false + privilege: + - key: allow-local-admins + value: "" + disabled: false + - key: client-admins + value: | + alice@domain + bob@domain2 + %mygroup@domain + cosmic carole@domain + disabled: false + proxy: + - key: proxy/auto + value: http://example.com/proxy.pac + disabled: false + - key: proxy/http + value: "" + disabled: true + - key: proxy/no-proxy + value: localhost,127.0.0.1,::1 + disabled: false + scripts: + - key: startup + value: | + script-machine-startup + subfolder/other-script + final-machine-script.sh + disabled: false + - key: shutdown + value: | + script-machine-shutdown + disabled: false + - key: logon + value: | + script-user-logon + disabled: false + - key: logoff + value: | + otherfolder/script-user-logoff + disabled: false diff --git a/internal/policies/testdata/cache/policies/all_entry_types/policies b/internal/policies/testdata/cache/policies/all_entry_types/policies index 61abf0957..a25406b76 100644 --- a/internal/policies/testdata/cache/policies/all_entry_types/policies +++ b/internal/policies/testdata/cache/policies/all_entry_types/policies @@ -56,3 +56,7 @@ gpos: disabled: true - key: proxy/no-proxy value: localhost,127.0.0.1,::1 + certificate: + - key: autoenroll + value: "7" + disabled: false diff --git a/internal/policies/testdata/cache/policies/certificate_failing/policies b/internal/policies/testdata/cache/policies/certificate_failing/policies new file mode 100644 index 000000000..a51413132 --- /dev/null +++ b/internal/policies/testdata/cache/policies/certificate_failing/policies @@ -0,0 +1,8 @@ +gpos: +- id: '{GPOId}' + name: GPOName + rules: + certificate: + - key: autoenroll + value: "NotANumber" + disabled: false diff --git a/internal/testutils/admock/samba/credentials/__init__.py b/internal/testutils/admock/samba/credentials/__init__.py index 6266c7dcc..2029abf3f 100644 --- a/internal/testutils/admock/samba/credentials/__init__.py +++ b/internal/testutils/admock/samba/credentials/__init__.py @@ -9,3 +9,6 @@ def set_kerberos_state(self, val): def guess(self, val): pass + + def get_username(self): + pass diff --git a/internal/testutils/admock/samba/dcerpc/preg.py b/internal/testutils/admock/samba/dcerpc/preg.py new file mode 100644 index 000000000..6c895c813 --- /dev/null +++ b/internal/testutils/admock/samba/dcerpc/preg.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass +from typing import Any + +@dataclass +class entry: + keyname: str = None + valuename: str = None + type: int = None + data: Any = None diff --git a/internal/testutils/admock/samba/param/__init__.py b/internal/testutils/admock/samba/param/__init__.py index 574eddfc1..f85b80128 100644 --- a/internal/testutils/admock/samba/param/__init__.py +++ b/internal/testutils/admock/samba/param/__init__.py @@ -1,2 +1,6 @@ -def LoadParm(): - return \ No newline at end of file +def LoadParm(smb_conf=None): + if smb_conf is None: + return + print('Loading smb.conf') + with open(smb_conf, 'r') as f: + print(f.read()) diff --git a/internal/testutils/admock/vendor_samba/__init__.py b/internal/testutils/admock/vendor_samba/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/internal/testutils/admock/vendor_samba/gp/__init__.py b/internal/testutils/admock/vendor_samba/gp/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/internal/testutils/admock/vendor_samba/gp/gp_cert_auto_enroll_ext.py b/internal/testutils/admock/vendor_samba/gp/gp_cert_auto_enroll_ext.py new file mode 100644 index 000000000..04de011af --- /dev/null +++ b/internal/testutils/admock/vendor_samba/gp/gp_cert_auto_enroll_ext.py @@ -0,0 +1,38 @@ +import os + +class gp_cert_auto_enroll_ext(object): + def __init__(self, _lp, _credentials, _username, _store): + pass + + def cache_get_all_attribute_values(self, _guid): + return {'ZXhhbXBsZS1DQQ==': '{"files": ["/var/lib/adsys/certs/galacticcafe-CA.0.crt"]}'} + + def __enroll(self, guid, entries, trust_dir, private_dir, global_trust_dir): + if os.getenv('ADSYS_WANT_AUTOENROLL_ERROR'): + raise Exception('Autoenroll error requested') + + print('Enroll called') + print() + print(f'guid: {guid}') + print(f'trust_dir: {trust_dir}; mode: {oct(os.stat(trust_dir).st_mode)}') + print(f'private_dir: {private_dir}; mode: {oct(os.stat(private_dir).st_mode)}') + print(f'global_trust_dir: {global_trust_dir}; mode: {oct(os.stat(global_trust_dir).st_mode)}') + + if entries == []: + return + + print('\nentries:') + for entry in entries: + print(f'''keyname: {entry.keyname} +valuename: {entry.valuename} +type: {entry.type} +data: {entry.data} +''') + + def clean(self, guid, remove=None): + if os.getenv('ADSYS_WANT_AUTOENROLL_ERROR'): + raise Exception('Autoenroll error requested') + + print('Unenroll called') + print(f'guid: {guid}') + print(f'remove: {remove}') diff --git a/internal/testutils/admock/vendor_samba/gp/gpclass.py b/internal/testutils/admock/vendor_samba/gp/gpclass.py new file mode 100644 index 000000000..0a1735aec --- /dev/null +++ b/internal/testutils/admock/vendor_samba/gp/gpclass.py @@ -0,0 +1,4 @@ +import os + +def GPOStorage(state_file): + print(f'Loading state file: {state_file}')