Skip to content
Merged
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
13 changes: 7 additions & 6 deletions app/cli/cmd/organization_apitoken_create.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2024 The Chainloop Authors.
// Copyright 2024-2025 The Chainloop Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -27,8 +27,8 @@ import (

func newAPITokenCreateCmd() *cobra.Command {
var (
description, name string
expiresIn time.Duration
description, name, projectName string
expiresIn time.Duration
)

cmd := &cobra.Command{
Expand All @@ -40,7 +40,7 @@ func newAPITokenCreateCmd() *cobra.Command {
duration = &expiresIn
}

res, err := action.NewAPITokenCreate(actionOpts).Run(context.Background(), name, description, duration)
res, err := action.NewAPITokenCreate(actionOpts).Run(context.Background(), name, description, projectName, duration)
if err != nil {
return fmt.Errorf("creating API token: %w", err)
}
Expand All @@ -61,6 +61,7 @@ func newAPITokenCreateCmd() *cobra.Command {
cmd.InheritedFlags().StringVarP(&flagOutputFormat, "output", "o", "table", "output format, valid options are table, json, token")
err := cmd.MarkFlagRequired("name")
cobra.CheckErr(err)
cmd.Flags().StringVar(&projectName, "project", "", "project name used to scope the token, if not set the token will be created at the organization level")

return cmd
}
Expand All @@ -77,9 +78,9 @@ func apiTokenListTableOutput(tokens []*action.APITokenItem) error {

t := newTableWriter()

t.AppendHeader(table.Row{"Name", "Description", "Created At", "Expires At", "Revoked At", "Last used at"})
t.AppendHeader(table.Row{"ID", "Name", "Scope", "Description", "Created At", "Expires At", "Revoked At", "Last used at"})
for _, p := range tokens {
r := table.Row{p.Name, p.Description, p.CreatedAt.Format(time.RFC822)}
r := table.Row{p.ID, p.Name, p.ScopedEntity.String(), p.Description, p.CreatedAt.Format(time.RFC822)}
if p.ExpiresAt != nil {
r = append(r, p.ExpiresAt.Format(time.RFC822))
} else {
Expand Down
25 changes: 22 additions & 3 deletions app/cli/cmd/organization_apitoken_list.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2023 The Chainloop Authors.
// Copyright 2023-2025 The Chainloop Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -18,20 +18,37 @@ package cmd
import (
"context"
"fmt"
"slices"

"github.com/chainloop-dev/chainloop/app/cli/internal/action"
"github.com/spf13/cobra"
)

func newAPITokenListCmd() *cobra.Command {
var includeRevoked bool
var (
includeRevoked bool
project string
scope string
)

var availableScopes = []string{
"project",
"global",
}

cmd := &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List API tokens in this organization",
PreRunE: func(_ *cobra.Command, _ []string) error {
if scope != "" && !slices.Contains(availableScopes, scope) {
return fmt.Errorf("invalid scope %q, please chose one of: %v", scope, availableScopes)
}

return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
res, err := action.NewAPITokenList(actionOpts).Run(context.Background(), includeRevoked)
res, err := action.NewAPITokenList(actionOpts).Run(context.Background(), includeRevoked, project, scope)
if err != nil {
return fmt.Errorf("listing API tokens: %w", err)
}
Expand All @@ -41,5 +58,7 @@ func newAPITokenListCmd() *cobra.Command {
}

cmd.Flags().BoolVarP(&includeRevoked, "all", "a", false, "show all API tokens including revoked ones")
cmd.Flags().StringVarP(&project, "project", "p", "", "filter by project name")
cmd.Flags().StringVarP(&scope, "scope", "s", "", fmt.Sprintf("filter by scope, available scopes: %v", availableScopes))
return cmd
}
10 changes: 5 additions & 5 deletions app/cli/cmd/organization_apitoken_revoke.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2024 The Chainloop Authors.
// Copyright 2024-2025 The Chainloop Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -24,13 +24,13 @@ import (
)

func newAPITokenRevokeCmd() *cobra.Command {
var name string
var id string

cmd := &cobra.Command{
Use: "revoke",
Short: "revoke API token",
RunE: func(cmd *cobra.Command, args []string) error {
if err := action.NewAPITokenRevoke(actionOpts).Run(context.Background(), name); err != nil {
if err := action.NewAPITokenRevoke(actionOpts).Run(context.Background(), id); err != nil {
return fmt.Errorf("revoking API token: %w", err)
}

Expand All @@ -39,8 +39,8 @@ func newAPITokenRevokeCmd() *cobra.Command {
},
}

cmd.Flags().StringVar(&name, "name", "", "API token name")
err := cmd.MarkFlagRequired("name")
cmd.Flags().StringVar(&id, "id", "", "API token ID")
err := cmd.MarkFlagRequired("id")
cobra.CheckErr(err)

return cmd
Expand Down
4 changes: 3 additions & 1 deletion app/cli/cmd/workflow_contract_describe.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2024 The Chainloop Authors.
// Copyright 2024-2025 The Chainloop Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -86,6 +86,8 @@ func contractDescribeTableOutput(contractWithVersion *action.WorkflowContractWit
t.SetTitle("Contract")
t.AppendRow(table.Row{"Name", c.Name})
t.AppendSeparator()
t.AppendRow(table.Row{"Scope", c.ScopedEntity.String()})
t.AppendSeparator()
t.AppendRow(table.Row{"Description", c.Description})
t.AppendSeparator()
t.AppendRow(table.Row{"Associated Workflows", stringifyAssociatedWorkflows(contractWithVersion)})
Expand Down
8 changes: 1 addition & 7 deletions app/cli/cmd/workflow_contract_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
package cmd

import (
"fmt"
"time"

"github.com/chainloop-dev/chainloop/app/cli/internal/action"
Expand Down Expand Up @@ -51,12 +50,7 @@ func contractListTableOutput(contracts []*action.WorkflowContractItem) error {

t.AppendHeader(table.Row{"Name", "Latest Revision", "Created At", "Updated At", "# Workflows", "Scope"})
for _, p := range contracts {
scope := "org"
if p.ScopedEntity != nil {
scope = fmt.Sprintf("%s/%s", p.ScopedEntity.Type, p.ScopedEntity.Name)
}

t.AppendRow(table.Row{p.Name, p.LatestRevision, p.CreatedAt.Format(time.RFC822), p.LatestRevisionCreatedAt.Format(time.RFC822), len(p.WorkflowRefs), scope})
t.AppendRow(table.Row{p.Name, p.LatestRevision, p.CreatedAt.Format(time.RFC822), p.LatestRevisionCreatedAt.Format(time.RFC822), len(p.WorkflowRefs), p.ScopedEntity.String()})
}

t.Render()
Expand Down
11 changes: 7 additions & 4 deletions app/cli/documentation/cli-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2044,6 +2044,7 @@ Options
--expiration duration optional API token expiration, in hours i.e 1h, 24h, 178h (week), ...
-h, --help help for create
--name string token name
--project string project name used to scope the token, if not set the token will be created at the organization level
```

Options inherited from parent commands
Expand Down Expand Up @@ -2108,8 +2109,10 @@ chainloop organization api-token list [flags]
Options

```
-a, --all show all API tokens including revoked ones
-h, --help help for list
-a, --all show all API tokens including revoked ones
-h, --help help for list
-p, --project string filter by project name
-s, --scope string filter by scope, available scopes: [project global]
```

Options inherited from parent commands
Expand Down Expand Up @@ -2139,8 +2142,8 @@ chainloop organization api-token revoke [flags]
Options

```
-h, --help help for revoke
--name string API token name
-h, --help help for revoke
--id string API token ID
```

Options inherited from parent commands
Expand Down
33 changes: 19 additions & 14 deletions app/cli/internal/action/apitoken_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,20 @@ func NewAPITokenCreate(cfg *ActionsOpts) *APITokenCreate {
return &APITokenCreate{cfg}
}

func (action *APITokenCreate) Run(ctx context.Context, name, description string, expiresIn *time.Duration) (*APITokenItem, error) {
func (action *APITokenCreate) Run(ctx context.Context, name, description, projectName string, expiresIn *time.Duration) (*APITokenItem, error) {
client := pb.NewAPITokenServiceClient(action.cfg.CPConnection)

req := &pb.APITokenServiceCreateRequest{Name: name, Description: &description}
if expiresIn != nil {
req.ExpiresIn = durationpb.New(*expiresIn)
}

if projectName != "" {
req.ProjectReference = &pb.IdentityReference{
Name: &projectName,
}
}

resp, err := client.Create(ctx, req)
if err != nil {
return nil, fmt.Errorf("creating API token: %w", err)
Expand All @@ -62,13 +68,12 @@ type APITokenItem struct {
Name string `json:"name"`
Description string `json:"description"`
// JWT is returned only during the creation
JWT string `json:"jwt,omitempty"`
CreatedAt *time.Time `json:"createdAt"`
RevokedAt *time.Time `json:"revokedAt,omitempty"`
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
ProjectID string `json:"projectId,omitempty"`
ProjectName string `json:"projectName,omitempty"`
JWT string `json:"jwt,omitempty"`
CreatedAt *time.Time `json:"createdAt"`
RevokedAt *time.Time `json:"revokedAt,omitempty"`
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
ScopedEntity *ScopedEntity `json:"scopedEntity,omitempty"`
}

func pbAPITokenItemToAPITokenItem(p *pb.APITokenItem) *APITokenItem {
Expand All @@ -95,12 +100,12 @@ func pbAPITokenItemToAPITokenItem(p *pb.APITokenItem) *APITokenItem {
item.LastUsedAt = toTimePtr(p.LastUsedAt.AsTime())
}

if p.ProjectId != "" {
item.ProjectID = p.ProjectId
}

if p.ProjectName != "" {
item.ProjectName = p.ProjectName
if p.ScopedEntity != nil {
item.ScopedEntity = &ScopedEntity{
Type: p.ScopedEntity.Type,
ID: p.ScopedEntity.Id,
Name: p.ScopedEntity.Name,
}
}

return item
Expand Down
27 changes: 24 additions & 3 deletions app/cli/internal/action/apitoken_list.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2024 The Chainloop Authors.
// Copyright 2024-2025 The Chainloop Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -30,9 +30,19 @@ func NewAPITokenList(cfg *ActionsOpts) *APITokenList {
return &APITokenList{cfg}
}

func (action *APITokenList) Run(ctx context.Context, includeRevoked bool) ([]*APITokenItem, error) {
func (action *APITokenList) Run(ctx context.Context, includeRevoked bool, project string, scope string) ([]*APITokenItem, error) {
client := pb.NewAPITokenServiceClient(action.cfg.CPConnection)
resp, err := client.List(ctx, &pb.APITokenServiceListRequest{IncludeRevoked: includeRevoked})

req := &pb.APITokenServiceListRequest{IncludeRevoked: includeRevoked}
if project != "" {
req.Project = &pb.IdentityReference{Name: &project}
}

if scope != "" {
req.Scope = mapScope(scope)
}

resp, err := client.List(ctx, req)
if err != nil {
return nil, fmt.Errorf("listing API tokens: %w", err)
}
Expand All @@ -44,3 +54,14 @@ func (action *APITokenList) Run(ctx context.Context, includeRevoked bool) ([]*AP

return result, nil
}

func mapScope(scope string) pb.APITokenServiceListRequest_Scope {
switch scope {
case "project":
return pb.APITokenServiceListRequest_SCOPE_PROJECT
case "global":
return pb.APITokenServiceListRequest_SCOPE_GLOBAL
default:
return pb.APITokenServiceListRequest_SCOPE_UNSPECIFIED
}
}
4 changes: 2 additions & 2 deletions app/cli/internal/action/apitoken_revoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ func NewAPITokenRevoke(cfg *ActionsOpts) *APITokenRevoke {
return &APITokenRevoke{cfg}
}

func (action *APITokenRevoke) Run(ctx context.Context, name string) error {
func (action *APITokenRevoke) Run(ctx context.Context, id string) error {
client := pb.NewAPITokenServiceClient(action.cfg.CPConnection)
if _, err := client.Revoke(ctx, &pb.APITokenServiceRevokeRequest{Name: name}); err != nil {
if _, err := client.Revoke(ctx, &pb.APITokenServiceRevokeRequest{Id: id}); err != nil {
return fmt.Errorf("revoking API token: %w", err)
}

Expand Down
9 changes: 9 additions & 0 deletions app/cli/internal/action/workflow_contract_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package action

import (
"context"
"fmt"
"time"

pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
Expand Down Expand Up @@ -45,6 +46,14 @@ type ScopedEntity struct {
Name string `json:"name"`
}

func (s *ScopedEntity) String() string {
if s == nil {
return "global"
}

return fmt.Sprintf("%s/%s", s.Type, s.Name)
}

type WorkflowRef struct {
ID string `json:"id"`
Name string `json:"name"`
Expand Down
Loading
Loading