first commit

This commit is contained in:
2024-10-28 23:04:48 +01:00
commit 1ee55157f1
911 changed files with 325331 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
/*
© Copyright IBM Corporation 2017, 2022
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package command contains code to run external commands
package command
import (
"context"
"fmt"
"os/exec"
)
// Run runs an OS command. On Linux it waits for the command to
// complete and returns the exit status (return code).
// Do not use this function to run shell built-ins (like "cd"), because
// the error handling works differently
func Run(name string, arg ...string) (string, int, error) {
return RunContext(context.Background(), name, arg...)
}
func RunContext(ctx context.Context, name string, arg ...string) (string, int, error) {
// Run the command and wait for completion
// #nosec G204
cmd := exec.CommandContext(ctx, name, arg...)
out, err := cmd.CombinedOutput()
rc := cmd.ProcessState.ExitCode()
if err != nil {
return string(out), rc, fmt.Errorf("%v: %v", cmd.Path, err)
}
return string(out), rc, nil
}

View File

@@ -0,0 +1,47 @@
/*
© Copyright IBM Corporation 2017
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package command
import (
"runtime"
"testing"
)
var commandTests = []struct {
name string
arg []string
rc int
}{
{"ls", []string{}, 0},
{"ls", []string{"madeup"}, 2},
{"bash", []string{"-c", "exit 99"}, 99},
}
func TestRun(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("Skipping tests for package which only works on Linux")
}
for _, table := range commandTests {
arg := table.arg
_, rc, err := Run(table.name, arg...)
if rc != table.rc {
t.Errorf("Run(%v,%v) - expected %v, got %v", table.name, table.arg, table.rc, rc)
}
if rc != 0 && err == nil {
t.Errorf("Run(%v,%v) - expected error for non-zero return code (rc=%v)", table.name, table.arg, rc)
}
}
}

View File

@@ -0,0 +1,287 @@
/*
The MIT License (MIT)
Copyright (c) 2018 Jessica Frazelle
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/*
The code for amicontained.go is forked from
https://github.com/genuinetools/bpfd/blob/434b609b3d4a5aeb461109b1167b68e000b72f69/proc/proc.go
The code was forked when the latest details are as "Latest commit 871fc34 on Sep 18, 2018"
*/
// Adding IBM Copyright since the forked code had to be modified to remove deprecated ioutil package
/*
© Copyright IBM Corporation 2023
*/
package containerruntime
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/syndtr/gocapability/capability"
"golang.org/x/sys/unix"
)
// ContainerRuntime is the type for the various container runtime strings.
type ContainerRuntime string
// SeccompMode is the type for the various seccomp mode strings.
type SeccompMode string
const (
// RuntimeDocker is the string for the docker runtime.
RuntimeDocker ContainerRuntime = "docker"
// RuntimeRkt is the string for the rkt runtime.
RuntimeRkt ContainerRuntime = "rkt"
// RuntimeNspawn is the string for the systemd-nspawn runtime.
RuntimeNspawn ContainerRuntime = "systemd-nspawn"
// RuntimeLXC is the string for the lxc runtime.
RuntimeLXC ContainerRuntime = "lxc"
// RuntimeLXCLibvirt is the string for the lxc-libvirt runtime.
RuntimeLXCLibvirt ContainerRuntime = "lxc-libvirt"
// RuntimeOpenVZ is the string for the openvz runtime.
RuntimeOpenVZ ContainerRuntime = "openvz"
// RuntimeKubernetes is the string for the kubernetes runtime.
RuntimeKubernetes ContainerRuntime = "kube"
// RuntimeGarden is the string for the garden runtime.
RuntimeGarden ContainerRuntime = "garden"
// RuntimePodman is the string for the podman runtime.
RuntimePodman ContainerRuntime = "podman"
// RuntimeNotFound is the string for when no container runtime is found.
RuntimeNotFound ContainerRuntime = "not-found"
// SeccompModeDisabled is equivalent to "0" in the /proc/{pid}/status file.
SeccompModeDisabled SeccompMode = "disabled"
// SeccompModeStrict is equivalent to "1" in the /proc/{pid}/status file.
SeccompModeStrict SeccompMode = "strict"
// SeccompModeFiltering is equivalent to "2" in the /proc/{pid}/status file.
SeccompModeFiltering SeccompMode = "filtering"
apparmorUnconfined = "unconfined"
uint32Max = 4294967295
statusFileValue = ":(.*)"
)
var (
// ContainerRuntimes contains all the container runtimes.
ContainerRuntimes = []ContainerRuntime{
RuntimeDocker,
RuntimeRkt,
RuntimeNspawn,
RuntimeLXC,
RuntimeLXCLibvirt,
RuntimeOpenVZ,
RuntimeKubernetes,
RuntimeGarden,
RuntimePodman,
}
seccompModes = map[string]SeccompMode{
"0": SeccompModeDisabled,
"1": SeccompModeStrict,
"2": SeccompModeFiltering,
}
statusFileValueRegex = regexp.MustCompile(statusFileValue)
)
// GetContainerRuntime returns the container runtime the process is running in.
// If pid is less than one, it returns the runtime for "self".
func GetContainerRuntime(tgid, pid int) ContainerRuntime {
file := "/proc/self/cgroup"
if pid > 0 {
if tgid > 0 {
file = fmt.Sprintf("/proc/%d/task/%d/cgroup", tgid, pid)
} else {
file = fmt.Sprintf("/proc/%d/cgroup", pid)
}
}
// read the cgroups file
a := readFileString(file)
runtime := getContainerRuntime(a)
if runtime != RuntimeNotFound {
return runtime
}
// /proc/vz exists in container and outside of the container, /proc/bc only outside of the container.
if fileExists("/proc/vz") && !fileExists("/proc/bc") {
return RuntimeOpenVZ
}
a = os.Getenv("container")
runtime = getContainerRuntime(a)
if runtime != RuntimeNotFound {
return runtime
}
// PID 1 might have dropped this information into a file in /run.
// Read from /run/systemd/container since it is better than accessing /proc/1/environ,
// which needs CAP_SYS_PTRACE
a = readFileString("/run/systemd/container")
runtime = getContainerRuntime(a)
if runtime != RuntimeNotFound {
return runtime
}
return RuntimeNotFound
}
func getContainerRuntime(input string) ContainerRuntime {
if len(strings.TrimSpace(input)) < 1 {
return RuntimeNotFound
}
for _, runtime := range ContainerRuntimes {
if strings.Contains(input, string(runtime)) {
return runtime
}
}
return RuntimeNotFound
}
func fileExists(file string) bool {
if _, err := os.Stat(file); !os.IsNotExist(err) {
return true
}
return false
}
func readFile(file string) []byte {
if !fileExists(file) {
return nil
}
// filepath.clean was added to resolve the gosec build failure
// with error "Potential file inclusion via variable"
// IBM Modified the below line to remove the deprecated ioutil dependency
b, err := os.ReadFile(filepath.Clean(file))
if err != nil {
return nil
}
return b
}
// GetCapabilities returns the allowed capabilities for the process.
// If pid is less than one, it returns the capabilities for "self".
func GetCapabilities(pid int) (map[string][]string, error) {
allCaps := capability.List()
caps, err := capability.NewPid(pid)
if err != nil {
return nil, err
}
allowedCaps := map[string][]string{}
allowedCaps["EFFECTIVE | PERMITTED | INHERITABLE"] = []string{}
allowedCaps["BOUNDING"] = []string{}
allowedCaps["AMBIENT"] = []string{}
for _, cap := range allCaps {
if caps.Get(capability.CAPS, cap) {
allowedCaps["EFFECTIVE | PERMITTED | INHERITABLE"] = append(allowedCaps["EFFECTIVE | PERMITTED | INHERITABLE"], cap.String())
}
if caps.Get(capability.BOUNDING, cap) {
allowedCaps["BOUNDING"] = append(allowedCaps["BOUNDING"], cap.String())
}
if caps.Get(capability.AMBIENT, cap) {
allowedCaps["AMBIENT"] = append(allowedCaps["AMBIENT"], cap.String())
}
}
return allowedCaps, nil
}
// GetSeccompEnforcingMode returns the seccomp enforcing level (disabled, filtering, strict)
// for a process.
// If pid is less than one, it returns the seccomp enforcing mode for "self".
func GetSeccompEnforcingMode(pid int) SeccompMode {
file := "/proc/self/status"
if pid > 0 {
file = fmt.Sprintf("/proc/%d/status", pid)
}
return getSeccompEnforcingMode(readFileString(file))
}
func getSeccompEnforcingMode(input string) SeccompMode {
mode := getStatusEntry(input, "Seccomp:")
sm, ok := seccompModes[mode]
if ok {
return sm
}
// Pre linux 3.8, check if Seccomp is supported, via CONFIG_SECCOMP.
if err := unix.Prctl(unix.PR_GET_SECCOMP, 0, 0, 0, 0); err != unix.EINVAL {
// Make sure the kernel has CONFIG_SECCOMP_FILTER.
if err := unix.Prctl(unix.PR_SET_SECCOMP, unix.SECCOMP_MODE_FILTER, 0, 0, 0); err != unix.EINVAL {
return SeccompModeStrict
}
}
return SeccompModeDisabled
}
// TODO: make this function more efficient and read the file line by line.
func getStatusEntry(input, find string) string {
// Split status file string by line
statusMappings := strings.Split(input, "\n")
statusMappings = deleteEmpty(statusMappings)
for _, line := range statusMappings {
if strings.Contains(line, find) {
matches := statusFileValueRegex.FindStringSubmatch(line)
if len(matches) > 1 {
return strings.TrimSpace(matches[1])
}
}
}
return ""
}
func deleteEmpty(s []string) []string {
var r []string
for _, str := range s {
if strings.TrimSpace(str) != "" {
r = append(r, strings.TrimSpace(str))
}
}
return r
}
func readFileString(file string) string {
b := readFile(file)
if b == nil {
return ""
}
return strings.TrimSpace(string(b))
}

View File

