Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
48 changes: 46 additions & 2 deletions auth/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,10 +262,54 @@ func IsColumnarPrivateRegistry(u *url.URL) bool {
}

var (
ErrNoTrialLicense = errors.New("no trial license found")
ErrTrialExpired = errors.New("trial license has expired")
ErrNoTrialLicense = errors.New("no trial license found")
ErrTrialExpired = errors.New("trial license has expired")
ErrLicenseWrongFilename = errors.New("source file is not named columnar.lic (use --force to override)")
ErrLicenseAlreadyExists = errors.New("license already exists (use --force to overwrite)")
)

func LicensePath() string {
return filepath.Join(filepath.Dir(credPath), "columnar.lic")
}

func InstallLicenseFromFile(srcPath string, force bool) error {
src, err := os.Open(srcPath)
if err != nil {
return fmt.Errorf("failed to read license file: %w", err)
}
defer src.Close()

if !force && filepath.Base(srcPath) != "columnar.lic" {
return ErrLicenseWrongFilename
}

destPath := LicensePath()

if !force {
if _, err := os.Stat(destPath); err == nil {
return ErrLicenseAlreadyExists
}
}

if err := os.MkdirAll(filepath.Dir(destPath), 0o700); err != nil {
return fmt.Errorf("failed to create credentials directory: %w", err)
}

dst, err := os.OpenFile(destPath, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0o600)
if err != nil {
return fmt.Errorf("failed to write license file: %w", err)
}
defer dst.Close()

if _, err := dst.ReadFrom(src); err != nil {
dst.Close()
os.Remove(destPath)
return fmt.Errorf("failed to write license file: %w", err)
}

return nil
}

