Skip to content

Commit 655a1fb

Browse files
authored
feat(controlplane): allow system tokens to flag CAS credentials as internal (#3199)
1 parent 0de50a3 commit 655a1fb

10 files changed

Lines changed: 173 additions & 11 deletions

app/controlplane/api/controlplane/v1/cas_credentials.pb.go

Lines changed: 17 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/api/controlplane/v1/cas_credentials.proto

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023-2025 The Chainloop Authors.
2+
// Copyright 2023-2026 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -35,6 +35,9 @@ message CASCredentialsServiceGetRequest {
3535
}];
3636
// during the download we need the digest to find the proper cas backend
3737
string digest = 2;
38+
// flag the minted token as internal platform traffic so the CAS skips audit
39+
// event emission for it. Only honored for system API tokens, rejected otherwise.
40+
bool source_internal = 3;
3841

3942
enum Role {
4043
ROLE_UNSPECIFIED = 0;

app/controlplane/api/controlplane/v1/cas_credentials_grpc.pb.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/api/gen/frontend/controlplane/v1/cas_credentials.ts

Lines changed: 19 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/api/gen/jsonschema/controlplane.v1.CASCredentialsServiceGetRequest.jsonschema.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/api/gen/jsonschema/controlplane.v1.CASCredentialsServiceGetRequest.schema.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/internal/service/cascredential.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023-2025 The Chainloop Authors.
2+
// Copyright 2023-2026 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ import (
1919
"context"
2020

2121
pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
22+
"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/entities"
2223
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz"
2324
errors "github.com/go-kratos/kratos/v2/errors"
2425
"github.com/google/uuid"
@@ -56,6 +57,12 @@ func (s *CASCredentialsService) Get(ctx context.Context, req *pb.CASCredentialsS
5657
return nil, err
5758
}
5859

60+
// Internal platform traffic can be flagged so the CAS skips audit event emission for it
61+
sourceInternal, err := resolveSourceInternal(req.GetSourceInternal(), currentAPIToken)
62+
if err != nil {
63+
return nil, err
64+
}
65+
5966
currentOrg, err := requireCurrentOrg(ctx)
6067
if err != nil {
6168
return nil, err
@@ -149,7 +156,7 @@ func (s *CASCredentialsService) Get(ctx context.Context, req *pb.CASCredentialsS
149156
return nil, errors.BadRequest("invalid argument", "cannot upload or download artifacts from an inline CAS backend")
150157
}
151158

152-
ref := &biz.CASCredsOpts{BackendType: string(backend.Provider), SecretPath: backend.SecretName, Role: role, MaxBytes: backend.Limits.MaxBytes, OrgID: backend.OrganizationID}
159+
ref := &biz.CASCredsOpts{BackendType: string(backend.Provider), SecretPath: backend.SecretName, Role: role, MaxBytes: backend.Limits.MaxBytes, OrgID: backend.OrganizationID, SourceInternal: sourceInternal}
153160
t, err := s.casUC.GenerateTemporaryCredentials(ref)
154161
if err != nil {
155162
return nil, handleUseCaseErr(err, s.log)
@@ -158,5 +165,19 @@ func (s *CASCredentialsService) Get(ctx context.Context, req *pb.CASCredentialsS
158165
return &pb.CASCredentialsServiceGetResponse{
159166
Result: &pb.CASCredentialsServiceGetResponse_Result{Token: t, Backend: bizCASBackendToPb(backend)},
160167
}, nil
168+
}
169+
170+
// resolveSourceInternal returns whether the minted CAS token must be flagged as internal
171+
// platform traffic. Only system API tokens can request it since they are minted exclusively
172+
// by internal code paths; any other caller asking for it is rejected.
173+
func resolveSourceInternal(requested bool, token *entities.APIToken) (bool, error) {
174+
if !requested {
175+
return false, nil
176+
}
177+
178+
if token == nil || !token.IsSystem {
179+
return false, errors.Forbidden("forbidden", "source_internal is restricted to system API tokens")
180+
}
161181

182+
return true, nil
162183
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//
2+
// Copyright 2026 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package service
17+
18+
import (
19+
"testing"
20+
21+
"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/entities"
22+
"github.com/go-kratos/kratos/v2/errors"
23+
"github.com/stretchr/testify/assert"
24+
"github.com/stretchr/testify/require"
25+
)
26+
27+
func TestResolveSourceInternal(t *testing.T) {
28+
testCases := []struct {
29+
name string
30+
requested bool
31+
token *entities.APIToken
32+
want bool
33+
wantErr bool
34+
}{
35+
{
36+
name: "not requested, no token (user auth)",
37+
requested: false,
38+
token: nil,
39+
want: false,
40+
},
41+
{
42+
name: "not requested, regular API token",
43+
requested: false,
44+
token: &entities.APIToken{},
45+
want: false,
46+
},
47+
{
48+
name: "not requested, system API token",
49+
requested: false,
50+
token: &entities.APIToken{IsSystem: true},
51+
want: false,
52+
},
53+
{
54+
name: "requested by system API token",
55+
requested: true,
56+
token: &entities.APIToken{IsSystem: true},
57+
want: true,
58+
},
59+
{
60+
name: "requested by regular API token is forbidden",
61+
requested: true,
62+
token: &entities.APIToken{},
63+
wantErr: true,
64+
},
65+
{
66+
name: "requested by user auth is forbidden",
67+
requested: true,
68+
token: nil,
69+
wantErr: true,
70+
},
71+
}
72+
73+
for _, tc := range testCases {
74+
t.Run(tc.name, func(t *testing.T) {
75+
got, err := resolveSourceInternal(tc.requested, tc.token)
76+
if tc.wantErr {
77+
require.Error(t, err)
78+
assert.True(t, errors.IsForbidden(err))
79+
return
80+
}
81+
82+
require.NoError(t, err)
83+
assert.Equal(t, tc.want, got)
84+
})
85+
}
86+
}

app/controlplane/internal/usercontext/apitoken_middleware.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ func setCurrentOrgAndAPIToken(ctx context.Context, apiTokenUC *biz.APITokenUseCa
243243
WorkflowName: token.WorkflowName,
244244
Policies: token.Policies,
245245
Scope: scope,
246+
IsSystem: token.IsSystem,
246247
})
247248

248249
// Set the authorization subject that will be used to check the policies

app/controlplane/internal/usercontext/entities/apitoken.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ type APIToken struct {
3636
// ACL policies for this token. Used for authorization checks.
3737
Policies []*authz.Policy
3838
Scope string
39+
// IsSystem marks tokens minted by internal code paths; these are hidden from the public API.
40+
IsSystem bool
3941
}
4042

4143
func WithCurrentAPIToken(ctx context.Context, token *APIToken) context.Context {

0 commit comments

Comments
 (0)