@@ -0,0 +1,124 @@
/*
© Copyright IBM Corporation 2019,2023
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package containerruntime
import (
"fmt"
"os"
"strings"
)
func DetectContainerRuntime() ContainerRuntime {
return GetContainerRuntime(0, 1)
}
func GetBaseImage() (string, error) {
buf, err := os.ReadFile("/etc/os-release")
if err != nil {
return "", fmt.Errorf("Failed to read /etc/os-release: %v", err)
}
lines := strings.Split(string(buf), "\n")
for _, l := range lines {
if strings.HasPrefix(l, "PRETTY_NAME=") {
words := strings.Split(l, "\"")
if len(words) >= 2 {
return words[1], nil
}
}
}
return "unknown", nil
}
// GetCapabilities gets the Linux capabilities (e.g. setuid, setgid). See https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
func GetContainerCapabilities() (map[string][]string, error) {
//passing the pid as 1 since runmqserver initializes, creates and starts a queue manager, as PID 1 in a container
return GetCapabilities(1)
}
// GetSeccomp gets the seccomp enforcing mode, which affects which kernel calls can be made
func GetSeccomp() SeccompMode {
//passing the pid as 1 since runmqserver initializes, creates and starts a queue manager, as PID 1 in a container
return GetSeccompEnforcingMode(1)
}
// GetSecurityAttributes gets the security attributes of the current process.
// The security attributes indicate whether AppArmor or SELinux are being used,
// and what the level of confinement is.
func GetSecurityAttributes() string {
a, err := readProc("/proc/self/attr/current")
// On some systems, if AppArmor or SELinux are not installed, you get an
// error when you try and read `/proc/self/attr/current`, even though the
// file exists.
if err != nil || a == "" {
a = "none"
}
return a
}
func readProc(filename string) (value string, err error) {
// #nosec G304
buf, err := os.ReadFile(filename)
if err != nil {
return "", err
}
return strings.TrimSpace(string(buf)), nil
}
func GetMounts() (map[string]string, error) {
all, err := readProc("/proc/mounts")
if err != nil {
return nil, fmt.Errorf("Couldn't read /proc/mounts")
}
result := make(map[string]string)
lines := strings.Split(all, "\n")
for i := range lines {
parts := strings.Split(lines[i], " ")
//dev := parts[0]
mountPoint := parts[1]
fsType := parts[2]
if strings.Contains(mountPoint, "/mnt/mqm") {
result[mountPoint] = fsType
}
}
return result, nil
}
func GetKernelVersion() (string, error) {
return readProc("/proc/sys/kernel/osrelease")
}
func GetMaxFileHandles() (string, error) {
return readProc("/proc/sys/fs/file-max")
}
// SupportedFilesystem returns true if the supplied filesystem type is supported for MQ data
func SupportedFilesystem(fsType string) bool {
switch fsType {
case "aufs", "overlayfs", "tmpfs":
return false
default:
return true
}
}
// ValidMultiInstanceFilesystem returns true if the supplied filesystem type is valid for a multi-instance queue manager
func ValidMultiInstanceFilesystem(fsType string) bool {
if !SupportedFilesystem(fsType) {
return false
}
// TODO : check for non-shared filesystems & shared filesystems which are known not to work
return true
}

View File

@@ -0,0 +1,115 @@
// +build linux
/*
© Copyright IBM Corporation 2017, 2019
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package containerruntime
import (
"golang.org/x/sys/unix"
)
// fsTypes contains file system identifier codes.
// This code will not compile on some operating systems - Linux only.
var fsTypes = map[int64]string{
0x61756673: "aufs",
0xef53: "ext",
0x6969: "nfs",
0x65735546: "fuse",
0x9123683e: "btrfs",
0x01021994: "tmpfs",
0x794c7630: "overlayfs",
0x58465342: "xfs",
// less popular codes
0xadf5: "adfs",
0xadff: "affs",
0x5346414F: "afs",
0x0187: "autofs",
0x73757245: "coda",
0x28cd3d45: "cramfs",
0x453dcd28: "cramfs",
0x64626720: "debugfs",
0x73636673: "securityfs",
0xf97cff8c: "selinux",
0x43415d53: "smack",
0x858458f6: "ramfs",
0x958458f6: "hugetlbfs",
0x73717368: "squashfs",
0xf15f: "ecryptfs",
0x414A53: "efs",
0xabba1974: "xenfs",
0x3434: "nilfs",
0xF2F52010: "f2fs",
0xf995e849: "hpfs",
0x9660: "isofs",
0x72b6: "jffs2",
0x6165676C: "pstorefs",
0xde5e81e4: "efivarfs",
0x00c0ffee: "hostfs",
0x137F: "minix_14", // minix v1 fs, 14 char names
0x138F: "minix_30", // minix v1 fs, 30 char names
0x2468: "minix2_14", // minix v2 fs, 14 char names
0x2478: "minix2_30", // minix v2 fs, 30 char names
0x4d5a: "minix3_60", // minix v3 fs, 60 char names
0x4d44: "msdos",
0x564c: "ncp",
0x7461636f: "ocfs2",
0x9fa1: "openprom",
0x002f: "qnx4",
0x68191122: "qnx6",
0x6B414653: "afs_fs",
0x52654973: "reiserfs",
0x517B: "smb",
0x27e0eb: "cgroup",
0x63677270: "cgroup2",
0x7655821: "rdtgroup",
0x57AC6E9D: "stack_end",
0x74726163: "tracefs",
0x01021997: "v9fs",
0x62646576: "bdevfs",
0x64646178: "daxfs",
0x42494e4d: "binfmtfs",
0x1cd1: "devpts",
0xBAD1DEA: "futexfs",
0x50495045: "pipefs",
0x9fa0: "proc",
0x534F434B: "sockfs",
0x62656572: "sysfs",
0x9fa2: "usbdevice",
0x11307854: "mtd_inode",
0x09041934: "anon_inode",
0x73727279: "btrfs",
0x6e736673: "nsfs",
0xcafe4a11: "bpf",
0x5a3c69f0: "aafs",
0x15013346: "udf",
0x13661366: "balloon_kvm",
0x58295829: "zsmalloc",
}
// GetFilesystem returns the filesystem type for the specified path
func GetFilesystem(path string) (string, error) {
statfs := &unix.Statfs_t{}
err := unix.Statfs(path, statfs)
if err != nil {
return "", err
}
// Use a type conversion to make type an int64. On s390x it's a uint32.
t, ok := fsTypes[int64(statfs.Type)]
if !ok {
return "unknown", nil
}
return t, nil
}

View File

@@ -0,0 +1,24 @@
// +build !linux
/*
© Copyright IBM Corporation 2018, 2019
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package containerruntime
// Dummy version of this function, only for non-Linux systems.
// Having this allows unit tests to be run on other platforms (e.g. macOS)
func checkFS(path string) error {
return nil
}

61
internal/copy/copy.go Normal file
View File

@@ -0,0 +1,61 @@
/*
© Copyright IBM Corporation 2019
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package copy
import (
"fmt"
"io"
"os"
"github.com/ibm-messaging/mq-container/internal/filecheck"
)
func CopyFileMode(src, dest string, perm os.FileMode) error {
err := filecheck.CheckFileSource(src)
if err != nil {
return fmt.Errorf("failed to open %s for copy: %v", src, err)
}
// #nosec G304 - filename variable 'src' is checked above to ensure it is valid
in, err := os.Open(src)
if err != nil {
return fmt.Errorf("failed to open %s for copy: %v", src, err)
}
// #nosec G307 - local to this function, pose no harm.
defer in.Close()
// #nosec G304 - this func creates based on the input filemode.
out, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY, perm)
if err != nil {
return fmt.Errorf("failed to open %s for copy: %v", dest, err)
}
// #nosec G307 - local to this function, pose no harm.
defer out.Close()
_, err = io.Copy(out, in)
if err != nil {
return err
}
err = out.Close()
return err
}
// CopyFile copies the specified file
func CopyFile(src, dest string) error {
return CopyFileMode(src, dest, 0770)
}

View File

@@ -0,0 +1,39 @@
/*
© Copyright IBM Corporation 2019, 2023
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package filecheck
import (
"fmt"
"path/filepath"
"strings"
"github.com/ibm-messaging/mq-container/internal/pathutils"
)
// CheckFileSource checks the filename is valid
func CheckFileSource(fileName string) error {
absFile, _ := filepath.Abs(fileName)
prefixes := []string{"bin", "boot", "dev", "lib", "lib64", "proc", "sbin", "sys"}
for _, prefix := range prefixes {
if strings.HasPrefix(absFile, pathutils.CleanPath("/", prefix)) {
return fmt.Errorf("Filename resolves to invalid path '%v'", absFile)
}
}
return nil
}

View File

@@ -0,0 +1,40 @@
/*
© Copyright IBM Corporation 2019
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package filecheck
import (
"testing"
)
func TestCheckFileSource(t *testing.T) {
invalidFilenames := []string{"/bin", "/boot", "/dev", "/lib", "/lib64", "/proc", "/sbin", "/sys", "/bin/myfile", "/boot/mydir/myfile", "/var/../dev", "/var/../lib/myfile"}
for _, invalidFilename := range invalidFilenames {
err := CheckFileSource(invalidFilename)
if err == nil {
t.Errorf("Expected to receive an error for filename '%v'", invalidFilename)
}
}
validFilenames := []string{"/var", "/mydir/dev", "/mydir/dev/myfile"}
for _, validFilename := range validFilenames {
err := CheckFileSource(validFilename)
if err != nil {
t.Errorf("Unexpected error received: %v", err)
}
}
}

96
internal/fips/fips.go Normal file
View File

@@ -0,0 +1,96 @@
/*
© Copyright IBM Corporation 2023
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package fips
import (
"os"
"strings"
"github.com/ibm-messaging/mq-container/internal/command"
"github.com/ibm-messaging/mq-container/pkg/logger"
)
var (
FIPSEnabledType int
)
// FIPS has been turned off either because OS is not FIPS enabled or
// MQ_ENABLE_FIPS environment variable is set to "false"
const FIPS_ENABLED_OFF = 0
// FIPS is turned ON
const FIPS_ENABLED_ON = 1
// FIPS enabled at operating system level
const FIPS_ENABLED_PLATFORM = 1
// FIPS enabled via environment variable
const FIPS_ENABLED_ENV_VAR = 2
// Get FIPS enabled type.
func ProcessFIPSType(logs *logger.Logger) {
// Run "sysctl crypto.fips_enabled" command to determine if FIPS has been enabled
// on OS.
FIPSEnabledType = FIPS_ENABLED_OFF
out, _, err := command.Run("sysctl", "crypto.fips_enabled")
if err == nil {
// Check the output of the command for expected output
if strings.Contains(out, "crypto.fips_enabled = 1") {
FIPSEnabledType = FIPS_ENABLED_PLATFORM
}
}
// Check if we have been asked to override FIPS cryptography
fipsOverride, fipsOverrideSet := os.LookupEnv("MQ_ENABLE_FIPS")
if fipsOverrideSet {
if strings.EqualFold(fipsOverride, "false") || strings.EqualFold(fipsOverride, "0") {
FIPSEnabledType = FIPS_ENABLED_OFF
} else if strings.EqualFold(fipsOverride, "true") || strings.EqualFold(fipsOverride, "1") {
// This is the case where OS may or may not be FIPS compliant but we have been asked
// to run MQ queue manager, web server and Native HA in FIPS mode. This case can also
// be used when running docker tests. If FIPS is enabled on host, then don't modify
// the original value.
if FIPSEnabledType != FIPS_ENABLED_PLATFORM {
FIPSEnabledType = FIPS_ENABLED_ENV_VAR
}
} else if strings.EqualFold(fipsOverride, "auto") {
// This is the default case. Leave it to the OS default as determined above.
} else {
// We don't recognise the value specified. Log a warning and carry on.
if logs != nil {
logs.Printf("Invalid value '%s' was specified for MQ_ENABLE_FIPS. The value has been ignored.\n", fipsOverride)
}
}
}
}
func IsFIPSEnabled() bool {
return FIPSEnabledType > FIPS_ENABLED_OFF
}
// Log a message on the console to indicate FIPS certified
// cryptography being used.
func PostInit(log *logger.Logger) {
message := "FIPS cryptography is not enabled."
if FIPSEnabledType == FIPS_ENABLED_PLATFORM {
message = "FIPS cryptography is enabled. FIPS cryptography setting on the host is 'true'."
} else if FIPSEnabledType == FIPS_ENABLED_ENV_VAR {
message = "FIPS cryptography is enabled. FIPS cryptography setting on the host is 'false'."
}
log.Println(message)
}

View File

@@ -0,0 +1,65 @@
/*
© Copyright IBM Corporation 2022
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package keystore contains code to create and update keystores
package fips
import (
"fmt"
"os"
"testing"
)
func TestEnableFIPSAuto(t *testing.T) {
ProcessFIPSType(nil)
// Test default "auto"
fipsType := IsFIPSEnabled()
if fipsType {
t.Errorf("Expected FIPS OFF but got %v\n", fipsType)
}
}
func TestEnableFIPSTrue(t *testing.T) {
// Test MQ_ENABLE_FIPS=true
os.Setenv("MQ_ENABLE_FIPS", "true")
fmt.Println(os.Getenv("MQ_ENABLE_FIPS"))
ProcessFIPSType(nil)
fipsType := IsFIPSEnabled()
if !fipsType {
t.Errorf("Expected FIPS ON but got %v\n", fipsType)
}
}
func TestEnableFIPSFalse(t *testing.T) {
// Test MQ_ENABLE_FIPS=false
os.Setenv("MQ_ENABLE_FIPS", "false")
ProcessFIPSType(nil)
fipsType := IsFIPSEnabled()
if fipsType {
t.Errorf("Expected FIPS OFF but got %v\n", fipsType)
}
}
func TestEnableFIPSInvalid(t *testing.T) {
// Test MQ_ENABLE_FIPS with invalid value
os.Setenv("MQ_ENABLE_FIPS", "falseOff")
ProcessFIPSType(nil)
fipsType := IsFIPSEnabled()
if fipsType {
t.Errorf("Expected FIPS OFF but got %v\n", fipsType)
}
}

215
internal/ha/ha.go Normal file
View File

@@ -0,0 +1,215 @@
/*
© Copyright IBM Corporation 2020, 2024
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package ha contains code for high availability
package ha
import (
"fmt"
"os"
"github.com/ibm-messaging/mq-container/internal/fips"
"github.com/ibm-messaging/mq-container/internal/mqtemplate"
"github.com/ibm-messaging/mq-container/internal/tls"
"github.com/ibm-messaging/mq-container/pkg/logger"
)
// ConfigureNativeHA configures native high availability
func ConfigureNativeHA(log *logger.Logger) error {
if os.Getenv("MQ_NATIVE_HA") != "true" {
return nil
}
fipsAvailable := fips.IsFIPSEnabled()
haCertLabel, haGroupCertLabel, _, _, err := tls.ConfigureHATLSKeystore()
if err != nil {
return fmt.Errorf("error loading tls keys: %w", err)
}
configFiles := map[string]string{
"/run/10-native-ha-instance.ini": "/etc/mqm/10-native-ha-instance.ini.tpl",
}
if haCertLabel != "" || haGroupCertLabel != "" {
configFiles["/run/10-native-ha-keystore.ini"] = "/etc/mqm/10-native-ha-keystore.ini.tpl"
}
if envConfigPresent() {
log.Println("Configuring Native HA using values provided in environment variables")
configFiles["/run/10-native-ha.ini"] = "/etc/mqm/10-native-ha.ini.tpl"
}
return loadConfigAndGenerate(configFiles, fipsAvailable, haCertLabel, haGroupCertLabel, log)
}
func loadConfigAndGenerate(templateConfigs map[string]string, fipsAvailable bool, haCertLabel, haGroupCertLabel string, log *logger.Logger) error {
cfg, err := loadConfigFromEnv(log)
if err != nil {
return err
}
err = cfg.updateTLS(fipsAvailable, haCertLabel, haGroupCertLabel)
if err != nil {
return err
}
for outputPath, templateFile := range templateConfigs {
err := cfg.generate(templateFile, outputPath, log)
if err != nil {
return err
}
}
return nil
}
func envConfigPresent() bool {
checkVars := []string{
"MQ_NATIVE_HA_INSTANCE_0_NAME",
"MQ_NATIVE_HA_INSTANCE_0_REPLICATION_ADDRESS",
"MQ_NATIVE_HA_INSTANCE_1_NAME",
"MQ_NATIVE_HA_INSTANCE_1_REPLICATION_ADDRESS",
"MQ_NATIVE_HA_INSTANCE_2_NAME",
"MQ_NATIVE_HA_INSTANCE_2_REPLICATION_ADDRESS",
"MQ_NATIVE_HA_TLS",
"MQ_NATIVE_HA_CIPHERSPEC",
}
for _, checkVar := range checkVars {
if os.Getenv(checkVar) != "" {
return true
}
}
return false
}
func loadConfigFromEnv(log *logger.Logger) (*haConfig, error) {
cfg := &haConfig{
Name: os.Getenv("HOSTNAME"),
Instances: [3]haInstance{
{
Name: os.Getenv("MQ_NATIVE_HA_INSTANCE_0_NAME"),
ReplicationAddress: os.Getenv("MQ_NATIVE_HA_INSTANCE_0_REPLICATION_ADDRESS"),
},
{
Name: os.Getenv("MQ_NATIVE_HA_INSTANCE_1_NAME"),
ReplicationAddress: os.Getenv("MQ_NATIVE_HA_INSTANCE_1_REPLICATION_ADDRESS"),
},
{
Name: os.Getenv("MQ_NATIVE_HA_INSTANCE_2_NAME"),
ReplicationAddress: os.Getenv("MQ_NATIVE_HA_INSTANCE_2_REPLICATION_ADDRESS"),
},
},
Group: haGroupConfig{
Local: haLocalGroupConfig{
Address: os.Getenv("MQ_NATIVE_HA_GROUP_LOCAL_ADDRESS"),
Name: os.Getenv("MQ_NATIVE_HA_GROUP_LOCAL_NAME"),
Role: os.Getenv("MQ_NATIVE_HA_GROUP_ROLE"),
},
Recovery: haRecoveryGroupConfig{
Address: os.Getenv("MQ_NATIVE_HA_GROUP_REPLICATION_ADDRESS"),
Name: os.Getenv("MQ_NATIVE_HA_GROUP_RECOVERY_NAME"),
Enabled: os.Getenv("MQ_NATIVE_HA_GROUP_RECOVERY_ENABLED") != "false",
},
CipherSpec: os.Getenv("MQ_NATIVE_HA_GROUP_CIPHERSPEC"),
},
CipherSpec: os.Getenv("MQ_NATIVE_HA_CIPHERSPEC"),
keyRepository: os.Getenv("MQ_NATIVE_HA_KEY_REPOSITORY"),
}
if cfg.Group.Recovery.Name == "" {
cfg.Group.Recovery.Enabled = false
}
return cfg, nil
}
type haConfig struct {
Name string
Instances [3]haInstance
Group haGroupConfig
haTLSEnabled bool
CipherSpec string
CertificateLabel string
keyRepository string
fipsAvailable bool
}
func (h haConfig) ShouldConfigureTLS() bool {
if h.haTLSEnabled {
return true
}
if h.Group.Local.Name != "" {
return true
}
return false
}
func (h haConfig) SSLFipsRequired() string {
return yesNo(h.fipsAvailable).String()
}
func (h *haConfig) updateTLS(fipsAvailable bool, haCertLabel, haGroupCertLabel string) error {
if haCertLabel != "" {
h.CertificateLabel = haCertLabel
h.haTLSEnabled = true
}
if haGroupCertLabel != "" {
h.Group.CertificateLabel = haGroupCertLabel
h.haTLSEnabled = true
}
h.fipsAvailable = fipsAvailable
return nil
}
func (h haConfig) generate(templatePath string, outputPath string, log *logger.Logger) error {
return mqtemplate.ProcessTemplateFile(templatePath, outputPath, h, log)
}
func (h haConfig) KeyRepository() string {
if h.keyRepository != "" {
return h.keyRepository
}
return "/run/runmqserver/ha/tls/key"
}
type haGroupConfig struct {
Local haLocalGroupConfig
Recovery haRecoveryGroupConfig
CipherSpec string
CertificateLabel string
}
type haLocalGroupConfig struct {
Name string
Role string
Address string
}
type haRecoveryGroupConfig struct {
Name string
Enabled yesNo
Address string
}
type haInstance struct {
Name string
ReplicationAddress string
}
type yesNo bool
func (yn yesNo) String() string {
if yn {
return "Yes"
}
return "No"
}

545
internal/ha/ha_test.go Normal file
View File

@@ -0,0 +1,545 @@
/*
© Copyright IBM Corporation 2024
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package ha
import (
"bytes"
"embed"
"fmt"
"os"
"path"
"strings"
"testing"
"github.com/ibm-messaging/mq-container/pkg/logger"
)
//go:embed test_fixtures
var testFixtures embed.FS
func TestConfigFromEnv(t *testing.T) {
tests := []struct {
TestName string
env map[string]string
overrides testOverrides
expected haConfig
}{
{
TestName: "Minimal config",
env: map[string]string{
"HOSTNAME": "minimal-config",
"MQ_NATIVE_HA_INSTANCE_0_NAME": "minimal-config-instance0",
"MQ_NATIVE_HA_INSTANCE_1_NAME": "minimal-config-instance1",
"MQ_NATIVE_HA_INSTANCE_2_NAME": "minimal-config-instance2",
"MQ_NATIVE_HA_INSTANCE_0_REPLICATION_ADDRESS": "minimal-config-instance0(9145)",
"MQ_NATIVE_HA_INSTANCE_1_REPLICATION_ADDRESS": "minimal-config-instance1(9145)",
"MQ_NATIVE_HA_INSTANCE_2_REPLICATION_ADDRESS": "minimal-config-instance2(9145)",
},
expected: haConfig{
Name: "minimal-config",
Instances: [3]haInstance{
{"minimal-config-instance0", "minimal-config-instance0(9145)"},
{"minimal-config-instance1", "minimal-config-instance1(9145)"},
{"minimal-config-instance2", "minimal-config-instance2(9145)"},
},
},
},
{
TestName: "Full TLS config",
env: map[string]string{
"HOSTNAME": "tls-config",
"MQ_NATIVE_HA_INSTANCE_0_NAME": "tls-config-instance0",
"MQ_NATIVE_HA_INSTANCE_1_NAME": "tls-config-instance1",
"MQ_NATIVE_HA_INSTANCE_2_NAME": "tls-config-instance2",
"MQ_NATIVE_HA_INSTANCE_0_REPLICATION_ADDRESS": "tls-config-instance0(9145)",
"MQ_NATIVE_HA_INSTANCE_1_REPLICATION_ADDRESS": "tls-config-instance1(9145)",
"MQ_NATIVE_HA_INSTANCE_2_REPLICATION_ADDRESS": "tls-config-instance2(9145)",
"MQ_NATIVE_HA_TLS": "true",
"MQ_NATIVE_HA_CIPHERSPEC": "a-cipher-spec",
"MQ_NATIVE_HA_KEY_REPOSITORY": "/path/to/repository",
},
overrides: testOverrides{
certificateLabel: asRef("cert-label-here"),
fips: asRef(false),
},
expected: haConfig{
Name: "tls-config",
Instances: [3]haInstance{
{"tls-config-instance0", "tls-config-instance0(9145)"},
{"tls-config-instance1", "tls-config-instance1(9145)"},
{"tls-config-instance2", "tls-config-instance2(9145)"},
},
haTLSEnabled: true,
CipherSpec: "a-cipher-spec",
keyRepository: "/path/to/repository",
CertificateLabel: "cert-label-here", // From override
fipsAvailable: false, // From override
},
},
{
TestName: "Group TLS (live plain) config",
env: map[string]string{
"HOSTNAME": "group-live-plain-config",
"MQ_NATIVE_HA_INSTANCE_0_NAME": "group-live-plain-config0",
"MQ_NATIVE_HA_INSTANCE_1_NAME": "group-live-plain-config1",
"MQ_NATIVE_HA_INSTANCE_2_NAME": "group-live-plain-config2",
"MQ_NATIVE_HA_INSTANCE_0_REPLICATION_ADDRESS": "group-live-plain-config0(9145)",
"MQ_NATIVE_HA_INSTANCE_1_REPLICATION_ADDRESS": "group-live-plain-config1(9145)",
"MQ_NATIVE_HA_INSTANCE_2_REPLICATION_ADDRESS": "group-live-plain-config2(9145)",
"MQ_NATIVE_HA_CIPHERSPEC": "NULL",
"MQ_NATIVE_HA_KEY_REPOSITORY": "/path/to/repository",
"MQ_NATIVE_HA_GROUP_RECOVERY_ENABLED": "true",
"MQ_NATIVE_HA_GROUP_LOCAL_NAME": "alpha",
"MQ_NATIVE_HA_GROUP_RECOVERY_NAME": "beta",
"MQ_NATIVE_HA_GROUP_CIPHERSPEC": "ANY_TLS",
"MQ_NATIVE_HA_GROUP_ROLE": "Live",
"MQ_NATIVE_HA_GROUP_LOCAL_ADDRESS": "(4445)",
"MQ_NATIVE_HA_GROUP_REPLICATION_ADDRESS": "beta-address(4445)",
},
overrides: testOverrides{
groupCertificateLabel: asRef("recovery-cert-label-here"),
fips: asRef(false),
},
expected: haConfig{
Name: "group-live-plain-config",
Instances: [3]haInstance{
{"group-live-plain-config0", "group-live-plain-config0(9145)"},
{"group-live-plain-config1", "group-live-plain-config1(9145)"},
{"group-live-plain-config2", "group-live-plain-config2(9145)"},
},
Group: haGroupConfig{
Local: haLocalGroupConfig{
Name: "alpha",
Role: "Live",
Address: "(4445)",
},
Recovery: haRecoveryGroupConfig{
Name: "beta",
Enabled: true,
Address: "beta-address(4445)",
},
CertificateLabel: "recovery-cert-label-here", // From override
CipherSpec: "ANY_TLS",
},
CipherSpec: "NULL",
keyRepository: "/path/to/repository",
haTLSEnabled: true,
fipsAvailable: false, // From override
},
},
}
for _, test := range tests {
t.Run(test.TestName, func(t *testing.T) {
// Set environment for test
savedEnv := make([]string, len(os.Environ()))
copy(savedEnv, os.Environ())
defer func() {
os.Clearenv()
for _, env := range savedEnv {
parts := strings.SplitN(env, "=", 2)
os.Setenv(parts[0], parts[1])
}
}()
for key, value := range test.env {
os.Setenv(key, value)
}
testLogger, logBuffer, err := newTestLogger(test.TestName)
if err != nil {
t.Fatalf("Failed to create test logger: %s", err.Error())
}
if !envConfigPresent() {
t.Fatalf("Check for Native HA config by environment variable unexpectedly reported false")
}
// Load config from env
cfg, err := loadConfigFromEnv(testLogger)
t.Log(logBuffer.String())
if err != nil {
t.Fatalf("Loading config failed: %s", err.Error())
}
test.overrides.apply(cfg)
// Validate
if *cfg != test.expected {
t.Fatalf("Configuration does not match expected:\n\tExpected: %#v\n\tActual: %#v\n", test.expected, *cfg)
}
})
}
}
func TestCheckEnv(t *testing.T) {
tests := []struct {
name string
env map[string]string
expect bool
}{
{
name: "empty env",
expect: false,
},
{
name: "Native HA with external config",
env: map[string]string{
"HOSTNAME": "external-config",
"MQ_NATIVE_HA": "true",
},
expect: false,
},
{
name: "Native HA with env config",
env: map[string]string{
"MQ_NATIVE_HA": "true",
"MQ_NATIVE_HA_INSTANCE_0_NAME": "minimal-config-instance0",
"MQ_NATIVE_HA_INSTANCE_1_NAME": "minimal-config-instance1",
"MQ_NATIVE_HA_INSTANCE_2_NAME": "minimal-config-instance2",
"MQ_NATIVE_HA_INSTANCE_0_REPLICATION_ADDRESS": "minimal-config-instance0(9145)",
"MQ_NATIVE_HA_INSTANCE_1_REPLICATION_ADDRESS": "minimal-config-instance1(9145)",
"MQ_NATIVE_HA_INSTANCE_2_REPLICATION_ADDRESS": "minimal-config-instance2(9145)",
},
expect: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// Set environment for test
savedEnv := make([]string, len(os.Environ()))
copy(savedEnv, os.Environ())
defer func() {
os.Clearenv()
for _, env := range savedEnv {
parts := strings.SplitN(env, "=", 2)
os.Setenv(parts[0], parts[1])
}
}()
for key, value := range test.env {
os.Setenv(key, value)
}
actual := envConfigPresent()
if actual != test.expect {
t.Fatalf("Incorrect result from environment variable check (actual: %v != expected: %v)", actual, test.expect)
}
})
}
}
func TestTemplatingFromConfig(t *testing.T) {
// Helper function to turn pairs of strings into a map
templateListToMap := func(paths ...string) map[string]string {
templates := map[string]string{}
for i := 0; i+1 < len(paths); i += 2 {
input := path.Join("../../ha/", paths[i])
output := paths[i+1]
templates[input] = output
}
return templates
}
tests := []struct {
TestName string
config haConfig
templates map[string]string
}{
{
TestName: "MinimalConfig (no-FIPS)",
config: haConfig{
fipsAvailable: false,
},
templates: templateListToMap(
"10-native-ha-instance.ini.tpl", "instance/no-fips.ini",
"10-native-ha.ini.tpl", "envcfg/minimal-config.ini",
),
}, {
TestName: "MinimalConfig (FIPS)",
config: haConfig{
fipsAvailable: true,
},
templates: templateListToMap(
"10-native-ha-instance.ini.tpl", "instance/fips.ini",
"10-native-ha.ini.tpl", "envcfg/minimal-config.ini",
),
},
{
TestName: "Base TLS config (no FIPS)",
config: haConfig{
haTLSEnabled: true,
CertificateLabel: "baseTLS",
fipsAvailable: false,
},
templates: templateListToMap(
"10-native-ha.ini.tpl", "envcfg/minimal-config.ini",
),
},
{
TestName: "Base TLS config (with FIPS)",
config: haConfig{
haTLSEnabled: true,
CertificateLabel: "baseTLS",
fipsAvailable: true,
},
templates: templateListToMap(
"10-native-ha-keystore.ini.tpl", "keystore/ha-only.ini",
"10-native-ha.ini.tpl", "envcfg/minimal-config.ini",
),
},
{
TestName: "Full TLS config (no-fips)",
config: haConfig{
haTLSEnabled: true,
CipherSpec: "some-cipher",
keyRepository: "/an/overridden/keystore",
fipsAvailable: false,
},
templates: templateListToMap(
"10-native-ha-instance.ini.tpl", "instance/no-fips.ini",
"10-native-ha-keystore.ini.tpl", "keystore/overridden-path.ini",
"10-native-ha.ini.tpl", "envcfg/tls-full.ini",
),
},
{
TestName: "Minimal live config",
config: haConfig{
Group: haGroupConfig{
Local: haLocalGroupConfig{
Name: "alpha",
Role: "Live",
},
Recovery: haRecoveryGroupConfig{
Name: "beta",
Enabled: true,
Address: "beta-address(4445)",
},
CertificateLabel: "recoveryTLS",
},
},
templates: templateListToMap(
"10-native-ha-instance.ini.tpl", "instance/no-fips.ini",
"10-native-ha-keystore.ini.tpl", "keystore/group-only.ini",
"10-native-ha.ini.tpl", "envcfg/group-live-minimal.ini",
),
},
{
TestName: "Minimal recovery config",
config: haConfig{
Group: haGroupConfig{
Local: haLocalGroupConfig{
Name: "beta",
Role: "Recovery",
},
Recovery: haRecoveryGroupConfig{
Name: "alpha",
Enabled: true,
Address: "alpha-address(4445)",
},
CertificateLabel: "recoveryTLS",
},
},
templates: templateListToMap(
"10-native-ha-instance.ini.tpl", "instance/no-fips.ini",
"10-native-ha-keystore.ini.tpl", "keystore/group-only.ini",
"10-native-ha.ini.tpl", "envcfg/group-recovery-minimal.ini",
),
},
{
TestName: "Group TLS (live plain) config",
config: haConfig{
Group: haGroupConfig{
Local: haLocalGroupConfig{
Name: "alpha",
Role: "Live",
Address: "(4445)",
},
Recovery: haRecoveryGroupConfig{
Name: "beta",
Enabled: true,
Address: "beta-address(4445)",
},
CertificateLabel: "recoveryTLS",
CipherSpec: "ANY_TLS",
},
CipherSpec: "NULL",
},
templates: templateListToMap(
"10-native-ha-instance.ini.tpl", "instance/no-fips.ini",
"10-native-ha-keystore.ini.tpl", "keystore/group-only.ini",
"10-native-ha.ini.tpl", "envcfg/group-live-plain-ha.ini",
),
},
{
TestName: "Separate HA and Group TLS config",
config: haConfig{
Group: haGroupConfig{
Local: haLocalGroupConfig{
Name: "alpha",
Role: "Live",
Address: "(4445)",
},
Recovery: haRecoveryGroupConfig{
Name: "beta",
Enabled: true,
Address: "beta-address(4445)",
},
CertificateLabel: "recoveryTLS",
CipherSpec: "ANY_TLS",
},
CertificateLabel: "baseTLS",
CipherSpec: "NULL",
},
templates: templateListToMap(
"10-native-ha-instance.ini.tpl", "instance/no-fips.ini",
"10-native-ha-keystore.ini.tpl", "keystore/ha-group.ini",
"10-native-ha.ini.tpl", "envcfg/group-live-plain-ha.ini",
),
},
}
for _, test := range tests {
t.Run(test.TestName, func(t *testing.T) {
for templateFile, expectedFile := range test.templates {
t.Run(templateFile, func(t *testing.T) {
t.Logf(`Runing templating test "%s"`, test.TestName)
t.Logf(`Expected to match template "%s"`, expectedFile)
testLogger, logBuffer, err := newTestLogger(test.TestName)
if err != nil {
t.Fatalf("Failed to create test logger: %s", err.Error())
}
// Load test config
cfg := applyTestDefaults(test.config)
// Generate template
tempOutputPath, err := os.CreateTemp("", "")
if err != nil {
t.Fatalf("Failed to create temporary output file: %s", err.Error())
}
defer func() { _ = os.Remove(tempOutputPath.Name()) }()
err = cfg.generate(templateFile, tempOutputPath.Name(), testLogger)
t.Log(logBuffer.String())
if err != nil {
t.Fatalf("Processing template to config failed: %s", err.Error())
}
actual, err := os.ReadFile(tempOutputPath.Name())
if err != nil {
t.Fatalf("Failed to read '%s': %s", test.TestName, err.Error())
}
// Validate
assertIniMatch(t, string(actual), expectedFile)
})
}
})
}
}
func applyTestDefaults(testConfig haConfig) haConfig {
baseName := "test-config"
setIfBlank(&testConfig.Name, baseName)
for i := 0; i < 3; i++ {
instName := fmt.Sprintf("%s-instance%d", baseName, i)
replAddress := fmt.Sprintf("%s(9145)", instName)
setIfBlank(&testConfig.Instances[i].Name, instName)
setIfBlank(&testConfig.Instances[i].ReplicationAddress, replAddress)
}
return testConfig
}
func setIfBlank[T comparable](setting *T, val T) {
var zero T
if *setting == zero {
*setting = val
}
}
func assertIniMatch(t *testing.T, actual string, expectedResultName string) {
expectedContent, err := testFixtures.ReadFile(fmt.Sprintf("test_fixtures/%s", expectedResultName))
if err != nil {
t.Fatalf("Failed to read expected results file (%s): %s", expectedResultName, err.Error())
}
expectedLines := strings.Split(string(expectedContent), "\n")
actualLines := strings.Split(actual, "\n")
filterBlank := func(lines *[]string) {
n := 0
for i := 0; i < len(*lines); i++ {
if strings.TrimSpace((*lines)[i]) == "" {
continue
}
(*lines)[n] = (*lines)[i]
n++
}
*lines = (*lines)[0:n]
}
filterBlank(&expectedLines)
filterBlank(&actualLines)
maxLine := len(expectedLines)
if len(actualLines) > maxLine {
maxLine = len(actualLines)
}
for i := 0; i < maxLine; i++ {
actLine, expLine := "", ""
if i < len(actualLines) {
actLine = actualLines[i]
}
if i < len(expectedLines) {
expLine = expectedLines[i]
}
if actLine != expLine {
t.Fatalf("Template does not match\n\nFirst difference at line %d:\n\tExpected: %s\n\tActual : %s\n\nExpected:\n\t%s\n\nActual:\n\t%s", i+1, expLine, actLine, strings.Join(expectedLines, "\n\t"), strings.Join(actualLines, "\n\t"))
}
}
}
func newTestLogger(name string) (*logger.Logger, *bytes.Buffer, error) {
buffer := new(bytes.Buffer)
l, err := logger.NewLogger(buffer, true, false, name)
return l, buffer, err
}
type testOverrides struct {
certificateLabel *string
groupCertificateLabel *string
fips *bool
}
func (t testOverrides) apply(cfg *haConfig) {
if t.certificateLabel != nil {
cfg.CertificateLabel = *t.certificateLabel
cfg.haTLSEnabled = true
}
if t.groupCertificateLabel != nil {
cfg.Group.CertificateLabel = *t.groupCertificateLabel
cfg.haTLSEnabled = true
}
if t.fips != nil {
cfg.fipsAvailable = *t.fips
}
}
func asRef[T any](val T) *T {
ref := &val
return ref
}

View File

@@ -0,0 +1,16 @@
NativeHALocalInstance:
GroupName=alpha
GroupRole=Live
NativeHAInstance:
Name=test-config-instance0
ReplicationAddress=test-config-instance0(9145)
NativeHAInstance:
Name=test-config-instance1
ReplicationAddress=test-config-instance1(9145)
NativeHAInstance:
Name=test-config-instance2
ReplicationAddress=test-config-instance2(9145)
NativeHARecoveryGroup:
GroupName=beta
Enabled=Yes
ReplicationAddress=beta-address(4445)

View File

@@ -0,0 +1,19 @@
NativeHALocalInstance:
CipherSpec=NULL
GroupName=alpha
GroupCipherSpec=ANY_TLS
GroupRole=Live
GroupLocalAddress=(4445)
NativeHAInstance:
Name=test-config-instance0
ReplicationAddress=test-config-instance0(9145)
NativeHAInstance:
Name=test-config-instance1
ReplicationAddress=test-config-instance1(9145)
NativeHAInstance:
Name=test-config-instance2
ReplicationAddress=test-config-instance2(9145)
NativeHARecoveryGroup:
GroupName=beta
Enabled=Yes
ReplicationAddress=beta-address(4445)

View File

@@ -0,0 +1,16 @@
NativeHALocalInstance:
GroupName=beta
GroupRole=Recovery
NativeHAInstance:
Name=test-config-instance0
ReplicationAddress=test-config-instance0(9145)
NativeHAInstance:
Name=test-config-instance1
ReplicationAddress=test-config-instance1(9145)
NativeHAInstance:
Name=test-config-instance2
ReplicationAddress=test-config-instance2(9145)
NativeHARecoveryGroup:
GroupName=alpha
Enabled=Yes
ReplicationAddress=alpha-address(4445)

View File

@@ -0,0 +1,10 @@
NativeHALocalInstance:
NativeHAInstance:
Name=test-config-instance0
ReplicationAddress=test-config-instance0(9145)
NativeHAInstance:
Name=test-config-instance1
ReplicationAddress=test-config-instance1(9145)
NativeHAInstance:
Name=test-config-instance2
ReplicationAddress=test-config-instance2(9145)

View File

@@ -0,0 +1,11 @@
NativeHALocalInstance:
CipherSpec=some-cipher
NativeHAInstance:
Name=test-config-instance0
ReplicationAddress=test-config-instance0(9145)
NativeHAInstance:
Name=test-config-instance1
ReplicationAddress=test-config-instance1(9145)
NativeHAInstance:
Name=test-config-instance2
ReplicationAddress=test-config-instance2(9145)

View File

@@ -0,0 +1,3 @@
NativeHALocalInstance:
Name=test-config
SSLFipsRequired=Yes

View File

@@ -0,0 +1,3 @@
NativeHALocalInstance:
Name=test-config
SSLFipsRequired=No

View File

@@ -0,0 +1,3 @@
NativeHALocalInstance:
GroupCertificateLabel=recoveryTLS
KeyRepository=/run/runmqserver/ha/tls/key

View File

@@ -0,0 +1,4 @@
NativeHALocalInstance:
CertificateLabel=baseTLS
GroupCertificateLabel=recoveryTLS
KeyRepository=/run/runmqserver/ha/tls/key

View File

@@ -0,0 +1,3 @@
NativeHALocalInstance:
CertificateLabel=baseTLS
KeyRepository=/run/runmqserver/ha/tls/key

View File

@@ -0,0 +1,2 @@
NativeHALocalInstance:
KeyRepository=/an/overridden/keystore

View File

@@ -0,0 +1,257 @@
/*
© Copyright IBM Corporation 2018, 2024
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package keystore contains code to create and update keystores
package keystore
import (
"bufio"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/ibm-messaging/mq-container/internal/command"
"github.com/ibm-messaging/mq-container/internal/fips"
)
// KeyStore describes information about a keystore file
type KeyStore struct {
Filename string
Password string
keyStoreType string
command string
fipsEnabled bool
}
// NewJKSKeyStore creates a new Java Key Store, managed by the runmqckm command
func NewJKSKeyStore(filename, password string) *KeyStore {
keyStore := &KeyStore{
Filename: filename,
Password: password,
keyStoreType: "jks",
command: "/opt/mqm/bin/runmqckm",
fipsEnabled: fips.IsFIPSEnabled(),
}
return keyStore
}
// NewCMSKeyStore creates a new MQ CMS Key Store, managed by the runmqakm command
func NewCMSKeyStore(filename, password string) *KeyStore {
keyStore := &KeyStore{
Filename: filename,
Password: password,
keyStoreType: "cms",
command: "/opt/mqm/bin/runmqakm",
fipsEnabled: fips.IsFIPSEnabled(),
}
return keyStore
}
// NewPKCS12KeyStore creates a new PKCS12 Key Store, managed by the runmqakm command
func NewPKCS12KeyStore(filename, password string) *KeyStore {
keyStore := &KeyStore{
Filename: filename,
Password: password,
keyStoreType: "p12",
command: "/opt/mqm/bin/runmqakm",
fipsEnabled: fips.IsFIPSEnabled(),
}
return keyStore
}
// Create a key store, if it doesn't already exist
func (ks *KeyStore) Create() error {
_, err := os.Stat(ks.Filename)
if err == nil {
// Keystore already exists so we should refresh it by deleting it.
extension := filepath.Ext(ks.Filename)
if ks.keyStoreType == "cms" {
// Only delete these when we are refreshing the kdb keystore
stashFile := ks.Filename[0:len(ks.Filename)-len(extension)] + ".sth"
rdbFile := ks.Filename[0:len(ks.Filename)-len(extension)] + ".rdb"
crlFile := ks.Filename[0:len(ks.Filename)-len(extension)] + ".crl"
err = os.Remove(stashFile)
if err != nil {
return err
}
err = os.Remove(rdbFile)
if err != nil {
return err
}
err = os.Remove(crlFile)
if err != nil {
return err
}
}
err = os.Remove(ks.Filename)
if err != nil {
return err
}
} else if !os.IsNotExist(err) {
// If the keystore exists but cannot be accessed then return the error
return err
}
// Create the keystore now we're sure it doesn't exist
out, _, err := command.Run(ks.command, "-keydb", "-create", ks.getFipsEnabledFlag(), "-type", ks.keyStoreType, "-db", ks.Filename, "-pw", ks.Password, "-stash")
if err != nil {
return fmt.Errorf("error running \"%v -keydb -create\": %v %s", ks.command, err, out)
}
return nil
}
// CreateStash creates a key stash, if it doesn't already exist
func (ks *KeyStore) CreateStash() error {
extension := filepath.Ext(ks.Filename)
stashFile := ks.Filename[0:len(ks.Filename)-len(extension)] + ".sth"
_, err := os.Stat(stashFile)
if err != nil {
if os.IsNotExist(err) {
out, _, err := command.Run(ks.command, "-keydb", ks.getFipsEnabledFlag(), "-stashpw", "-type", ks.keyStoreType, "-db", ks.Filename, "-pw", ks.Password)
if err != nil {
return fmt.Errorf("error running \"%v -keydb -stashpw\": %v %s", ks.command, err, out)
}
}
return err
}
return nil
}
// Import imports a certificate file in the keystore
func (ks *KeyStore) Import(inputFile, password string) error {
out, _, err := command.Run(ks.command, "-cert", "-import", ks.getFipsEnabledFlag(), "-file", inputFile, "-pw", password, "-target", ks.Filename, "-target_pw", ks.Password, "-target_type", ks.keyStoreType)
if err != nil {
return fmt.Errorf("error running \"%v -cert -import\": %v %s", ks.command, err, out)
}
return nil
}
// CreateSelfSignedCertificate creates a self-signed certificate in the keystore
func (ks *KeyStore) CreateSelfSignedCertificate(label, dn, hostname string) error {
out, _, err := command.Run(ks.command, "-cert", "-create", ks.getFipsEnabledFlag(), "-db", ks.Filename, "-pw", ks.Password, "-label", label, "-dn", dn, "-san_dnsname", hostname, "-size 2048 -sig_alg sha512 -eku serverAuth")
if err != nil {
return fmt.Errorf("error running \"%v -cert -create\": %v %s", ks.command, err, out)
}
return nil
}
// Add adds a CA certificate to the keystore
func (ks *KeyStore) Add(inputFile, label string) error {
out, _, err := command.Run(ks.command, "-cert", "-add", ks.getFipsEnabledFlag(), "-db", ks.Filename, "-type", ks.keyStoreType, "-pw", ks.Password, "-file", inputFile, "-label", label)
if err != nil {
return fmt.Errorf("error running \"%v -cert -add\": %v %s", ks.command, err, out)
}
return nil
}
// Add adds a CA certificate to the keystore
func (ks *KeyStore) AddNoLabel(inputFile string) error {
out, _, err := command.Run(ks.command, "-cert", "-add", ks.getFipsEnabledFlag(), "-db", ks.Filename, "-type", ks.keyStoreType, "-pw", ks.Password, "-file", inputFile)
if err != nil {
return fmt.Errorf("error running \"%v -cert -add\": %v %s", ks.command, err, out)
}
return nil
}
// GetCertificateLabels returns the labels of all certificates in the key store
func (ks *KeyStore) GetCertificateLabels() ([]string, error) {
out, _, err := command.Run(ks.command, "-cert", "-list", ks.getFipsEnabledFlag(), "-type", ks.keyStoreType, "-db", ks.Filename, "-pw", ks.Password)
if err != nil {
return nil, fmt.Errorf("error running \"%v -cert -list\": %v %s", ks.command, err, out)
}
scanner := bufio.NewScanner(strings.NewReader(out))
var labels []string
for scanner.Scan() {
s := scanner.Text()
if strings.HasPrefix(s, "-") || strings.HasPrefix(s, "*-") {
s := strings.TrimLeft(s, "-*")
labels = append(labels, strings.TrimSpace(s))
}
}
err = scanner.Err()
if err != nil {
return nil, err
}
return labels, nil
}
// RenameCertificate renames the specified certificate
func (ks *KeyStore) RenameCertificate(from, to string) error {
if ks.command == "/opt/mqm/bin/runmqakm" {
// runmqakm can't handle certs with ' in them so just use capicmd
// Overriding gosec here as this function is in an internal package and only callable by our internal functions.
// #nosec G204
cmd := exec.Command("/opt/mqm/gskit8/bin/gsk8capicmd_64", "-cert", "-rename", "-db", ks.Filename, "-pw", ks.Password, "-label", from, "-new_label", to)
cmd.Env = append(os.Environ(), "LD_LIBRARY_PATH=/opt/mqm/gskit8/lib64/:/opt/mqm/gskit8/lib")
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("error running \"%v -cert -rename\": %v %s", "/opt/mqm/gskit8/bin/gsk8capicmd_64", err, out)
}
} else {
out, _, err := command.Run(ks.command, "-cert", "-rename", "-db", ks.Filename, "-pw", ks.Password, "-label", from, "-new_label", to)
if err != nil {
return fmt.Errorf("error running \"%v -cert -rename\": %v %s", ks.command, err, out)
}
}
return nil
}
// ListAllCertificates Lists all certificates in the keystore
func (ks *KeyStore) ListAllCertificates() ([]string, error) {
out, _, err := command.Run(ks.command, "-cert", "-list", ks.getFipsEnabledFlag(), "-type", ks.keyStoreType, "-db", ks.Filename, "-pw", ks.Password)
if err != nil {
return nil, fmt.Errorf("error running \"%v -cert -list\": %v %s", ks.command, err, out)
}
scanner := bufio.NewScanner(strings.NewReader(out))
var labels []string
for scanner.Scan() {
s := scanner.Text()
// Check for trusted certficates as well here as this method can
// be called for trusted store as well.
if strings.HasPrefix(s, "-") || strings.HasPrefix(s, "*-") || strings.HasPrefix(s, "!") {
s := strings.TrimLeft(s, "-*!")
labels = append(labels, strings.TrimSpace(s))
}
}
err = scanner.Err()
if err != nil {
return nil, err
}
return labels, nil
}
// Returns the FIPS flag. True if enabled else false
func (ks *KeyStore) IsFIPSEnabled() bool {
return ks.fipsEnabled
}
// getFipsEnabledFlag returns the appropriate flag for runmqakm/runmqckm commands
// to enable or disable FIPS.
func (ks *KeyStore) getFipsEnabledFlag() string {
if ks.fipsEnabled {
return "-fips"
} else {
// In the GSKit command line, FIPS mode is enabled by default, so explicitly disable it
return "-fips false"
}
}

View File

@@ -0,0 +1,191 @@
/*
© Copyright IBM Corporation 2018, 2019
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package metrics contains code to provide metrics for the queue manager
package metrics
import (
"github.com/ibm-messaging/mq-container/pkg/logger"
"github.com/prometheus/client_golang/prometheus"
)
const (
namespace = "ibmmq"
qmgrPrefix = "qmgr"
qmgrLabel = "qmgr"
objectPrefix = "object"
objectLabel = "object"
)
type exporter struct {
qmName string
gaugeMap map[string]*prometheus.GaugeVec
counterMap map[string]*prometheus.CounterVec
firstCollect bool
log *logger.Logger
}
func newExporter(qmName string, log *logger.Logger) *exporter {
return &exporter{
qmName: qmName,
gaugeMap: make(map[string]*prometheus.GaugeVec),
counterMap: make(map[string]*prometheus.CounterVec),
firstCollect: true,
log: log,
}
}
// Describe provides details of all available metrics
func (e *exporter) Describe(ch chan<- *prometheus.Desc) {
requestChannel <- false
response := <-responseChannel
for key, metric := range response {
if metric.isDelta {
// For delta type metrics - allocate a Prometheus Counter
counterVec := createCounterVec(metric.name, metric.description, metric.objectType)
e.counterMap[key] = counterVec
// Describe metric
counterVec.Describe(ch)
} else {
// For non-delta type metrics - allocate a Prometheus Gauge
gaugeVec := createGaugeVec(metric.name, metric.description, metric.objectType)
e.gaugeMap[key] = gaugeVec
// Describe metric
gaugeVec.Describe(ch)
}
}
}
// Collect is called at regular intervals to provide the current metric data
func (e *exporter) Collect(ch chan<- prometheus.Metric) {
requestChannel <- true
response := <-responseChannel
for key, metric := range response {
if metric.isDelta {
// For delta type metrics - update their Prometheus Counter
counterVec := e.counterMap[key]
// Populate Prometheus Counter with metric values
// - Skip on first collect to avoid build-up of accumulated values
if !e.firstCollect {
for label, value := range metric.values {
var err error
var counter prometheus.Counter
if label == qmgrLabelValue {
counter, err = counterVec.GetMetricWithLabelValues(e.qmName)
} else {
counter, err = counterVec.GetMetricWithLabelValues(label, e.qmName)
}
if err == nil {
counter.Add(value)
} else {
e.log.Errorf("Metrics Error: %s", err.Error())
}
}
}
// Collect metric
counterVec.Collect(ch)
} else {
// For non-delta type metrics - reset their Prometheus Gauge
gaugeVec := e.gaugeMap[key]
gaugeVec.Reset()
// Populate Prometheus Gauge with metric values
// - Skip on first collect to avoid build-up of accumulated values
if !e.firstCollect {
for label, value := range metric.values {
var err error
var gauge prometheus.Gauge
if label == qmgrLabelValue {
gauge, err = gaugeVec.GetMetricWithLabelValues(e.qmName)
} else {
gauge, err = gaugeVec.GetMetricWithLabelValues(label, e.qmName)
}
if err == nil {
gauge.Set(value)
} else {
e.log.Errorf("Metrics Error: %s", err.Error())
}
}
}
// Collect metric
gaugeVec.Collect(ch)
}
}
if e.firstCollect {
e.firstCollect = false
}
}
// createCounterVec returns a Prometheus CounterVec populated with metric details
func createCounterVec(name, description string, objectType bool) *prometheus.CounterVec {
prefix, labels := getVecDetails(objectType)
counterVec := prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: namespace,
Name: prefix + "_" + name,
Help: description,
},
labels,
)
return counterVec
}
// createGaugeVec returns a Prometheus GaugeVec populated with metric details
func createGaugeVec(name, description string, objectType bool) *prometheus.GaugeVec {
prefix, labels := getVecDetails(objectType)
gaugeVec := prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: namespace,
Name: prefix + "_" + name,
Help: description,
},
labels,
)
return gaugeVec
}
// getVecDetails returns the required prefix and labels for a metric
func getVecDetails(objectType bool) (prefix string, labels []string) {
prefix = qmgrPrefix
labels = []string{qmgrLabel}
if objectType {
prefix = objectPrefix
labels = []string{objectLabel, qmgrLabel}
}
return prefix, labels
}

View File

@@ -0,0 +1,204 @@
/*
© Copyright IBM Corporation 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package metrics
import (
"testing"
"time"
"github.com/ibm-messaging/mq-golang/ibmmq"
"github.com/ibm-messaging/mq-golang/mqmetric"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
)
func TestDescribe_Counter(t *testing.T) {
testDescribe(t, true)
}
func TestDescribe_Gauge(t *testing.T) {
testDescribe(t, false)
}
func testDescribe(t *testing.T, isDelta bool) {
teardownTestCase := setupTestCase(false)
defer teardownTestCase()
log := getTestLogger()
ch := make(chan *prometheus.Desc)
go func() {
exporter := newExporter("qmName", log)
exporter.Describe(ch)
}()
collect := <-requestChannel
if collect {
t.Errorf("Received unexpected collect request")
}
if isDelta {
mqmetric.Metrics.Classes[0].Types[0].Elements[0].Datatype = ibmmq.MQIAMO_MONITOR_DELTA
}
metrics, _ := initialiseMetrics(log)
responseChannel <- metrics
select {
case prometheusDesc := <-ch:
expected := "Desc{fqName: \"ibmmq_qmgr_" + testElement1Name + "\", help: \"" + testElement1Description + "\", constLabels: {}, variableLabels: [qmgr]}"
actual := prometheusDesc.String()
if actual != expected {
t.Errorf("Expected value=%s; actual %s", expected, actual)
}
case <-time.After(1 * time.Second):
t.Error("Did not receive channel response from describe")
}
}
func TestCollect_Counter(t *testing.T) {
testCollect(t, true)
}
func TestCollect_Gauge(t *testing.T) {
testCollect(t, false)
}
func testCollect(t *testing.T, isDelta bool) {
teardownTestCase := setupTestCase(false)
defer teardownTestCase()
log := getTestLogger()
exporter := newExporter("qmName", log)
if isDelta {
exporter.counterMap[testKey1] = createCounterVec(testElement1Name, testElement1Description, false)
} else {
exporter.gaugeMap[testKey1] = createGaugeVec(testElement1Name, testElement1Description, false)
}
for i := 1; i <= 3; i++ {
ch := make(chan prometheus.Metric)
go func() {
exporter.Collect(ch)
close(ch)
}()
collect := <-requestChannel
if !collect {
t.Errorf("Received unexpected describe request")
}
populateTestMetrics(i, false)
if isDelta {
mqmetric.Metrics.Classes[0].Types[0].Elements[0].Datatype = ibmmq.MQIAMO_MONITOR_DELTA
}
metrics, _ := initialiseMetrics(log)
updateMetrics(metrics)
responseChannel <- metrics
select {
case <-ch:
var actual float64
prometheusMetric := dto.Metric{}
if isDelta {
exporter.counterMap[testKey1].WithLabelValues("qmName").Write(&prometheusMetric)
actual = prometheusMetric.GetCounter().GetValue()
} else {
exporter.gaugeMap[testKey1].WithLabelValues("qmName").Write(&prometheusMetric)
actual = prometheusMetric.GetGauge().GetValue()
}
if i == 1 {
if actual != float64(0) {
t.Errorf("Expected values to be zero on first collect; actual %f", actual)
}
} else if isDelta && i != 2 {
if actual != float64(i+(i-1)) {
t.Errorf("Expected value=%f; actual %f", float64(i+(i-1)), actual)
}
} else if actual != float64(i) {
t.Errorf("Expected value=%f; actual %f", float64(i), actual)
}
case <-time.After(1 * time.Second):
t.Error("Did not receive channel response from collect")
}
}
}
func TestCreateCounterVec(t *testing.T) {
ch := make(chan *prometheus.Desc)
counterVec := createCounterVec("MetricName", "MetricDescription", false)
go func() {
counterVec.Describe(ch)
}()
description := <-ch
expected := "Desc{fqName: \"ibmmq_qmgr_MetricName\", help: \"MetricDescription\", constLabels: {}, variableLabels: [qmgr]}"
actual := description.String()
if actual != expected {
t.Errorf("Expected value=%s; actual %s", expected, actual)
}
}
func TestCreateCounterVec_ObjectLabel(t *testing.T) {
ch := make(chan *prometheus.Desc)
counterVec := createCounterVec("MetricName", "MetricDescription", true)
go func() {
counterVec.Describe(ch)
}()
description := <-ch
expected := "Desc{fqName: \"ibmmq_object_MetricName\", help: \"MetricDescription\", constLabels: {}, variableLabels: [object qmgr]}"
actual := description.String()
if actual != expected {
t.Errorf("Expected value=%s; actual %s", expected, actual)
}
}
func TestCreateGaugeVec(t *testing.T) {
ch := make(chan *prometheus.Desc)
gaugeVec := createGaugeVec("MetricName", "MetricDescription", false)
go func() {
gaugeVec.Describe(ch)
}()
description := <-ch
expected := "Desc{fqName: \"ibmmq_qmgr_MetricName\", help: \"MetricDescription\", constLabels: {}, variableLabels: [qmgr]}"
actual := description.String()
if actual != expected {
t.Errorf("Expected value=%s; actual %s", expected, actual)
}
}
func TestCreateGaugeVec_ObjectLabel(t *testing.T) {
ch := make(chan *prometheus.Desc)
gaugeVec := createGaugeVec("MetricName", "MetricDescription", true)
go func() {
gaugeVec.Describe(ch)
}()
description := <-ch
expected := "Desc{fqName: \"ibmmq_object_MetricName\", help: \"MetricDescription\", constLabels: {}, variableLabels: [object qmgr]}"
actual := description.String()
if actual != expected {
t.Errorf("Expected value=%s; actual %s", expected, actual)
}
}

124
internal/metrics/mapping.go Normal file
View File

@@ -0,0 +1,124 @@
/*
© Copyright IBM Corporation 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package metrics contains code to provide metrics for the queue manager
package metrics
type metricLookup struct {
name string
enabled bool
}
// generateMetricNamesMap generates metric names mapped from their description
func generateMetricNamesMap() map[string]metricLookup {
metricNamesMap := map[string]metricLookup{
"CPU/SystemSummary/CPU load - one minute average": metricLookup{"cpu_load_one_minute_average_percentage", true},
"CPU/SystemSummary/CPU load - five minute average": metricLookup{"cpu_load_five_minute_average_percentage", true},
"CPU/SystemSummary/CPU load - fifteen minute average": metricLookup{"cpu_load_fifteen_minute_average_percentage", true},
"CPU/SystemSummary/System CPU time percentage": metricLookup{"system_cpu_time_percentage", true},
"CPU/SystemSummary/User CPU time percentage": metricLookup{"user_cpu_time_percentage", true},
"CPU/SystemSummary/RAM free percentage": metricLookup{"ram_free_percentage", true},
"CPU/SystemSummary/RAM total bytes": metricLookup{"system_ram_size_bytes", false},
"CPU/QMgrSummary/System CPU time - percentage estimate for queue manager": metricLookup{"system_cpu_time_estimate_for_queue_manager_percentage", true},
"CPU/QMgrSummary/User CPU time - percentage estimate for queue manager": metricLookup{"user_cpu_time_estimate_for_queue_manager_percentage", true},
"CPU/QMgrSummary/RAM total bytes - estimate for queue manager": metricLookup{"ram_usage_estimate_for_queue_manager_bytes", true},
"DISK/SystemSummary/MQ trace file system - free space": metricLookup{"trace_file_system_free_space_percentage", true},
"DISK/SystemSummary/MQ trace file system - bytes in use": metricLookup{"trace_file_system_in_use_bytes", true},
"DISK/SystemSummary/MQ errors file system - free space": metricLookup{"errors_file_system_free_space_percentage", true},
"DISK/SystemSummary/MQ errors file system - bytes in use": metricLookup{"errors_file_system_in_use_bytes", true},
"DISK/SystemSummary/MQ FDC file count": metricLookup{"fdc_files", true},
"DISK/QMgrSummary/Queue Manager file system - free space": metricLookup{"queue_manager_file_system_free_space_percentage", true},
"DISK/QMgrSummary/Queue Manager file system - bytes in use": metricLookup{"queue_manager_file_system_in_use_bytes", true},
"DISK/Log/Log - logical bytes written": metricLookup{"log_logical_written_bytes_total", true},
"DISK/Log/Log - physical bytes written": metricLookup{"log_physical_written_bytes_total", true},
"DISK/Log/Log - current primary space in use": metricLookup{"log_primary_space_in_use_percentage", true},
"DISK/Log/Log - workload primary space utilization": metricLookup{"log_workload_primary_space_utilization_percentage", true},
"DISK/Log/Log - write latency": metricLookup{"log_write_latency_seconds", true},
"DISK/Log/Log - bytes max": metricLookup{"log_max_bytes", true},
"DISK/Log/Log - write size": metricLookup{"log_write_size_bytes", true},
"DISK/Log/Log - bytes in use": metricLookup{"log_in_use_bytes", true},
"DISK/Log/Log file system - bytes max": metricLookup{"log_file_system_max_bytes", true},
"DISK/Log/Log file system - bytes in use": metricLookup{"log_file_system_in_use_bytes", true},
"DISK/Log/Log - bytes occupied by reusable extents": metricLookup{"log_occupied_by_reusable_extents_bytes", true},
"DISK/Log/Log - bytes occupied by extents waiting to be archived": metricLookup{"log_occupied_by_extents_waiting_to_be_archived_bytes", true},
"DISK/Log/Log - bytes required for media recovery": metricLookup{"log_required_for_media_recovery_bytes", true},
"STATMQI/SUBSCRIBE/Create durable subscription count": metricLookup{"durable_subscription_create_total", true},
"STATMQI/SUBSCRIBE/Alter durable subscription count": metricLookup{"durable_subscription_alter_total", true},
"STATMQI/SUBSCRIBE/Resume durable subscription count": metricLookup{"durable_subscription_resume_total", true},
"STATMQI/SUBSCRIBE/Delete durable subscription count": metricLookup{"durable_subscription_delete_total", true},
"STATMQI/SUBSCRIBE/Create non-durable subscription count": metricLookup{"non_durable_subscription_create_total", true},
"STATMQI/SUBSCRIBE/Delete non-durable subscription count": metricLookup{"non_durable_subscription_delete_total", true},
"STATMQI/SUBSCRIBE/Failed create/alter/resume subscription count": metricLookup{"failed_subscription_create_alter_resume_total", true},
"STATMQI/SUBSCRIBE/Subscription delete failure count": metricLookup{"failed_subscription_delete_total", true},
"STATMQI/SUBSCRIBE/MQSUBRQ count": metricLookup{"mqsubrq_total", true},
"STATMQI/SUBSCRIBE/Failed MQSUBRQ count": metricLookup{"failed_mqsubrq_total", true},
"STATMQI/SUBSCRIBE/Durable subscriber - high water mark": metricLookup{"durable_subscriber_high_water_mark", false},
"STATMQI/SUBSCRIBE/Durable subscriber - low water mark": metricLookup{"durable_subscriber_low_water_mark", false},
"STATMQI/SUBSCRIBE/Non-durable subscriber - high water mark": metricLookup{"non_durable_subscriber_high_water_mark", false},
"STATMQI/SUBSCRIBE/Non-durable subscriber - low water mark": metricLookup{"non_durable_subscriber_low_water_mark", false},
"STATMQI/PUBLISH/Topic MQPUT/MQPUT1 interval total": metricLookup{"topic_mqput_mqput1_total", true},
"STATMQI/PUBLISH/Interval total topic bytes put": metricLookup{"topic_put_bytes_total", true},
"STATMQI/PUBLISH/Failed topic MQPUT/MQPUT1 count": metricLookup{"failed_topic_mqput_mqput1_total", true},
"STATMQI/PUBLISH/Persistent - topic MQPUT/MQPUT1 count": metricLookup{"persistent_topic_mqput_mqput1_total", true},
"STATMQI/PUBLISH/Non-persistent - topic MQPUT/MQPUT1 count": metricLookup{"non_persistent_topic_mqput_mqput1_total", true},
"STATMQI/PUBLISH/Published to subscribers - message count": metricLookup{"published_to_subscribers_message_total", true},
"STATMQI/PUBLISH/Published to subscribers - byte count": metricLookup{"published_to_subscribers_bytes_total", true},
"STATMQI/CONNDISC/MQCONN/MQCONNX count": metricLookup{"mqconn_mqconnx_total", true},
"STATMQI/CONNDISC/Failed MQCONN/MQCONNX count": metricLookup{"failed_mqconn_mqconnx_total", true},
"STATMQI/CONNDISC/MQDISC count": metricLookup{"mqdisc_total", true},
"STATMQI/CONNDISC/Concurrent connections - high water mark": metricLookup{"concurrent_connections_high_water_mark", false},
"STATMQI/OPENCLOSE/MQOPEN count": metricLookup{"mqopen_total", true},
"STATMQI/OPENCLOSE/Failed MQOPEN count": metricLookup{"failed_mqopen_total", true},
"STATMQI/OPENCLOSE/MQCLOSE count": metricLookup{"mqclose_total", true},
"STATMQI/OPENCLOSE/Failed MQCLOSE count": metricLookup{"failed_mqclose_total", true},
"STATMQI/INQSET/MQINQ count": metricLookup{"mqinq_total", true},
"STATMQI/INQSET/Failed MQINQ count": metricLookup{"failed_mqinq_total", true},
"STATMQI/INQSET/MQSET count": metricLookup{"mqset_total", true},
"STATMQI/INQSET/Failed MQSET count": metricLookup{"failed_mqset_total", true},
"STATMQI/PUT/Persistent message MQPUT count": metricLookup{"persistent_message_mqput_total", true},
"STATMQI/PUT/Persistent message MQPUT1 count": metricLookup{"persistent_message_mqput1_total", true},
"STATMQI/PUT/Put persistent messages - byte count": metricLookup{"persistent_message_put_bytes_total", true},
"STATMQI/PUT/Non-persistent message MQPUT count": metricLookup{"non_persistent_message_mqput_total", true},
"STATMQI/PUT/Non-persistent message MQPUT1 count": metricLookup{"non_persistent_message_mqput1_total", true},
"STATMQI/PUT/Put non-persistent messages - byte count": metricLookup{"non_persistent_message_put_bytes_total", true},
"STATMQI/PUT/Interval total MQPUT/MQPUT1 count": metricLookup{"mqput_mqput1_total", true},
"STATMQI/PUT/Interval total MQPUT/MQPUT1 byte count": metricLookup{"mqput_mqput1_bytes_total", true},
"STATMQI/PUT/Failed MQPUT count": metricLookup{"failed_mqput_total", true},
"STATMQI/PUT/Failed MQPUT1 count": metricLookup{"failed_mqput1_total", true},
"STATMQI/PUT/MQSTAT count": metricLookup{"mqstat_total", true},
"STATMQI/GET/Persistent message destructive get - count": metricLookup{"persistent_message_destructive_get_total", true},
"STATMQI/GET/Persistent message browse - count": metricLookup{"persistent_message_browse_total", true},
"STATMQI/GET/Got persistent messages - byte count": metricLookup{"persistent_message_get_bytes_total", true},
"STATMQI/GET/Persistent message browse - byte count": metricLookup{"persistent_message_browse_bytes_total", true},
"STATMQI/GET/Non-persistent message destructive get - count": metricLookup{"non_persistent_message_destructive_get_total", true},
"STATMQI/GET/Non-persistent message browse - count": metricLookup{"non_persistent_message_browse_total", true},
"STATMQI/GET/Got non-persistent messages - byte count": metricLookup{"non_persistent_message_get_bytes_total", true},
"STATMQI/GET/Non-persistent message browse - byte count": metricLookup{"non_persistent_message_browse_bytes_total", true},
"STATMQI/GET/Interval total destructive get- count": metricLookup{"destructive_get_total", true},
"STATMQI/GET/Interval total destructive get - byte count": metricLookup{"destructive_get_bytes_total", true},
"STATMQI/GET/Failed MQGET - count": metricLookup{"failed_mqget_total", true},
"STATMQI/GET/Failed browse count": metricLookup{"failed_browse_total", true},
"STATMQI/GET/MQCTL count": metricLookup{"mqctl_total", true},
"STATMQI/GET/Expired message count": metricLookup{"expired_message_total", true},
"STATMQI/GET/Purged queue count": metricLookup{"purged_queue_total", true},
"STATMQI/GET/MQCB count": metricLookup{"mqcb_total", true},
"STATMQI/GET/Failed MQCB count": metricLookup{"failed_mqcb_total", true},
"STATMQI/SYNCPOINT/Commit count": metricLookup{"commit_total", true},
"STATMQI/SYNCPOINT/Rollback count": metricLookup{"rollback_total", true},
}
return metricNamesMap
}

View File

@@ -0,0 +1,37 @@
/*
© Copyright IBM Corporation 2018
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package metrics
import "testing"
func TestGenerateMetricNamesMap(t *testing.T) {
metricNamesMap := generateMetricNamesMap()
if len(metricNamesMap) != 93 {
t.Errorf("Expected mapping-size=%d; actual %d", 93, len(metricNamesMap))
}
actual, ok := metricNamesMap[testKey1]
if !ok {
t.Errorf("No metric name mapping found for %s", testKey1)
} else {
if actual.name != testElement1Name {
t.Errorf("Expected metric name=%s; actual %s", testElement1Name, actual.name)
}
}
}

123
internal/metrics/metrics.go Normal file
View File

@@ -0,0 +1,123 @@
/*
© Copyright IBM Corporation 2018, 2023
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package metrics contains code to provide metrics for the queue manager
package metrics
import (
"context"
"fmt"
"net/http"
"time"
"github.com/ibm-messaging/mq-container/internal/ready"
"github.com/ibm-messaging/mq-container/pkg/logger"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const (
defaultPort = "9157"
)
var (
metricsEnabled = false
// #nosec G112 - this code is changing soon to use https.
// for now we will ignore the gosec.
metricsServer = &http.Server{Addr: ":" + defaultPort}
)
// GatherMetrics gathers metrics for the queue manager
func GatherMetrics(qmName string, log *logger.Logger) {
// If running in standby mode - wait until the queue manager becomes active
for {
status, _ := ready.Status(context.Background(), qmName)
if status.ActiveQM() {
break
}
time.Sleep(requestTimeout * time.Second)
}
metricsEnabled = true
err := startMetricsGathering(qmName, log)
if err != nil {
log.Errorf("Metrics Error: %s", err.Error())
StopMetricsGathering(log)
}
}
// startMetricsGathering starts gathering metrics for the queue manager
func startMetricsGathering(qmName string, log *logger.Logger) error {
defer func() {
if r := recover(); r != nil {
log.Errorf("Metrics Error: %v", r)
}
}()
log.Println("Starting metrics gathering")
// Start processing metrics
go processMetrics(log, qmName)
// Wait for metrics to be ready before starting the Prometheus handler
<-startChannel
// Register metrics
metricsExporter := newExporter(qmName, log)
err := prometheus.Register(metricsExporter)
if err != nil {
return fmt.Errorf("Failed to register metrics: %v", err)
}
// Setup HTTP server to handle requests from Prometheus
http.Handle("/metrics", promhttp.Handler())
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
// #nosec G104
w.Write([]byte("Status: METRICS ACTIVE"))
})
go func() {
err = metricsServer.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
log.Errorf("Metrics Error: Failed to handle metrics request: %v", err)
StopMetricsGathering(log)
}
}()
return nil
}
// StopMetricsGathering stops gathering metrics for the queue manager
func StopMetricsGathering(log *logger.Logger) {
if metricsEnabled {
// Stop processing metrics
stopChannel <- true
// Shutdown HTTP server
timeout, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := metricsServer.Shutdown(timeout)
if err != nil {
log.Errorf("Failed to shutdown metrics server: %v", err)
}
}
}

227
internal/metrics/update.go Normal file
View File

@@ -0,0 +1,227 @@
/*
© Copyright IBM Corporation 2018, 2019
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package metrics contains code to provide metrics for the queue manager
package metrics
import (
"fmt"
"strings"
"time"
"github.com/ibm-messaging/mq-container/pkg/logger"
"github.com/ibm-messaging/mq-golang/ibmmq"
"github.com/ibm-messaging/mq-golang/mqmetric"
)
const (
qmgrLabelValue = mqmetric.QMgrMapKey
requestTimeout = 10
)
var (
startChannel = make(chan bool)
stopChannel = make(chan bool, 2)
requestChannel = make(chan bool)
responseChannel = make(chan map[string]*metricData)
)
type metricData struct {
name string
description string
objectType bool
values map[string]float64
isDelta bool
}
// processMetrics processes publications of metric data and handles describe/collect/stop requests
func processMetrics(log *logger.Logger, qmName string) {
var err error
var firstConnect = true
var metrics map[string]*metricData
for {
// Connect to queue manager and discover available metrics
err = doConnect(qmName)
if err == nil {
if firstConnect {
firstConnect = false
startChannel <- true
}
// #nosec G104
metrics, _ = initialiseMetrics(log)
}
// Now loop until something goes wrong
for err == nil {
// Process publications of metric data
// TODO: If we have a large number of metrics to process, then we could be blocked from responding to stop requests
err = mqmetric.ProcessPublications()
// Handle describe/collect/stop requests
if err == nil {
select {
case collect := <-requestChannel:
if collect {
updateMetrics(metrics)
}
responseChannel <- metrics
case <-stopChannel:
log.Println("Stopping metrics gathering")
mqmetric.EndConnection()
return
case <-time.After(requestTimeout * time.Second):
log.Debugf("Metrics: No requests received within timeout period (%d seconds)", requestTimeout)
}
}
}
log.Errorf("Metrics Error: %s", err.Error())
// Close the connection
mqmetric.EndConnection()
// Handle stop requests
select {
case <-stopChannel:
log.Println("Stopping metrics gathering")
return
case <-time.After(requestTimeout * time.Second):
log.Println("Retrying metrics gathering")
}
}
}
// doConnect connects to the queue manager and discovers available metrics
func doConnect(qmName string) error {
// Set connection configuration
var connConfig mqmetric.ConnectionConfig
connConfig.ClientMode = false
connConfig.UserId = ""
connConfig.Password = ""
// Connect to the queue manager - open the command and dynamic reply queues
err := mqmetric.InitConnectionStats(qmName, "SYSTEM.DEFAULT.MODEL.QUEUE", "", &connConfig)
if err != nil {
return fmt.Errorf("Failed to connect to queue manager %s: %v", qmName, err)
}
// Discover available metrics for the queue manager and subscribe to them
err = mqmetric.DiscoverAndSubscribe("", true, "")
if err != nil {
return fmt.Errorf("Failed to discover and subscribe to metrics: %v", err)
}
return nil
}
// initialiseMetrics sets initial details for all available metrics
func initialiseMetrics(log *logger.Logger) (map[string]*metricData, error) {
metrics := make(map[string]*metricData)
validMetrics := true
metricNamesMap := generateMetricNamesMap()
for _, metricClass := range mqmetric.Metrics.Classes {
for _, metricType := range metricClass.Types {
if !strings.Contains(metricType.ObjectTopic, "%s") {
for _, metricElement := range metricType.Elements {
// Get unique metric key
key := makeKey(metricElement)
// Get metric name from mapping
if metricLookup, found := metricNamesMap[key]; found {
// Check if metric is enabled
if metricLookup.enabled {
// Check if metric is a delta type
isDelta := false
if metricElement.Datatype == ibmmq.MQIAMO_MONITOR_DELTA {
isDelta = true
}
// Set metric details
metric := metricData{
name: metricLookup.name,
description: metricElement.Description,
isDelta: isDelta,
}
// Add metric
if _, exists := metrics[key]; !exists {
metrics[key] = &metric
} else {
log.Errorf("Metrics Error: Found duplicate metric key [%s]", key)
validMetrics = false
}
} else {
log.Debugf("Metrics: Skipping metric, metric is not enabled for key [%s]", key)
}
} else {
log.Errorf("Metrics Error: Skipping metric, unexpected key [%s]", key)
validMetrics = false
}
}
}
}
}
if !validMetrics {
return metrics, fmt.Errorf("Invalid metrics data")
}
return metrics, nil
}
// updateMetrics updates values for all available metrics
func updateMetrics(metrics map[string]*metricData) {
for _, metricClass := range mqmetric.Metrics.Classes {
for _, metricType := range metricClass.Types {
if !strings.Contains(metricType.ObjectTopic, "%s") {
for _, metricElement := range metricType.Elements {
// Unexpected metric elements (with no defined mapping) are handled in 'initialiseMetrics'
// - if any exist, they are logged as errors and skipped (they are not added to the metrics map)
// Therefore we can ignore handling any unexpected metric elements found here
// - this avoids us logging excessive errors, as this function is called frequently
metric, ok := metrics[makeKey(metricElement)]
if ok {
// Clear existing metric values
metric.values = make(map[string]float64)
// Update metric with cached values of publication data
for label, value := range metricElement.Values {
normalisedValue := mqmetric.Normalise(metricElement, label, value)
metric.values[label] = normalisedValue
}
}
// Reset cached values of publication data for this metric
metricElement.Values = make(map[string]int64)
}
}
}
}
}
// makeKey builds a unique key for each metric
func makeKey(metricElement *mqmetric.MonElement) string {
return metricElement.Parent.Parent.Name + "/" + metricElement.Parent.Name + "/" + metricElement.Description
}

View File

@@ -0,0 +1,197 @@
/*
© Copyright IBM Corporation 2018, 2019
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package metrics
import (
"os"
"testing"
"github.com/ibm-messaging/mq-container/pkg/logger"
"github.com/ibm-messaging/mq-golang/mqmetric"
)
const (
testClassName = "CPU"
testTypeName = "SystemSummary"
testElement1Name = "cpu_load_five_minute_average_percentage"
testElement2Name = "cpu_load_fifteen_minute_average_percentage"
testElement1Description = "CPU load - five minute average"
testElement2Description = "CPU load - fifteen minute average"
testKey1 = testClassName + "/" + testTypeName + "/" + testElement1Description
testKey2 = testClassName + "/" + testTypeName + "/" + testElement2Description
)
func TestInitialiseMetrics(t *testing.T) {
teardownTestCase := setupTestCase(false)
defer teardownTestCase()
metrics, err := initialiseMetrics(getTestLogger())
metric, ok := metrics[testKey1]
if err != nil {
t.Errorf("Unexpected error %s", err.Error())
}
if !ok {
t.Error("Expected metric not found in map")
} else {
if metric.name != testElement1Name {
t.Errorf("Expected name=%s; actual %s", testElement1Name, metric.name)
}
if metric.description != testElement1Description {
t.Errorf("Expected description=%s; actual %s", testElement1Description, metric.description)
}
if metric.objectType != false {
t.Errorf("Expected objectType=%v; actual %v", false, metric.objectType)
}
if len(metric.values) != 0 {
t.Errorf("Expected values-size=%d; actual %d", 0, len(metric.values))
}
}
_, ok = metrics[testKey2]
if ok {
t.Errorf("Unexpected metric found in map, %%s object topics should be ignored")
}
if len(metrics) != 1 {
t.Errorf("Map contains unexpected metrics, map size=%d", len(metrics))
}
}
func TestInitialiseMetrics_UnexpectedKey(t *testing.T) {
teardownTestCase := setupTestCase(false)
defer teardownTestCase()
mqmetric.Metrics.Classes[0].Types[0].Elements[0].Description = "New Metric"
_, err := initialiseMetrics(getTestLogger())
if err == nil {
t.Error("Expected skipping metric error")
}
}
func TestInitialiseMetrics_DuplicateKeys(t *testing.T) {
teardownTestCase := setupTestCase(true)
defer teardownTestCase()
_, err := initialiseMetrics(getTestLogger())
if err == nil {
t.Error("Expected duplicate keys error")
}
}
func TestUpdateMetrics(t *testing.T) {
teardownTestCase := setupTestCase(false)
defer teardownTestCase()
metrics, _ := initialiseMetrics(getTestLogger())
updateMetrics(metrics)
metric, _ := metrics[testKey1]
actual, ok := metric.values[qmgrLabelValue]
if !ok {
t.Error("No metric values found for queue manager label")
} else {
if actual != float64(1) {
t.Errorf("Expected metric value=%f; actual %f", float64(1), actual)
}
if len(metric.values) != 1 {
t.Errorf("Expected values-size=%d; actual %d", 1, len(metric.values))
}
}
if len(mqmetric.Metrics.Classes[0].Types[0].Elements[0].Values) != 0 {
t.Error("Unexpected cached value; publication data should have been reset")
}
updateMetrics(metrics)
if len(metric.values) != 0 {
t.Errorf("Unexpected metric value; data should have been cleared")
}
}
func TestMakeKey(t *testing.T) {
teardownTestCase := setupTestCase(false)
defer teardownTestCase()
expected := testKey1
actual := makeKey(mqmetric.Metrics.Classes[0].Types[0].Elements[0])
if actual != expected {
t.Errorf("Expected value=%s; actual %s", expected, actual)
}
}
func setupTestCase(duplicateKey bool) func() {
populateTestMetrics(1, duplicateKey)
return func() {
cleanTestMetrics()
}
}
func populateTestMetrics(testValue int, duplicateKey bool) {
metricClass := new(mqmetric.MonClass)
metricType1 := new(mqmetric.MonType)
metricType2 := new(mqmetric.MonType)
metricElement1 := new(mqmetric.MonElement)
metricElement2 := new(mqmetric.MonElement)
metricClass.Name = testClassName
metricType1.Name = testTypeName
metricType2.Name = testTypeName
metricElement1.MetricName = "Element1Name"
metricElement1.Description = testElement1Description
metricElement1.Values = make(map[string]int64)
metricElement1.Values[qmgrLabelValue] = int64(testValue)
metricElement2.MetricName = "Element2Name"
metricElement2.Description = testElement2Description
metricElement2.Values = make(map[string]int64)
metricType1.ObjectTopic = "ObjectTopic"
metricType2.ObjectTopic = "%s"
metricElement1.Parent = metricType1
metricElement2.Parent = metricType2
metricType1.Parent = metricClass
metricType2.Parent = metricClass
metricType1.Elements = make(map[int]*mqmetric.MonElement)
metricType2.Elements = make(map[int]*mqmetric.MonElement)
metricType1.Elements[0] = metricElement1
if duplicateKey {
metricType1.Elements[1] = metricElement1
}
metricType2.Elements[0] = metricElement2
metricClass.Types = make(map[int]*mqmetric.MonType)
metricClass.Types[0] = metricType1
metricClass.Types[1] = metricType2
mqmetric.Metrics.Classes = make(map[int]*mqmetric.MonClass)
mqmetric.Metrics.Classes[0] = metricClass
}
func cleanTestMetrics() {
mqmetric.Metrics.Classes = make(map[int]*mqmetric.MonClass)
}
func getTestLogger() *logger.Logger {
log, _ := logger.NewLogger(os.Stdout, false, false, "test")
return log
}

View File

@@ -0,0 +1,264 @@
/*
© Copyright IBM Corporation 2019
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mqscredact
import (
"bufio"
"io"
"regexp"
"strings"
)
/* List of sensitive MQ Parameters */
var sensitiveParameters = []string{"LDAPPWD", "PASSWORD", "SSLCRYP"}
// redactionString is what sensitive paramters will be replaced with
const redactionString = "(*********)"
func findEndOfParamterString(stringDenoter rune, r *bufio.Reader) string {
parameter := ""
for {
char, _, err := r.ReadRune()
if err != nil {
return parameter
}
parameter = parameter + string(char)
if char == stringDenoter {
break
} else if char == '\n' {
// Check if we're on a comment line
NewLineLoop:
for {
// Look at next character without moving buffer forwards
chars, err := r.Peek(1)
if err != nil {
return parameter
}
// Check if we're at the beginning of some data.
startOutput, _ := regexp.MatchString(`[^:0-9\s]`, string(chars[0]))
if startOutput {
// We are at the start, check if we're on a comment line
if chars[0] == '*' {
// found a comment line. go to the next newline chraracter
CommentLoop:
for {
char, _, err = r.ReadRune()
if err != nil {
return parameter
}
parameter = parameter + string(char)
if char == '\n' {
break CommentLoop
}
}
// Go round again as we're now on a new line
continue NewLineLoop
}
// We've checked for comment and it isn't a comment line so break without moving buffer forwards
break NewLineLoop
}
// Move the buffer forward and try again
char, _, _ = r.ReadRune()
parameter = parameter + string(char)
}
}
}
return parameter
}
// getParameterString reads from r in order to find the end of the MQSC Parameter value. This is enclosed in ( ).
// This function will return what it finds and will increment the reader pointer along as it goes.
func getParameterString(r *bufio.Reader) string {
// Add the ( in as it will have been dropped before.
parameter := "("
Loop:
for {
char, _, err := r.ReadRune()
if err != nil {
return parameter
}
parameter = parameter + string(char)
switch char {
case ')':
break Loop
// TODO: Duplicate code..
case '\'', '"':
parameter = parameter + findEndOfParamterString(char, r)
}
}
return parameter
}
func resetAllParameters(currentVerb, originalString *string, lineContinuation, foundGap, parameterNext, redacting, checkComment *bool) {
*currentVerb = ""
*originalString = ""
*lineContinuation = false
*foundGap = false
*parameterNext = false
*redacting = false
*checkComment = true
}
// Redact is the main function for redacting sensitive parameters in MQSC strings
// It accepts a string and redacts sensitive paramters such as LDAPPWD or PASSWORD
func Redact(out string) (string, error) {
out = strings.TrimSpace(out)
var returnStr, currentVerb, originalString string
var lineContinuation, foundGap, parameterNext, redacting, checkComment bool
newline := true
resetAllParameters(&currentVerb, &originalString, &lineContinuation, &foundGap, &parameterNext, &redacting, &checkComment)
r := bufio.NewReader(strings.NewReader(out))
MainLoop:
for {
// We have found a opening ( so use special parameter parsing
if parameterNext {
parameterStr := getParameterString(r)
if !redacting {
returnStr = returnStr + parameterStr
} else {
returnStr = returnStr + redactionString
}
resetAllParameters(&currentVerb, &originalString, &lineContinuation, &foundGap, &parameterNext, &redacting, &checkComment)
}
// Loop round getting hte next parameter
char, _, err := r.ReadRune()
if err == io.EOF {
if originalString != "" {
returnStr = returnStr + originalString
}
break
} else if err != nil {
return returnStr, err
}
/* We need to push forward until we find a non-whitespace, digit or colon character */
if newline {
startOutput, _ := regexp.MatchString(`[^:0-9\s]`, string(char))
if !startOutput {
originalString = originalString + string(char)
continue MainLoop
}
newline = false
}
switch char {
// Found a line continuation character
case '+', '-':
lineContinuation = true
foundGap = false
originalString = originalString + string(char)
continue MainLoop
// Found whitespace/new line
case '\n':
checkComment = true
newline = true
fallthrough
case '\t', '\r', ' ':
if !lineContinuation {
foundGap = true
}
originalString = originalString + string(char)
continue MainLoop
// Found a paramter value
case '(':
parameterNext = true
/* Do not continue as we need to do some checks */
// Found a comment, parse in a special manner
case '*':
if checkComment {
originalString = originalString + string(char)
// Loop round until we find the new line character that marks the end of the comment
CommentLoop:
for {
char, _, err := r.ReadRune()
if err == io.EOF {
if originalString != "" {
returnStr = returnStr + originalString
}
break MainLoop
} else if err != nil {
return returnStr, err
}
originalString = originalString + string(char)
if char == '\n' {
break CommentLoop
}
}
//Comment has been read and added to original string, go back to start
checkComment = true
newline = true
continue MainLoop
}
/* Do not continue as we need to do some checks */
} //end of switch
checkComment = false
if lineContinuation {
lineContinuation = false
}
if foundGap || parameterNext {
// we've completed an parameter so check whether it is sensitive
currentVerb = strings.ToUpper(currentVerb)
if isSensitiveCommand(currentVerb) {
redacting = true
}
// Add the unedited string to the return string
returnStr = returnStr + originalString
//reset some of the parameters
originalString = ""
currentVerb = ""
foundGap = false
lineContinuation = false
}
originalString = originalString + string(char)
currentVerb = currentVerb + string(char)
}
return returnStr, nil
}
// isSensitiveCommand checks whether the given string contains a sensitive parameter.
// We use contains here because we can't determine whether a line continuation seperates
// parts of a parameter or two different parameters.
func isSensitiveCommand(command string) bool {
for _, v := range sensitiveParameters {
if strings.Contains(command, v) {
return true
}
}
return false
}

