Skip to content

Commit 760bdc0

Browse files
authored
feat(foundry): adds initial auth commands for generating API keys (#180)
1 parent 9c5e1e0 commit 760bdc0

File tree

13 files changed

+774
-66
lines changed

13 files changed

+774
-66
lines changed

foundry/api/Earthfile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ deps:
1212
ENV GOMODCACHE=/go/modcache
1313
CACHE --persist --sharing shared /go
1414

15+
# Copy local deps
16+
COPY ../../lib/tools+src/src /lib/tools
17+
1518
COPY go.mod go.sum .
1619
RUN go mod download
1720

@@ -76,12 +79,12 @@ docker:
7679

7780
ARG TARGETOS
7881
ARG TARGETARCH
79-
ARG USERPLATFORM
82+
ARG TARGETPLATFORM
8083

8184
RUN apt-get update && apt-get install -y curl postgresql-client
8285

8386
COPY \
84-
--platform=$USERPLATFORM \
87+
--platform=$TARGETPLATFORM \
8588
(+build/foundry-api \
8689
--GOOS=$TARGETOS \
8790
--GOARCH=$TARGETARCH \

foundry/api/cmd/api/auth/auth.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package auth
2+
3+
// AuthCmd represents the auth subcommand category
4+
type AuthCmd struct {
5+
Generate GenerateCmd `kong:"cmd,help='Generate authentication tokens'"`
6+
Init InitCmd `kong:"cmd,help='Initialize authentication configuration'"`
7+
Validate ValidateCmd `kong:"cmd,help='Validate authentication tokens'"`
8+
}

foundry/api/cmd/api/auth/generate.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package auth
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"github.com/input-output-hk/catalyst-forge/foundry/api/internal/auth"
8+
)
9+
10+
type GenerateCmd struct {
11+
Admin bool `kong:"short='a',help='Generate admin token'"`
12+
Expiration time.Duration `kong:"short='e',help='Expiration time for the token',default='1h'"`
13+
Permissions []auth.Permission `kong:"short='p',help='Permissions to generate'"`
14+
PrivateKey string `kong:"short='k',help='Path to the private key to use for signing',type='existingfile'"`
15+
}
16+
17+
func (g *GenerateCmd) Run() error {
18+
am, err := auth.NewAuthManager(g.PrivateKey, "")
19+
if err != nil {
20+
return err
21+
}
22+
23+
if g.Admin {
24+
token, err := am.GenerateToken("admin", auth.AllPermissions, g.Expiration)
25+
if err != nil {
26+
return err
27+
}
28+
fmt.Println(token)
29+
return nil
30+
}
31+
32+
token, err := am.GenerateToken("user", g.Permissions, g.Expiration)
33+
if err != nil {
34+
return err
35+
}
36+
fmt.Println(token)
37+
38+
return nil
39+
}

foundry/api/cmd/api/auth/init.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package auth
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
8+
"github.com/input-output-hk/catalyst-forge/foundry/api/internal/auth"
9+
)
10+
11+
type InitCmd struct {
12+
OutputDir string `kong:"help='Output directory for generated keys',default='./auth-keys'"`
13+
}
14+
15+
// Run executes the auth init subcommand
16+
func (i *InitCmd) Run() error {
17+
if err := os.MkdirAll(i.OutputDir, 0755); err != nil {
18+
return fmt.Errorf("failed to create output directory: %w", err)
19+
}
20+
21+
keyPair, err := auth.GenerateES256Keys()
22+
if err != nil {
23+
return fmt.Errorf("failed to generate ES256 keys: %w", err)
24+
}
25+
26+
privateKeyPath := filepath.Join(i.OutputDir, "private.pem")
27+
if err := os.WriteFile(privateKeyPath, keyPair.PrivateKeyPEM, 0600); err != nil {
28+
return fmt.Errorf("failed to write private key: %w", err)
29+
}
30+
31+
publicKeyPath := filepath.Join(i.OutputDir, "public.pem")
32+
if err := os.WriteFile(publicKeyPath, keyPair.PublicKeyPEM, 0644); err != nil {
33+
return fmt.Errorf("failed to write public key: %w", err)
34+
}
35+
36+
fmt.Printf("✅ Successfully generated ES256 key pair\n")
37+
fmt.Printf("📁 Private key: %s\n", privateKeyPath)
38+
fmt.Printf("📁 Public key: %s\n", publicKeyPath)
39+
fmt.Printf("🔐 Key type: ES256 (ECDSA with P-256 curve and SHA-256)\n")
40+
fmt.Printf("⚠️ Keep your private key secure and never share it!\n")
41+
42+
return nil
43+
}

