Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): rm auth credentials and genericize stored profile info #5

Merged
merged 22 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
69b050a
feat(core): driver factory signature enhancements
jakedoublev Jan 7, 2025
1d59326
make global config of the profiles an internal concern
jakedoublev Jan 7, 2025
86b8507
do not export store driver types and structs
jakedoublev Jan 7, 2025
5a8f50b
update readme
jakedoublev Jan 7, 2025
0660fe3
comments
jakedoublev Jan 7, 2025
2fa84ab
experimental custom store support
jakedoublev Jan 7, 2025
9452d42
feat(core): rm auth credentials and genericize stored profile info
jakedoublev Jan 7, 2025
afb9d59
move to bucket approach for saved profile struct
jakedoublev Jan 7, 2025
cc76106
Merge branch 'feat/driver-cleanup' into feat/rm-creds
jakedoublev Jan 7, 2025
94a5d56
readme
jakedoublev Jan 7, 2025
403d991
rm test that requires building tool in favor of other future tests
jakedoublev Jan 7, 2025
1877d93
remove test profile
jakedoublev Jan 7, 2025
320a8a2
Merge branch 'main' into feat/driver-cleanup
jakedoublev Jan 7, 2025
a51f9a6
Merge branch 'feat/driver-cleanup' into feat/rm-creds
jakedoublev Jan 7, 2025
f52d9c7
rm test for now
jakedoublev Jan 7, 2025
4b054f4
Merge branch 'feat/driver-cleanup' into feat/rm-creds
jakedoublev Jan 7, 2025
eb1272f
wip
jakedoublev Jan 8, 2025
bc8413b
skip interface rewrite as interfaces on method signatures aren't supp…
jakedoublev Jan 8, 2025
d3d1464
platform package for shared utilities
jakedoublev Jan 8, 2025
74b106a
use generics to allow defining a concrete type downstream for success…
jakedoublev Jan 9, 2025
f2d4dc1
allow successful deletion using generic and updation with save to store
jakedoublev Jan 10, 2025
c143bc0
cleanup
jakedoublev Jan 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,9 @@ A Go library to simplify the creation and management of application profiles nat

