Skip to content

Commit fcc9b29

Browse files
authored
feat(api): adds auth support using GHA tokens (#182)
1 parent 66a1bde commit fcc9b29

File tree

50 files changed

+2914
-65
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+2914
-65
lines changed

.github/workflows/test_oidc.yml

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
name: Validate GHA OIDC
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
8+
permissions:
9+
id-token: write
10+
contents: read
11+
12+
env:
13+
API_URL: "http://localhost:5000"
14+
OIDC_AUDIENCE: "forge"
15+
16+
jobs:
17+
test-auth-endpoint:
18+
runs-on: ubuntu-latest
19+
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- name: Install Local Forge
24+
id: install-local
25+
uses: input-output-hk/catalyst-forge/actions/install-local@master
26+
- name: Check forge version
27+
id: local
28+
run: |
29+
forge version
30+
31+
- name: Setup CI
32+
uses: input-output-hk/catalyst-forge/actions/setup@master
33+
with:
34+
skip_docker: 'true'
35+
skip_github: 'true'
36+
37+
- name: Build image
38+
run: |
39+
forge run ./foundry/api+docker
40+
41+
- name: Start API
42+
run: |
43+
docker compose -f ./foundry/api/docker-compose.yml up -d auth auth-jwt api postgres
44+
45+
- name: Wait for API to be ready
46+
run: |
47+
sleep 1
48+
49+
- name: Create GHA auth
50+
run: |
51+
forge -vvv --api-url "http://localhost:5050" api login "$(cat ./foundry/api/.secret/jwt.txt)"
52+
forge -vvv --api-url "http://localhost:5050" api auth gha create -a input-output-hk/catalyst-forge
53+
rm -rf $HOME/.config/forge/config.toml
54+
55+
- name: Get OIDC token
56+
id: oidc
57+
uses: actions/github-script@v7
58+
with:
59+
result-encoding: string
60+
script: |
61+
const token = await core.getIDToken('forge');
62+
core.setOutput('token', token);
63+
64+
- name: Login with OIDC ID token
65+
id: call-api
66+
shell: bash
67+
run: |
68+
forge -vvv --api-url "http://localhost:5050" api login -t "gha" "${{ steps.oidc.outputs.token }}"
69+
70+
- name: Verify returned JWT token
71+
shell: bash
72+
run: |
73+
forge -vvv --api-url "http://localhost:5050" api auth gha list -j

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ node_modules
1212
!/.vscode/tasks.recommended.json
1313

1414
.env
15+
.secret

cli/Earthfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ deps:
1212
ENV GOMODCACHE=/go/modcache
1313
CACHE --persist --sharing shared /go
1414

15+
COPY ../foundry/api+src/src /foundry/api
1516
COPY ../lib/project+src/src /lib/project
1617
COPY ../lib/providers+src/src /lib/providers
1718
COPY ../lib/schema+src/src /lib/schema

cli/cmd/cmds/api/auth/cmd.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package auth
2+
3+
import "github.com/input-output-hk/catalyst-forge/cli/cmd/cmds/api/auth/gha"
4+
5+
type AuthCmd struct {
6+
GHA gha.GhaCmd `cmd:"" help:"Manage GitHub Actions authentication."`
7+
}

cli/cmd/cmds/api/auth/gha/cmd.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package gha
2+
3+
type GhaCmd struct {
4+
Create CreateCmd `cmd:"" help:"Create a new GHA authentication entry."`
5+
Get GetCmd `cmd:"" help:"Get a GHA authentication entry."`
6+
Update UpdateCmd `cmd:"" help:"Update a GHA authentication entry."`
7+
Delete DeleteCmd `cmd:"" help:"Delete a GHA authentication entry."`
8+
List ListCmd `cmd:"" help:"List all GHA authentication entries."`
9+
}

cli/cmd/cmds/api/auth/gha/common.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package gha
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/charmbracelet/lipgloss"
9+
"github.com/charmbracelet/lipgloss/table"
10+
"github.com/input-output-hk/catalyst-forge/foundry/api/client"
11+
)
12+
13+
func outputJSON(auth *client.GHARepositoryAuth) error {
14+
jsonData, err := json.MarshalIndent(auth, "", " ")
15+
if err != nil {
16+
return fmt.Errorf("failed to marshal JSON: %w", err)
17+
}
18+
fmt.Println(string(jsonData))
19+
return nil
20+
}
21+
22+
func outputTable(auth *client.GHARepositoryAuth) error {
23+
t := table.New().
24+
Border(lipgloss.RoundedBorder()).
25+
BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("62"))).
26+
StyleFunc(func(row, col int) lipgloss.Style {
27+
switch {
28+
case row == 0:
29+
return lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("99"))
30+
case row%2 == 0:
31+
return lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
32+
default:
33+
return lipgloss.NewStyle().Foreground(lipgloss.Color("252"))
34+
}
35+
}).
36+
Headers("ID", "Repository", "Enabled", "Description", "Permissions").
37+
Rows(
38+
[]string{
39+
fmt.Sprintf("%d", auth.ID),
40+
auth.Repository,
41+
fmt.Sprintf("%t", auth.Enabled),
42+
auth.Description,
43+
strings.Join(auth.Permissions, "\n"),
44+
},
45+
)
46+
47+
fmt.Println(t)
48+
return nil
49+
}