foundry/api/cmd/api/auth/validate.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package auth
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/input-output-hk/catalyst-forge/foundry/api/internal/auth"
7+
)
8+
9+
type ValidateCmd struct {
10+
Token string `kong:"arg='',help='Token to validate'"`
11+
PublicKey string `kong:"short='k',help='Path to the public key to use for validation',type='existingfile'"`
12+
}
13+
14+
func (g *ValidateCmd) Run() error {
15+
am, err := auth.NewAuthManager("", g.PublicKey)
16+
if err != nil {
17+
return err
18+
}
19+
20+
claims, err := am.ValidateToken(g.Token)
21+
if err != nil {
22+
return err
23+
}
24+
fmt.Println(claims)
25+
26+
return nil
27+
}

foundry/api/cmd/api/main.go

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ package main
22

33
import (
44
"context"
5+
"fmt"
56
"log"
67
"os"
78
"os/signal"
9+
"runtime"
810
"syscall"
911
"time"
1012

13+
"github.com/alecthomas/kong"
14+
"github.com/input-output-hk/catalyst-forge/foundry/api/cmd/api/auth"
1115
"github.com/input-output-hk/catalyst-forge/foundry/api/internal/api"
1216
"github.com/input-output-hk/catalyst-forge/foundry/api/internal/config"
1317
"github.com/input-output-hk/catalyst-forge/foundry/api/internal/models"
@@ -19,30 +23,53 @@ import (
1923
"gorm.io/gorm"
2024
)
2125

26+
var version = "dev"
27+
2228
var mockK8sClient = mocks.ClientMock{
2329
CreateDeploymentFunc: func(ctx context.Context, deployment *models.ReleaseDeployment) error {
2430
return nil
2531
},
2632
}
2733

28-
func main() {
29-
// Load configuration
30-
cfg, err := config.Load()
31-
if err != nil {
32-
log.Fatalf("Failed to load configuration: %v", err)
34+
// CLI represents the command-line interface structure
35+
type CLI struct {
36+
Run RunCmd `kong:"cmd,help='Start the API server'"`
37+
Version VersionCmd `kong:"cmd,help='Show version information'"`
38+
Auth auth.AuthCmd `kong:"cmd,help='Authentication management commands'"`
39+
}
40+
41+
// RunCmd represents the run subcommand
42+
type RunCmd struct {
43+
config.Config `kong:"embed"`
44+
}
45+
46+
// VersionCmd represents the version subcommand
47+
type VersionCmd struct{}
48+
49+
// Run executes the version subcommand
50+
func (v *VersionCmd) Run() error {
51+
fmt.Printf("foundry api version %s %s/%s\n", version, runtime.GOOS, runtime.GOARCH)
52+
return nil
53+
}
54+
55+
// Run executes the run subcommand
56+
func (r *RunCmd) Run() error {
57+
// Validate configuration
58+
if err := r.Validate(); err != nil {
59+
return err
3360
}
3461

3562
// Initialize logger
36-
logger, err := cfg.GetLogger()
63+
logger, err := r.GetLogger()
3764
if err != nil {
38-
log.Fatalf("Failed to initialize logger: %v", err)
65+
return err
3966
}
4067

4168
// Connect to the database
42-
db, err := gorm.Open(postgres.Open(cfg.GetDSN()), &gorm.Config{})
69+
db, err := gorm.Open(postgres.Open(r.GetDSN()), &gorm.Config{})
4370
if err != nil {
4471
logger.Error("Failed to connect to database", "error", err)
45-
os.Exit(1)
72+
return err
4673
}
4774

4875
// Run migrations
@@ -56,17 +83,17 @@ func main() {
5683
)
5784
if err != nil {
5885
logger.Error("Failed to run migrations", "error", err)
59-
os.Exit(1)
86+
return err
6087
}
6188

6289
// Initialize Kubernetes client if enabled
6390
var k8sClient k8s.Client
64-
if cfg.Kubernetes.Enabled {
65-
logger.Info("Initializing Kubernetes client", "namespace", cfg.Kubernetes.Namespace)
66-
k8sClient, err = k8s.New(cfg.Kubernetes.Namespace, logger)
91+
if r.Kubernetes.Enabled {
92+
logger.Info("Initializing Kubernetes client", "namespace", r.Kubernetes.Namespace)
93+
k8sClient, err = k8s.New(r.Kubernetes.Namespace, logger)
6794
if err != nil {
6895
logger.Error("Failed to initialize Kubernetes client", "error", err)
69-
os.Exit(1)
96+
return err
7097
}
7198
} else {
7299
k8sClient = &mockK8sClient
@@ -88,7 +115,7 @@ func main() {
88115
router := api.SetupRouter(releaseService, deploymentService, db, logger)
89116

90117
// Initialize server
91-
server := api.NewServer(cfg.GetServerAddr(), router, logger)
118+
server := api.NewServer(r.GetServerAddr(), router, logger)
92119

93120
// Handle graceful shutdown
94121
quit := make(chan os.Signal, 1)
@@ -102,7 +129,7 @@ func main() {
102129
}
103130
}()
104131

105-
logger.Info("API server started", "addr", cfg.GetServerAddr())
132+
logger.Info("API server started", "addr", r.GetServerAddr())
106133

107134
// Wait for shutdown signal
108135
<-quit
@@ -118,4 +145,23 @@ func main() {
118145
}
119146

120147
logger.Info("Server exiting")
148+
return nil
149+
}
150+
151+
func main() {
152+
var cli CLI
153+
ctx := kong.Parse(&cli,
154+
kong.Name("foundry-api"),
155+
kong.Description("Catalyst Foundry API Server"),
156+
kong.UsageOnError(),
157+
kong.ConfigureHelp(kong.HelpOptions{
158+
Compact: true,
159+
}),
160+
)
161+
162+
// Execute the selected subcommand
163+
err := ctx.Run()
164+
if err != nil {
165+
log.Fatalf("Command failed: %v", err)
166+
}
121167
}

foundry/api/entrypoint.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,4 @@ if [[ -n "${DB_INIT:-}" ]]; then
4242
fi
4343

4444
echo "Starting Foundry API server..."
45-
exec "/app/foundry-api"
45+
exec /app/foundry-api run

foundry/api/go.mod

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
module github.com/input-output-hk/catalyst-forge/foundry/api
22

3-
go 1.23.0
3+
go 1.24.2
44

55
require (
66
github.com/alecthomas/kong v1.2.1
77
github.com/gin-gonic/gin v1.10.0
8-
github.com/stretchr/testify v1.9.0
8+
github.com/golang-jwt/jwt/v5 v5.2.3
9+
github.com/input-output-hk/catalyst-forge/lib/tools v0.0.0-00010101000000-000000000000
10+
github.com/stretchr/testify v1.10.0
911
gorm.io/driver/postgres v1.5.11
1012
gorm.io/gorm v1.25.12
1113
k8s.io/apimachinery v0.32.3
@@ -17,10 +19,12 @@ require (
1719
github.com/bytedance/sonic/loader v0.1.1 // indirect
1820
github.com/cloudwego/base64x v0.1.4 // indirect
1921
github.com/cloudwego/iasm v0.2.0 // indirect
22+
github.com/cyphar/filepath-securejoin v0.3.6 // indirect
2023
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
2124
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
2225
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
2326
github.com/gin-contrib/sse v0.1.0 // indirect
27+
github.com/go-git/go-billy/v5 v5.5.0 // indirect
2428
github.com/go-logr/logr v1.4.2 // indirect
2529
github.com/go-playground/locales v0.14.1 // indirect
2630
github.com/go-playground/universal-translator v0.18.1 // indirect
@@ -37,27 +41,26 @@ require (
3741
github.com/jinzhu/now v1.1.5 // indirect
3842
github.com/json-iterator/go v1.1.12 // indirect
3943
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
40-
github.com/kr/pretty v0.3.1 // indirect
4144
github.com/leodido/go-urn v1.4.0 // indirect
4245
github.com/mattn/go-isatty v0.0.20 // indirect
4346
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
4447
github.com/modern-go/reflect2 v1.0.2 // indirect
4548
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
46-
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
49+
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
4750
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
4851
github.com/rogpeppe/go-internal v1.14.1 // indirect
4952
github.com/spf13/pflag v1.0.5 // indirect
5053
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
5154
github.com/ugorji/go/codec v1.2.12 // indirect
5255
github.com/x448/float16 v0.8.4 // indirect
5356
golang.org/x/arch v0.8.0 // indirect
54-
golang.org/x/crypto v0.28.0 // indirect
55-
golang.org/x/net v0.30.0 // indirect
57+
golang.org/x/crypto v0.32.0 // indirect
58+
golang.org/x/net v0.34.0 // indirect
5659
golang.org/x/oauth2 v0.23.0 // indirect
57-
golang.org/x/sync v0.8.0 // indirect
58-
golang.org/x/sys v0.26.0 // indirect
59-
golang.org/x/term v0.25.0 // indirect
60-
golang.org/x/text v0.19.0 // indirect
60+
golang.org/x/sync v0.10.0 // indirect
61+
golang.org/x/sys v0.29.0 // indirect
62+
golang.org/x/term v0.28.0 // indirect
63+
golang.org/x/text v0.21.0 // indirect
6164
golang.org/x/time v0.7.0 // indirect
6265
google.golang.org/protobuf v1.35.1 // indirect
6366
gopkg.in/inf.v0 v0.9.1 // indirect
@@ -68,3 +71,5 @@ require (
6871
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
6972
sigs.k8s.io/yaml v1.4.0 // indirect
7073
)
74+
75+
replace github.com/input-output-hk/catalyst-forge/lib/tools => ../../lib/tools

0 commit comments

Comments
 (0)