View File

@@ -0,0 +1,171 @@
/*
© Copyright IBM Corporation 2019
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mqscredact
import (
"strings"
"testing"
)
const passwordString = passwordHalf1 + passwordHalf2
const passwordHalf1 = "hippo"
const passwordHalf2 = "123456"
var testStrings = [...]string{
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD('" + passwordString + "')",
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD(\"" + passwordString + "\")",
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD ('" + passwordString + "')",
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD\t\t('" + passwordString + "')",
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) ldappwd('" + passwordString + "')",
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LdApPwD('" + passwordString + "')",
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) PASSWORD('" + passwordString + "')",
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) PASSWORD(\"" + passwordString + "\")",
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) PASSWORD ('" + passwordString + "')",
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) PASSWORD\t\t('" + passwordString + "')",
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) password('" + passwordString + "')",
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) pAsSwOrD('" + passwordString + "')",
"ALTER QMGR SSLCRYP('" + passwordString + "')",
"ALTER QMGR SSLCRYP(\"" + passwordString + "\")",
"ALTER QMGR SSLCRYP ('" + passwordString + "')",
"ALTER QMGR SSLCRYP\t\t('" + passwordString + "')",
"ALTER QMGR sslcryp('" + passwordString + "')",
"ALTER QMGR sslCRYP('" + passwordString + "')",
// Line continuation ones
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD(\"" + passwordHalf1 + "+\n " + passwordHalf2 + "\")",
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD(\"" + passwordHalf1 + "+\n\t" + passwordHalf2 + "\")",
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD(\"" + passwordHalf1 + "+\n\t " + passwordHalf2 + "\")",
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD('" + passwordHalf1 + "+\n " + passwordHalf2 + "')",
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD('" + passwordHalf1 + "+\n\t" + passwordHalf2 + "')",
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD('" + passwordHalf1 + "+\n\t " + passwordHalf2 + "')",
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD(\"" + passwordHalf1 + "-\n" + passwordHalf2 + "\")",
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD('" + passwordHalf1 + "-\n" + passwordHalf2 + "')",
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD(\"" + passwordHalf1 + "+ \n " + passwordHalf2 + "\")",
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD(\"" + passwordHalf1 + "+\t\n " + passwordHalf2 + "\")",
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD(\"" + passwordHalf1 + "- \n" + passwordHalf2 + "\")",
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD(\"" + passwordHalf1 + "-\t\n" + passwordHalf2 + "\")",
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) PASSWORD(\"" + passwordHalf1 + "+\n " + passwordHalf2 + "\")",
"ALTER QMGR SSLCRYP(\"" + passwordHalf1 + "+\n " + passwordHalf2 + "\")",
//edge cases
"ALTER QMGR SSLCRYP(\"" + passwordHalf1 + "+\n 123+\n 456\")",
"ALTER QMGR SSLCRYP(\"" + passwordHalf1 + "-\n123-\n456\")",
"ALTER QMGR SSLCRYP(\"" + passwordHalf1 + "+\n 1+\n 2+\n 3+\n 4+\n 5+\n 6\")",
"ALTER QMGR SSLCRYP(\"" + passwordHalf1 + "-\n1-\n2-\n3-\n4-\n5-\n6\")",
"ALTER QMGR SSLCRYP + \n (\"" + passwordHalf1 + "+\n 1+\n 2+\n 3+\n 4+\n 5+\n 6\")",
"ALTER QMGR SSLCRYP - \n(\"" + passwordHalf1 + "-\n1-\n2-\n3-\n4-\n5-\n6\")",
"ALTER QMGR SSL + \n CRYP(\"" + passwordHalf1 + "+\n 1+\n 2+\n 3+\n 4+\n 5+\n 6\")",
"ALTER QMGR SSL - \nCRYP(\"" + passwordHalf1 + "-\n1-\n2-\n3-\n4-\n5-\n6\")",
"ALTER QMGR + \n SSL +\n CRYP(\"" + passwordHalf1 + "+\n 1+\n 2+\n 3+\n 4+\n 5+\n 6\") +\n TEST(1234)",
"ALTER QMGR -\nSSL -\nCRYP(\"" + passwordHalf1 + "-\n1-\n2-\n3-\n4-\n5-\n6\") -\nTEST(1234)",
"ALTER QMGR +\n * COMMENT\n SSL +\n * COMMENT IN MIDDLE\n CRYP('" + passwordString + "')",
" 1: ALTER CHANNEL(TEST2) CHLTYPE(SDR) PASS+\n : *test comment\n : WORD('" + passwordString + "')",
" 2: ALTER CHANNEL(TEST3) CHLTYPE(SDR) PASSWORD('" + passwordHalf1 + "-\n*commentinmiddle with ' \n" + passwordHalf2 + "')",
" 3: ALTER CHANNEL(TEST3) CHLTYPE(SDR) PASSWORD('" + passwordHalf1 + "-\n*commentinmiddle with ') \n" + passwordHalf2 + "')",
}
var expected = [...]string{
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD " + redactionString,
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD\t\t" + redactionString,
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) ldappwd" + redactionString,
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LdApPwD" + redactionString,
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) PASSWORD" + redactionString,
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) PASSWORD" + redactionString,
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) PASSWORD " + redactionString,
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) PASSWORD\t\t" + redactionString,
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) password" + redactionString,
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) pAsSwOrD" + redactionString,
"ALTER QMGR SSLCRYP" + redactionString,
"ALTER QMGR SSLCRYP" + redactionString,
"ALTER QMGR SSLCRYP " + redactionString,
"ALTER QMGR SSLCRYP\t\t" + redactionString,
"ALTER QMGR sslcryp" + redactionString,
"ALTER QMGR sslCRYP" + redactionString,
// Line continuation ones
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) PASSWORD" + redactionString,
"ALTER QMGR SSLCRYP" + redactionString,
//edge cases
"ALTER QMGR SSLCRYP" + redactionString,
"ALTER QMGR SSLCRYP" + redactionString,
"ALTER QMGR SSLCRYP" + redactionString,
"ALTER QMGR SSLCRYP" + redactionString,
"ALTER QMGR SSLCRYP + \n \t " + redactionString,
"ALTER QMGR SSLCRYP - \n " + redactionString,
"ALTER QMGR SSL + \n CRYP" + redactionString,
"ALTER QMGR SSL - \nCRYP" + redactionString,
"ALTER QMGR + \n SSL +\n CRYP" + redactionString + " +\n TEST(1234)",
"ALTER QMGR -\nSSL -\nCRYP" + redactionString + " -\nTEST(1234)",
"ALTER QMGR +\n * COMMENT\n SSL +\n * COMMENT IN MIDDLE\n CRYP" + redactionString,
"1: ALTER CHANNEL(TEST2) CHLTYPE(SDR) PASS+\n : *test comment\n : WORD" + redactionString,
"2: ALTER CHANNEL(TEST3) CHLTYPE(SDR) PASSWORD" + redactionString,
"3: ALTER CHANNEL(TEST3) CHLTYPE(SDR) PASSWORD" + redactionString,
}
// Returns true if the 2 strings are equal ignoring whitespace characters
func compareIgnoreWhiteSpace(str1, str2 string) bool {
whiteSpaces := [...]string{" ", "\t", "\n", "\r"}
for _, w := range whiteSpaces {
str1 = strings.Replace(str1, w, "", -1)
str2 = strings.Replace(str2, w, "", -1)
}
return str1 == str2
}
func TestAll(t *testing.T) {
for i, v := range testStrings {
back, _ := Redact(v)
if strings.Contains(back, passwordHalf1) || strings.Contains(back, passwordHalf2) || strings.Contains(back, passwordString) {
t.Errorf("MAJOR FAIL[%d]: Found an instance of the password. ", i)
}
if !compareIgnoreWhiteSpace(back, expected[i]) {
t.Errorf("FAIL[%d]:\nGave :%s\nexpected:%s\ngot :%s", i, v, expected[i], back)
}
}
}

View File

@@ -0,0 +1,62 @@
/*
© Copyright IBM Corporation 2018, 2020
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package mqtemplate contains code to process template files
package mqtemplate
import (
"os"
"path"
"text/template"
"github.com/ibm-messaging/mq-container/pkg/logger"
)
// ProcessTemplateFile takes a Go templateFile, and processes it with the
// supplied data, writing to destFile
func ProcessTemplateFile(templateFile, destFile string, data interface{}, log *logger.Logger) error {
// Re-configure channel if app password not set
t, err := template.ParseFiles(templateFile)
if err != nil {
log.Error(err)
return err
}
dir := path.Dir(destFile)
_, err = os.Stat(dir)
if err != nil {
if os.IsNotExist(err) {
// #nosec G301
err = os.MkdirAll(dir, 0770)
if err != nil {
log.Error(err)
return err
}
} else {
return err
}
}
// #nosec G302 G304 G306 - its a read by owner/s group, and pose no harm.
f, err := os.OpenFile(destFile, os.O_CREATE|os.O_WRONLY, 0660)
// #nosec G307 - local to this function, pose no harm.
defer f.Close()
err = t.Execute(f, data)
if err != nil {
log.Error(err)
return err
}
return nil
}

View File

@@ -0,0 +1,97 @@
/*
© Copyright IBM Corporation 2020
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mqversion
import (
"fmt"
"strconv"
"strings"
"github.com/ibm-messaging/mq-container/internal/command"
)
// Get will return the current MQ version
func Get() (string, error) {
mqVersion, _, err := command.Run("dspmqver", "-b", "-f", "2")
if err != nil {
return "", fmt.Errorf("Error Getting MQ version: %v", err)
}
return strings.TrimSpace(mqVersion), nil
}
// Compare returns an integer comparing two MQ version strings lexicographically. The result will be 0 if currentVersion==checkVersion, -1 if currentVersion < checkVersion, and +1 if currentVersion > checkVersion
func Compare(checkVersion string) (int, error) {
currentVersion, err := Get()
if err != nil {
return 0, err
}
currentVRMF, err := parseVRMF(currentVersion)
if err != nil {
return 0, err
}
compareVRMF, err := parseVRMF(checkVersion)
if err != nil {
return 0, fmt.Errorf("failed to parse compare version: %w", err)
}
return currentVRMF.compare(*compareVRMF), nil
}
type vrmf [4]int
func (v vrmf) String() string {
return fmt.Sprintf("%d.%d.%d.%d", v[0], v[1], v[2], v[3])
}
func (v vrmf) compare(to vrmf) int {
for idx := 0; idx < 4; idx++ {
if v[idx] < to[idx] {
return -1
}
if v[idx] > to[idx] {
return 1
}
}
return 0
}
func parseVRMF(vrmfString string) (*vrmf, error) {
versionParts := strings.Split(vrmfString, ".")
if len(versionParts) != 4 {
return nil, fmt.Errorf("incorrect number of parts to version string: expected 4, got %d", len(versionParts))
}
vmrfPartNames := []string{"version", "release", "minor", "fix"}
parsed := vrmf{}
for idx, value := range versionParts {
partName := vmrfPartNames[idx]
if value == "" {
return nil, fmt.Errorf("empty %s found in VRMF", partName)
}
val, err := strconv.Atoi(value)
if err != nil {
return nil, fmt.Errorf("non-numeric %s found in VRMF", partName)
}
if val < 0 {
return nil, fmt.Errorf("negative %s found in VRMF", partName)
}
if idx == 0 && val == 0 {
return nil, fmt.Errorf("zero value for version not allowed")
}
parsed[idx] = val
}
return &parsed, nil
}

View File

@@ -0,0 +1,147 @@
/*
© Copyright IBM Corporation 2020
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mqversion
import (
"fmt"
"testing"
)
func TestCompareLower(t *testing.T) {
checkVersion := "99.99.99.99"
mqVersionCheck, err := Compare(checkVersion)
if err != nil {
t.Fatalf("Failed to compare MQ versions: %v", err)
}
if mqVersionCheck != -1 {
t.Errorf("MQ version compare result failed. Expected -1, Got %v", mqVersionCheck)
}
}
func TestCompareHigher(t *testing.T) {
checkVersion := "1.1.1.1"
mqVersionCheck, err := Compare(checkVersion)
if err != nil {
t.Fatalf("Failed to compare MQ versions: %v", err)
}
if mqVersionCheck != 1 {
t.Errorf("MQ version compare result failed. Expected 1, Got %v", mqVersionCheck)
}
}
func TestCompareEqual(t *testing.T) {
checkVersion, err := Get()
if err != nil {
t.Fatalf("Failed to get current MQ version: %v", err)
}
mqVersionCheck, err := Compare(checkVersion)
if err != nil {
t.Fatalf("Failed to compare MQ versions: %v", err)
}
if mqVersionCheck != 0 {
t.Errorf("MQ version compare result failed. Expected 0, Got %v", mqVersionCheck)
}
}
func TestVersionValid(t *testing.T) {
checkVersion, err := Get()
if err != nil {
t.Fatalf("Failed to get current MQ version: %v", err)
}
_, err = parseVRMF(checkVersion)
if err != nil {
t.Fatalf("Validation of MQ version failed: %v", err)
}
}
func TestValidVRMF(t *testing.T) {
validVRMFs := map[string]vrmf{
"1.0.0.0": {1, 0, 0, 0},
"10.0.0.0": {10, 0, 0, 0},
"1.10.0.0": {1, 10, 0, 0},
"1.0.10.0": {1, 0, 10, 0},
"1.0.0.10": {1, 0, 0, 10},
"999.998.997.996": {999, 998, 997, 996},
}
for test, expect := range validVRMFs {
t.Run(test, func(t *testing.T) {
parsed, err := parseVRMF(test)
if err != nil {
t.Fatalf("Unexpectedly failed to parse VRMF '%s': %s", test, err.Error())
}
if *parsed != expect {
t.Fatalf("VRMF not parsed as expected. Expected '%v', got '%v'", parsed, expect)
}
})
}
}
func TestInvalidVRMF(t *testing.T) {
invalidVRMFs := []string{
"not-a-number",
"9.8.7.string",
"0.1.2.3",
"1.0.0.-10",
}
for _, test := range invalidVRMFs {
t.Run(test, func(t *testing.T) {
parsed, err := parseVRMF(test)
if err == nil {
t.Fatalf("Expected error when parsing VRMF '%s', but got none. VRMF returned: %v", test, parsed)
}
})
}
}
func TestCompare(t *testing.T) {
tests := []struct {
current string
compare string
expect int
}{
{"1.0.0.1", "1.0.0.1", 0},
{"1.0.0.1", "1.0.0.0", 1},
{"1.0.0.1", "1.0.0.2", -1},
{"9.9.9.9", "10.0.0.0", -1},
{"9.9.9.9", "9.10.0.0", -1},
{"9.9.9.9", "9.9.10.0", -1},
{"9.9.9.9", "9.9.9.10", -1},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%s-%s", test.current, test.compare), func(t *testing.T) {
baseVRMF, err := parseVRMF(test.current)
if err != nil {
t.Fatalf("Could not parse base version '%s': %s", test.current, err.Error())
}
compareVRMF, err := parseVRMF(test.compare)
if err != nil {
t.Fatalf("Could not parse current version '%s': %s", test.current, err.Error())
}
result := baseVRMF.compare(*compareVRMF)
if result != test.expect {
t.Fatalf("Expected %d but got %d when comparing '%s' with '%s'", test.expect, result, test.current, test.compare)
}
if test.expect == 0 {
return
}
resultReversed := compareVRMF.compare(*baseVRMF)
if resultReversed != test.expect*-1 {
t.Fatalf("Expected %d but got %d when comparing '%s' with '%s'", test.expect*-1, resultReversed, test.compare, test.current)
}
})
}
}

View File

@@ -0,0 +1,39 @@
/*
© Copyright IBM Corporation 2023
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package pathutils contains code to provide sanitised file paths
package pathutils
import (
"path"
"path/filepath"
)
// CleanPath returns the result of joining a series of sanitised file paths (preventing directory traversal for each path)
// If the first path is relative, a relative path is returned
func CleanPath(paths ...string) string {
if len(paths) == 0 {
return ""
}
var combined string
if !path.IsAbs(paths[0]) {
combined = "./"
}
for _, part := range paths {
combined = filepath.Join(combined, filepath.FromSlash(path.Clean("/"+part)))
}
return combined
}

View File

@@ -0,0 +1,45 @@
/*
© Copyright IBM Corporation 2023
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package pathutils
import (
"strings"
"testing"
)
func TestClean(t *testing.T) {
tests := []struct {
uncleaned string
filepath string
cleaned string
}{
{"/a/rooted/path", "some.file", "/a/rooted/path/some.file"},
{"../../../a/relative/path", "abc.txt", "a/relative/path/abc.txt"},
{"a/path" + ".p12", "some.file", "a/path.p12/some.file"},
{"/", "bin", "/bin"},
{"abc/def", "../../a/relative/path", "abc/def/a/relative/path"},
}
for _, test := range tests {
cleaned := CleanPath(test.uncleaned, test.filepath)
if !strings.EqualFold(cleaned, test.cleaned) {
t.Fatalf("file path sanitisation failed. Expected %s but got %s\n", test.cleaned, cleaned)
}
}
}

166
internal/ready/ready.go Normal file
View File

@@ -0,0 +1,166 @@
/*
© Copyright IBM Corporation 2018, 2024
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package ready contains code to provide a ready signal mechanism between processes
package ready
import (
"context"
"os"
"strconv"
"strings"
"time"
"github.com/ibm-messaging/mq-container/internal/command"
)
const readyFile string = "/run/runmqserver/ready"
const readyToSyncFile string = "/run/runmqserver/ready-to-sync"
func fileExists(fileName string) (bool, error) {
_, err := os.Stat(fileName)
if err != nil {
if !os.IsNotExist(err) {
return false, err
}
return false, nil
}
return true, nil
}
// Clear ensures that any readiness state is cleared
func Clear() error {
err := clearFile(readyFile)
if err != nil {
return err
}
err = clearFile(readyToSyncFile)
if err != nil {
return err
}
return nil
}
// clearFile removes the specified file if it exists
func clearFile(fileName string) error {
exist, err := fileExists(fileName)
if err != nil {
return err
}
if exist {
return os.Remove(fileName)
}
return nil
}
// Set lets any subsequent calls to `CheckReady` know that the queue
// manager has finished its configuration step
func Set() error {
// #nosec G306 - this gives permissions to owner/s group only.
return os.WriteFile(readyFile, []byte("1"), 0770)
}
// Check checks whether or not the queue manager has finished its
// configuration steps
func Check() (bool, error) {
exists, err := fileExists(readyFile)
if err != nil {
return false, err
}
return exists, nil
}
// SetReadyToSync is used to indicate that a Native-HA queue manager instance is ready-to-sync
func SetReadyToSync() error {
exists, err := fileExists(readyToSyncFile)
if err != nil {
return err
} else if exists {
return nil
}
readyToSyncStartTime := strconv.FormatInt(time.Now().Unix(), 10)
// #nosec G306 - required permissions
return os.WriteFile(readyToSyncFile, []byte(readyToSyncStartTime), 0660)
}
// GetReadyToSyncStartTime returns the start-time a Native-HA queue manager instance was ready-to-sync
func GetReadyToSyncStartTime() (bool, time.Time, error) {
exists, err := fileExists(readyToSyncFile)
if err != nil {
return exists, time.Time{}, err
}
if exists {
buf, err := os.ReadFile(readyToSyncFile)
if err != nil {
return true, time.Time{}, err
}
readyToSyncStartTime, err := strconv.ParseInt(string(buf), 10, 64)
if err != nil {
return true, time.Time{}, err
}
return true, time.Unix(readyToSyncStartTime, 0), nil
}
return false, time.Time{}, nil
}
// Status returns an enum representing the current running status of the queue manager
func Status(ctx context.Context, name string) (QMStatus, error) {
out, _, err := command.RunContext(ctx, "dspmq", "-n", "-m", name)
if err != nil {
return StatusUnknown, err
}
if strings.Contains(string(out), "(RUNNING)") {
return StatusActiveQM, nil
}
if strings.Contains(string(out), "(RUNNING AS STANDBY)") {
return StatusStandbyQM, nil
}
if strings.Contains(string(out), "(REPLICA)") {
return StatusStandbyQM, nil
}
if strings.Contains(string(out), "(RECOVERY GROUP LEADER)") {
return StatusRecoveryQM, nil
}
return StatusUnknown, nil
}
type QMStatus int
const (
StatusUnknown QMStatus = iota
StatusActiveQM
StatusStandbyQM
StatusReplicaQM
StatusRecoveryQM
)
// ActiveQM returns true if the queue manager is running in active mode
func (s QMStatus) ActiveQM() bool { return s == StatusActiveQM }
// StandbyQM returns true if the queue manager is running in standby mode
func (s QMStatus) StandbyQM() bool { return s == StatusStandbyQM }
// ReplicaQM returns true if the queue manager is running in replica mode
func (s QMStatus) ReplicaQM() bool { return s == StatusReplicaQM }
// ReplicaQM returns true if the queue manager is running in recovery mode
func (s QMStatus) RecoveryQM() bool { return s == StatusRecoveryQM }

View File

@@ -0,0 +1,61 @@
/*
© Copyright IBM Corporation 2024
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Look for go standards for package comment
// Package securityUtility contains code to use securityUtility tool from opt/mqm/web/bin directory
// to encode the passwords
package securityutility
import (
"fmt"
"os"
"os/exec"
"strings"
)
// EncodeSecrets takes a secret/password as an input and encodes the password using securityUtility
// and returns the encoded password
func EncodeSecrets(secret string) (string, error) {
_, err := os.Stat("/opt/mqm/web/bin/securityUtility")
if err != nil && os.IsNotExist(err) {
return "", err
}
if len(secret) > 256 {
return "", fmt.Errorf("length of password is greater than the maximum length of 256 characters, length of password is %d", len(secret))
}
// Set the java environment required for running securityUtility tool and then run the securityUtility tool
// to encode the password using "aes" encoding
// #nosec G204
cmd := exec.Command("/bin/sh", "-c", "source setmqenv -s;/opt/mqm/web/bin/server; /opt/mqm/web/bin/securityUtility encode --encoding=aes "+secret)
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, "JAVA_HOME=/opt/mqm/java/jre64/jre")
out, err := cmd.CombinedOutput()
if err != nil {
return "", err
}
encodedSecret := ""
cmdOutput := strings.Split(string(out), "\n")
// When the JVM is in FIPS 140-2 mode and the IBMJCEPlusFIPS provider is used the following message is displayed
// The IBMJCEPlusFIPS provider is configured for FIPS 140-2. Please note that the 140-2 configuration may be removed in the future.
// Hence read only the encoded password and ignore the above message
for _, line := range cmdOutput {
if strings.Contains(line, "{aes}") {
encodedSecret = line
}
}
return strings.TrimSpace(encodedSecret), nil
}

View File

@@ -0,0 +1,135 @@
/*
© Copyright IBM Corporation 2020, 2024
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
//This is a developer only configuration and not recommended for production usage.
package simpleauth
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/ibm-messaging/mq-container/internal/securityutility"
"github.com/ibm-messaging/mq-container/pkg/logger"
)
const MQ_APP_PWD_ENV = "MQ_APP_PASSWORD"
const MQ_APP_PWD_SECURE_ENV = "MQ_APP_PASSWORD_SECURE"
const MQ_ADMIN_PWD_ENV = "MQ_ADMIN_PASSWORD"
const MQ_ADMIN_PWD_SECURE_ENV = "MQ_ADMIN_PASSWORD_SECURE"
const MQ_CONNAUTH_USE_HTP_ENV = "MQ_CONNAUTH_USE_HTP"
// #nosec G101
const MQ_APP_USER_SECRET_PATH = "/run/secrets/mqAppPassword"
// #nosec G101
const MQ_ADMIN_USER_SECRET_PATH = "/run/secrets/mqAdminPassword"
// IsEnabled will return a boolean value if the MQ_CONNAUTH_USER_HTP_ENV is set to true and if the app/admin
// user passwords are set as environment variables or set as secrets
func IsEnabled() bool {
mqSimpleAuthEnabled := false
enableHtPwd, set := os.LookupEnv(MQ_CONNAUTH_USE_HTP_ENV)
adminPassword, adminPwdSet := os.LookupEnv(MQ_ADMIN_PWD_ENV)
adminSecret, adminSecretSet := os.LookupEnv(MQ_ADMIN_PWD_SECURE_ENV)
appPassword, appPwdSet := os.LookupEnv(MQ_APP_PWD_ENV)
appSecret, appSecretSet := os.LookupEnv(MQ_APP_PWD_SECURE_ENV)
if set && strings.EqualFold(enableHtPwd, "true") &&
(adminPwdSet && len(strings.TrimSpace(adminPassword)) > 0 || appPwdSet && len(strings.TrimSpace(appPassword)) > 0 ||
appSecretSet && len(strings.TrimSpace(appSecret)) > 0 || adminSecretSet && len(strings.TrimSpace(adminSecret)) > 0) {
mqSimpleAuthEnabled = true
}
return mqSimpleAuthEnabled
}
// CheckForPasswords checks if the user has provided the app & admin user passwords via the environment variable
// or via the secrets. The secrets will be in /run/secrets path
func CheckForPasswords(log *logger.Logger) error {
adminPassword, adminPwdSet := os.LookupEnv(MQ_ADMIN_PWD_ENV)
appPassword, appPwdSet := os.LookupEnv(MQ_APP_PWD_ENV)
if adminPwdSet && len(strings.TrimSpace(adminPassword)) > 0 {
encodedAdminPassword, err := securityutility.EncodeSecrets(adminPassword)
if err != nil {
return fmt.Errorf("encoding Admin password for web server failed with error %v", err)
}
err = os.Setenv(MQ_ADMIN_PWD_SECURE_ENV, encodedAdminPassword)
if err != nil {
return fmt.Errorf("setting encoded admin user password to environment variable failed with error %v", err)
}
log.Printf("Environment variable MQ_ADMIN_PASSWORD is deprecated, use secrets to set the passwords")
} else {
if _, err := os.Stat(MQ_ADMIN_USER_SECRET_PATH); err == nil {
encodedAdminSecret, err := readMQSecrets(MQ_ADMIN_USER_SECRET_PATH)
if err != nil {
return fmt.Errorf("encoding mqAdminPassword secret for web server failed with error %v", err)
}
if len(encodedAdminSecret) > 0 {
err = os.Setenv(MQ_ADMIN_PWD_SECURE_ENV, encodedAdminSecret)
if err != nil {
return fmt.Errorf("setting encoded admin user password to environment variable failed with error %v", err)
}
}
}
}
if appPwdSet && len(strings.TrimSpace(appPassword)) > 0 {
encodedAppPassword, err := securityutility.EncodeSecrets(appPassword)
if err != nil {
return fmt.Errorf("encoding App password for web server failed with error %v", err)
}
err = os.Setenv(MQ_APP_PWD_SECURE_ENV, encodedAppPassword)
if err != nil {
return fmt.Errorf("setting encoded app user password to environment variable failed with error %v", err)
}
log.Printf("Environment variable MQ_APP_PASSWORD is deprecated, use secrets to set the passwords")
} else {
// If environment variables are not set check if secrets were used to set the passwords
if _, err := os.Stat(MQ_APP_USER_SECRET_PATH); err == nil {
encodedAppSecret, err := readMQSecrets(MQ_APP_USER_SECRET_PATH)
if err != nil {
return fmt.Errorf("encoding mqAppPassword secret for web server failed with error %v", err)
}
if len(encodedAppSecret) > 0 {
err := os.Setenv(MQ_APP_PWD_SECURE_ENV, encodedAppSecret)
if err != nil {
return fmt.Errorf("setting encoded app user password to environment variable failed with error %v", err)
}
}
}
}
return nil
}
// readMQSecrets takes the secret file as an input and encodes the secret and returns an encoded password
func readMQSecrets(secretName string) (string, error) {
passwordBuf, err := os.ReadFile(filepath.Clean(secretName))
if err != nil {
return "", err
}
if len(passwordBuf) > 256 {
err = fmt.Errorf("the length of the password cannot be more than 256 characters, length of the password was %v", len(passwordBuf))
return "", err
}
encodedPassword, err := securityutility.EncodeSecrets(string(passwordBuf))
if err != nil {
return "", err
}
return encodedPassword, nil
}

745
internal/tls/tls.go Normal file
View File

@@ -0,0 +1,745 @@
/*
© Copyright IBM Corporation 2019, 2024
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package tls
import (
"bufio"
"fmt"
pwr "math/rand"
"os"
"path/filepath"
"strings"
"time"
"crypto/sha512"
"crypto/x509"
"encoding/pem"
pkcs "software.sslmate.com/src/go-pkcs12"
"github.com/ibm-messaging/mq-container/internal/keystore"
"github.com/ibm-messaging/mq-container/internal/mqtemplate"
"github.com/ibm-messaging/mq-container/internal/pathutils"
"github.com/ibm-messaging/mq-container/pkg/logger"
)
// cmsKeystoreName is the name of the CMS Keystore
const cmsKeystoreName = "key.kdb"
// p12TruststoreName is the name of the PKCS#12 Truststore
const p12TruststoreName = "trust.p12"
// keystoreDirDefault is the location for the default CMS Keystore & PKCS#12 Truststore
const keystoreDirDefault = "/run/runmqserver/tls/"
// keystoreDirHA is the location for the HA CMS Keystore
const keystoreDirHA = "/run/runmqserver/ha/tls/"
// keyDirDefault is the location of the default keys to import
const keyDirDefault = "/etc/mqm/pki/keys"
// keyDirHA is the location of the HA keys to import
const keyDirHA = "/etc/mqm/ha/pki/keys"
// keyDirGroupHA is the location of the GroupHA keys to import
const keyDirGroupHA = "/etc/mqm/groupha/pki/keys"
// trustDirDefault is the location of the trust certificates to import
const trustDirDefault = "/etc/mqm/pki/trust"
// trustDirGroupDefault is the location of the GroupHA trust certificates to import
const trustDirGroupHA = "/etc/mqm/groupha/pki/trust"
type KeyStoreData struct {
Keystore *keystore.KeyStore
Password string
TrustedCerts []*pem.Block
KnownFingerPrints []string
KeyLabels []string
}
type P12KeyFiles struct {
Keystores []string
Password string
}
type TLSStore struct {
Keystore KeyStoreData
Truststore KeyStoreData
}
func configureTLSKeystores(keystoreDir string, keyDirs, trustDirs []string, p12TruststoreRequired bool, nativeTLSHA bool) ([]string, KeyStoreData, KeyStoreData, error) {
var keyLabel string
cmsKeystoreRequired := false
allDirs := append([]string{}, keyDirs...)
allDirs = append(allDirs, trustDirs...)
for _, dir := range allDirs {
if haveKeysAndCerts(dir) {
cmsKeystoreRequired = true
break
}
}
// Create the CMS Keystore & PKCS#12 Truststore (if required)
tlsStore, err := generateAllKeystores(keystoreDir, cmsKeystoreRequired, p12TruststoreRequired, nativeTLSHA)
if err != nil {
return nil, tlsStore.Keystore, tlsStore.Truststore, err
}
keyLabels := make([]string, len(keyDirs))
if tlsStore.Keystore.Keystore != nil {
for idx, keyDir := range keyDirs {
// Process all keys - add them to the CMS KeyStore
keyLabel, err = processKeys(&tlsStore, keystoreDir, keyDir)
if err != nil {
return nil, tlsStore.Keystore, tlsStore.Truststore, err
}
keyLabels[idx] = keyLabel
}
}
for _, trustDir := range trustDirs {
// Process all trust certificates - add them to the CMS KeyStore & PKCS#12 Truststore (if required)
err = processTrustCertificates(&tlsStore, trustDir)
if err != nil {
return nil, tlsStore.Keystore, tlsStore.Truststore, err
}
}
return keyLabels, tlsStore.Keystore, tlsStore.Truststore, err
}
// ConfigureDefaultTLSKeystores configures the CMS Keystore & PKCS#12 Truststore
func ConfigureDefaultTLSKeystores() (string, KeyStoreData, KeyStoreData, error) {
certLabels, keyStore, trustStore, err := configureTLSKeystores(keystoreDirDefault, []string{keyDirDefault}, []string{trustDirDefault}, true, false)
if err != nil {
return "", keyStore, trustStore, err
}
certLabel := ""
if len(certLabels) > 0 {
certLabel = certLabels[0]
}
return certLabel, keyStore, trustStore, err
}
// ConfigureHATLSKeystore configures the CMS Keystore & PKCS#12 Truststore
func ConfigureHATLSKeystore() (string, string, KeyStoreData, KeyStoreData, error) {
// *.crt files mounted to the HA TLS dir keyDirHA will be processed as trusted in the CMS keystore
keyDirs := []string{keyDirHA, keyDirGroupHA}
haCertLabels, haKeystore, haTruststore, err := configureTLSKeystores(keystoreDirHA, keyDirs, keyDirs, false, true)
if err != nil {
return "", "", haKeystore, haTruststore, err
}
if len(haCertLabels) != len(keyDirs) {
return "", "", haKeystore, haTruststore, fmt.Errorf("incorrect number of certificate labels returned (expected %d, got %d)", len(keyDirs), len(haCertLabels))
}
return haCertLabels[0], haCertLabels[1], haKeystore, haTruststore, err
}
// ConfigureTLS configures TLS for the queue manager
func ConfigureTLS(keyLabel string, cmsKeystore KeyStoreData, devMode bool, log *logger.Logger) error {
const mqscLink string = "/run/15-tls.mqsc"
const mqscTemplate string = "/etc/mqm/15-tls.mqsc.tpl"
sslKeyRing := ""
var fipsEnabled = "NO"
// Don't set SSLKEYR if no keys or crts are not supplied
// Key label will be blank if no private keys were added during processing keys and certs.
if cmsKeystore.Keystore != nil && len(keyLabel) > 0 {
certList, _ := cmsKeystore.Keystore.ListAllCertificates()
if len(certList) > 0 {
sslKeyRing = strings.TrimSuffix(cmsKeystore.Keystore.Filename, ".kdb")
}
if cmsKeystore.Keystore.IsFIPSEnabled() {
fipsEnabled = "YES"
}
}
err := mqtemplate.ProcessTemplateFile(mqscTemplate, mqscLink, map[string]string{
"SSLKeyR": sslKeyRing,
"CertificateLabel": keyLabel,
"SSLFips": fipsEnabled,
}, log)
if err != nil {
return err
}
if devMode && keyLabel != "" {
err = configureTLSDev(log)
if err != nil {
return err
}
}
return nil
}
// configureTLSDev configures TLS for the developer defaults
func configureTLSDev(log *logger.Logger) error {
const mqscLink string = "/run/20-dev-tls.mqsc"
const mqscTemplate string = "/etc/mqm/20-dev-tls.mqsc.tpl"
if os.Getenv("MQ_DEV") == "true" {
err := mqtemplate.ProcessTemplateFile(mqscTemplate, mqscLink, map[string]string{}, log)
if err != nil {
return err
}
}
return nil
}
// generateAllKeystores creates the CMS Keystore & PKCS#12 Truststore (if required)
func generateAllKeystores(keystoreDir string, createCMSKeystore bool, p12TruststoreRequired bool, nativeTLSHA bool) (TLSStore, error) {
var cmsKeystore, p12Truststore KeyStoreData
// Generate a pasword for use with both the CMS Keystore & PKCS#12 Truststore
pw := generateRandomPassword()
cmsKeystore.Password = pw
p12Truststore.Password = pw
// Create the Keystore directory - if it does not already exist
// #nosec G301 - write group permissions are required
err := os.MkdirAll(keystoreDir, 0770)
if err != nil {
return TLSStore{cmsKeystore, p12Truststore}, fmt.Errorf("Failed to create Keystore directory: %v", err)
}
// Create the CMS Keystore if we have been provided keys and certificates
if createCMSKeystore {
cmsKeystore.Keystore = keystore.NewCMSKeyStore(pathutils.CleanPath(keystoreDir, cmsKeystoreName), cmsKeystore.Password)
err = cmsKeystore.Keystore.Create()
if err != nil {
return TLSStore{cmsKeystore, p12Truststore}, fmt.Errorf("Failed to create CMS Keystore: %v", err)
}
}
// Create the PKCS#12 Truststore (if required)
if p12TruststoreRequired {
p12Truststore.Keystore = keystore.NewPKCS12KeyStore(pathutils.CleanPath(keystoreDir, p12TruststoreName), p12Truststore.Password)
err = p12Truststore.Keystore.Create()
if err != nil {
return TLSStore{cmsKeystore, p12Truststore}, fmt.Errorf("Failed to create PKCS#12 Truststore: %v", err)
}
}
return TLSStore{cmsKeystore, p12Truststore}, nil
}
// processKeys processes all keys - adding them to the CMS KeyStore
func processKeys(tlsStore *TLSStore, keystoreDir string, keyDir string) (string, error) {
// Key label - will be set to the label of the first set of keys
keyLabel := ""
// Process all keys
keyList, err := os.ReadDir(keyDir)
if err == nil && len(keyList) > 0 {
// Process each set of keys - each set should contain files: *.key & *.crt
for _, keySet := range keyList {
keys, _ := os.ReadDir(pathutils.CleanPath(keyDir, keySet.Name()))
// Ensure the label of the set of keys does not match the name of the PKCS#12 Truststore
if keySet.Name() == p12TruststoreName[0:len(p12TruststoreName)-len(filepath.Ext(p12TruststoreName))] {
return "", fmt.Errorf("key label cannot be set to the Truststore name: %v", keySet.Name())
}
// Process private key (*.key)
privateKey, keyPrefix, err := processPrivateKey(keyDir, keySet.Name(), keys)
if err != nil {
return "", err
}
// If private key does not exist - skip this set of keys
if privateKey == nil {
continue
}
// Process certificates (*.crt) - public certificate & optional CA certificate
publicCertificate, caCertificate, err := processCertificates(keyDir, keySet.Name(), keyPrefix, keys, &tlsStore.Keystore, &tlsStore.Truststore)
if err != nil {
return "", err
}
// Return an error if corresponding public certificate was not found. Both private key and
// it's corresponding public certificate are required.
if publicCertificate == nil {
return "", fmt.Errorf("Failed to find public certificate in directory %s", keyDir)
}
// Validate certificates for duplicate Subject DNs
if len(caCertificate) > 0 {
errCertValid := validateCertificates(publicCertificate, caCertificate)
if errCertValid != nil {
return "", errCertValid
}
}
// Create a new PKCS#12 Keystore - containing private key, public certificate & optional CA certificate
file, err := pkcs.Modern.Encode(privateKey, publicCertificate, caCertificate, tlsStore.Keystore.Password)
if err != nil {
return "", fmt.Errorf("Failed to encode PKCS#12 Keystore %s: %v", keySet.Name()+".p12", err)
}
keystorePath := pathutils.CleanPath(keystoreDir, keySet.Name()+".p12")
// #nosec G306 - this gives permissions to owner/s group only.
err = os.WriteFile(keystorePath, file, 0644)
if err != nil {
return "", fmt.Errorf("Failed to write PKCS#12 Keystore %s: %v", keystorePath, err)
}
// Import the new PKCS#12 Keystore into the CMS Keystore
err = tlsStore.Keystore.Keystore.Import(keystorePath, tlsStore.Keystore.Password)
if err != nil {
return "", fmt.Errorf("Failed to import keys from %s into CMS Keystore: %v", keystorePath, err)
}
// Relabel the certificate in the CMS Keystore
err = relabelCertificate(keySet.Name(), &tlsStore.Keystore)
if err != nil {
return "", err
}
// Set key label - for first set of keys only
if keyLabel == "" {
keyLabel = keySet.Name()
}
}
}
return keyLabel, nil
}
// processTrustCertificates processes all trust certificates - adding them to the CMS KeyStore & PKCS#12 Truststore (if required)
func processTrustCertificates(tlsStore *TLSStore, trustDir string) error {
// Process all trust certiifcates
trustList, err := os.ReadDir(trustDir)
if err == nil && len(trustList) > 0 {
// Process each set of keys
for _, trustSet := range trustList {
keys, _ := os.ReadDir(pathutils.CleanPath(trustDir, trustSet.Name()))
for _, key := range keys {
if strings.HasSuffix(key.Name(), ".crt") {
trustSetPath := pathutils.CleanPath(trustDir, trustSet.Name(), key.Name())
// #nosec G304 - filename variable is derived from contents of 'trustDir' which is a defined constant
file, err := os.ReadFile(trustSetPath)
if err != nil {
return fmt.Errorf("Failed to read file %s: %v", trustSetPath, err)
}
for string(file) != "" {
var block *pem.Block
block, file = pem.Decode(file)
if block == nil {
break
}
// Add to known certificates for the CMS Keystore
err = addToKnownCertificates(block, &tlsStore.Keystore, true)
if err != nil {
return fmt.Errorf("Failed to add to know certificates for CMS Keystore")
}
if tlsStore.Truststore.Keystore != nil {
// Add to known certificates for the PKCS#12 Truststore
err = addToKnownCertificates(block, &tlsStore.Truststore, true)
if err != nil {
return fmt.Errorf("Failed to add to know certificates for PKCS#12 Truststore")
}
}
}
}
}
}
}
// Add all trust certificates to PKCS#12 Truststore (if required)
if tlsStore.Truststore.Keystore != nil && len(tlsStore.Truststore.TrustedCerts) > 0 {
err = addCertificatesToTruststore(&tlsStore.Truststore)
if err != nil {
return err
}
}
// Add all trust certificates to CMS Keystore
if len(tlsStore.Keystore.TrustedCerts) > 0 {
err = addCertificatesToCMSKeystore(&tlsStore.Keystore)
if err != nil {
return err
}
}
return nil
}
// processPrivateKey processes the private key (*.key) from a set of keys
func processPrivateKey(keyDir string, keySetName string, keys []os.DirEntry) (interface{}, string, error) {
var privateKey interface{}
keyPrefix := ""
for _, key := range keys {
privateKeyPath := pathutils.CleanPath(keyDir, keySetName, key.Name())
if strings.HasSuffix(key.Name(), ".key") {
// #nosec G304 - filename variable is derived from contents of 'keyDir' which is a defined constant
file, err := os.ReadFile(privateKeyPath)
if err != nil {
return nil, "", fmt.Errorf("Failed to read private key %s: %v", privateKeyPath, err)
}
block, _ := pem.Decode(file)
if block == nil {
return nil, "", fmt.Errorf("Failed to decode private key %s: pem.Decode returned nil", privateKeyPath)
}
// Check if the private key is PKCS1
privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
// Check if the private key is PKCS8
privateKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, "", fmt.Errorf("Failed to parse private key %s: %v", privateKeyPath, err)
}
}
keyPrefix = key.Name()[0 : len(key.Name())-len(filepath.Ext(key.Name()))]
}
}
return privateKey, keyPrefix, nil
}
// processCertificates processes the certificates (*.crt) from a set of keys
func processCertificates(keyDir string, keySetName, keyPrefix string, keys []os.DirEntry, cmsKeystore, p12Truststore *KeyStoreData) (*x509.Certificate, []*x509.Certificate, error) {
var publicCertificate *x509.Certificate
var caCertificate []*x509.Certificate
for _, key := range keys {
keystorePath := pathutils.CleanPath(keyDir, keySetName, key.Name())
if strings.HasPrefix(key.Name(), keyPrefix) && strings.HasSuffix(key.Name(), ".crt") {
// #nosec G304 - filename variable is derived from contents of 'keyDir' which is a defined constant
file, err := os.ReadFile(keystorePath)
if err != nil {
return nil, nil, fmt.Errorf("Failed to read public certificate %s: %v", keystorePath, err)
}
block, file := pem.Decode(file)
if block == nil {
return nil, nil, fmt.Errorf("Failed to decode public certificate %s: pem.Decode returned nil", keystorePath)
}
publicCertificate, err = x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, nil, fmt.Errorf("Failed to parse public certificate %s: %v", keystorePath, err)
}
// Add to known certificates for the CMS Keystore
err = addToKnownCertificates(block, cmsKeystore, false)
if err != nil {
return nil, nil, fmt.Errorf("Failed to add to known certificates for CMS Keystore")
}
// Add to known certificates for the CMS Keystore
err = addToKnownCertificates(block, cmsKeystore, false)
if err != nil {
return nil, nil, fmt.Errorf("Failed to add to known certificates for CMS Keystore")
}
// Pick up any other intermediate certificates
for string(file) != "" {
var block *pem.Block
block, file = pem.Decode(file)
if block == nil {
break
}
// Add to known certificates for the CMS Keystore
err = addToKnownCertificates(block, cmsKeystore, false)
if err != nil {
return nil, nil, fmt.Errorf("Failed to add to known certificates for CMS Keystore")
}
if p12Truststore.Keystore != nil {
// Add to known certificates for the PKCS#12 Truststore
err = addToKnownCertificates(block, p12Truststore, true)
if err != nil {
return nil, nil, fmt.Errorf("Failed to add to known certificates for PKCS#12 Truststore")
}
}
certificate, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, nil, fmt.Errorf("Failed to parse CA certificate %s: %v", keystorePath, err)
}
caCertificate = append(caCertificate, certificate)
}
} else if strings.HasSuffix(key.Name(), ".crt") {
// #nosec G304 - filename variable is derived from contents of 'keyDir' which is a defined constant
file, err := os.ReadFile(keystorePath)
if err != nil {
return nil, nil, fmt.Errorf("Failed to read CA certificate %s: %v", keystorePath, err)
}
for string(file) != "" {
var block *pem.Block
block, file = pem.Decode(file)
if block == nil {
break
}
// Add to known certificates for the CMS Keystore
err = addToKnownCertificates(block, cmsKeystore, false)
if err != nil {
return nil, nil, fmt.Errorf("Failed to add to known certificates for CMS Keystore")
}
if p12Truststore.Keystore != nil {
// Add to known certificates for the PKCS#12 Truststore
err = addToKnownCertificates(block, p12Truststore, true)
if err != nil {
return nil, nil, fmt.Errorf("Failed to add to known certificates for PKCS#12 Truststore")
}
}
certificate, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, nil, fmt.Errorf("Failed to parse CA certificate %s: %v", keystorePath, err)
}
caCertificate = append(caCertificate, certificate)
}
}
}
return publicCertificate, caCertificate, nil
}
// relabelCertificate sets a new label for a certificate in the CMS Keystore
func relabelCertificate(newLabel string, cmsKeystore *KeyStoreData) error {
allLabels, err := cmsKeystore.Keystore.GetCertificateLabels()
if err != nil {
return fmt.Errorf("Failed to get list of all certificate labels from CMS Keystore: %v", err)
}
relabelled := false
for _, label := range allLabels {
found := false
for _, keyLabel := range cmsKeystore.KeyLabels {
if strings.Trim(label, "\"") == keyLabel {
found = true
break
}
}
if !found {
err = cmsKeystore.Keystore.RenameCertificate(strings.Trim(label, "\""), newLabel)
if err != nil {
return err
}
relabelled = true
cmsKeystore.KeyLabels = append(cmsKeystore.KeyLabels, newLabel)
break
}
}
if !relabelled {
return fmt.Errorf("Failed to relabel certificate for %s in CMS keystore", newLabel)
}
return nil
}
// addCertificatesToTruststore adds trust certificates to the PKCS#12 Truststore
func addCertificatesToTruststore(p12Truststore *KeyStoreData) error {
temporaryPemFile := pathutils.CleanPath("/tmp", "trust.pem")
_, err := os.Stat(temporaryPemFile)
if err == nil {
err = os.Remove(temporaryPemFile)
if err != nil {
return fmt.Errorf("Failed to remove file %v: %v", temporaryPemFile, err)
}
}
err = writeCertificatesToFile(temporaryPemFile, p12Truststore.TrustedCerts)
if err != nil {
return err
}
err = p12Truststore.Keystore.AddNoLabel(temporaryPemFile)
if err != nil {
return fmt.Errorf("Failed to add certificates to PKCS#12 Truststore: %v", err)
}
// Relabel all certiifcates
allCertificates, err := p12Truststore.Keystore.ListAllCertificates()
if err != nil || len(allCertificates) <= 0 {
return fmt.Errorf("Failed to get any certificates from PKCS#12 Truststore: %v", err)
}
for i, certificate := range allCertificates {
certificate = strings.Trim(certificate, "\"")
certificate = strings.TrimSpace(certificate)
newLabel := fmt.Sprintf("Trust%d", i)
err = p12Truststore.Keystore.RenameCertificate(certificate, newLabel)
if err != nil || len(allCertificates) <= 0 {
return fmt.Errorf("Failed to rename certificate %s to %s in PKCS#12 Truststore: %v", certificate, newLabel, err)
}
}
return nil
}
// addCertificatesToCMSKeystore adds trust certificates to the CMS keystore
func addCertificatesToCMSKeystore(cmsKeystore *KeyStoreData) error {
temporaryPemFile := pathutils.CleanPath("/tmp", "cmsTrust.pem")
_, err := os.Stat(temporaryPemFile)
if err == nil {
err = os.Remove(temporaryPemFile)
if err != nil {
return fmt.Errorf("Failed to remove file %v: %v", temporaryPemFile, err)
}
}
err = writeCertificatesToFile(temporaryPemFile, cmsKeystore.TrustedCerts)
if err != nil {
return err
}
err = cmsKeystore.Keystore.AddNoLabel(temporaryPemFile)
if err != nil {
return fmt.Errorf("Failed to add certificates to CMS keystore: %v", err)
}
return nil
}
// generateRandomPassword generates a random 12 character password from the characters a-z, A-Z, 0-9
func generateRandomPassword() string {
pwr.Seed(time.Now().Unix())
validChars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
validcharArray := []byte(validChars)
password := ""
for i := 0; i < 12; i++ {
// #nosec G404 - this is only for internal keystore and using math/rand pose no harm.
password = password + string(validcharArray[pwr.Intn(len(validcharArray))])
}
return password
}
// addToKnownCertificates adds to the list of known certificates for a Keystore
func addToKnownCertificates(block *pem.Block, keyData *KeyStoreData, addToKeystore bool) error {
sha512str, err := getCertificateFingerprint(block)
if err != nil {
return err
}
known := false
for _, fingerprint := range keyData.KnownFingerPrints {
if fingerprint == sha512str {
known = true
break
}
}
if !known {
if addToKeystore {
keyData.TrustedCerts = append(keyData.TrustedCerts, block)
}
keyData.KnownFingerPrints = append(keyData.KnownFingerPrints, sha512str)
}
return nil
}
// getCertificateFingerprint returns a fingerprint for a certificate
func getCertificateFingerprint(block *pem.Block) (string, error) {
certificate, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return "", fmt.Errorf("Failed to parse x509 certificate: %v", err)
}
sha512Sum := sha512.Sum512(certificate.Raw)
sha512str := string(sha512Sum[:])
return sha512str, nil
}
// writeCertificatesToFile writes a list of certificates to a file
func writeCertificatesToFile(file string, certificates []*pem.Block) error {
// #nosec G304 - this is a temporary pem file to write certs.
f, err := os.Create(file)
if err != nil {
return fmt.Errorf("Failed to create file %s: %v", file, err)
}
// #nosec G307 - local to this function, pose no harm.
defer f.Close()
w := bufio.NewWriter(f)
for i, c := range certificates {
err := pem.Encode(w, c)
if err != nil {
return fmt.Errorf("Failed to encode certificate number %d: %v", i, err)
}
err = w.Flush()
if err != nil {
return fmt.Errorf("Failed to write certificate to file %s: %v", file, err)
}
}
return nil
}
// Search the specified directory for .key and .crt files.
// Return true if at least one .key or .crt file is found else false
func haveKeysAndCerts(keyDir string) bool {
fileList, err := os.ReadDir(keyDir)
if err == nil && len(fileList) > 0 {
for _, fileInfo := range fileList {
// Keys and certs will be supplied in an user defined subdirectory.
// Do a listing of the subdirectory and then search for .key and .cert files
keys, _ := os.ReadDir(pathutils.CleanPath(keyDir, fileInfo.Name()))
for _, key := range keys {
if strings.HasSuffix(key.Name(), ".key") || strings.HasSuffix(key.Name(), ".crt") {
// We found at least one key/crt file.
return true
}
}
}
}
return false
}
// Iterate through the certificates to ensure there are no two certificates with same Subject DN.
// GSKit does not allow two certificates with same Subject DN/Friendly Names
func validateCertificates(personalCert *x509.Certificate, caCertificates []*x509.Certificate) error {
// Check if we have been asked to override certificate validation by setting
// MQ_ENABLE_CERT_VALIDATION to false
enableValidation, enableValidationSet := os.LookupEnv("MQ_ENABLE_CERT_VALIDATION")
if !enableValidationSet || (enableValidationSet && !strings.EqualFold(strings.Trim(enableValidation, ""), "false")) {
for _, caCert := range caCertificates {
if strings.EqualFold(personalCert.Subject.String(), caCert.Subject.String()) {
return fmt.Errorf("Error: The Subject DN of the Issuer Certificate and the Queue Manager are same")
}
}
}
return nil
}

99
internal/tls/tls_web.go Normal file
View File

@@ -0,0 +1,99 @@
/*
© Copyright IBM Corporation 2019, 2023
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package tls
import (
"fmt"
"os"
"github.com/ibm-messaging/mq-container/internal/keystore"
"github.com/ibm-messaging/mq-container/internal/mqtemplate"
"github.com/ibm-messaging/mq-container/internal/pathutils"
"github.com/ibm-messaging/mq-container/internal/securityutility"
"github.com/ibm-messaging/mq-container/pkg/logger"
)
// webKeystoreDefault is the name of the default web server Keystore
const webKeystoreDefault = "default.p12"
// ConfigureWebTLS configures TLS for the web server
func ConfigureWebTLS(keyLabel string, log *logger.Logger, password string) error {
// Return immediately if we have no certificate to use as identity
if keyLabel == "" && os.Getenv("MQ_GENERATE_CERTIFICATE_HOSTNAME") == "" {
return nil
}
tlsConfigLink := "/run/tls.xml"
tlsConfigTemplate := "/etc/mqm/web/installations/Installation1/servers/mqweb/tls.xml.tpl"
encryptedPassword, err := securityutility.EncodeSecrets(password)
if err != nil {
log.Printf("Password encoding for Web Keystore failed with error %v", err)
// We couldn't encode the passwords so using an empty string as password
encryptedPassword = ""
}
// Password successfully encoded using securityUtility use the encoded password the template
templateErr := mqtemplate.ProcessTemplateFile(tlsConfigTemplate, tlsConfigLink, map[string]string{"password": encryptedPassword}, log)
if templateErr != nil {
return templateErr
}
return nil
}
// ConfigureWebKeyStore configures the Web Keystore
func ConfigureWebKeystore(p12Truststore KeyStoreData, keyLabel string) (string, error) {
webKeystore := webKeystoreDefault
if keyLabel != "" {
webKeystore = keyLabel + ".p12"
}
webKeystoreFile := pathutils.CleanPath(keystoreDirDefault, webKeystore)
// Check if a new self-signed certificate should be generated
if keyLabel == "" {
// Get hostname to use for self-signed certificate
genHostName := os.Getenv("MQ_GENERATE_CERTIFICATE_HOSTNAME")
// Create the Web Keystore
newWebKeystore := keystore.NewPKCS12KeyStore(webKeystoreFile, p12Truststore.Password)
err := newWebKeystore.Create()
if err != nil {
return "", fmt.Errorf("failed to create Web Keystore %s: %v", webKeystoreFile, err)
}
// Generate a new self-signed certificate in the Web Keystore
err = newWebKeystore.CreateSelfSignedCertificate("default", fmt.Sprintf("CN=%s", genHostName), genHostName)
if err != nil {
return "", fmt.Errorf("failed to generate certificate in Web Keystore %s with DN of 'CN=%s': %v", webKeystoreFile, genHostName, err)
}
} else {
// Check Web Keystore already exists
_, err := os.Stat(webKeystoreFile)
if err != nil {
return "", fmt.Errorf("failed to find existing Web Keystore %s: %v", webKeystoreFile, err)
}
}
// Check Web Truststore already exists
_, err := os.Stat(p12Truststore.Keystore.Filename)
if err != nil {
return "", fmt.Errorf("failed to find existing Web Truststore %s: %v", p12Truststore.Keystore.Filename, err)
}
return webKeystore, nil
}

41
internal/user/user.go Normal file
View File

@@ -0,0 +1,41 @@
/*
© Copyright IBM Corporation 2018, 2020
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package user
import (
"golang.org/x/sys/unix"
)
// User holds information on primary and supplemental OS groups
type User struct {
UID int
PrimaryGID int
SupplementalGID []int
}
// GetUser returns the current user and group information
func GetUser() (User, error) {
u := User{
UID: unix.Geteuid(),
PrimaryGID: unix.Getgid(),
}
groups, err := unix.Getgroups()
if err != nil {
return u, err
}
u.SupplementalGID = groups
return u, nil
}