This project was born out of [OpenTDF](https://github.com/opentdf/platform) and [otdfctl](https://github.com/opentdf/otdfctl).

The `profiles` implementation still contains aspects that are otdfctl-specific or OpenTDF platform specific and should be removed.

Further next steps:

1. update store abstraction to more of a typical key/value?
2. make the value stored for each profile more generic (`interface{}` or `map[string]interface{}`)
3. OS drivers so file system locations (and any other/future concerns) are properly set based on the OS
4. tests / CI
5. docs
6. example CLI
2. tests
3. docs
4. test OS platform directories work as desired
1 change: 0 additions & 1 deletion errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package profiles
import "errors"

var (
ErrDeletingDefaultProfile = errors.New("error: cannot delete the default profile")
ErrProfileNameConflict = errors.New("error: profile name already exists in storage")
ErrMissingCurrentProfile = errors.New("error: current profile not set")
ErrMissingDefaultProfile = errors.New("error: default profile not set")
Expand Down
27 changes: 25 additions & 2 deletions globalConfig.go → internal/global/config.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
package profiles
package global

import (
"encoding/json"

"github.com/jrschumacher/go-osprofiles/pkg/store"
)

// Define constants for the different storage drivers and store keys
const (
PROFILE_DRIVER_KEYRING ProfileDriver = "keyring"
PROFILE_DRIVER_IN_MEMORY ProfileDriver = "in-memory"
PROFILE_DRIVER_FILE ProfileDriver = "file"
// Experimental: enables definition of custom storage driver
PROFILE_DRIVER_CUSTOM ProfileDriver = "custom"
PROFILE_DRIVER_DEFAULT = PROFILE_DRIVER_FILE
STORE_KEY_PROFILE = "profile"
STORE_KEY_GLOBAL = "global"
)

type ProfileDriver string

// This variable is used to store the version of the profiles. Since the profiles structure might
// change in the future, this variable is used to keep track of the version of the profiles and will
// be used to determine how to handle migration of the profiles.
Expand Down Expand Up @@ -40,7 +56,14 @@ func LoadGlobalConfig(configName string, newStore store.NewStoreInterface) (*Glo
}

if p.store.Exists() {
err := p.store.Get(&p.config)
data, err := p.store.Get()
if err != nil {
return nil, err
}
err = json.Unmarshal(data, &p.config)
if err != nil {
return nil, err
}

// check the version of the profiles
if p.config.ProfilesVersion != PROFILES_VERSION_LATEST {
Expand Down
5 changes: 5 additions & 0 deletions internal/global/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package global

import "errors"

var ErrDeletingDefaultProfile = errors.New("error: cannot delete the default profile")
35 changes: 35 additions & 0 deletions pkg/platform/darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package platform

import "path/filepath"

type PlatformDarwin struct {
username string
serviceNamespace string
userHomeDir string
}

func NewPlatformDarwin(username, serviceNamespace, userHomeDir string) Platform {
return PlatformDarwin{username, serviceNamespace, userHomeDir}
}

// TODO: validate these are correct

// GetUsername returns the username for macOS.
func (p PlatformDarwin) GetUsername() string {
return p.username
}

// GetUserHomeDir returns the user's home directory on macOS.
func (p PlatformDarwin) GetUserHomeDir() string {
return p.userHomeDir
}

// GetDataDirectory returns the data directory for macOS.
func (p PlatformDarwin) GetDataDirectory() string {
return filepath.Join(p.userHomeDir, "Library", "Application Support")
}

// GetConfigDirectory returns the config directory for macOS.
func (p PlatformDarwin) GetConfigDirectory() string {
return filepath.Join(p.userHomeDir, "Library", "Preferences")
}
5 changes: 5 additions & 0 deletions pkg/platform/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package platform

import "errors"

var ErrGettingUserOS = errors.New("error getting current user from operating system")
35 changes: 35 additions & 0 deletions pkg/platform/linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package platform

import "path/filepath"

type PlatformLinux struct {
username string
serviceNamespace string
userHomeDir string
}

func NewPlatformLinux(username, serviceNamespace, userHomeDir string) PlatformLinux {
return PlatformLinux{username, serviceNamespace, userHomeDir}
}

// TODO: validate these are correct

// GetUsername returns the username for the Linux OS.
func (p PlatformLinux) GetUsername() string {
return p.username
}

// GetUserHomeDir returns the user's home directory on the Linux OS.
func (p PlatformLinux) GetUserHomeDir() string {
return p.userHomeDir
}

// GetDataDirectory returns the data directory for Linux.
func (p PlatformLinux) GetDataDirectory() string {
return filepath.Join(p.userHomeDir, ".local", "share")
}

// GetConfigDirectory returns the config directory for Linux.
func (p PlatformLinux) GetConfigDirectory() string {
return filepath.Join(p.userHomeDir, ".config")
}
65 changes: 65 additions & 0 deletions pkg/platform/platform.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package platform

import (
"os"
"os/user"
"runtime"
)

type Platform interface {
// Get the username as known to the operating system
GetUsername() string
// Get the user's home directory
GetUserHomeDir() string
// Get the data directory for the platform
GetDataDirectory() string
// Get the config directory for the platform
GetConfigDirectory() string
}

// NewPlatform creates a new platform object based on the current operating system
func NewPlatform(serviceNamespace string) (Platform, error) {
username, userHomeDir, err := getCurrentUserOS()
if err != nil {
return nil, err
}

switch runtime.GOOS {
case "linux":
return NewPlatformLinux(username, serviceNamespace, userHomeDir), nil
case "windows":
return NewPlatformWindows(username, serviceNamespace, userHomeDir), nil
case "darwin":
return NewPlatformDarwin(username, serviceNamespace, userHomeDir), nil
default:
return nil, ErrGettingUserOS
}
}

// getCurrentUserOS gets the current username and home directory
func getCurrentUserOS() (string, string, error) {
usrHomeDir, err := os.UserHomeDir()
if err != nil {
return "", "", ErrGettingUserOS
}
var usr *user.User
// Check platform
if runtime.GOOS == "windows" {
// On Windows, use user.Current() if available, else fallback to environment variable
usr, err = user.Current()
if err != nil {
// TODO: test this on windows
usr = &user.User{Username: os.Getenv("USERNAME")}
if usr.Username == "" {
return "", "", ErrGettingUserOS
}
}
} else {
// On Unix-like systems (Linux, macOS), use user.Current()
usr, err = user.Current()
if err != nil {
return "", "", ErrGettingUserOS
}
}
return usr.Username, usrHomeDir, nil
}
35 changes: 35 additions & 0 deletions pkg/platform/windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package platform

import "path/filepath"

type PlatformWindows struct {
username string
serviceNamespace string
userHomeDir string
}

func NewPlatformWindows(username, serviceNamespace, userHomeDir string) PlatformWindows {
return PlatformWindows{username, serviceNamespace, userHomeDir}
}

// TODO: validate these are correct

// GetUsername returns the username for Windows.
func (p PlatformWindows) GetUsername() string {
return p.username
}

// GetUserHomeDir returns the user's home directory on Windows.
func (p PlatformWindows) GetUserHomeDir() string {
return p.userHomeDir
}

// GetDataDirectory returns the data directory for Windows.
func (p PlatformWindows) GetDataDirectory() string {
return filepath.Join(p.userHomeDir, "AppData", "Roaming")
}

// GetConfigDirectory returns the config directory for Windows.
func (p PlatformWindows) GetConfigDirectory() string {
return filepath.Join(p.userHomeDir, "AppData", "Local")
}
2 changes: 2 additions & 0 deletions pkg/store/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ var (
ErrValueBadCharacters = errors.New("error: value contains invalid characters")

ErrLengthExceeded = errors.New("error: length exceeded")

ErrStoreDriverSetup = errors.New("error: store driver setup failed")
)
33 changes: 21 additions & 12 deletions pkg/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,51 +6,60 @@ import (
"regexp"
)

// StoreInterface is an interface for a store of a single key and value under a namespace.
type NewStoreInterface func(namespace string, key string) (StoreInterface, error)
// DriverOpt is a variadic function to apply any driver-specific options, which
// apply any side effects/hooks necessary for the driver.
type DriverOpt func() error

// TODO: should we reconfigure this abstraction so we have a more traditional key-value store?
// StoreInterface is an interface for a store of a single key and value under a namespace.
// The key is unique within the namespace, and the stored value is a JSON-serialized struct.
//
// In a CLI 'example_cli' consuming this store to save user profiles, the namespace would be 'example_cli',
// and the key would be the specific CLI user's profile name.
type NewStoreInterface func(serviceNamespace, key string, driverOpt ...DriverOpt) (StoreInterface, error)

// StoreInterface is a reusable interface that varied drivers can share to implement a store.
// TODO: should we reconfigure this abstraction so we have a more traditional key-value store?
type StoreInterface interface {
// Exists returns true if the value exists in the store.
Exists() bool
// Get retrieves the entry from the store and unmarshals it into the provided value.
Get(value interface{}) error
// Get retrieves the entry bytes from the store.
Get() ([]byte, error)
// Set marshals the provided value into JSON and stores it.
Set(value interface{}) error
// Delete removes the entry from the store.
Delete() error

}

// NewCustomStore is a package global to init a custom store implementation.
var NewCustomStore NewStoreInterface

const maxFileNameLength = 255

// ValidateNamespaceKey ensures the namespace and key are valid and within length bounds.
func ValidateNamespaceKey(namespace, key string) error {
func ValidateNamespaceKey(serviceNamespace, key string) error {
// Regular expression for allowed characters (alphanumerics, underscore, hyphen)
validName := regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)

if len(namespace) == 0 {
if len(serviceNamespace) == 0 {
return errors.Join(ErrNamespaceInvalid, ErrValueEmpty)
}

if len(key) == 0 {
return errors.Join(ErrKeyInvalid, ErrValueEmpty)
}

if !validName.MatchString(namespace) {
return fmt.Errorf("%w, %w, namespace: %s", ErrNamespaceInvalid, ErrValueBadCharacters, namespace)
if !validName.MatchString(serviceNamespace) {
return fmt.Errorf("%w, %w, namespace: %s", ErrNamespaceInvalid, ErrValueBadCharacters, serviceNamespace)
}
if !validName.MatchString(key) {
return fmt.Errorf("%w, %w, key: %s", ErrKeyInvalid, ErrValueBadCharacters, key)
}

// Ensure the filename is within length bounds when including a file extension
filename := fmt.Sprintf("%s_%s.ext", namespace, key)
filename := fmt.Sprintf("%s_%s.ext", serviceNamespace, key)
if len(filename) > maxFileNameLength {
return fmt.Errorf("%w, <namespace_key>.ext exceeds maximum length (%d): %s", ErrLengthExceeded, maxFileNameLength, filename)
}

return nil
}
}
Loading
Loading