cli/cmd/cmds/api/auth/gha/create.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package gha
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/input-output-hk/catalyst-forge/cli/pkg/run"
8+
"github.com/input-output-hk/catalyst-forge/foundry/api/client"
9+
"github.com/input-output-hk/catalyst-forge/foundry/api/pkg/auth"
10+
)
11+
12+
type CreateCmd struct {
13+
Admin bool `short:"a" help:"Whether the authentication entry is an admin entry." default:"false"`
14+
Enabled bool `short:"e" help:"Whether the authentication entry is enabled." default:"true"`
15+
Description string `short:"d" help:"The description of the authentication entry." default:""`
16+
Repository string `arg:"" help:"The repository to create the authentication entry for."`
17+
Permissions []auth.Permission `short:"p" help:"The permissions to grant to the authentication entry."`
18+
JSON bool `short:"j" help:"Output as prettified JSON instead of table."`
19+
}
20+
21+
func (c *CreateCmd) Run(ctx run.RunContext, cl client.Client) error {
22+
var permissions []auth.Permission
23+
if c.Admin {
24+
permissions = auth.AllPermissions
25+
} else {
26+
permissions = c.Permissions
27+
}
28+
29+
auth, err := cl.CreateAuth(context.Background(), &client.CreateAuthRequest{
30+
Repository: c.Repository,
31+
Permissions: permissions,
32+
Description: c.Description,
33+
Enabled: c.Enabled,
34+
})
35+
if err != nil {
36+
return fmt.Errorf("failed to create authentication entry: %w", err)
37+
}
38+
39+
if c.JSON {
40+
return outputJSON(auth)
41+
}
42+
43+
return outputTable(auth)
44+
}

cli/cmd/cmds/api/auth/gha/delete.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package gha
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/input-output-hk/catalyst-forge/cli/pkg/run"
8+
"github.com/input-output-hk/catalyst-forge/foundry/api/client"
9+
)
10+
11+
type DeleteCmd struct {
12+
ID uint `arg:"" help:"The ID of the authentication entry to delete."`
13+
}
14+
15+
func (c *DeleteCmd) Run(ctx run.RunContext, cl client.Client) error {
16+
err := cl.DeleteAuth(context.Background(), c.ID)
17+
if err != nil {
18+
return fmt.Errorf("failed to delete authentication entry: %w", err)
19+
}
20+
21+
ctx.Logger.Info("Authentication entry deleted", "id", c.ID)
22+
return nil
23+
}

