From 7fe79cdba5bea5e242b3ce43356ebb21867d9c79 Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Tue, 10 Feb 2026 15:41:28 +0100 Subject: [PATCH 1/3] show instance admin status Signed-off-by: Sylwester Piskozub --- app/cli/cmd/organization_describe.go | 7 +++++- app/cli/pkg/action/config_current_context.go | 22 +++++++++-------- .../controlplane/v1/response_messages.pb.go | 13 ++++++++-- .../controlplane/v1/response_messages.proto | 1 + .../controlplane/v1/response_messages.ts | 24 ++++++++++++++++++- .../controlplane.v1.User.jsonschema.json | 6 +++++ .../controlplane.v1.User.schema.json | 6 +++++ app/controlplane/internal/service/context.go | 18 ++++++++++++-- 8 files changed, 81 insertions(+), 16 deletions(-) diff --git a/app/cli/cmd/organization_describe.go b/app/cli/cmd/organization_describe.go index a48a6d7c6..ca207645d 100644 --- a/app/cli/cmd/organization_describe.go +++ b/app/cli/cmd/organization_describe.go @@ -46,7 +46,12 @@ func newOrganizationDescribeCmd() *cobra.Command { func contextTableOutput(config *action.ConfigContextItem) error { gt := output.NewTableWriter() gt.SetTitle("Current Context") - gt.AppendRow(table.Row{"Logged in as", config.CurrentUser.PrintUserProfileWithEmail()}) + + userInfo := config.CurrentUser.PrintUserProfileWithEmail() + if config.CurrentUser.InstanceAdmin { + userInfo += "\nInstance admin=true" + } + gt.AppendRow(table.Row{"Logged in as", userInfo}) gt.AppendSeparator() if m := config.CurrentMembership; m != nil { diff --git a/app/cli/pkg/action/config_current_context.go b/app/cli/pkg/action/config_current_context.go index 16cf9d16c..7b36d96f9 100644 --- a/app/cli/pkg/action/config_current_context.go +++ b/app/cli/pkg/action/config_current_context.go @@ -37,11 +37,12 @@ type ConfigContextItem struct { } type UserItem struct { - ID string `json:"id"` - Email string `json:"email"` - FirstName string `json:"firstName"` - LastName string `json:"lastName"` - CreatedAt *time.Time `json:"createdAt"` + ID string `json:"id"` + Email string `json:"email"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + CreatedAt *time.Time `json:"createdAt"` + InstanceAdmin bool `json:"instanceAdmin,omitempty"` } // PrintUserProfileWithEmail formats the user's profile with their email. @@ -87,10 +88,11 @@ func pbUserItemToAction(in *pb.User) *UserItem { } return &UserItem{ - ID: in.Id, - Email: in.Email, - FirstName: in.FirstName, - LastName: in.LastName, - CreatedAt: toTimePtr(in.CreatedAt.AsTime()), + ID: in.Id, + Email: in.Email, + FirstName: in.FirstName, + LastName: in.LastName, + CreatedAt: toTimePtr(in.CreatedAt.AsTime()), + InstanceAdmin: in.InstanceAdmin, } } diff --git a/app/controlplane/api/controlplane/v1/response_messages.pb.go b/app/controlplane/api/controlplane/v1/response_messages.pb.go index 1ee65fdab..f167448ec 100644 --- a/app/controlplane/api/controlplane/v1/response_messages.pb.go +++ b/app/controlplane/api/controlplane/v1/response_messages.pb.go @@ -1716,6 +1716,7 @@ type User struct { UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` FirstName string `protobuf:"bytes,4,opt,name=first_name,json=firstName,proto3" json:"first_name,omitempty"` LastName string `protobuf:"bytes,5,opt,name=last_name,json=lastName,proto3" json:"last_name,omitempty"` + InstanceAdmin bool `protobuf:"varint,7,opt,name=instance_admin,json=instanceAdmin,proto3" json:"instance_admin,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1792,6 +1793,13 @@ func (x *User) GetLastName() string { return "" } +func (x *User) GetInstanceAdmin() bool { + if x != nil { + return x.InstanceAdmin + } + return false +} + type OrgMembershipItem struct { state protoimpl.MessageState `protogen:"open.v1"` Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` @@ -2769,7 +2777,7 @@ const file_controlplane_v1_response_messages_proto_rawDesc = "" + "\n" + "FORMAT_CUE\x10\x03B\n" + "\n" + - "\bcontract\"\xde\x01\n" + + "\bcontract\"\x85\x02\n" + "\x04User\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" + "\x05email\x18\x02 \x01(\tR\x05email\x129\n" + @@ -2779,7 +2787,8 @@ const file_controlplane_v1_response_messages_proto_rawDesc = "" + "updated_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\x12\x1d\n" + "\n" + "first_name\x18\x04 \x01(\tR\tfirstName\x12\x1b\n" + - "\tlast_name\x18\x05 \x01(\tR\blastName\"\xbf\x02\n" + + "\tlast_name\x18\x05 \x01(\tR\blastName\x12%\n" + + "\x0einstance_admin\x18\a \x01(\bR\rinstanceAdmin\"\xbf\x02\n" + "\x11OrgMembershipItem\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12*\n" + "\x03org\x18\x02 \x01(\v2\x18.controlplane.v1.OrgItemR\x03org\x12)\n" + diff --git a/app/controlplane/api/controlplane/v1/response_messages.proto b/app/controlplane/api/controlplane/v1/response_messages.proto index 902792268..f71cd36c2 100644 --- a/app/controlplane/api/controlplane/v1/response_messages.proto +++ b/app/controlplane/api/controlplane/v1/response_messages.proto @@ -253,6 +253,7 @@ message User { google.protobuf.Timestamp updated_at = 6; string first_name = 4; string last_name = 5; + bool instance_admin = 7; } message OrgMembershipItem { diff --git a/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts b/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts index e31106e26..f3d24d11f 100644 --- a/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts +++ b/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts @@ -589,6 +589,7 @@ export interface User { updatedAt?: Date; firstName: string; lastName: string; + instanceAdmin: boolean; } export interface OrgMembershipItem { @@ -3514,7 +3515,15 @@ export const WorkflowContractVersionItem_RawBody = { }; function createBaseUser(): User { - return { id: "", email: "", createdAt: undefined, updatedAt: undefined, firstName: "", lastName: "" }; + return { + id: "", + email: "", + createdAt: undefined, + updatedAt: undefined, + firstName: "", + lastName: "", + instanceAdmin: false, + }; } export const User = { @@ -3537,6 +3546,9 @@ export const User = { if (message.lastName !== "") { writer.uint32(42).string(message.lastName); } + if (message.instanceAdmin === true) { + writer.uint32(56).bool(message.instanceAdmin); + } return writer; }, @@ -3589,6 +3601,13 @@ export const User = { message.lastName = reader.string(); continue; + case 7: + if (tag !== 56) { + break; + } + + message.instanceAdmin = reader.bool(); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -3606,6 +3625,7 @@ export const User = { updatedAt: isSet(object.updatedAt) ? fromJsonTimestamp(object.updatedAt) : undefined, firstName: isSet(object.firstName) ? String(object.firstName) : "", lastName: isSet(object.lastName) ? String(object.lastName) : "", + instanceAdmin: isSet(object.instanceAdmin) ? Boolean(object.instanceAdmin) : false, }; }, @@ -3617,6 +3637,7 @@ export const User = { message.updatedAt !== undefined && (obj.updatedAt = message.updatedAt.toISOString()); message.firstName !== undefined && (obj.firstName = message.firstName); message.lastName !== undefined && (obj.lastName = message.lastName); + message.instanceAdmin !== undefined && (obj.instanceAdmin = message.instanceAdmin); return obj; }, @@ -3632,6 +3653,7 @@ export const User = { message.updatedAt = object.updatedAt ?? undefined; message.firstName = object.firstName ?? ""; message.lastName = object.lastName ?? ""; + message.instanceAdmin = object.instanceAdmin ?? false; return message; }, }; diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.User.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.User.jsonschema.json index c441c984f..512fb52e2 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.User.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.User.jsonschema.json @@ -9,6 +9,9 @@ "^(first_name)$": { "type": "string" }, + "^(instance_admin)$": { + "type": "boolean" + }, "^(last_name)$": { "type": "string" }, @@ -29,6 +32,9 @@ "id": { "type": "string" }, + "instanceAdmin": { + "type": "boolean" + }, "lastName": { "type": "string" }, diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.User.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.User.schema.json index 4943ad921..dbae32e59 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.User.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.User.schema.json @@ -9,6 +9,9 @@ "^(firstName)$": { "type": "string" }, + "^(instanceAdmin)$": { + "type": "boolean" + }, "^(lastName)$": { "type": "string" }, @@ -29,6 +32,9 @@ "id": { "type": "string" }, + "instance_admin": { + "type": "boolean" + }, "last_name": { "type": "string" }, diff --git a/app/controlplane/internal/service/context.go b/app/controlplane/internal/service/context.go index ef8a3c7d8..28ee8e1de 100644 --- a/app/controlplane/internal/service/context.go +++ b/app/controlplane/internal/service/context.go @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2024-2026 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. @@ -17,9 +17,11 @@ package service import ( "context" + "slices" pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" "github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/entities" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" errors "github.com/go-kratos/kratos/v2/errors" "google.golang.org/protobuf/types/known/timestamppb" @@ -67,7 +69,7 @@ func (s *ContextService) Current(ctx context.Context, _ *pb.ContextServiceCurren } } else if currentUser != nil { res.CurrentUser = &pb.User{ - Id: currentUser.ID, Email: currentUser.Email, FirstName: currentUser.FirstName, LastName: currentUser.LastName, CreatedAt: timestamppb.New(*currentUser.CreatedAt), + Id: currentUser.ID, Email: currentUser.Email, FirstName: currentUser.FirstName, LastName: currentUser.LastName, CreatedAt: timestamppb.New(*currentUser.CreatedAt), InstanceAdmin: isInstanceAdmin(ctx), } // For regular users, we need to load the membership manually @@ -143,3 +145,15 @@ func bizPolicyViolationBlockingStrategyToPb(blockOnPolicyViolation bool) pb.OrgI return pb.OrgItem_POLICY_VIOLATION_BLOCKING_STRATEGY_ADVISORY } + +// isInstanceAdmin checks if the current user has instance admin role +func isInstanceAdmin(ctx context.Context) bool { + m := entities.CurrentMembership(ctx) + if m == nil { + return false + } + + return slices.ContainsFunc(m.Resources, func(r *entities.ResourceMembership) bool { + return r.Role == authz.RoleInstanceAdmin && r.ResourceType == authz.ResourceTypeInstance + }) +} From 5c834f041b9f9a372aca1bfe0e1ac4524bed5f76 Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Tue, 10 Feb 2026 15:50:42 +0100 Subject: [PATCH 2/3] update headers Signed-off-by: Sylwester Piskozub --- app/cli/cmd/organization_describe.go | 2 +- app/cli/pkg/action/config_current_context.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/cli/cmd/organization_describe.go b/app/cli/cmd/organization_describe.go index ca207645d..139fa5073 100644 --- a/app/cli/cmd/organization_describe.go +++ b/app/cli/cmd/organization_describe.go @@ -1,5 +1,5 @@ // -// Copyright 2023-2025 The Chainloop Authors. +// Copyright 2023-2026 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. diff --git a/app/cli/pkg/action/config_current_context.go b/app/cli/pkg/action/config_current_context.go index 7b36d96f9..7aa7c9d17 100644 --- a/app/cli/pkg/action/config_current_context.go +++ b/app/cli/pkg/action/config_current_context.go @@ -1,5 +1,5 @@ // -// Copyright 2023 The Chainloop Authors. +// Copyright 2023-2026 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. From 258e8cdc1cd347205b8d38f7303973ae9d0e4e83 Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Tue, 10 Feb 2026 16:24:21 +0100 Subject: [PATCH 3/3] update output Signed-off-by: Sylwester Piskozub --- app/cli/cmd/organization_describe.go | 6 +----- app/cli/pkg/action/config_current_context.go | 12 ++++++++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/cli/cmd/organization_describe.go b/app/cli/cmd/organization_describe.go index 139fa5073..653eb5352 100644 --- a/app/cli/cmd/organization_describe.go +++ b/app/cli/cmd/organization_describe.go @@ -47,11 +47,7 @@ func contextTableOutput(config *action.ConfigContextItem) error { gt := output.NewTableWriter() gt.SetTitle("Current Context") - userInfo := config.CurrentUser.PrintUserProfileWithEmail() - if config.CurrentUser.InstanceAdmin { - userInfo += "\nInstance admin=true" - } - gt.AppendRow(table.Row{"Logged in as", userInfo}) + gt.AppendRow(table.Row{"Logged in as", config.CurrentUser.PrintUserProfileWithEmail()}) gt.AppendSeparator() if m := config.CurrentMembership; m != nil { diff --git a/app/cli/pkg/action/config_current_context.go b/app/cli/pkg/action/config_current_context.go index 7aa7c9d17..7cb7bf05c 100644 --- a/app/cli/pkg/action/config_current_context.go +++ b/app/cli/pkg/action/config_current_context.go @@ -59,11 +59,19 @@ func (u *UserItem) PrintUserProfileWithEmail() string { name = u.LastName } + var result string // If we have a name, format with email, otherwise just return email if name != "" { - return name + " <" + u.Email + ">" + result = name + " <" + u.Email + ">" + } else { + result = u.Email } - return u.Email + + if u.InstanceAdmin { + result += " (Instance admin)" + } + + return result } func (action *ConfigCurrentContext) Run() (*ConfigContextItem, error) {