func FetchColumnarLicense(cred *Credential) error {
licensePath := filepath.Join(filepath.Dir(credPath), "columnar.lic")
_, err := os.Stat(licensePath)
Expand Down
71 changes: 69 additions & 2 deletions cmd/dbc/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,18 @@ import (
)

type AuthCmd struct {
Login *LoginCmd `arg:"subcommand" help:"Authenticate with a driver registry"`
Logout *LogoutCmd `arg:"subcommand" help:"Log out from a driver registry"`
Login *LoginCmd `arg:"subcommand" help:"Authenticate with a driver registry"`
Logout *LogoutCmd `arg:"subcommand" help:"Log out from a driver registry"`
License *LicenseCmd `arg:"subcommand" help:"Manage license files"`
}

type LicenseCmd struct {
Install *LicenseInstallCmd `arg:"subcommand" help:"Install a license file"`
}

type LicenseInstallCmd struct {
LicensePath string `arg:"positional,required" help:"Path to the license file to install"`
Force bool `arg:"--force" help:"Overwrite existing license and skip filename check"`
}

type LoginCmd struct {
Expand Down Expand Up @@ -306,3 +316,60 @@ func (m logoutModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}

func (m logoutModel) View() tea.View { return tea.NewView("") }

func (l LicenseInstallCmd) GetModelCustom(baseModel baseModel) tea.Model {
return licenseInstallModel{
baseModel: baseModel,
licensePath: l.LicensePath,
force: l.Force,
}
}

func (l LicenseInstallCmd) GetModel() tea.Model {
return l.GetModelCustom(
baseModel{
getDriverRegistry: getDriverRegistry,
downloadPkg: downloadPkg,
},
)
}

type licenseInstalledMsg struct{}

type licenseInstallModel struct {
baseModel

licensePath string
force bool
installed bool
}

func (m licenseInstallModel) Init() tea.Cmd {
return func() tea.Msg {
if err := auth.InstallLicenseFromFile(m.licensePath, m.force); err != nil {
return err
}
return licenseInstalledMsg{}
}
}

func (m licenseInstallModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.(type) {
case licenseInstalledMsg:
m.installed = true
return m, tea.Quit
}

base, cmd := m.baseModel.Update(msg)
m.baseModel = base.(baseModel)
return m, cmd
}

func (m licenseInstallModel) FinalOutput() string {
if !m.installed {
return ""
}
return "License installed to " + auth.LicensePath()
}

func (m licenseInstallModel) View() tea.View { return tea.NewView("") }
132 changes: 132 additions & 0 deletions cmd/dbc/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,3 +328,135 @@ func (suite *SubcommandTestSuite) TestLogoutCmdInvalidURL() {
// The error will occur when trying to remove credentials
suite.Contains(out, "Error:")
}

func (suite *SubcommandTestSuite) TestLicenseInstallFileNotFound() {
tmpDir := suite.T().TempDir()
credPath := filepath.Join(tmpDir, "credentials.toml")
restore := auth.SetCredPathForTesting(credPath)
defer restore()

cmd := LicenseInstallCmd{LicensePath: "/nonexistent/columnar.lic"}
m := cmd.GetModelCustom(baseModel{
getDriverRegistry: getTestDriverRegistry,
downloadPkg: downloadTestPkg,
})

out := suite.runCmdErr(m)
suite.Contains(out, "failed to read license file")
}

func (suite *SubcommandTestSuite) TestLicenseInstallWrongFilename() {
tmpDir := suite.T().TempDir()
credPath := filepath.Join(tmpDir, "creds", "credentials.toml")
restore := auth.SetCredPathForTesting(credPath)
defer restore()

// Create a source file with wrong name
srcFile := filepath.Join(tmpDir, "my-license.lic")
suite.Require().NoError(os.WriteFile(srcFile, []byte("license-data"), 0o600))

cmd := LicenseInstallCmd{LicensePath: srcFile}
m := cmd.GetModelCustom(baseModel{
getDriverRegistry: getTestDriverRegistry,
downloadPkg: downloadTestPkg,
})

out := suite.runCmdErr(m)
suite.Contains(out, "--force")
}

func (suite *SubcommandTestSuite) TestLicenseInstallWrongFilenameWithForce() {
tmpDir := suite.T().TempDir()
credPath := filepath.Join(tmpDir, "creds", "credentials.toml")
restore := auth.SetCredPathForTesting(credPath)
defer restore()

srcFile := filepath.Join(tmpDir, "my-license.lic")
suite.Require().NoError(os.WriteFile(srcFile, []byte("license-data"), 0o600))

cmd := LicenseInstallCmd{LicensePath: srcFile, Force: true}
m := cmd.GetModelCustom(baseModel{
getDriverRegistry: getTestDriverRegistry,
downloadPkg: downloadTestPkg,
})

out := suite.runCmd(m)
suite.Contains(out, "License installed")

installed, err := os.ReadFile(auth.LicensePath())
suite.Require().NoError(err)
suite.Equal("license-data", string(installed))
}

func (suite *SubcommandTestSuite) TestLicenseInstallAlreadyExists() {
tmpDir := suite.T().TempDir()
credPath := filepath.Join(tmpDir, "credentials.toml")
restore := auth.SetCredPathForTesting(credPath)
defer restore()

// Create existing license at destination
suite.Require().NoError(os.WriteFile(filepath.Join(tmpDir, "columnar.lic"), []byte("old"), 0o600))

// Create source file
srcFile := filepath.Join(suite.T().TempDir(), "columnar.lic")
suite.Require().NoError(os.WriteFile(srcFile, []byte("new"), 0o600))

cmd := LicenseInstallCmd{LicensePath: srcFile}
m := cmd.GetModelCustom(baseModel{
getDriverRegistry: getTestDriverRegistry,
downloadPkg: downloadTestPkg,
})

out := suite.runCmdErr(m)
suite.Contains(out, "--force")
}

func (suite *SubcommandTestSuite) TestLicenseInstallAlreadyExistsWithForce() {
tmpDir := suite.T().TempDir()
credPath := filepath.Join(tmpDir, "credentials.toml")
restore := auth.SetCredPathForTesting(credPath)
defer restore()

// Create existing license at destination
suite.Require().NoError(os.WriteFile(filepath.Join(tmpDir, "columnar.lic"), []byte("old"), 0o600))

// Create source file
srcFile := filepath.Join(suite.T().TempDir(), "columnar.lic")
suite.Require().NoError(os.WriteFile(srcFile, []byte("new"), 0o600))

cmd := LicenseInstallCmd{LicensePath: srcFile, Force: true}
m := cmd.GetModelCustom(baseModel{
getDriverRegistry: getTestDriverRegistry,
downloadPkg: downloadTestPkg,
})

out := suite.runCmd(m)
suite.Contains(out, "License installed")

installed, err := os.ReadFile(auth.LicensePath())
suite.Require().NoError(err)
suite.Equal("new", string(installed))
}

func (suite *SubcommandTestSuite) TestLicenseInstallHappyPath() {
tmpDir := suite.T().TempDir()
credPath := filepath.Join(tmpDir, "credentials.toml")
restore := auth.SetCredPathForTesting(credPath)
defer restore()

srcFile := filepath.Join(suite.T().TempDir(), "columnar.lic")
suite.Require().NoError(os.WriteFile(srcFile, []byte("license-data"), 0o600))

cmd := LicenseInstallCmd{LicensePath: srcFile}
m := cmd.GetModelCustom(baseModel{
getDriverRegistry: getTestDriverRegistry,
downloadPkg: downloadTestPkg,
})

out := suite.runCmd(m)
suite.Contains(out, "License installed")

installed, err := os.ReadFile(auth.LicensePath())
suite.Require().NoError(err)
suite.Equal("license-data", string(installed))
}
75 changes: 75 additions & 0 deletions cmd/dbc/auth_unix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright 2026 Columnar Technologies Inc.
//
// 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.

//go:build !windows

package main

import (
"os"
"path/filepath"

"github.com/columnar-tech/dbc/auth"
"github.com/columnar-tech/dbc/config"
)

func (suite *SubcommandTestSuite) TestLicenseInstallUnreadableSource() {
if suite.configLevel == config.ConfigSystem {
suite.T().Skip("file permission tests are not effective when running as root")
}

tmpDir := suite.T().TempDir()
credPath := filepath.Join(tmpDir, "credentials.toml")
restore := auth.SetCredPathForTesting(credPath)
defer restore()

// Create source file with no read permissions
srcFile := filepath.Join(suite.T().TempDir(), "columnar.lic")
suite.Require().NoError(os.WriteFile(srcFile, []byte("license-data"), 0o000))

cmd := LicenseInstallCmd{LicensePath: srcFile}
m := cmd.GetModelCustom(baseModel{
getDriverRegistry: getTestDriverRegistry,
downloadPkg: downloadTestPkg,
})

out := suite.runCmdErr(m)
suite.Contains(out, "failed to read license file")
}

func (suite *SubcommandTestSuite) TestLicenseInstallUnwritableTarget() {
if suite.configLevel == config.ConfigSystem {
suite.T().Skip("file permission tests are not effective when running as root")
}

tmpDir := suite.T().TempDir()
// Point credPath into an unwritable directory
unwritableDir := filepath.Join(tmpDir, "readonly")
suite.Require().NoError(os.MkdirAll(unwritableDir, 0o500))
credPath := filepath.Join(unwritableDir, "nested", "credentials.toml")
restore := auth.SetCredPathForTesting(credPath)
defer restore()

srcFile := filepath.Join(suite.T().TempDir(), "columnar.lic")
suite.Require().NoError(os.WriteFile(srcFile, []byte("license-data"), 0o600))

cmd := LicenseInstallCmd{LicensePath: srcFile, Force: true}
m := cmd.GetModelCustom(baseModel{
getDriverRegistry: getTestDriverRegistry,
downloadPkg: downloadTestPkg,
})

out := suite.runCmdErr(m)
suite.Contains(out, "failed to create credentials directory")
}
Loading
Loading