Skip to content

Commit 5751fb0

Browse files
authored
feat(crafting-schema): add organization resolver (#2465)
Signed-off-by: Sylwester Piskozub <sylwesterpiskozub@gmail.com>
1 parent a66f0ef commit 5751fb0

File tree

2 files changed

+217
-2
lines changed

2 files changed

+217
-2
lines changed

app/controlplane/internal/usercontext/currentorganization_middleware.go

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ package usercontext
1717

1818
import (
1919
"context"
20+
"encoding/json"
2021
"errors"
2122
"fmt"
2223
"time"
2324

2425
v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
2526
"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/entities"
2627
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz"
28+
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/unmarshal"
2729
"github.com/go-kratos/kratos/v2/log"
2830
"github.com/go-kratos/kratos/v2/middleware"
2931
"github.com/google/uuid"
@@ -68,8 +70,15 @@ func WithCurrentOrganizationMiddleware(userUseCase biz.UserOrgFinder, logger *lo
6870
return nil, fmt.Errorf("error getting organization name: %w", err)
6971
}
7072

73+
// Extract organization from resource metadata, takes precedence over header
74+
if orgFromResource, err := getFromResource(req); err != nil {
75+
return nil, fmt.Errorf("organization from resource: %w", err)
76+
} else if orgFromResource != "" {
77+
orgName = orgFromResource
78+
}
79+
7180
if orgName != "" {
72-
ctx, err = setCurrentOrganizationFromHeader(ctx, u, orgName, userUseCase)
81+
ctx, err = setCurrentMembershipFromOrgName(ctx, u, orgName, userUseCase)
7382
if err != nil {
7483
return nil, v1.ErrorUserNotMemberOfOrgErrorNotInOrg("user is not a member of organization %s", orgName)
7584
}
@@ -131,7 +140,7 @@ func ResetMembershipsCache() {
131140
membershipsCache.Purge()
132141
}
133142

134-
func setCurrentOrganizationFromHeader(ctx context.Context, user *entities.User, orgName string, userUC biz.UserOrgFinder) (context.Context, error) {
143+
func setCurrentMembershipFromOrgName(ctx context.Context, user *entities.User, orgName string, userUC biz.UserOrgFinder) (context.Context, error) {
135144
membership, err := userUC.MembershipInOrg(ctx, user.ID, orgName)
136145
if err != nil {
137146
return nil, fmt.Errorf("failed to find membership: %w", err)
@@ -166,3 +175,72 @@ func setCurrentOrganizationFromDB(ctx context.Context, user *entities.User, user
166175

167176
return ctx, nil
168177
}
178+
179+
// Gets organization from resource metadata
180+
// The metadata organization field acts as a namespace for organization resources
181+
func getFromResource(req interface{}) (string, error) {
182+
if req == nil {
183+
return "", nil
184+
}
185+
186+
switch v := req.(type) {
187+
case *v1.WorkflowContractServiceCreateRequest, *v1.WorkflowContractServiceUpdateRequest:
188+
return extractOrg(v)
189+
}
190+
191+
return "", nil
192+
}
193+
194+
type ResourceBase struct {
195+
Metadata struct {
196+
Organization string `json:"organization"`
197+
} `json:"metadata"`
198+
}
199+
200+
// Extracts organization from request with raw contract data
201+
func extractOrg(req interface{}) (string, error) {
202+
// Get raw data
203+
rawData, err := getRawData(req)
204+
if err != nil {
205+
return "", err
206+
}
207+
208+
if len(rawData) == 0 {
209+
return "", nil
210+
}
211+
212+
// Identify format
213+
format, err := unmarshal.IdentifyFormat(rawData)
214+
if err != nil {
215+
return "", err
216+
}
217+
218+
jsonData, err := unmarshal.LoadJSONBytes(rawData, "."+string(format))
219+
if err != nil {
220+
return "", err
221+
}
222+
223+
// Unmarshal to extract organization
224+
var resourceBase ResourceBase
225+
if err := json.Unmarshal(jsonData, &resourceBase); err != nil {
226+
// If unmarshaling fails, return empty string (no error)
227+
// This allows old format schemas to work without the metadata field
228+
return "", nil
229+
}
230+
231+
return resourceBase.Metadata.Organization, nil
232+
}
233+
234+
type RequestWithRawContract interface {
235+
GetRawContract() []byte
236+
}
237+
238+
// Extracts raw data
239+
func getRawData(req interface{}) ([]byte, error) {
240+
// Check if the request implements RequestWithRawContract
241+
if rawContractReq, ok := req.(RequestWithRawContract); ok {
242+
return rawContractReq.GetRawContract(), nil
243+
}
244+
245+
return nil, fmt.Errorf("request does not have raw contract")
246+
}

app/controlplane/internal/usercontext/currentorganization_middleware_test.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"io"
2121
"testing"
2222

23+
v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
2324
"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/entities"
2425
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz"
2526
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz"
@@ -29,6 +30,7 @@ import (
2930
"github.com/go-kratos/kratos/v2/log"
3031
"github.com/google/uuid"
3132
"github.com/stretchr/testify/assert"
33+
"github.com/stretchr/testify/require"
3234
)
3335

3436
func TestWithCurrentOrganizationMiddleware(t *testing.T) {
@@ -102,3 +104,138 @@ func TestWithCurrentOrganizationMiddleware(t *testing.T) {
102104
})
103105
}
104106
}
107+
108+
func TestGetFromResourceWorkflowContract(t *testing.T) {
109+
tests := []struct {
110+
name string
111+
rawContract []byte
112+
expectedOrg string
113+
expectedError bool
114+
}{
115+
{
116+
name: "valid contract with organization in metadata",
117+
rawContract: []byte(`apiVersion: chainloop.dev/v1
118+
kind: Contract
119+
metadata:
120+
name: test-contract
121+
organization: test-org
122+
spec:
123+
materials:
124+
- name: my-image
125+
type: CONTAINER_IMAGE`),
126+
expectedOrg: "test-org",
127+
expectedError: false,
128+
},
129+
{
130+
name: "valid contract without organization",
131+
rawContract: []byte(`apiVersion: chainloop.dev/v1
132+
kind: Contract
133+
metadata:
134+
name: test-contract
135+
spec:
136+
materials:
137+
- name: my-image
138+
type: CONTAINER_IMAGE`),
139+
expectedOrg: "",
140+
expectedError: false,
141+
},
142+
{
143+
name: "JSON format contract with organization",
144+
rawContract: []byte(`{
145+
"apiVersion": "chainloop.dev/v1",
146+
"kind": "Contract",
147+
"metadata": {
148+
"name": "test-contract",
149+
"organization": "json-org"
150+
},
151+
"spec": {
152+
"materials": [
153+
{
154+
"name": "my-image",
155+
"type": "CONTAINER_IMAGE"
156+
}
157+
]
158+
}
159+
}`),
160+
expectedOrg: "json-org",
161+
expectedError: false,
162+
},
163+
{
164+
name: "empty raw contract",
165+
rawContract: []byte{},
166+
expectedOrg: "",
167+
expectedError: false,
168+
},
169+
{
170+
name: "nil raw contract",
171+
rawContract: nil,
172+
expectedOrg: "",
173+
expectedError: false,
174+
},
175+
{
176+
name: "old schema format (CraftingSchema) - no organization field",
177+
rawContract: []byte(`schemaVersion: v1
178+
materials:
179+
- name: my-image
180+
type: CONTAINER_IMAGE
181+
runner:
182+
type: GITHUB_ACTION`),
183+
expectedOrg: "",
184+
expectedError: false,
185+
},
186+
{
187+
name: "old schema format JSON - no organization field",
188+
rawContract: []byte(`{
189+
"schemaVersion": "v1",
190+
"materials": [
191+
{
192+
"name": "my-image",
193+
"type": "CONTAINER_IMAGE"
194+
}
195+
],
196+
"runner": {
197+
"type": "GITHUB_ACTION"
198+
}
199+
}`),
200+
expectedOrg: "",
201+
expectedError: false,
202+
},
203+
{
204+
name: "invalid format",
205+
rawContract: []byte(`invalid yaml content
206+
this is not parseable`),
207+
expectedOrg: "",
208+
expectedError: false,
209+
},
210+
}
211+
212+
for _, tt := range tests {
213+
t.Run(tt.name, func(t *testing.T) {
214+
// Test with CreateRequest
215+
createReq := &v1.WorkflowContractServiceCreateRequest{
216+
RawContract: tt.rawContract,
217+
}
218+
219+
org, err := getFromResource(createReq)
220+
if tt.expectedError {
221+
require.Error(t, err)
222+
} else {
223+
require.NoError(t, err)
224+
assert.Equal(t, tt.expectedOrg, org)
225+
}
226+
227+
// Test with UpdateRequest
228+
updateReq := &v1.WorkflowContractServiceUpdateRequest{
229+
RawContract: tt.rawContract,
230+
}
231+
232+
org, err = getFromResource(updateReq)
233+
if tt.expectedError {
234+
require.Error(t, err)
235+
} else {
236+
require.NoError(t, err)
237+
assert.Equal(t, tt.expectedOrg, org)
238+
}
239+
})
240+
}
241+
}

0 commit comments

Comments
 (0)