Skip to content

Commit

Permalink
initial api scoping checks (#414)
Browse files Browse the repository at this point in the history
* initial api scoping checks

* never use optional
  • Loading branch information
mcncl authored Dec 11, 2024
1 parent deeafdb commit c49a422
Show file tree
Hide file tree
Showing 3 changed files with 215 additions and 0 deletions.
76 changes: 76 additions & 0 deletions internal/config/token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package config

import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"sync"
)

type AccessToken struct {
UUID string `json:"uuid"`
Scopes []string `json:"scopes"`
ClientID string `json:"client_id"`
}

var (
tokenInfo *AccessToken
tokenInfoOnce sync.Once
)

// GetTokenScopes fetches and returns the scopes associated with the current API token
func (c *Config) GetTokenScopes() []string {
var err error
tokenInfoOnce.Do(func() {
tokenInfo, err = c.fetchTokenInfo(context.Background())
})

if err != nil {
// Log the error but don't expose it to the caller
// as this is called in the middle of command execution
log.Printf("Error fetching token info: %v", err)
return nil
}

if tokenInfo == nil {
return nil
}

return tokenInfo.Scopes
}

// fetchTokenInfo retrieves the token information from the Buildkite API
func (c *Config) fetchTokenInfo(ctx context.Context) (*AccessToken, error) {
req, err := http.NewRequestWithContext(
ctx,
"GET",
fmt.Sprintf("%s/v2/access-token", c.RESTAPIEndpoint()),
nil,
)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}

req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.APIToken()))

resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("fetching token info: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body))
}

var token AccessToken
if err := json.NewDecoder(resp.Body).Decode(&token); err != nil {
return nil, fmt.Errorf("decoding response: %w", err)
}

return &token, nil
}
117 changes: 117 additions & 0 deletions internal/scopes/scopes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package scopes

import (
"fmt"
"strings"

"github.com/spf13/cobra"
)

type Scope string

const (
// Agent scopes
ReadAgents Scope = "read_agents"
WriteAgents Scope = "write_agents"

// Cluster scopes
ReadClusters Scope = "read_clusters"
WriteClusters Scope = "write_clusters"

// Team scopes
ReadTeams Scope = "read_teams"
WriteTeams Scope = "write_teams"

// Artifact scopes
ReadArtifacts Scope = "read_artifacts"
WriteArtifacts Scope = "write_artifacts"

// Build scopes
ReadBuilds Scope = "read_builds"
WriteBuilds Scope = "write_builds"

// Build logs and environment scopes
ReadJobEnv Scope = "read_job_env"
ReadBuildLogs Scope = "read_build_logs"
WriteBuildLogs Scope = "write_build_logs"

// Organization scopes
ReadOrganizations Scope = "read_organizations"

// Pipeline scopes
ReadPipelines Scope = "read_pipelines"
WritePipelines Scope = "write_pipelines"

// Pipeline template scopes
ReadPipelineTemplates Scope = "read_pipeline_templates"
WritePipelineTemplates Scope = "write_pipeline_templates"

// Rule scopes
ReadRules Scope = "read_rules"
WriteRules Scope = "write_rules"

// User scopes
ReadUser Scope = "read_user"

// Test suite scopes
ReadSuites Scope = "read_suites"
WriteSuites Scope = "write_suites"

// Test plan scopes
ReadTestPlan Scope = "read_test_plan"
WriteTestPlan Scope = "write_test_plan"

// Registry scopes
ReadRegistries Scope = "read_registries"
WriteRegistries Scope = "write_registries"
DeleteRegistries Scope = "delete_registries"

// Package scopes
ReadPackages Scope = "read_packages"
WritePackages Scope = "write_packages"
DeletePackages Scope = "delete_packages"

// GraphQL scope
GraphQL Scope = "graphql"
)

type CommandScopes struct {
Required []Scope
}

func GetCommandScopes(cmd *cobra.Command) CommandScopes {
required := []Scope{}

if reqScopes, ok := cmd.Annotations["requiredScopes"]; ok {
for _, scope := range strings.Split(reqScopes, ",") {
required = append(required, Scope(strings.TrimSpace(scope)))
}
}

return CommandScopes{
Required: required,
}
}

func ValidateScopes(cmdScopes CommandScopes, tokenScopes []string) error {
missingRequired := []string{}

for _, requiredScope := range cmdScopes.Required {
found := false
for _, tokenScope := range tokenScopes {
if string(requiredScope) == tokenScope {
found = true
break
}
}
if !found {
missingRequired = append(missingRequired, string(requiredScope))
}
}

if len(missingRequired) > 0 {
return fmt.Errorf("missing required scopes: %s", strings.Join(missingRequired, ", "))
}

return nil
}
22 changes: 22 additions & 0 deletions pkg/cmd/build/new.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/buildkite/cli/v3/internal/io"
"github.com/buildkite/cli/v3/internal/pipeline/resolver"
"github.com/buildkite/cli/v3/internal/scopes"
"github.com/buildkite/cli/v3/internal/util"
"github.com/buildkite/cli/v3/pkg/cmd/factory"
"github.com/buildkite/go-buildkite/v4"
Expand Down Expand Up @@ -46,6 +47,23 @@ func NewCmdBuildNew(f *factory.Factory) *cobra.Command {
$ bk build new -e "FOO=BAR" -e "BAR=BAZ"
`),
PreRunE: func(cmd *cobra.Command, args []string) error {
// Get the command's required and optional scopes
cmdScopes := scopes.GetCommandScopes(cmd)

// Get the token scopes from the factory
tokenScopes := f.Config.GetTokenScopes()
if len(tokenScopes) == 0 {
return fmt.Errorf("no scopes found in token. Please ensure you're using a token with appropriate scopes")
}

// Validate the scopes
if err := scopes.ValidateScopes(cmdScopes, tokenScopes); err != nil {
return err
}

return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
resolvers := resolver.NewAggregateResolver(
resolver.ResolveFromFlag(pipeline, f.Config),
Expand Down Expand Up @@ -90,6 +108,10 @@ func NewCmdBuildNew(f *factory.Factory) *cobra.Command {
},
}

cmd.Annotations = map[string]string{
"requiredScopes": string(scopes.WriteBuilds),
}

cmd.Flags().StringVarP(&message, "message", "m", "", "Description of the build. If left blank, the commit message will be used once the build starts.")
cmd.Flags().StringVarP(&commit, "commit", "c", "HEAD", "The commit to build.")
cmd.Flags().StringVarP(&branch, "branch", "b", "", "The branch to build. Defaults to the default branch of the pipeline.")
Expand Down

0 comments on commit c49a422

Please sign in to comment.