cli/cmd/cmds/api/auth/gha/get.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package gha
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/input-output-hk/catalyst-forge/cli/pkg/run"
8+
"github.com/input-output-hk/catalyst-forge/foundry/api/client"
9+
)
10+
11+
type GetCmd struct {
12+
ID *uint `short:"i" help:"The ID of the authentication entry to retrieve."`
13+
Repository *string `short:"r" help:"The repository to retrieve the authentication entry for."`
14+
JSON bool `short:"j" help:"Output as prettified JSON instead of table."`
15+
}
16+
17+
func (c *GetCmd) Run(ctx run.RunContext, cl client.Client) error {
18+
if c.ID == nil && c.Repository == nil {
19+
return fmt.Errorf("either --id or --repository must be specified")
20+
}
21+
22+
if c.ID != nil && c.Repository != nil {
23+
return fmt.Errorf("only one of --id or --repository can be specified")
24+
}
25+
26+
auth, err := c.retrieveAuth(cl)
27+
if err != nil {
28+
return err
29+
}
30+
31+
if c.JSON {
32+
return outputJSON(auth)
33+
}
34+
35+
return outputTable(auth)
36+
}
37+
38+
func (c *GetCmd) retrieveAuth(cl client.Client) (*client.GHARepositoryAuth, error) {
39+
if c.ID != nil {
40+
auth, err := cl.GetAuth(context.Background(), *c.ID)
41+
if err != nil {
42+
return nil, fmt.Errorf("failed to get authentication entry by ID: %w", err)
43+
}
44+
return auth, nil
45+
}
46+
47+
auth, err := cl.GetAuthByRepository(context.Background(), *c.Repository)
48+
if err != nil {
49+
return nil, fmt.Errorf("failed to get authentication entry by repository: %w", err)
50+
}
51+
return auth, nil
52+
}

cli/cmd/cmds/api/auth/gha/list.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package gha
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"strings"
8+
9+
"github.com/charmbracelet/lipgloss"
10+
"github.com/charmbracelet/lipgloss/table"
11+
"github.com/input-output-hk/catalyst-forge/cli/pkg/run"
12+
"github.com/input-output-hk/catalyst-forge/foundry/api/client"
13+
)
14+
15+
type ListCmd struct {
16+
JSON bool `short:"j" help:"Output as prettified JSON instead of table."`
17+
}
18+
19+
func (c *ListCmd) Run(ctx run.RunContext, cl client.Client) error {
20+
auths, err := cl.ListAuths(context.Background())
21+
if err != nil {
22+
return fmt.Errorf("failed to list authentication entries: %w", err)
23+
}
24+
25+
if c.JSON {
26+
return outputJSONList(auths)
27+
}
28+
29+
return outputTableList(auths)
30+
}
31+
32+
func outputJSONList(auths []client.GHARepositoryAuth) error {
33+
jsonData, err := json.MarshalIndent(auths, "", " ")
34+
if err != nil {
35+
return fmt.Errorf("failed to marshal JSON: %w", err)
36+
}
37+
fmt.Println(string(jsonData))
38+
return nil
39+
}
40+
41+
func outputTableList(auths []client.GHARepositoryAuth) error {
42+
if len(auths) == 0 {
43+
fmt.Println("No authentication entries found.")
44+
return nil
45+
}
46+
47+
var rows [][]string
48+
for _, auth := range auths {
49+
// Truncate permissions if too long, show count if many
50+
permissions := auth.Permissions
51+
if len(permissions) > 3 {
52+
permissions = permissions[:3]
53+
}
54+
permissionsStr := strings.Join(permissions, ", ")
55+
if len(auth.Permissions) > 3 {
56+
permissionsStr += fmt.Sprintf(" (+%d more)", len(auth.Permissions)-3)
57+
}
58+
59+
rows = append(rows, []string{
60+
fmt.Sprintf("%d", auth.ID),
61+
auth.Repository,
62+
fmt.Sprintf("%t", auth.Enabled),
63+
auth.Description,
64+
permissionsStr,
65+
})
66+
}
67+
68+
t := table.New().
69+
Border(lipgloss.RoundedBorder()).
70+
BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("62"))).
71+
StyleFunc(func(row, col int) lipgloss.Style {
72+
switch {
73+
case row == 0:
74+
return lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("99"))
75+
case row%2 == 0:
76+
return lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
77+
default:
78+
return lipgloss.NewStyle().Foreground(lipgloss.Color("252"))
79+
}
80+
}).
81+
Headers("ID", "Repository", "Enabled", "Description", "Permissions").
82+
Rows(rows...).
83+
Width(120)
84+
85+
fmt.Println(t)
86+
return nil
87+
}

0 commit comments

Comments
 (0)