diff --git a/client/api/omni/specs/auth.pb.go b/client/api/omni/specs/auth.pb.go index c46512e2c..e95429d94 100644 --- a/client/api/omni/specs/auth.pb.go +++ b/client/api/omni/specs/auth.pb.go @@ -979,6 +979,106 @@ func (x *IdentityStatusSpec) GetLastActive() string { return "" } +// RoleSpec describes a set of permission rules. +type RoleSpec struct { + state protoimpl.MessageState `protogen:"open.v1"` + Rules []*RoleSpec_Rule `protobuf:"bytes,1,rep,name=rules,proto3" json:"rules,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RoleSpec) Reset() { + *x = RoleSpec{} + mi := &file_omni_specs_auth_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RoleSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RoleSpec) ProtoMessage() {} + +func (x *RoleSpec) ProtoReflect() protoreflect.Message { + mi := &file_omni_specs_auth_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RoleSpec.ProtoReflect.Descriptor instead. +func (*RoleSpec) Descriptor() ([]byte, []int) { + return file_omni_specs_auth_proto_rawDescGZIP(), []int{15} +} + +func (x *RoleSpec) GetRules() []*RoleSpec_Rule { + if x != nil { + return x.Rules + } + return nil +} + +// RoleBindingSpec binds a Role to a set of subjects (identities). +type RoleBindingSpec struct { + state protoimpl.MessageState `protogen:"open.v1"` + // RoleRef is the ID of the Role resource this binding references. + RoleRef string `protobuf:"bytes,1,opt,name=role_ref,json=roleRef,proto3" json:"role_ref,omitempty"` + // Subjects is the list of identities this binding applies to. + Subjects []*RoleBindingSpec_Subject `protobuf:"bytes,2,rep,name=subjects,proto3" json:"subjects,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RoleBindingSpec) Reset() { + *x = RoleBindingSpec{} + mi := &file_omni_specs_auth_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RoleBindingSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RoleBindingSpec) ProtoMessage() {} + +func (x *RoleBindingSpec) ProtoReflect() protoreflect.Message { + mi := &file_omni_specs_auth_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RoleBindingSpec.ProtoReflect.Descriptor instead. +func (*RoleBindingSpec) Descriptor() ([]byte, []int) { + return file_omni_specs_auth_proto_rawDescGZIP(), []int{16} +} + +func (x *RoleBindingSpec) GetRoleRef() string { + if x != nil { + return x.RoleRef + } + return "" +} + +func (x *RoleBindingSpec) GetSubjects() []*RoleBindingSpec_Subject { + if x != nil { + return x.Subjects + } + return nil +} + type ServiceAccountStatusSpec struct { state protoimpl.MessageState `protogen:"open.v1"` Role string `protobuf:"bytes,1,opt,name=role,proto3" json:"role,omitempty"` @@ -989,7 +1089,7 @@ type ServiceAccountStatusSpec struct { func (x *ServiceAccountStatusSpec) Reset() { *x = ServiceAccountStatusSpec{} - mi := &file_omni_specs_auth_proto_msgTypes[15] + mi := &file_omni_specs_auth_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1001,7 +1101,7 @@ func (x *ServiceAccountStatusSpec) String() string { func (*ServiceAccountStatusSpec) ProtoMessage() {} func (x *ServiceAccountStatusSpec) ProtoReflect() protoreflect.Message { - mi := &file_omni_specs_auth_proto_msgTypes[15] + mi := &file_omni_specs_auth_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1014,7 +1114,7 @@ func (x *ServiceAccountStatusSpec) ProtoReflect() protoreflect.Message { // Deprecated: Use ServiceAccountStatusSpec.ProtoReflect.Descriptor instead. func (*ServiceAccountStatusSpec) Descriptor() ([]byte, []int) { - return file_omni_specs_auth_proto_rawDescGZIP(), []int{15} + return file_omni_specs_auth_proto_rawDescGZIP(), []int{17} } func (x *ServiceAccountStatusSpec) GetRole() string { @@ -1044,7 +1144,7 @@ type EulaAcceptanceSpec struct { func (x *EulaAcceptanceSpec) Reset() { *x = EulaAcceptanceSpec{} - mi := &file_omni_specs_auth_proto_msgTypes[16] + mi := &file_omni_specs_auth_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1056,7 +1156,7 @@ func (x *EulaAcceptanceSpec) String() string { func (*EulaAcceptanceSpec) ProtoMessage() {} func (x *EulaAcceptanceSpec) ProtoReflect() protoreflect.Message { - mi := &file_omni_specs_auth_proto_msgTypes[16] + mi := &file_omni_specs_auth_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1069,7 +1169,7 @@ func (x *EulaAcceptanceSpec) ProtoReflect() protoreflect.Message { // Deprecated: Use EulaAcceptanceSpec.ProtoReflect.Descriptor instead. func (*EulaAcceptanceSpec) Descriptor() ([]byte, []int) { - return file_omni_specs_auth_proto_rawDescGZIP(), []int{16} + return file_omni_specs_auth_proto_rawDescGZIP(), []int{18} } func (x *EulaAcceptanceSpec) GetAcceptedByName() string { @@ -1098,7 +1198,7 @@ type AuthConfigSpec_Auth0 struct { func (x *AuthConfigSpec_Auth0) Reset() { *x = AuthConfigSpec_Auth0{} - mi := &file_omni_specs_auth_proto_msgTypes[17] + mi := &file_omni_specs_auth_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1110,7 +1210,7 @@ func (x *AuthConfigSpec_Auth0) String() string { func (*AuthConfigSpec_Auth0) ProtoMessage() {} func (x *AuthConfigSpec_Auth0) ProtoReflect() protoreflect.Message { - mi := &file_omni_specs_auth_proto_msgTypes[17] + mi := &file_omni_specs_auth_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1167,7 +1267,7 @@ type AuthConfigSpec_OIDC struct { func (x *AuthConfigSpec_OIDC) Reset() { *x = AuthConfigSpec_OIDC{} - mi := &file_omni_specs_auth_proto_msgTypes[18] + mi := &file_omni_specs_auth_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1179,7 +1279,7 @@ func (x *AuthConfigSpec_OIDC) String() string { func (*AuthConfigSpec_OIDC) ProtoMessage() {} func (x *AuthConfigSpec_OIDC) ProtoReflect() protoreflect.Message { - mi := &file_omni_specs_auth_proto_msgTypes[18] + mi := &file_omni_specs_auth_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1240,7 +1340,7 @@ type AuthConfigSpec_Webauthn struct { func (x *AuthConfigSpec_Webauthn) Reset() { *x = AuthConfigSpec_Webauthn{} - mi := &file_omni_specs_auth_proto_msgTypes[19] + mi := &file_omni_specs_auth_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1252,7 +1352,7 @@ func (x *AuthConfigSpec_Webauthn) String() string { func (*AuthConfigSpec_Webauthn) ProtoMessage() {} func (x *AuthConfigSpec_Webauthn) ProtoReflect() protoreflect.Message { - mi := &file_omni_specs_auth_proto_msgTypes[19] + mi := &file_omni_specs_auth_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1300,7 +1400,7 @@ type AuthConfigSpec_SAML struct { func (x *AuthConfigSpec_SAML) Reset() { *x = AuthConfigSpec_SAML{} - mi := &file_omni_specs_auth_proto_msgTypes[20] + mi := &file_omni_specs_auth_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1312,7 +1412,7 @@ func (x *AuthConfigSpec_SAML) String() string { func (*AuthConfigSpec_SAML) ProtoMessage() {} func (x *AuthConfigSpec_SAML) ProtoReflect() protoreflect.Message { - mi := &file_omni_specs_auth_proto_msgTypes[20] + mi := &file_omni_specs_auth_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1381,7 +1481,7 @@ type AccessPolicyUserGroup_User struct { func (x *AccessPolicyUserGroup_User) Reset() { *x = AccessPolicyUserGroup_User{} - mi := &file_omni_specs_auth_proto_msgTypes[23] + mi := &file_omni_specs_auth_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1393,7 +1493,7 @@ func (x *AccessPolicyUserGroup_User) String() string { func (*AccessPolicyUserGroup_User) ProtoMessage() {} func (x *AccessPolicyUserGroup_User) ProtoReflect() protoreflect.Message { - mi := &file_omni_specs_auth_proto_msgTypes[23] + mi := &file_omni_specs_auth_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1440,7 +1540,7 @@ type AccessPolicyClusterGroup_Cluster struct { func (x *AccessPolicyClusterGroup_Cluster) Reset() { *x = AccessPolicyClusterGroup_Cluster{} - mi := &file_omni_specs_auth_proto_msgTypes[24] + mi := &file_omni_specs_auth_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1452,7 +1552,7 @@ func (x *AccessPolicyClusterGroup_Cluster) String() string { func (*AccessPolicyClusterGroup_Cluster) ProtoMessage() {} func (x *AccessPolicyClusterGroup_Cluster) ProtoReflect() protoreflect.Message { - mi := &file_omni_specs_auth_proto_msgTypes[24] + mi := &file_omni_specs_auth_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1491,7 +1591,7 @@ type AccessPolicyRule_Kubernetes struct { func (x *AccessPolicyRule_Kubernetes) Reset() { *x = AccessPolicyRule_Kubernetes{} - mi := &file_omni_specs_auth_proto_msgTypes[25] + mi := &file_omni_specs_auth_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1503,7 +1603,7 @@ func (x *AccessPolicyRule_Kubernetes) String() string { func (*AccessPolicyRule_Kubernetes) ProtoMessage() {} func (x *AccessPolicyRule_Kubernetes) ProtoReflect() protoreflect.Message { - mi := &file_omni_specs_auth_proto_msgTypes[25] + mi := &file_omni_specs_auth_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1535,7 +1635,7 @@ type AccessPolicyRule_Kubernetes_Impersonate struct { func (x *AccessPolicyRule_Kubernetes_Impersonate) Reset() { *x = AccessPolicyRule_Kubernetes_Impersonate{} - mi := &file_omni_specs_auth_proto_msgTypes[26] + mi := &file_omni_specs_auth_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1547,7 +1647,7 @@ func (x *AccessPolicyRule_Kubernetes_Impersonate) String() string { func (*AccessPolicyRule_Kubernetes_Impersonate) ProtoMessage() {} func (x *AccessPolicyRule_Kubernetes_Impersonate) ProtoReflect() protoreflect.Message { - mi := &file_omni_specs_auth_proto_msgTypes[26] + mi := &file_omni_specs_auth_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1580,7 +1680,7 @@ type AccessPolicyTest_Expected struct { func (x *AccessPolicyTest_Expected) Reset() { *x = AccessPolicyTest_Expected{} - mi := &file_omni_specs_auth_proto_msgTypes[27] + mi := &file_omni_specs_auth_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1592,7 +1692,7 @@ func (x *AccessPolicyTest_Expected) String() string { func (*AccessPolicyTest_Expected) ProtoMessage() {} func (x *AccessPolicyTest_Expected) ProtoReflect() protoreflect.Message { - mi := &file_omni_specs_auth_proto_msgTypes[27] + mi := &file_omni_specs_auth_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1632,7 +1732,7 @@ type AccessPolicyTest_User struct { func (x *AccessPolicyTest_User) Reset() { *x = AccessPolicyTest_User{} - mi := &file_omni_specs_auth_proto_msgTypes[28] + mi := &file_omni_specs_auth_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1644,7 +1744,7 @@ func (x *AccessPolicyTest_User) String() string { func (*AccessPolicyTest_User) ProtoMessage() {} func (x *AccessPolicyTest_User) ProtoReflect() protoreflect.Message { - mi := &file_omni_specs_auth_proto_msgTypes[28] + mi := &file_omni_specs_auth_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1683,7 +1783,7 @@ type AccessPolicyTest_Cluster struct { func (x *AccessPolicyTest_Cluster) Reset() { *x = AccessPolicyTest_Cluster{} - mi := &file_omni_specs_auth_proto_msgTypes[29] + mi := &file_omni_specs_auth_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1695,7 +1795,7 @@ func (x *AccessPolicyTest_Cluster) String() string { func (*AccessPolicyTest_Cluster) ProtoMessage() {} func (x *AccessPolicyTest_Cluster) ProtoReflect() protoreflect.Message { - mi := &file_omni_specs_auth_proto_msgTypes[29] + mi := &file_omni_specs_auth_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1727,7 +1827,7 @@ type AccessPolicyTest_Expected_Kubernetes struct { func (x *AccessPolicyTest_Expected_Kubernetes) Reset() { *x = AccessPolicyTest_Expected_Kubernetes{} - mi := &file_omni_specs_auth_proto_msgTypes[30] + mi := &file_omni_specs_auth_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1739,7 +1839,7 @@ func (x *AccessPolicyTest_Expected_Kubernetes) String() string { func (*AccessPolicyTest_Expected_Kubernetes) ProtoMessage() {} func (x *AccessPolicyTest_Expected_Kubernetes) ProtoReflect() protoreflect.Message { - mi := &file_omni_specs_auth_proto_msgTypes[30] + mi := &file_omni_specs_auth_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1771,7 +1871,7 @@ type AccessPolicyTest_Expected_Kubernetes_Impersonate struct { func (x *AccessPolicyTest_Expected_Kubernetes_Impersonate) Reset() { *x = AccessPolicyTest_Expected_Kubernetes_Impersonate{} - mi := &file_omni_specs_auth_proto_msgTypes[31] + mi := &file_omni_specs_auth_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1783,7 +1883,7 @@ func (x *AccessPolicyTest_Expected_Kubernetes_Impersonate) String() string { func (*AccessPolicyTest_Expected_Kubernetes_Impersonate) ProtoMessage() {} func (x *AccessPolicyTest_Expected_Kubernetes_Impersonate) ProtoReflect() protoreflect.Message { - mi := &file_omni_specs_auth_proto_msgTypes[31] + mi := &file_omni_specs_auth_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1806,6 +1906,143 @@ func (x *AccessPolicyTest_Expected_Kubernetes_Impersonate) GetGroups() []string return nil } +// Rule describes a single permission grant. +type RoleSpec_Rule struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Resources is the list of resource family names, or ["*"] for all. + Resources []string `protobuf:"bytes,1,rep,name=resources,proto3" json:"resources,omitempty"` + // Verbs is the list of allowed verbs: "read", "write", or ["*"] for both. + Verbs []string `protobuf:"bytes,2,rep,name=verbs,proto3" json:"verbs,omitempty"` + // Clusters is an optional list of cluster name patterns (fnmatch). If empty, the rule is global. + Clusters []string `protobuf:"bytes,3,rep,name=clusters,proto3" json:"clusters,omitempty"` + // KubernetesGroups is an optional list of K8s groups to impersonate when this rule matches a cluster. + KubernetesGroups []string `protobuf:"bytes,4,rep,name=kubernetes_groups,json=kubernetesGroups,proto3" json:"kubernetes_groups,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RoleSpec_Rule) Reset() { + *x = RoleSpec_Rule{} + mi := &file_omni_specs_auth_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RoleSpec_Rule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RoleSpec_Rule) ProtoMessage() {} + +func (x *RoleSpec_Rule) ProtoReflect() protoreflect.Message { + mi := &file_omni_specs_auth_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RoleSpec_Rule.ProtoReflect.Descriptor instead. +func (*RoleSpec_Rule) Descriptor() ([]byte, []int) { + return file_omni_specs_auth_proto_rawDescGZIP(), []int{15, 0} +} + +func (x *RoleSpec_Rule) GetResources() []string { + if x != nil { + return x.Resources + } + return nil +} + +func (x *RoleSpec_Rule) GetVerbs() []string { + if x != nil { + return x.Verbs + } + return nil +} + +func (x *RoleSpec_Rule) GetClusters() []string { + if x != nil { + return x.Clusters + } + return nil +} + +func (x *RoleSpec_Rule) GetKubernetesGroups() []string { + if x != nil { + return x.KubernetesGroups + } + return nil +} + +// Subject identifies an identity by name, pattern, or labels. +type RoleBindingSpec_Subject struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Name is the exact identity ID. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Match is an fnmatch pattern matched against identity IDs. + Match string `protobuf:"bytes,2,opt,name=match,proto3" json:"match,omitempty"` + // LabelSelectors is a list of label selectors matched against Identity resource labels. + LabelSelectors []string `protobuf:"bytes,3,rep,name=label_selectors,json=labelSelectors,proto3" json:"label_selectors,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RoleBindingSpec_Subject) Reset() { + *x = RoleBindingSpec_Subject{} + mi := &file_omni_specs_auth_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RoleBindingSpec_Subject) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RoleBindingSpec_Subject) ProtoMessage() {} + +func (x *RoleBindingSpec_Subject) ProtoReflect() protoreflect.Message { + mi := &file_omni_specs_auth_proto_msgTypes[38] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RoleBindingSpec_Subject.ProtoReflect.Descriptor instead. +func (*RoleBindingSpec_Subject) Descriptor() ([]byte, []int) { + return file_omni_specs_auth_proto_rawDescGZIP(), []int{16, 0} +} + +func (x *RoleBindingSpec_Subject) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *RoleBindingSpec_Subject) GetMatch() string { + if x != nil { + return x.Match + } + return "" +} + +func (x *RoleBindingSpec_Subject) GetLabelSelectors() []string { + if x != nil { + return x.LabelSelectors + } + return nil +} + type ServiceAccountStatusSpec_PgpPublicKey struct { state protoimpl.MessageState `protogen:"open.v1"` Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` @@ -1819,7 +2056,7 @@ type ServiceAccountStatusSpec_PgpPublicKey struct { func (x *ServiceAccountStatusSpec_PgpPublicKey) Reset() { *x = ServiceAccountStatusSpec_PgpPublicKey{} - mi := &file_omni_specs_auth_proto_msgTypes[35] + mi := &file_omni_specs_auth_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1831,7 +2068,7 @@ func (x *ServiceAccountStatusSpec_PgpPublicKey) String() string { func (*ServiceAccountStatusSpec_PgpPublicKey) ProtoMessage() {} func (x *ServiceAccountStatusSpec_PgpPublicKey) ProtoReflect() protoreflect.Message { - mi := &file_omni_specs_auth_proto_msgTypes[35] + mi := &file_omni_specs_auth_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1844,7 +2081,7 @@ func (x *ServiceAccountStatusSpec_PgpPublicKey) ProtoReflect() protoreflect.Mess // Deprecated: Use ServiceAccountStatusSpec_PgpPublicKey.ProtoReflect.Descriptor instead. func (*ServiceAccountStatusSpec_PgpPublicKey) Descriptor() ([]byte, []int) { - return file_omni_specs_auth_proto_rawDescGZIP(), []int{15, 0} + return file_omni_specs_auth_proto_rawDescGZIP(), []int{17, 0} } func (x *ServiceAccountStatusSpec_PgpPublicKey) GetId() string { @@ -2020,7 +2257,21 @@ const file_omni_specs_auth_proto_rawDesc = "" + "\auser_id\x18\x01 \x01(\tR\x06userId\x12\x12\n" + "\x04role\x18\x02 \x01(\tR\x04role\x12\x1f\n" + "\vlast_active\x18\x03 \x01(\tR\n" + - "lastActive\"\xe3\x02\n" + + "lastActive\"\xbc\x01\n" + + "\bRoleSpec\x12*\n" + + "\x05rules\x18\x01 \x03(\v2\x14.specs.RoleSpec.RuleR\x05rules\x1a\x83\x01\n" + + "\x04Rule\x12\x1c\n" + + "\tresources\x18\x01 \x03(\tR\tresources\x12\x14\n" + + "\x05verbs\x18\x02 \x03(\tR\x05verbs\x12\x1a\n" + + "\bclusters\x18\x03 \x03(\tR\bclusters\x12+\n" + + "\x11kubernetes_groups\x18\x04 \x03(\tR\x10kubernetesGroups\"\xc6\x01\n" + + "\x0fRoleBindingSpec\x12\x19\n" + + "\brole_ref\x18\x01 \x01(\tR\aroleRef\x12:\n" + + "\bsubjects\x18\x02 \x03(\v2\x1e.specs.RoleBindingSpec.SubjectR\bsubjects\x1a\\\n" + + "\aSubject\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + + "\x05match\x18\x02 \x01(\tR\x05match\x12'\n" + + "\x0flabel_selectors\x18\x03 \x03(\tR\x0elabelSelectors\"\xe3\x02\n" + "\x18ServiceAccountStatusSpec\x12\x12\n" + "\x04role\x18\x01 \x01(\tR\x04role\x12M\n" + "\vpublic_keys\x18\x02 \x03(\v2,.specs.ServiceAccountStatusSpec.PgpPublicKeyR\n" + @@ -2050,7 +2301,7 @@ func file_omni_specs_auth_proto_rawDescGZIP() []byte { } var file_omni_specs_auth_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_omni_specs_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 36) +var file_omni_specs_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 40) var file_omni_specs_auth_proto_goTypes = []any{ (PublicKeySpec_Type)(0), // 0: specs.PublicKeySpec.Type (*AuthConfigSpec)(nil), // 1: specs.AuthConfigSpec @@ -2068,66 +2319,72 @@ var file_omni_specs_auth_proto_goTypes = []any{ (*IdentityLastActiveSpec)(nil), // 13: specs.IdentityLastActiveSpec (*PublicKeyLastActiveSpec)(nil), // 14: specs.PublicKeyLastActiveSpec (*IdentityStatusSpec)(nil), // 15: specs.IdentityStatusSpec - (*ServiceAccountStatusSpec)(nil), // 16: specs.ServiceAccountStatusSpec - (*EulaAcceptanceSpec)(nil), // 17: specs.EulaAcceptanceSpec - (*AuthConfigSpec_Auth0)(nil), // 18: specs.AuthConfigSpec.Auth0 - (*AuthConfigSpec_OIDC)(nil), // 19: specs.AuthConfigSpec.OIDC - (*AuthConfigSpec_Webauthn)(nil), // 20: specs.AuthConfigSpec.Webauthn - (*AuthConfigSpec_SAML)(nil), // 21: specs.AuthConfigSpec.SAML - nil, // 22: specs.AuthConfigSpec.SAML.LabelRulesEntry - nil, // 23: specs.AuthConfigSpec.SAML.AttributeRulesEntry - (*AccessPolicyUserGroup_User)(nil), // 24: specs.AccessPolicyUserGroup.User - (*AccessPolicyClusterGroup_Cluster)(nil), // 25: specs.AccessPolicyClusterGroup.Cluster - (*AccessPolicyRule_Kubernetes)(nil), // 26: specs.AccessPolicyRule.Kubernetes - (*AccessPolicyRule_Kubernetes_Impersonate)(nil), // 27: specs.AccessPolicyRule.Kubernetes.Impersonate - (*AccessPolicyTest_Expected)(nil), // 28: specs.AccessPolicyTest.Expected - (*AccessPolicyTest_User)(nil), // 29: specs.AccessPolicyTest.User - (*AccessPolicyTest_Cluster)(nil), // 30: specs.AccessPolicyTest.Cluster - (*AccessPolicyTest_Expected_Kubernetes)(nil), // 31: specs.AccessPolicyTest.Expected.Kubernetes - (*AccessPolicyTest_Expected_Kubernetes_Impersonate)(nil), // 32: specs.AccessPolicyTest.Expected.Kubernetes.Impersonate - nil, // 33: specs.AccessPolicyTest.User.LabelsEntry - nil, // 34: specs.AccessPolicySpec.UserGroupsEntry - nil, // 35: specs.AccessPolicySpec.ClusterGroupsEntry - (*ServiceAccountStatusSpec_PgpPublicKey)(nil), // 36: specs.ServiceAccountStatusSpec.PgpPublicKey - (*timestamppb.Timestamp)(nil), // 37: google.protobuf.Timestamp + (*RoleSpec)(nil), // 16: specs.RoleSpec + (*RoleBindingSpec)(nil), // 17: specs.RoleBindingSpec + (*ServiceAccountStatusSpec)(nil), // 18: specs.ServiceAccountStatusSpec + (*EulaAcceptanceSpec)(nil), // 19: specs.EulaAcceptanceSpec + (*AuthConfigSpec_Auth0)(nil), // 20: specs.AuthConfigSpec.Auth0 + (*AuthConfigSpec_OIDC)(nil), // 21: specs.AuthConfigSpec.OIDC + (*AuthConfigSpec_Webauthn)(nil), // 22: specs.AuthConfigSpec.Webauthn + (*AuthConfigSpec_SAML)(nil), // 23: specs.AuthConfigSpec.SAML + nil, // 24: specs.AuthConfigSpec.SAML.LabelRulesEntry + nil, // 25: specs.AuthConfigSpec.SAML.AttributeRulesEntry + (*AccessPolicyUserGroup_User)(nil), // 26: specs.AccessPolicyUserGroup.User + (*AccessPolicyClusterGroup_Cluster)(nil), // 27: specs.AccessPolicyClusterGroup.Cluster + (*AccessPolicyRule_Kubernetes)(nil), // 28: specs.AccessPolicyRule.Kubernetes + (*AccessPolicyRule_Kubernetes_Impersonate)(nil), // 29: specs.AccessPolicyRule.Kubernetes.Impersonate + (*AccessPolicyTest_Expected)(nil), // 30: specs.AccessPolicyTest.Expected + (*AccessPolicyTest_User)(nil), // 31: specs.AccessPolicyTest.User + (*AccessPolicyTest_Cluster)(nil), // 32: specs.AccessPolicyTest.Cluster + (*AccessPolicyTest_Expected_Kubernetes)(nil), // 33: specs.AccessPolicyTest.Expected.Kubernetes + (*AccessPolicyTest_Expected_Kubernetes_Impersonate)(nil), // 34: specs.AccessPolicyTest.Expected.Kubernetes.Impersonate + nil, // 35: specs.AccessPolicyTest.User.LabelsEntry + nil, // 36: specs.AccessPolicySpec.UserGroupsEntry + nil, // 37: specs.AccessPolicySpec.ClusterGroupsEntry + (*RoleSpec_Rule)(nil), // 38: specs.RoleSpec.Rule + (*RoleBindingSpec_Subject)(nil), // 39: specs.RoleBindingSpec.Subject + (*ServiceAccountStatusSpec_PgpPublicKey)(nil), // 40: specs.ServiceAccountStatusSpec.PgpPublicKey + (*timestamppb.Timestamp)(nil), // 41: google.protobuf.Timestamp } var file_omni_specs_auth_proto_depIdxs = []int32{ - 18, // 0: specs.AuthConfigSpec.auth0:type_name -> specs.AuthConfigSpec.Auth0 - 20, // 1: specs.AuthConfigSpec.webauthn:type_name -> specs.AuthConfigSpec.Webauthn - 21, // 2: specs.AuthConfigSpec.saml:type_name -> specs.AuthConfigSpec.SAML - 19, // 3: specs.AuthConfigSpec.oidc:type_name -> specs.AuthConfigSpec.OIDC - 37, // 4: specs.PublicKeySpec.expiration:type_name -> google.protobuf.Timestamp + 20, // 0: specs.AuthConfigSpec.auth0:type_name -> specs.AuthConfigSpec.Auth0 + 22, // 1: specs.AuthConfigSpec.webauthn:type_name -> specs.AuthConfigSpec.Webauthn + 23, // 2: specs.AuthConfigSpec.saml:type_name -> specs.AuthConfigSpec.SAML + 21, // 3: specs.AuthConfigSpec.oidc:type_name -> specs.AuthConfigSpec.OIDC + 41, // 4: specs.PublicKeySpec.expiration:type_name -> google.protobuf.Timestamp 5, // 5: specs.PublicKeySpec.identity:type_name -> specs.Identity 0, // 6: specs.PublicKeySpec.type:type_name -> specs.PublicKeySpec.Type - 24, // 7: specs.AccessPolicyUserGroup.users:type_name -> specs.AccessPolicyUserGroup.User - 25, // 8: specs.AccessPolicyClusterGroup.clusters:type_name -> specs.AccessPolicyClusterGroup.Cluster - 26, // 9: specs.AccessPolicyRule.kubernetes:type_name -> specs.AccessPolicyRule.Kubernetes - 29, // 10: specs.AccessPolicyTest.user:type_name -> specs.AccessPolicyTest.User - 30, // 11: specs.AccessPolicyTest.cluster:type_name -> specs.AccessPolicyTest.Cluster - 28, // 12: specs.AccessPolicyTest.expected:type_name -> specs.AccessPolicyTest.Expected - 34, // 13: specs.AccessPolicySpec.user_groups:type_name -> specs.AccessPolicySpec.UserGroupsEntry - 35, // 14: specs.AccessPolicySpec.cluster_groups:type_name -> specs.AccessPolicySpec.ClusterGroupsEntry + 26, // 7: specs.AccessPolicyUserGroup.users:type_name -> specs.AccessPolicyUserGroup.User + 27, // 8: specs.AccessPolicyClusterGroup.clusters:type_name -> specs.AccessPolicyClusterGroup.Cluster + 28, // 9: specs.AccessPolicyRule.kubernetes:type_name -> specs.AccessPolicyRule.Kubernetes + 31, // 10: specs.AccessPolicyTest.user:type_name -> specs.AccessPolicyTest.User + 32, // 11: specs.AccessPolicyTest.cluster:type_name -> specs.AccessPolicyTest.Cluster + 30, // 12: specs.AccessPolicyTest.expected:type_name -> specs.AccessPolicyTest.Expected + 36, // 13: specs.AccessPolicySpec.user_groups:type_name -> specs.AccessPolicySpec.UserGroupsEntry + 37, // 14: specs.AccessPolicySpec.cluster_groups:type_name -> specs.AccessPolicySpec.ClusterGroupsEntry 9, // 15: specs.AccessPolicySpec.rules:type_name -> specs.AccessPolicyRule 10, // 16: specs.AccessPolicySpec.tests:type_name -> specs.AccessPolicyTest - 37, // 17: specs.IdentityLastActiveSpec.last_active:type_name -> google.protobuf.Timestamp - 37, // 18: specs.PublicKeyLastActiveSpec.last_used:type_name -> google.protobuf.Timestamp - 36, // 19: specs.ServiceAccountStatusSpec.public_keys:type_name -> specs.ServiceAccountStatusSpec.PgpPublicKey - 22, // 20: specs.AuthConfigSpec.SAML.label_rules:type_name -> specs.AuthConfigSpec.SAML.LabelRulesEntry - 23, // 21: specs.AuthConfigSpec.SAML.attribute_rules:type_name -> specs.AuthConfigSpec.SAML.AttributeRulesEntry - 27, // 22: specs.AccessPolicyRule.Kubernetes.impersonate:type_name -> specs.AccessPolicyRule.Kubernetes.Impersonate - 31, // 23: specs.AccessPolicyTest.Expected.kubernetes:type_name -> specs.AccessPolicyTest.Expected.Kubernetes - 33, // 24: specs.AccessPolicyTest.User.labels:type_name -> specs.AccessPolicyTest.User.LabelsEntry - 32, // 25: specs.AccessPolicyTest.Expected.Kubernetes.impersonate:type_name -> specs.AccessPolicyTest.Expected.Kubernetes.Impersonate - 7, // 26: specs.AccessPolicySpec.UserGroupsEntry.value:type_name -> specs.AccessPolicyUserGroup - 8, // 27: specs.AccessPolicySpec.ClusterGroupsEntry.value:type_name -> specs.AccessPolicyClusterGroup - 37, // 28: specs.ServiceAccountStatusSpec.PgpPublicKey.expiration:type_name -> google.protobuf.Timestamp - 37, // 29: specs.ServiceAccountStatusSpec.PgpPublicKey.created:type_name -> google.protobuf.Timestamp - 37, // 30: specs.ServiceAccountStatusSpec.PgpPublicKey.last_used:type_name -> google.protobuf.Timestamp - 31, // [31:31] is the sub-list for method output_type - 31, // [31:31] is the sub-list for method input_type - 31, // [31:31] is the sub-list for extension type_name - 31, // [31:31] is the sub-list for extension extendee - 0, // [0:31] is the sub-list for field type_name + 41, // 17: specs.IdentityLastActiveSpec.last_active:type_name -> google.protobuf.Timestamp + 41, // 18: specs.PublicKeyLastActiveSpec.last_used:type_name -> google.protobuf.Timestamp + 38, // 19: specs.RoleSpec.rules:type_name -> specs.RoleSpec.Rule + 39, // 20: specs.RoleBindingSpec.subjects:type_name -> specs.RoleBindingSpec.Subject + 40, // 21: specs.ServiceAccountStatusSpec.public_keys:type_name -> specs.ServiceAccountStatusSpec.PgpPublicKey + 24, // 22: specs.AuthConfigSpec.SAML.label_rules:type_name -> specs.AuthConfigSpec.SAML.LabelRulesEntry + 25, // 23: specs.AuthConfigSpec.SAML.attribute_rules:type_name -> specs.AuthConfigSpec.SAML.AttributeRulesEntry + 29, // 24: specs.AccessPolicyRule.Kubernetes.impersonate:type_name -> specs.AccessPolicyRule.Kubernetes.Impersonate + 33, // 25: specs.AccessPolicyTest.Expected.kubernetes:type_name -> specs.AccessPolicyTest.Expected.Kubernetes + 35, // 26: specs.AccessPolicyTest.User.labels:type_name -> specs.AccessPolicyTest.User.LabelsEntry + 34, // 27: specs.AccessPolicyTest.Expected.Kubernetes.impersonate:type_name -> specs.AccessPolicyTest.Expected.Kubernetes.Impersonate + 7, // 28: specs.AccessPolicySpec.UserGroupsEntry.value:type_name -> specs.AccessPolicyUserGroup + 8, // 29: specs.AccessPolicySpec.ClusterGroupsEntry.value:type_name -> specs.AccessPolicyClusterGroup + 41, // 30: specs.ServiceAccountStatusSpec.PgpPublicKey.expiration:type_name -> google.protobuf.Timestamp + 41, // 31: specs.ServiceAccountStatusSpec.PgpPublicKey.created:type_name -> google.protobuf.Timestamp + 41, // 32: specs.ServiceAccountStatusSpec.PgpPublicKey.last_used:type_name -> google.protobuf.Timestamp + 33, // [33:33] is the sub-list for method output_type + 33, // [33:33] is the sub-list for method input_type + 33, // [33:33] is the sub-list for extension type_name + 33, // [33:33] is the sub-list for extension extendee + 0, // [0:33] is the sub-list for field type_name } func init() { file_omni_specs_auth_proto_init() } @@ -2141,7 +2398,7 @@ func file_omni_specs_auth_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_omni_specs_auth_proto_rawDesc), len(file_omni_specs_auth_proto_rawDesc)), NumEnums: 1, - NumMessages: 36, + NumMessages: 40, NumExtensions: 0, NumServices: 0, }, diff --git a/client/api/omni/specs/auth.proto b/client/api/omni/specs/auth.proto index 815c105b5..c6926e2a0 100644 --- a/client/api/omni/specs/auth.proto +++ b/client/api/omni/specs/auth.proto @@ -210,6 +210,41 @@ message IdentityStatusSpec { string last_active = 3; } +// RoleSpec describes a set of permission rules. +message RoleSpec { + // Rule describes a single permission grant. + message Rule { + // Resources is the list of resource family names, or ["*"] for all. + repeated string resources = 1; + // Verbs is the list of allowed verbs: "read", "write", or ["*"] for both. + repeated string verbs = 2; + // Clusters is an optional list of cluster name patterns (fnmatch). If empty, the rule is global. + repeated string clusters = 3; + // KubernetesGroups is an optional list of K8s groups to impersonate when this rule matches a cluster. + repeated string kubernetes_groups = 4; + } + + repeated Rule rules = 1; +} + +// RoleBindingSpec binds a Role to a set of subjects (identities). +message RoleBindingSpec { + // Subject identifies an identity by name, pattern, or labels. + message Subject { + // Name is the exact identity ID. + string name = 1; + // Match is an fnmatch pattern matched against identity IDs. + string match = 2; + // LabelSelectors is a list of label selectors matched against Identity resource labels. + repeated string label_selectors = 3; + } + + // RoleRef is the ID of the Role resource this binding references. + string role_ref = 1; + // Subjects is the list of identities this binding applies to. + repeated Subject subjects = 2; +} + message ServiceAccountStatusSpec { message PgpPublicKey { string id = 1; diff --git a/client/api/omni/specs/auth_vtproto.pb.go b/client/api/omni/specs/auth_vtproto.pb.go index 59a61af91..b96179bc9 100644 --- a/client/api/omni/specs/auth_vtproto.pb.go +++ b/client/api/omni/specs/auth_vtproto.pb.go @@ -641,6 +641,112 @@ func (m *IdentityStatusSpec) CloneMessageVT() proto.Message { return m.CloneVT() } +func (m *RoleSpec_Rule) CloneVT() *RoleSpec_Rule { + if m == nil { + return (*RoleSpec_Rule)(nil) + } + r := new(RoleSpec_Rule) + if rhs := m.Resources; rhs != nil { + tmpContainer := make([]string, len(rhs)) + copy(tmpContainer, rhs) + r.Resources = tmpContainer + } + if rhs := m.Verbs; rhs != nil { + tmpContainer := make([]string, len(rhs)) + copy(tmpContainer, rhs) + r.Verbs = tmpContainer + } + if rhs := m.Clusters; rhs != nil { + tmpContainer := make([]string, len(rhs)) + copy(tmpContainer, rhs) + r.Clusters = tmpContainer + } + if rhs := m.KubernetesGroups; rhs != nil { + tmpContainer := make([]string, len(rhs)) + copy(tmpContainer, rhs) + r.KubernetesGroups = tmpContainer + } + if len(m.unknownFields) > 0 { + r.unknownFields = make([]byte, len(m.unknownFields)) + copy(r.unknownFields, m.unknownFields) + } + return r +} + +func (m *RoleSpec_Rule) CloneMessageVT() proto.Message { + return m.CloneVT() +} + +func (m *RoleSpec) CloneVT() *RoleSpec { + if m == nil { + return (*RoleSpec)(nil) + } + r := new(RoleSpec) + if rhs := m.Rules; rhs != nil { + tmpContainer := make([]*RoleSpec_Rule, len(rhs)) + for k, v := range rhs { + tmpContainer[k] = v.CloneVT() + } + r.Rules = tmpContainer + } + if len(m.unknownFields) > 0 { + r.unknownFields = make([]byte, len(m.unknownFields)) + copy(r.unknownFields, m.unknownFields) + } + return r +} + +func (m *RoleSpec) CloneMessageVT() proto.Message { + return m.CloneVT() +} + +func (m *RoleBindingSpec_Subject) CloneVT() *RoleBindingSpec_Subject { + if m == nil { + return (*RoleBindingSpec_Subject)(nil) + } + r := new(RoleBindingSpec_Subject) + r.Name = m.Name + r.Match = m.Match + if rhs := m.LabelSelectors; rhs != nil { + tmpContainer := make([]string, len(rhs)) + copy(tmpContainer, rhs) + r.LabelSelectors = tmpContainer + } + if len(m.unknownFields) > 0 { + r.unknownFields = make([]byte, len(m.unknownFields)) + copy(r.unknownFields, m.unknownFields) + } + return r +} + +func (m *RoleBindingSpec_Subject) CloneMessageVT() proto.Message { + return m.CloneVT() +} + +func (m *RoleBindingSpec) CloneVT() *RoleBindingSpec { + if m == nil { + return (*RoleBindingSpec)(nil) + } + r := new(RoleBindingSpec) + r.RoleRef = m.RoleRef + if rhs := m.Subjects; rhs != nil { + tmpContainer := make([]*RoleBindingSpec_Subject, len(rhs)) + for k, v := range rhs { + tmpContainer[k] = v.CloneVT() + } + r.Subjects = tmpContainer + } + if len(m.unknownFields) > 0 { + r.unknownFields = make([]byte, len(m.unknownFields)) + copy(r.unknownFields, m.unknownFields) + } + return r +} + +func (m *RoleBindingSpec) CloneMessageVT() proto.Message { + return m.CloneVT() +} + func (m *ServiceAccountStatusSpec_PgpPublicKey) CloneVT() *ServiceAccountStatusSpec_PgpPublicKey { if m == nil { return (*ServiceAccountStatusSpec_PgpPublicKey)(nil) @@ -1542,6 +1648,158 @@ func (this *IdentityStatusSpec) EqualMessageVT(thatMsg proto.Message) bool { } return this.EqualVT(that) } +func (this *RoleSpec_Rule) EqualVT(that *RoleSpec_Rule) bool { + if this == that { + return true + } else if this == nil || that == nil { + return false + } + if len(this.Resources) != len(that.Resources) { + return false + } + for i, vx := range this.Resources { + vy := that.Resources[i] + if vx != vy { + return false + } + } + if len(this.Verbs) != len(that.Verbs) { + return false + } + for i, vx := range this.Verbs { + vy := that.Verbs[i] + if vx != vy { + return false + } + } + if len(this.Clusters) != len(that.Clusters) { + return false + } + for i, vx := range this.Clusters { + vy := that.Clusters[i] + if vx != vy { + return false + } + } + if len(this.KubernetesGroups) != len(that.KubernetesGroups) { + return false + } + for i, vx := range this.KubernetesGroups { + vy := that.KubernetesGroups[i] + if vx != vy { + return false + } + } + return string(this.unknownFields) == string(that.unknownFields) +} + +func (this *RoleSpec_Rule) EqualMessageVT(thatMsg proto.Message) bool { + that, ok := thatMsg.(*RoleSpec_Rule) + if !ok { + return false + } + return this.EqualVT(that) +} +func (this *RoleSpec) EqualVT(that *RoleSpec) bool { + if this == that { + return true + } else if this == nil || that == nil { + return false + } + if len(this.Rules) != len(that.Rules) { + return false + } + for i, vx := range this.Rules { + vy := that.Rules[i] + if p, q := vx, vy; p != q { + if p == nil { + p = &RoleSpec_Rule{} + } + if q == nil { + q = &RoleSpec_Rule{} + } + if !p.EqualVT(q) { + return false + } + } + } + return string(this.unknownFields) == string(that.unknownFields) +} + +func (this *RoleSpec) EqualMessageVT(thatMsg proto.Message) bool { + that, ok := thatMsg.(*RoleSpec) + if !ok { + return false + } + return this.EqualVT(that) +} +func (this *RoleBindingSpec_Subject) EqualVT(that *RoleBindingSpec_Subject) bool { + if this == that { + return true + } else if this == nil || that == nil { + return false + } + if this.Name != that.Name { + return false + } + if this.Match != that.Match { + return false + } + if len(this.LabelSelectors) != len(that.LabelSelectors) { + return false + } + for i, vx := range this.LabelSelectors { + vy := that.LabelSelectors[i] + if vx != vy { + return false + } + } + return string(this.unknownFields) == string(that.unknownFields) +} + +func (this *RoleBindingSpec_Subject) EqualMessageVT(thatMsg proto.Message) bool { + that, ok := thatMsg.(*RoleBindingSpec_Subject) + if !ok { + return false + } + return this.EqualVT(that) +} +func (this *RoleBindingSpec) EqualVT(that *RoleBindingSpec) bool { + if this == that { + return true + } else if this == nil || that == nil { + return false + } + if this.RoleRef != that.RoleRef { + return false + } + if len(this.Subjects) != len(that.Subjects) { + return false + } + for i, vx := range this.Subjects { + vy := that.Subjects[i] + if p, q := vx, vy; p != q { + if p == nil { + p = &RoleBindingSpec_Subject{} + } + if q == nil { + q = &RoleBindingSpec_Subject{} + } + if !p.EqualVT(q) { + return false + } + } + } + return string(this.unknownFields) == string(that.unknownFields) +} + +func (this *RoleBindingSpec) EqualMessageVT(thatMsg proto.Message) bool { + that, ok := thatMsg.(*RoleBindingSpec) + if !ok { + return false + } + return this.EqualVT(that) +} func (this *ServiceAccountStatusSpec_PgpPublicKey) EqualVT(that *ServiceAccountStatusSpec_PgpPublicKey) bool { if this == that { return true @@ -3243,7 +3501,7 @@ func (m *IdentityStatusSpec) MarshalToSizedBufferVT(dAtA []byte) (int, error) { return len(dAtA) - i, nil } -func (m *ServiceAccountStatusSpec_PgpPublicKey) MarshalVT() (dAtA []byte, err error) { +func (m *RoleSpec_Rule) MarshalVT() (dAtA []byte, err error) { if m == nil { return nil, nil } @@ -3256,12 +3514,12 @@ func (m *ServiceAccountStatusSpec_PgpPublicKey) MarshalVT() (dAtA []byte, err er return dAtA[:n], nil } -func (m *ServiceAccountStatusSpec_PgpPublicKey) MarshalToVT(dAtA []byte) (int, error) { +func (m *RoleSpec_Rule) MarshalToVT(dAtA []byte) (int, error) { size := m.SizeVT() return m.MarshalToSizedBufferVT(dAtA[:size]) } -func (m *ServiceAccountStatusSpec_PgpPublicKey) MarshalToSizedBufferVT(dAtA []byte) (int, error) { +func (m *RoleSpec_Rule) MarshalToSizedBufferVT(dAtA []byte) (int, error) { if m == nil { return 0, nil } @@ -3273,49 +3531,271 @@ func (m *ServiceAccountStatusSpec_PgpPublicKey) MarshalToSizedBufferVT(dAtA []by i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } - if m.LastUsed != nil { - size, err := (*timestamppb1.Timestamp)(m.LastUsed).MarshalToSizedBufferVT(dAtA[:i]) - if err != nil { - return 0, err + if len(m.KubernetesGroups) > 0 { + for iNdEx := len(m.KubernetesGroups) - 1; iNdEx >= 0; iNdEx-- { + i -= len(m.KubernetesGroups[iNdEx]) + copy(dAtA[i:], m.KubernetesGroups[iNdEx]) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.KubernetesGroups[iNdEx]))) + i-- + dAtA[i] = 0x22 } - i -= size - i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) - i-- - dAtA[i] = 0x2a } - if m.Created != nil { - size, err := (*timestamppb1.Timestamp)(m.Created).MarshalToSizedBufferVT(dAtA[:i]) - if err != nil { - return 0, err + if len(m.Clusters) > 0 { + for iNdEx := len(m.Clusters) - 1; iNdEx >= 0; iNdEx-- { + i -= len(m.Clusters[iNdEx]) + copy(dAtA[i:], m.Clusters[iNdEx]) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Clusters[iNdEx]))) + i-- + dAtA[i] = 0x1a } - i -= size - i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) - i-- - dAtA[i] = 0x22 } - if m.Expiration != nil { - size, err := (*timestamppb1.Timestamp)(m.Expiration).MarshalToSizedBufferVT(dAtA[:i]) - if err != nil { - return 0, err + if len(m.Verbs) > 0 { + for iNdEx := len(m.Verbs) - 1; iNdEx >= 0; iNdEx-- { + i -= len(m.Verbs[iNdEx]) + copy(dAtA[i:], m.Verbs[iNdEx]) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Verbs[iNdEx]))) + i-- + dAtA[i] = 0x12 } - i -= size - i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) - i-- - dAtA[i] = 0x1a } - if len(m.Armored) > 0 { - i -= len(m.Armored) - copy(dAtA[i:], m.Armored) - i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Armored))) - i-- - dAtA[i] = 0x12 - } - if len(m.Id) > 0 { - i -= len(m.Id) - copy(dAtA[i:], m.Id) - i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Id))) - i-- - dAtA[i] = 0xa + if len(m.Resources) > 0 { + for iNdEx := len(m.Resources) - 1; iNdEx >= 0; iNdEx-- { + i -= len(m.Resources[iNdEx]) + copy(dAtA[i:], m.Resources[iNdEx]) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Resources[iNdEx]))) + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + +func (m *RoleSpec) MarshalVT() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVT(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *RoleSpec) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *RoleSpec) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if len(m.Rules) > 0 { + for iNdEx := len(m.Rules) - 1; iNdEx >= 0; iNdEx-- { + size, err := m.Rules[iNdEx].MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + +func (m *RoleBindingSpec_Subject) MarshalVT() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVT(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *RoleBindingSpec_Subject) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *RoleBindingSpec_Subject) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if len(m.LabelSelectors) > 0 { + for iNdEx := len(m.LabelSelectors) - 1; iNdEx >= 0; iNdEx-- { + i -= len(m.LabelSelectors[iNdEx]) + copy(dAtA[i:], m.LabelSelectors[iNdEx]) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.LabelSelectors[iNdEx]))) + i-- + dAtA[i] = 0x1a + } + } + if len(m.Match) > 0 { + i -= len(m.Match) + copy(dAtA[i:], m.Match) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Match))) + i-- + dAtA[i] = 0x12 + } + if len(m.Name) > 0 { + i -= len(m.Name) + copy(dAtA[i:], m.Name) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Name))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *RoleBindingSpec) MarshalVT() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVT(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *RoleBindingSpec) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *RoleBindingSpec) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if len(m.Subjects) > 0 { + for iNdEx := len(m.Subjects) - 1; iNdEx >= 0; iNdEx-- { + size, err := m.Subjects[iNdEx].MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x12 + } + } + if len(m.RoleRef) > 0 { + i -= len(m.RoleRef) + copy(dAtA[i:], m.RoleRef) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.RoleRef))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *ServiceAccountStatusSpec_PgpPublicKey) MarshalVT() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVT(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *ServiceAccountStatusSpec_PgpPublicKey) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *ServiceAccountStatusSpec_PgpPublicKey) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if m.LastUsed != nil { + size, err := (*timestamppb1.Timestamp)(m.LastUsed).MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x2a + } + if m.Created != nil { + size, err := (*timestamppb1.Timestamp)(m.Created).MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x22 + } + if m.Expiration != nil { + size, err := (*timestamppb1.Timestamp)(m.Expiration).MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x1a + } + if len(m.Armored) > 0 { + i -= len(m.Armored) + copy(dAtA[i:], m.Armored) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Armored))) + i-- + dAtA[i] = 0x12 + } + if len(m.Id) > 0 { + i -= len(m.Id) + copy(dAtA[i:], m.Id) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Id))) + i-- + dAtA[i] = 0xa } return len(dAtA) - i, nil } @@ -4036,48 +4516,48 @@ func (m *IdentityStatusSpec) SizeVT() (n int) { return n } -func (m *ServiceAccountStatusSpec_PgpPublicKey) SizeVT() (n int) { +func (m *RoleSpec_Rule) SizeVT() (n int) { if m == nil { return 0 } var l int _ = l - l = len(m.Id) - if l > 0 { - n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } - l = len(m.Armored) - if l > 0 { - n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + if len(m.Resources) > 0 { + for _, s := range m.Resources { + l = len(s) + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } } - if m.Expiration != nil { - l = (*timestamppb1.Timestamp)(m.Expiration).SizeVT() - n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + if len(m.Verbs) > 0 { + for _, s := range m.Verbs { + l = len(s) + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } } - if m.Created != nil { - l = (*timestamppb1.Timestamp)(m.Created).SizeVT() - n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + if len(m.Clusters) > 0 { + for _, s := range m.Clusters { + l = len(s) + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } } - if m.LastUsed != nil { - l = (*timestamppb1.Timestamp)(m.LastUsed).SizeVT() - n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + if len(m.KubernetesGroups) > 0 { + for _, s := range m.KubernetesGroups { + l = len(s) + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } } n += len(m.unknownFields) return n } -func (m *ServiceAccountStatusSpec) SizeVT() (n int) { +func (m *RoleSpec) SizeVT() (n int) { if m == nil { return 0 } var l int _ = l - l = len(m.Role) - if l > 0 { - n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) - } - if len(m.PublicKeys) > 0 { - for _, e := range m.PublicKeys { + if len(m.Rules) > 0 { + for _, e := range m.Rules { l = e.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) } @@ -4086,13 +4566,107 @@ func (m *ServiceAccountStatusSpec) SizeVT() (n int) { return n } -func (m *EulaAcceptanceSpec) SizeVT() (n int) { +func (m *RoleBindingSpec_Subject) SizeVT() (n int) { if m == nil { return 0 } var l int _ = l - l = len(m.AcceptedByName) + l = len(m.Name) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + l = len(m.Match) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if len(m.LabelSelectors) > 0 { + for _, s := range m.LabelSelectors { + l = len(s) + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + } + n += len(m.unknownFields) + return n +} + +func (m *RoleBindingSpec) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.RoleRef) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if len(m.Subjects) > 0 { + for _, e := range m.Subjects { + l = e.SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + } + n += len(m.unknownFields) + return n +} + +func (m *ServiceAccountStatusSpec_PgpPublicKey) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Id) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + l = len(m.Armored) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.Expiration != nil { + l = (*timestamppb1.Timestamp)(m.Expiration).SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.Created != nil { + l = (*timestamppb1.Timestamp)(m.Created).SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.LastUsed != nil { + l = (*timestamppb1.Timestamp)(m.LastUsed).SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + n += len(m.unknownFields) + return n +} + +func (m *ServiceAccountStatusSpec) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Role) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if len(m.PublicKeys) > 0 { + for _, e := range m.PublicKeys { + l = e.SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + } + n += len(m.unknownFields) + return n +} + +func (m *EulaAcceptanceSpec) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.AcceptedByName) if l > 0 { n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) } @@ -8286,6 +8860,534 @@ func (m *IdentityStatusSpec) UnmarshalVT(dAtA []byte) error { } return nil } +func (m *RoleSpec_Rule) UnmarshalVT(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: RoleSpec_Rule: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: RoleSpec_Rule: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Resources", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Resources = append(m.Resources, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Verbs", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Verbs = append(m.Verbs, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Clusters", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Clusters = append(m.Clusters, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field KubernetesGroups", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.KubernetesGroups = append(m.KubernetesGroups, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *RoleSpec) UnmarshalVT(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: RoleSpec: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: RoleSpec: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Rules", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Rules = append(m.Rules, &RoleSpec_Rule{}) + if err := m.Rules[len(m.Rules)-1].UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *RoleBindingSpec_Subject) UnmarshalVT(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: RoleBindingSpec_Subject: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: RoleBindingSpec_Subject: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Name = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Match", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Match = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field LabelSelectors", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.LabelSelectors = append(m.LabelSelectors, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *RoleBindingSpec) UnmarshalVT(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: RoleBindingSpec: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: RoleBindingSpec: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field RoleRef", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.RoleRef = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Subjects", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Subjects = append(m.Subjects, &RoleBindingSpec_Subject{}) + if err := m.Subjects[len(m.Subjects)-1].UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *ServiceAccountStatusSpec_PgpPublicKey) UnmarshalVT(dAtA []byte) error { l := len(dAtA) iNdEx := 0 diff --git a/client/pkg/omni/resources/auth/auth.go b/client/pkg/omni/resources/auth/auth.go index adf3fef7b..631e6e221 100644 --- a/client/pkg/omni/resources/auth/auth.go +++ b/client/pkg/omni/resources/auth/auth.go @@ -20,4 +20,6 @@ func init() { registry.MustRegisterResource(SAMLAssertionType, &SAMLAssertion{}) registry.MustRegisterResource(SAMLLabelRuleType, &SAMLLabelRule{}) registry.MustRegisterResource(ServiceAccountStatusType, &ServiceAccountStatus{}) + registry.MustRegisterResource(RoleType, &Role{}) + registry.MustRegisterResource(RoleBindingType, &RoleBinding{}) } diff --git a/client/pkg/omni/resources/auth/role.go b/client/pkg/omni/resources/auth/role.go new file mode 100644 index 000000000..b9a992525 --- /dev/null +++ b/client/pkg/omni/resources/auth/role.go @@ -0,0 +1,49 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package auth + +import ( + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/resource/meta" + "github.com/cosi-project/runtime/pkg/resource/protobuf" + "github.com/cosi-project/runtime/pkg/resource/typed" + + "github.com/siderolabs/omni/client/api/omni/specs" + "github.com/siderolabs/omni/client/pkg/omni/resources" +) + +const ( + // RoleType is the type of Role resource. + // + // tsgen:RoleType + RoleType = resource.Type("Roles.omni.sidero.dev") +) + +// NewRole creates new Role resource. +func NewRole(id string) *Role { + return typed.NewResource[RoleSpec, RoleExtension]( + resource.NewMetadata(resources.DefaultNamespace, RoleType, id, resource.VersionUndefined), + protobuf.NewResourceSpec(&specs.RoleSpec{}), + ) +} + +// Role resource describes an RBAC role. +type Role = typed.Resource[RoleSpec, RoleExtension] + +// RoleSpec wraps specs.RoleSpec. +type RoleSpec = protobuf.ResourceSpec[specs.RoleSpec, *specs.RoleSpec] + +// RoleExtension provides auxiliary methods for Role resource. +type RoleExtension struct{} + +// ResourceDefinition implements [typed.Extension] interface. +func (RoleExtension) ResourceDefinition() meta.ResourceDefinitionSpec { + return meta.ResourceDefinitionSpec{ + Type: RoleType, + Aliases: []resource.Type{}, + DefaultNamespace: resources.DefaultNamespace, + PrintColumns: []meta.PrintColumn{}, + } +} diff --git a/client/pkg/omni/resources/auth/role_binding.go b/client/pkg/omni/resources/auth/role_binding.go new file mode 100644 index 000000000..fc1d6aeda --- /dev/null +++ b/client/pkg/omni/resources/auth/role_binding.go @@ -0,0 +1,49 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package auth + +import ( + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/resource/meta" + "github.com/cosi-project/runtime/pkg/resource/protobuf" + "github.com/cosi-project/runtime/pkg/resource/typed" + + "github.com/siderolabs/omni/client/api/omni/specs" + "github.com/siderolabs/omni/client/pkg/omni/resources" +) + +const ( + // RoleBindingType is the type of RoleBinding resource. + // + // tsgen:RoleBindingType + RoleBindingType = resource.Type("RoleBindings.omni.sidero.dev") +) + +// NewRoleBinding creates new RoleBinding resource. +func NewRoleBinding(id string) *RoleBinding { + return typed.NewResource[RoleBindingSpec, RoleBindingExtension]( + resource.NewMetadata(resources.DefaultNamespace, RoleBindingType, id, resource.VersionUndefined), + protobuf.NewResourceSpec(&specs.RoleBindingSpec{}), + ) +} + +// RoleBinding resource describes an RBAC role binding. +type RoleBinding = typed.Resource[RoleBindingSpec, RoleBindingExtension] + +// RoleBindingSpec wraps specs.RoleBindingSpec. +type RoleBindingSpec = protobuf.ResourceSpec[specs.RoleBindingSpec, *specs.RoleBindingSpec] + +// RoleBindingExtension provides auxiliary methods for RoleBinding resource. +type RoleBindingExtension struct{} + +// ResourceDefinition implements [typed.Extension] interface. +func (RoleBindingExtension) ResourceDefinition() meta.ResourceDefinitionSpec { + return meta.ResourceDefinitionSpec{ + Type: RoleBindingType, + Aliases: []resource.Type{}, + DefaultNamespace: resources.DefaultNamespace, + PrintColumns: []meta.PrintColumn{}, + } +} diff --git a/cmd/omni/cmd/cmd.go b/cmd/omni/cmd/cmd.go index 7db88e0d8..7d6786a7a 100644 --- a/cmd/omni/cmd/cmd.go +++ b/cmd/omni/cmd/cmd.go @@ -77,6 +77,9 @@ func buildRootCommand() (*cobra.Command, error) { loggerConfig.Level.SetLevel(zap.DebugLevel) } + // TODO(Utku): for debugging, revert + loggerConfig.Level.SetLevel(zap.ErrorLevel) + logger, err := loggerConfig.Build( zap.AddStacktrace(zapcore.FatalLevel), // only print stack traces for fatal errors ) diff --git a/cmd/omni/pkg/app/app.go b/cmd/omni/pkg/app/app.go index c88f1ac8f..04e7168cc 100644 --- a/cmd/omni/pkg/app/app.go +++ b/cmd/omni/pkg/app/app.go @@ -35,6 +35,7 @@ import ( "github.com/siderolabs/omni/internal/pkg/auth" "github.com/siderolabs/omni/internal/pkg/auth/actor" "github.com/siderolabs/omni/internal/pkg/auth/user" + "github.com/siderolabs/omni/internal/pkg/rbac" "github.com/siderolabs/omni/internal/pkg/config" "github.com/siderolabs/omni/internal/pkg/ctxstore" "github.com/siderolabs/omni/internal/pkg/eula" @@ -109,6 +110,10 @@ func Run(ctx context.Context, state *omni.State, cfg *config.Params, logger *zap return fmt.Errorf("failed to write initial user resources to state: %w", err) } + if err = rbac.EnsureBuiltinRoles(ctx, state.Default(), logger); err != nil { + return fmt.Errorf("failed to create built-in RBAC roles: %w", err) + } + if cfg.EulaAccept.GetName() != "" && cfg.EulaAccept.GetEmail() != "" { if err = eula.Accept(ctx, state.Default(), eula.AcceptParams{Name: cfg.EulaAccept.GetName(), Email: cfg.EulaAccept.GetEmail()}); err != nil { return fmt.Errorf("failed to accept EULA: %w", err) diff --git a/frontend/src/api/omni/specs/auth.pb.ts b/frontend/src/api/omni/specs/auth.pb.ts index f8ea51adf..25a27f2bd 100644 --- a/frontend/src/api/omni/specs/auth.pb.ts +++ b/frontend/src/api/omni/specs/auth.pb.ts @@ -169,6 +169,28 @@ export type IdentityStatusSpec = { last_active?: string } +export type RoleSpecRule = { + resources?: string[] + verbs?: string[] + clusters?: string[] + kubernetes_groups?: string[] +} + +export type RoleSpec = { + rules?: RoleSpecRule[] +} + +export type RoleBindingSpecSubject = { + name?: string + match?: string + label_selectors?: string[] +} + +export type RoleBindingSpec = { + role_ref?: string + subjects?: RoleBindingSpecSubject[] +} + export type ServiceAccountStatusSpecPgpPublicKey = { id?: string armored?: string diff --git a/frontend/src/api/resources.ts b/frontend/src/api/resources.ts index 5dbb78a28..c351b308c 100644 --- a/frontend/src/api/resources.ts +++ b/frontend/src/api/resources.ts @@ -241,6 +241,8 @@ export const LabelIdentityUserID = "user-id"; export const LabelIdentityTypeServiceAccount = "type-service-account"; export const PublicKeyType = "PublicKeys.omni.sidero.dev"; export const PublicKeyLastActiveType = "PublicKeyLastActives.omni.sidero.dev"; +export const RoleType = "Roles.omni.sidero.dev"; +export const RoleBindingType = "RoleBindings.omni.sidero.dev"; export const SAMLLabelRuleType = "SAMLLabelRules.omni.sidero.dev"; export const ServiceAccountStatusType = "ServiceAccountStatuses.omni.sidero.dev"; export const UserType = "Users.omni.sidero.dev"; diff --git a/internal/backend/runtime/omni/controllers/omni/auth/rbac_binding.go b/internal/backend/runtime/omni/controllers/omni/auth/rbac_binding.go new file mode 100644 index 000000000..b260fc94a --- /dev/null +++ b/internal/backend/runtime/omni/controllers/omni/auth/rbac_binding.go @@ -0,0 +1,344 @@ +// Copyright (c) 2026 Sidero Labs, Inc. +// +// Use of this software is governed by the Business Source License +// included in the LICENSE file. + +package auth + +import ( + "context" + "crypto/sha256" + "fmt" + "log" + "path/filepath" + "strings" + + "github.com/cosi-project/runtime/pkg/controller" + "github.com/cosi-project/runtime/pkg/controller/generic/qtransform" + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/safe" + "github.com/cosi-project/runtime/pkg/state" + "github.com/siderolabs/gen/xerrors" + "go.uber.org/zap" + + "github.com/siderolabs/omni/client/api/omni/specs" + authres "github.com/siderolabs/omni/client/pkg/omni/resources/auth" + "github.com/siderolabs/omni/internal/pkg/rbac" +) + +// RBACBindingControllerName is the name of the controller. +const RBACBindingControllerName = "RBACBindingController" + +// RBACBindingController creates RoleBindings for each Identity based on its User's role field, +// and translates AccessPolicy rules into Roles and RoleBindings. +// Primary input is Identity (ID = email), so binding IDs are human-readable. +type RBACBindingController = qtransform.QController[*authres.Identity, *authres.RoleBinding] + +// NewRBACBindingController creates a new RBACBindingController. +func NewRBACBindingController() *RBACBindingController { + return qtransform.NewQController( + qtransform.Settings[*authres.Identity, *authres.RoleBinding]{ + Name: RBACBindingControllerName, + MapMetadataFunc: func(identity *authres.Identity) *authres.RoleBinding { + return authres.NewRoleBinding("user-role:" + identity.Metadata().ID()) + }, + UnmapMetadataFunc: func(binding *authres.RoleBinding) *authres.Identity { + return authres.NewIdentity(strings.TrimPrefix(binding.Metadata().ID(), "user-role:")) + }, + TransformExtraOutputFunc: func(ctx context.Context, r controller.ReaderWriter, logger *zap.Logger, identity *authres.Identity, binding *authres.RoleBinding) error { + identityID := identity.Metadata().ID() // email + userID := identity.TypedSpec().Value.GetUserId() + + if userID == "" { + log.Printf("[RBAC] controller: identity=%s has no user ID, skipping", identityID) + + return xerrors.NewTagged[qtransform.SkipReconcileTag](fmt.Errorf("identity %q has no user ID", identityID)) + } + + // Read the User to get the legacy role. + user, err := safe.ReaderGetByID[*authres.User](ctx, r, userID) + if err != nil { + if state.IsNotFoundError(err) { + log.Printf("[RBAC] controller: identity=%s user=%s not found, skipping", identityID, userID) + + return xerrors.NewTagged[qtransform.SkipReconcileTag](err) + } + + return fmt.Errorf("failed to get user %q: %w", userID, err) + } + + legacyRole := user.TypedSpec().Value.GetRole() + + log.Printf("[RBAC] controller: reconciling identity=%s user=%s legacyRole=%s", identityID, userID, legacyRole) + + builtinRoleID := mapLegacyRole(legacyRole) + if builtinRoleID == "" { + log.Printf("[RBAC] controller: identity=%s has no mappable role (legacy=%s), skipping", identityID, legacyRole) + + return xerrors.NewTagged[qtransform.SkipReconcileTag](fmt.Errorf("user %q has no role", userID)) + } + + binding.TypedSpec().Value.RoleRef = builtinRoleID + binding.TypedSpec().Value.Subjects = []*specs.RoleBindingSpec_Subject{ + {Name: identityID}, + } + + log.Printf("[RBAC] controller: identity=%s -> RoleBinding id=%s roleRef=%s", identityID, binding.Metadata().ID(), builtinRoleID) + + // Process AccessPolicy. + if err := reconcileACLBindings(ctx, r, logger, user); err != nil { + log.Printf("[RBAC] controller: identity=%s ACL reconciliation error: %v", identityID, err) + } + + return nil + }, + FinalizerRemovalExtraOutputFunc: func(ctx context.Context, r controller.ReaderWriter, _ *zap.Logger, identity *authres.Identity) error { + userID := identity.TypedSpec().Value.GetUserId() + + log.Printf("[RBAC] controller: identity=%s deleted, cleaning up ACL bindings for user=%s", identity.Metadata().ID(), userID) + + return cleanupACLBindings(ctx, r, userID) + }, + }, + // When User changes (role update), re-reconcile its identities. + qtransform.WithExtraMappedInput[*authres.User]( + func(ctx context.Context, _ *zap.Logger, r controller.QRuntime, user controller.ReducedResourceMetadata) ([]resource.Pointer, error) { + identities, err := safe.ReaderListAll[*authres.Identity](ctx, r, state.WithLabelQuery( + resource.LabelEqual(authres.LabelIdentityUserID, user.ID()), + )) + if err != nil { + return nil, err + } + + var pointers []resource.Pointer + + for id := range identities.All() { + pointers = append(pointers, id.Metadata()) + } + + log.Printf("[RBAC] controller: User %s changed, re-reconciling %d identities", user.ID(), len(pointers)) + + return pointers, nil + }, + ), + // When AccessPolicy changes, re-reconcile all identities. + qtransform.WithExtraMappedInput[*authres.AccessPolicy]( + func(ctx context.Context, _ *zap.Logger, r controller.QRuntime, ap controller.ReducedResourceMetadata) ([]resource.Pointer, error) { + log.Printf("[RBAC] controller: AccessPolicy changed (id=%s), re-reconciling all identities", ap.ID()) + + identities, err := safe.ReaderListAll[*authres.Identity](ctx, r) + if err != nil { + return nil, err + } + + var pointers []resource.Pointer + + for id := range identities.All() { + pointers = append(pointers, id.Metadata()) + } + + log.Printf("[RBAC] controller: will re-reconcile %d identities due to AccessPolicy change", len(pointers)) + + return pointers, nil + }, + ), + qtransform.WithExtraOutputs( + controller.Output{ + Type: authres.RoleType, + Kind: controller.OutputShared, + }, + ), + ) +} + +func mapLegacyRole(legacyRole string) string { + switch strings.ToLower(legacyRole) { + case "admin": + return rbac.RoleIDAdmin + case "operator": + return rbac.RoleIDOperator + case "reader": + return rbac.RoleIDReader + case "none", "": + return "" + default: + return "" + } +} + +func reconcileACLBindings(ctx context.Context, r controller.ReaderWriter, logger *zap.Logger, user *authres.User) error { + userID := user.Metadata().ID() + + accessPolicy, err := safe.ReaderGetByID[*authres.AccessPolicy](ctx, r, authres.AccessPolicyID) + if err != nil { + if state.IsNotFoundError(err) { + log.Printf("[RBAC] controller: user=%s no AccessPolicy found, cleaning up ACL bindings", userID) + + return cleanupACLBindings(ctx, r, userID) + } + + return fmt.Errorf("failed to get access policy: %w", err) + } + + spec := accessPolicy.TypedSpec().Value + ruleCount := len(spec.GetRules()) + + log.Printf("[RBAC] controller: user=%s processing AccessPolicy with %d rules", userID, ruleCount) + + keepBindings := map[string]struct{}{} + + for i, rule := range spec.GetRules() { + matches := aclRuleMatchesUser(spec, rule, userID) + + log.Printf("[RBAC] controller: user=%s ACL rule[%d] users=%v clusters=%v role=%s -> matches=%v", + userID, i, rule.GetUsers(), rule.GetClusters(), rule.GetRole(), matches) + + if !matches { + continue + } + + roleID := aclRoleID(i, rule) + bindingID := aclBindingID(userID, roleID) + + keepBindings[bindingID] = struct{}{} + + if err := ensureACLRole(ctx, r, roleID, rule); err != nil { + return fmt.Errorf("failed to ensure ACL role %q: %w", roleID, err) + } + + log.Printf("[RBAC] controller: user=%s created/updated ACL role=%s", userID, roleID) + + if err := safe.WriterModify(ctx, r, authres.NewRoleBinding(bindingID), func(rb *authres.RoleBinding) error { + rb.TypedSpec().Value.RoleRef = roleID + rb.TypedSpec().Value.Subjects = []*specs.RoleBindingSpec_Subject{ + {Name: userID}, + } + + rb.Metadata().Labels().Set("omni.sidero.dev/source", "access-policy") + rb.Metadata().Labels().Set("omni.sidero.dev/user", userID) + + return nil + }); err != nil { + return fmt.Errorf("failed to ensure ACL binding %q: %w", bindingID, err) + } + + log.Printf("[RBAC] controller: user=%s created/updated ACL binding=%s -> role=%s", userID, bindingID, roleID) + } + + return cleanupStaleACLBindings(ctx, r, userID, keepBindings) +} + +func ensureACLRole(ctx context.Context, r controller.ReaderWriter, roleID string, rule *specs.AccessPolicyRule) error { + return safe.WriterModify(ctx, r, authres.NewRole(roleID), func(role *authres.Role) error { + rbacRules := translateACLRule(rule) + role.TypedSpec().Value.Rules = rbacRules + role.Metadata().Labels().Set("omni.sidero.dev/source", "access-policy") + + return nil + }) +} + +func translateACLRule(rule *specs.AccessPolicyRule) []*specs.RoleSpec_Rule { + verbs := []string{"get", "list", "watch"} + + switch strings.ToLower(rule.GetRole()) { + case "operator": + verbs = []string{"get", "list", "watch", "create", "update", "destroy"} + case "admin": + verbs = []string{"*"} + } + + clusters := rule.GetClusters() + + rbacRule := &specs.RoleSpec_Rule{ + Resources: []string{rbac.FamilyClusters}, + Verbs: verbs, + Clusters: clusters, + KubernetesGroups: rule.GetKubernetes().GetImpersonate().GetGroups(), + } + + switch strings.ToLower(rule.GetRole()) { + case "operator", "admin": + rbacRule.KubernetesGroups = append(rbacRule.KubernetesGroups, "system:masters") + } + + log.Printf("[RBAC] controller: translated ACL rule -> resources=%v verbs=%v clusters=%v k8sGroups=%v", + rbacRule.GetResources(), rbacRule.GetVerbs(), rbacRule.GetClusters(), rbacRule.GetKubernetesGroups()) + + return []*specs.RoleSpec_Rule{rbacRule} +} + +func cleanupACLBindings(ctx context.Context, r controller.ReaderWriter, userID string) error { + return cleanupStaleACLBindings(ctx, r, userID, nil) +} + +func cleanupStaleACLBindings(ctx context.Context, r controller.ReaderWriter, userID string, keep map[string]struct{}) error { + bindings, err := safe.ReaderListAll[*authres.RoleBinding](ctx, r, state.WithLabelQuery( + resource.LabelEqual("omni.sidero.dev/source", "access-policy"), + resource.LabelEqual("omni.sidero.dev/user", userID), + )) + if err != nil { + return err + } + + for binding := range bindings.All() { + if binding.Metadata().Owner() != RBACBindingControllerName { + continue + } + + if _, ok := keep[binding.Metadata().ID()]; ok { + continue + } + + log.Printf("[RBAC] controller: user=%s destroying stale ACL binding=%s", userID, binding.Metadata().ID()) + + if err := r.Destroy(ctx, binding.Metadata()); err != nil && !state.IsNotFoundError(err) { + return fmt.Errorf("failed to destroy stale ACL binding %q: %w", binding.Metadata().ID(), err) + } + } + + return nil +} + +func aclRuleMatchesUser(spec *specs.AccessPolicySpec, rule *specs.AccessPolicyRule, userID string) bool { + for _, userRef := range rule.GetUsers() { + if strings.HasPrefix(userRef, "group/") { + groupName := strings.TrimPrefix(userRef, "group/") + + group, ok := spec.GetUserGroups()[groupName] + if !ok { + continue + } + + for _, u := range group.GetUsers() { + if u.GetName() != "" && u.GetName() == userID { + return true + } + + if pattern := u.GetMatch(); pattern != "" { + if matched, _ := filepath.Match(pattern, userID); matched { + return true + } + } + } + } else if userRef == userID { + return true + } + } + + return false +} + +func aclRoleID(index int, rule *specs.AccessPolicyRule) string { + h := sha256.New() + fmt.Fprintf(h, "%d:%s:%v", index, rule.GetRole(), rule.GetClusters()) + + return fmt.Sprintf("acl-%x", h.Sum(nil)[:8]) +} + +func aclBindingID(userID, roleID string) string { + h := sha256.New() + fmt.Fprintf(h, "%s:%s", userID, roleID) + + return fmt.Sprintf("acl-binding-%x", h.Sum(nil)[:8]) +} diff --git a/internal/backend/runtime/omni/omni.go b/internal/backend/runtime/omni/omni.go index 08de41945..2b9b49425 100644 --- a/internal/backend/runtime/omni/omni.go +++ b/internal/backend/runtime/omni/omni.go @@ -264,6 +264,7 @@ func NewRuntime(cfg *config.Params, talosClientFactory *talos.ClientFactory, dns omnictrl.NewDiscoveryAffiliateDeleteTaskController(clockwork.NewRealClock(), discoveryClientCache), omnictrl.NewServiceAccountStatusController(), authctrl.NewIdentityStatusController(), + authctrl.NewRBACBindingController(), omnictrl.NewInfraMachineRegistrationController(), omnictrl.NewSiderolinkAPIConfigController(cfg.Services.MachineAPI.URL(), cfg.Services.Siderolink), omnictrl.NewProviderJoinConfigController(), @@ -358,7 +359,7 @@ func NewRuntime(cfg *config.Params, talosClientFactory *talos.ClientFactory, dns clusterValidationOptions(defaultState, cfg.EtcdBackup, cfg.Services.EmbeddedDiscoveryService), relationLabelsValidationOptions(), accessPolicyValidationOptions(), - authorizationValidationOptions(defaultState), + authorizationValidationOptions(defaultState, logger), roleValidationOptions(), machineSetNodeValidationOptions(defaultState), machineSetValidationOptions(defaultState, storeFactory), diff --git a/internal/backend/runtime/omni/state_access.go b/internal/backend/runtime/omni/state_access.go index 60ca3c22b..ee2f2fbd7 100644 --- a/internal/backend/runtime/omni/state_access.go +++ b/internal/backend/runtime/omni/state_access.go @@ -13,6 +13,7 @@ import ( "github.com/cosi-project/runtime/pkg/resource/meta" "github.com/cosi-project/runtime/pkg/state" "github.com/siderolabs/gen/xslices" + "go.uber.org/zap" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -24,6 +25,7 @@ import ( "github.com/siderolabs/omni/client/pkg/omni/resources/system" "github.com/siderolabs/omni/client/pkg/omni/resources/virtual" "github.com/siderolabs/omni/internal/backend/runtime/omni/infraprovider" + "github.com/siderolabs/omni/internal/pkg/rbac" "github.com/siderolabs/omni/internal/backend/runtime/omni/validated" "github.com/siderolabs/omni/internal/pkg/auth" "github.com/siderolabs/omni/internal/pkg/auth/accesspolicy" @@ -117,7 +119,10 @@ var ( // authorizationValidationOptions returns the validation options responsible for all authorization checks. // // These include the regular checks on the user's Omni-wide role, as well as the ACLs that can authorize the user to perform additional actions on specific clusters. -func authorizationValidationOptions(st state.State) []validated.StateOption { +func authorizationValidationOptions(st state.State, logger *zap.Logger) []validated.StateOption { + // Initialize RBAC engine hooks. + rbac.IsClusterScoped = isClusterRelatedType + return []validated.StateOption{ validated.WithListValidations( func(ctx context.Context, kind resource.Kind, opt ...state.ListOption) error { @@ -128,11 +133,11 @@ func authorizationValidationOptions(st state.State) []validated.StateOption { } if len(opts.LabelQueries) == 0 { - return checkForKindAccess(ctx, st, state.List, kind, nil) + return checkForKindAccess(ctx, st, state.List, kind, nil, logger) } for _, query := range opts.LabelQueries { - if err := checkForKindAccess(ctx, st, state.List, kind, query.Terms); err != nil { + if err := checkForKindAccess(ctx, st, state.List, kind, query.Terms, logger); err != nil { return err } } @@ -149,11 +154,11 @@ func authorizationValidationOptions(st state.State) []validated.StateOption { } if len(opts.LabelQueries) == 0 { - return checkForKindAccess(ctx, st, state.Watch, kind, nil) + return checkForKindAccess(ctx, st, state.Watch, kind, nil, logger) } for _, query := range opts.LabelQueries { - if err := checkForKindAccess(ctx, st, state.Watch, kind, query.Terms); err != nil { + if err := checkForKindAccess(ctx, st, state.Watch, kind, query.Terms, logger); err != nil { return err } } @@ -172,7 +177,7 @@ func authorizationValidationOptions(st state.State) []validated.StateOption { ResourceType: ptr.Type(), ResourceID: ptr.ID(), Verb: state.Get, - }, clusterID, false) + }, clusterID, false, logger) } clusterID := clusterIDFromMetadata(res.Metadata()) @@ -182,7 +187,7 @@ func authorizationValidationOptions(st state.State) []validated.StateOption { ResourceType: res.Metadata().Type(), ResourceID: res.Metadata().ID(), Verb: state.Get, - }, clusterID, false) + }, clusterID, false, logger) }, ), validated.WithWatchValidations( @@ -197,7 +202,7 @@ func authorizationValidationOptions(st state.State) []validated.StateOption { ResourceType: ptr.Type(), ResourceID: ptr.ID(), Verb: state.Watch, - }, clusterID, false) + }, clusterID, false, logger) }, ), validated.WithCreateValidations( @@ -209,7 +214,7 @@ func authorizationValidationOptions(st state.State) []validated.StateOption { ResourceType: res.Metadata().Type(), ResourceID: res.Metadata().ID(), Verb: state.Create, - }, clusterID, false) + }, clusterID, false, logger) }, ), validated.WithUpdateValidations( @@ -223,7 +228,7 @@ func authorizationValidationOptions(st state.State) []validated.StateOption { ResourceType: newRes.Metadata().Type(), ResourceID: newRes.Metadata().ID(), Verb: state.Update, - }, newClusterID, false) + }, newClusterID, false, logger) } existingClusterID := clusterIDFromMetadata(existingRes.Metadata()) @@ -233,7 +238,7 @@ func authorizationValidationOptions(st state.State) []validated.StateOption { ResourceType: existingRes.Metadata().Type(), ResourceID: existingRes.Metadata().ID(), Verb: state.Update, - }, existingClusterID, false); err != nil { + }, existingClusterID, false, logger); err != nil { return err } @@ -249,7 +254,7 @@ func authorizationValidationOptions(st state.State) []validated.StateOption { ResourceType: newRes.Metadata().Type(), ResourceID: newRes.Metadata().ID(), Verb: state.Update, - }, newClusterID, false) + }, newClusterID, false, logger) }, ), validated.WithDestroyValidations( @@ -263,7 +268,7 @@ func authorizationValidationOptions(st state.State) []validated.StateOption { ResourceType: ptr.Type(), ResourceID: ptr.ID(), Verb: state.Destroy, - }, clusterID, false) + }, clusterID, false, logger) } clusterID := clusterIDFromMetadata(res.Metadata()) @@ -273,13 +278,32 @@ func authorizationValidationOptions(st state.State) []validated.StateOption { ResourceType: res.Metadata().Type(), ResourceID: res.Metadata().ID(), Verb: state.Destroy, - }, clusterID, false) + }, clusterID, false, logger) }, ), } } -func checkForRole(ctx context.Context, st state.State, access state.Access, clusterID resource.ID, requireAll bool) error { +func checkForRole(ctx context.Context, st state.State, access state.Access, clusterID resource.ID, requireAll bool, logger *zap.Logger) error { + // Infra provider resources have their own authz path (infraprovider.State). + if infraprovider.IsInfraProviderResource(access.ResourceNamespace, access.ResourceType) { + return nil + } + + // When requireAll is true (List/WatchKind without cluster filter), pass empty clusterID. + // RBAC handles this: only global rules (no clusters field) match when clusterID is empty. + effectiveClusterID := clusterID + if requireAll { + effectiveClusterID = "" + } + + return rbac.Check(ctx, st, access.ResourceType, access.Verb, effectiveClusterID, logger) +} + +// checkForRoleLegacy is the original implementation, kept for reference during RBAC v2 development. +// +//nolint:unused +func checkForRoleLegacy(ctx context.Context, st state.State, access state.Access, clusterID resource.ID, requireAll bool) error { if actor.ContextIsInternalActor(ctx) { return nil } @@ -295,7 +319,6 @@ func checkForRole(ctx context.Context, st state.State, access state.Access, clus } if clusterRole != role.None && (!requireAll || matchesAll) { - // override the role in the context with the computed role for this cluster ctx = ctxstore.WithValue(ctx, auth.RoleContextKey{Role: clusterRole}) } } @@ -303,7 +326,7 @@ func checkForRole(ctx context.Context, st state.State, access state.Access, clus return filterAccess(ctx, access) } -func checkForKindAccess(ctx context.Context, st state.State, verb state.Verb, kind resource.Kind, labelTerms []resource.LabelTerm) error { +func checkForKindAccess(ctx context.Context, st state.State, verb state.Verb, kind resource.Kind, labelTerms []resource.LabelTerm, logger *zap.Logger) error { clusterID := "" requireAll := false @@ -317,7 +340,7 @@ func checkForKindAccess(ctx context.Context, st state.State, verb state.Verb, ki ResourceNamespace: kind.Namespace(), ResourceType: kind.Type(), Verb: verb, - }, clusterID, requireAll) + }, clusterID, requireAll, logger) } func isClusterRelatedType(typ resource.Type) bool { diff --git a/internal/pkg/rbac/builtin.go b/internal/pkg/rbac/builtin.go new file mode 100644 index 000000000..16d428bad --- /dev/null +++ b/internal/pkg/rbac/builtin.go @@ -0,0 +1,112 @@ +// Copyright (c) 2026 Sidero Labs, Inc. +// +// Use of this software is governed by the Business Source License +// included in the LICENSE file. + +package rbac + +import ( + "context" + "fmt" + "log" + + "github.com/cosi-project/runtime/pkg/state" + "go.uber.org/zap" + + "github.com/siderolabs/omni/client/api/omni/specs" + authres "github.com/siderolabs/omni/client/pkg/omni/resources/auth" +) + +// BuiltinOwner is the owner string for built-in RBAC resources. +// Using a non-empty owner makes these resources impossible to delete or modify via the user-facing API, +// because COSI enforces that only the owner can modify/destroy a resource. +// No controller uses this owner string, so nothing will ever tear them down. +const BuiltinOwner = "rbac-builtin" + +// Built-in role IDs. +const ( + RoleIDNone = "none" + RoleIDReader = "reader" + RoleIDOperator = "operator" + RoleIDAdmin = "admin" +) + +// builtinRoles defines the 4 built-in roles that map to the legacy role hierarchy. +var builtinRoles = []struct { + id string + rules []*specs.RoleSpec_Rule +}{ + { + id: RoleIDNone, + rules: nil, + }, + { + id: RoleIDReader, + rules: []*specs.RoleSpec_Rule{ + { + Resources: []string{FamilyClusters, FamilyMachines}, + Verbs: []string{"get", "list", "watch"}, + }, + }, + }, + { + id: RoleIDOperator, + rules: []*specs.RoleSpec_Rule{ + { + Resources: []string{FamilyClusters, FamilyMachines}, + Verbs: []string{"get", "list", "watch", "create", "update", "destroy"}, + KubernetesGroups: []string{"system:masters"}, + }, + }, + }, + { + id: RoleIDAdmin, + rules: []*specs.RoleSpec_Rule{ + { + Resources: []string{"*"}, + Verbs: []string{"*"}, + KubernetesGroups: []string{"system:masters"}, + }, + }, + }, +} + +// EnsureBuiltinRoles creates the built-in Role resources if they don't exist. +// They are created with a special owner so that users cannot delete or modify them. +func EnsureBuiltinRoles(ctx context.Context, st state.State, logger *zap.Logger) error { + log.Printf("[RBAC] ensuring built-in roles exist...") + + for _, def := range builtinRoles { + role := authres.NewRole(def.id) + role.TypedSpec().Value.Rules = def.rules + role.Metadata().Labels().Set("omni.sidero.dev/built-in", "true") + + existing, err := st.Get(ctx, role.Metadata()) + if err != nil && !state.IsNotFoundError(err) { + return fmt.Errorf("failed to check built-in role %q: %w", def.id, err) + } + + if existing != nil { + log.Printf("[RBAC] built-in role %q already exists, skipping", def.id) + + continue + } + + if err := st.Create(ctx, role, state.WithCreateOwner(BuiltinOwner)); err != nil { + return fmt.Errorf("failed to create built-in role %q: %w", def.id, err) + } + + ruleCount := len(def.rules) + if ruleCount > 0 { + r := def.rules[0] + log.Printf("[RBAC] created built-in role %q with %d rule(s), first rule: resources=%v verbs=%v clusters=%v k8sGroups=%v", + def.id, ruleCount, r.GetResources(), r.GetVerbs(), r.GetClusters(), r.GetKubernetesGroups()) + } else { + log.Printf("[RBAC] created built-in role %q with 0 rules (no permissions)", def.id) + } + } + + log.Printf("[RBAC] built-in roles ensured") + + return nil +} diff --git a/internal/pkg/rbac/engine.go b/internal/pkg/rbac/engine.go new file mode 100644 index 000000000..d0af69b55 --- /dev/null +++ b/internal/pkg/rbac/engine.go @@ -0,0 +1,303 @@ +// Copyright (c) 2026 Sidero Labs, Inc. +// +// Use of this software is governed by the Business Source License +// included in the LICENSE file. + +package rbac + +import ( + "context" + "log" + "path/filepath" + "slices" + + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/safe" + "github.com/cosi-project/runtime/pkg/state" + "github.com/siderolabs/omni/internal/pkg/ctxstore" + "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/siderolabs/omni/client/api/omni/specs" + authres "github.com/siderolabs/omni/client/pkg/omni/resources/auth" + pkgauth "github.com/siderolabs/omni/internal/pkg/auth" + "github.com/siderolabs/omni/internal/pkg/auth/actor" +) + +// Check evaluates RBAC for the given resource access. +func Check(ctx context.Context, st state.State, resourceType resource.Type, verb state.Verb, clusterID resource.ID, logger *zap.Logger) error { + // 1. Internal actor bypass. + if actor.ContextIsInternalActor(ctx) { + return nil + } + + // 2. AuthConfig is public (no auth required). + if resourceType == authres.AuthConfigType { + return nil + } + + // 3. Look up type in family registry. + config, ok := Registry[resourceType] + if !ok { + log.Printf("[RBAC] DENIED type=%s not in registry", resourceType) + + return status.Errorf(codes.PermissionDenied, "no access is permitted on resource type %q", resourceType) + } + + // 4. System family: only requires authentication. + if config.Family == FamilySystem { + err := checkAuthenticated(ctx) + if err != nil { + log.Printf("[RBAC] DENIED type=%s family=system reason=unauthenticated", resourceType) + } + + return err + } + + // 5. Check if this COSI verb is allowed on this resource type at all (system invariant). + verbStr := verbToString(verb) + + if _, allowed := config.AllowedVerbs[verb]; !allowed { + log.Printf("[RBAC] DENIED type=%s family=%s verb=%s reason=verb-not-allowed", resourceType, config.Family, verbStr) + + return status.Errorf(codes.PermissionDenied, "%s access is not permitted on resource type %q", verbStr, resourceType) + } + + // 6. Get user identity from context. + identity, ok := getIdentity(ctx) + if !ok { + log.Printf("[RBAC] DENIED type=%s family=%s verb=%s reason=no-identity", resourceType, config.Family, verbStr) + + return status.Error(codes.Unauthenticated, "no identity in context") + } + + // 8. Collect matching RoleBindings. + internalCtx := actor.MarkContextAsInternalActor(ctx) + + bindings, err := safe.StateListAll[*authres.RoleBinding](internalCtx, st) + if err != nil { + log.Printf("[RBAC] ERROR identity=%s listing bindings: %v", identity, err) + + return status.Errorf(codes.Internal, "failed to list role bindings: %v", err) + } + + bindingCount := 0 + for iter := bindings.Iterator(); iter.Next(); { + bindingCount++ + } + + log.Printf("[RBAC] CHECK identity=%s type=%s family=%s verb=%s cluster=%s bindings_total=%d", + identity, resourceType, config.Family, verbStr, clusterID, bindingCount) + + // Get the identity resource for label matching. + identityRes, identityErr := safe.StateGetByID[*authres.Identity](internalCtx, st, identity) + + var matchedRule *matchResult + + // Re-iterate bindings (iterator was consumed above). + bindings, _ = safe.StateListAll[*authres.RoleBinding](internalCtx, st) + + for iter := bindings.Iterator(); iter.Next(); { + binding := iter.Value() + + if !subjectMatches(binding.TypedSpec().Value, identity, identityRes, identityErr) { + continue + } + + roleID := binding.TypedSpec().Value.GetRoleRef() + if roleID == "" { + log.Printf("[RBAC] binding=%s matched but has empty roleRef, skipping", binding.Metadata().ID()) + + continue + } + + log.Printf("[RBAC] binding=%s matched, roleRef=%s", binding.Metadata().ID(), roleID) + + roleRes, roleErr := safe.StateGetByID[*authres.Role](internalCtx, st, roleID) + if roleErr != nil { + log.Printf("[RBAC] role=%s not found: %v", roleID, roleErr) + + continue + } + + for ruleIdx, rule := range roleRes.TypedSpec().Value.GetRules() { + matches := ruleMatches(rule, config.Family, verbStr, clusterID, resourceType) + + log.Printf("[RBAC] role=%s rule[%d] resources=%v verbs=%v clusters=%v -> match=%v", + roleID, ruleIdx, rule.GetResources(), rule.GetVerbs(), rule.GetClusters(), matches) + + if matches { + matchedRule = &matchResult{ + roleID: roleID, + bindingID: binding.Metadata().ID(), + rule: rule, + } + + break + } + } + + if matchedRule != nil { + break + } + } + + // 9. Decision. + if matchedRule != nil { + log.Printf("[RBAC] ALLOWED identity=%s type=%s family=%s verb=%s cluster=%s via role=%s binding=%s", + identity, resourceType, config.Family, verbStr, clusterID, matchedRule.roleID, matchedRule.bindingID) + + return nil + } + + log.Printf("[RBAC] DENIED identity=%s type=%s family=%s verb=%s cluster=%s reason=no-matching-rule", + identity, resourceType, config.Family, verbStr, clusterID) + + return status.Errorf(codes.PermissionDenied, "insufficient permissions: %s %s on %s", verbStr, config.Family, resourceType) +} + +// IsClusterScoped returns true if the resource type is in clusterIDTypeSet or clusterLabelTypeSet. +var IsClusterScoped func(resource.Type) bool + + +type matchResult struct { + roleID string + bindingID string + rule *specs.RoleSpec_Rule +} + +// verbToString maps a COSI verb to its string representation for RBAC rules. +func verbToString(v state.Verb) string { + switch v { + case state.Get: + return "get" + case state.List: + return "list" + case state.Watch: + return "watch" + case state.Create: + return "create" + case state.Update: + return "update" + case state.Destroy: + return "destroy" + default: + return "unknown" + } +} + +// matchesVerb checks if a rule's verb list grants the requested verb. +// Supports individual verbs and the "*" wildcard. +func matchesVerb(ruleVerbs []string, verb string) bool { + for _, v := range ruleVerbs { + if v == "*" || v == verb { + return true + } + } + + return false +} + +func checkAuthenticated(ctx context.Context) error { + authVal, ok := ctxstore.Value[pkgauth.EnabledAuthContextKey](ctx) + if ok && !authVal.Enabled { + return nil + } + + if _, ok = getIdentity(ctx); !ok { + return status.Error(codes.Unauthenticated, "authentication required") + } + + return nil +} + +func getIdentity(ctx context.Context) (string, bool) { + val, ok := ctxstore.Value[pkgauth.IdentityContextKey](ctx) + if !ok || val.Identity == "" { + return "", false + } + + return val.Identity, true +} + +func subjectMatches(spec *specs.RoleBindingSpec, identity string, identityRes *authres.Identity, identityErr error) bool { + for _, subject := range spec.GetSubjects() { + if subject.GetName() != "" && subject.GetName() == identity { + return true + } + + if pattern := subject.GetMatch(); pattern != "" { + if matched, _ := filepath.Match(pattern, identity); matched { + return true + } + } + + if selectors := subject.GetLabelSelectors(); len(selectors) > 0 && identityErr == nil && identityRes != nil { + if labelsMatch(identityRes.Metadata(), selectors) { + return true + } + } + } + + return false +} + +func labelsMatch(md *resource.Metadata, selectors []string) bool { + for _, sel := range selectors { + if !md.Labels().Matches(resource.LabelTerm{ + Key: sel, + }) { + return false + } + } + + return true +} + +func ruleMatches(rule *specs.RoleSpec_Rule, family, verb string, clusterID resource.ID, resourceType resource.Type) bool { + if !sliceContains(rule.GetResources(), family) && !sliceContains(rule.GetResources(), "*") { + return false + } + + if !matchesVerb(rule.GetVerbs(), verb) { + return false + } + + clusters := rule.GetClusters() + if len(clusters) > 0 { + if IsClusterScoped == nil || !IsClusterScoped(resourceType) { + return false + } + + if clusterID == "" { + return false + } + + matched := false + + for _, pattern := range clusters { + if pattern == "*" { + matched = true + + break + } + + if ok, _ := filepath.Match(pattern, string(clusterID)); ok { + matched = true + + break + } + } + + if !matched { + return false + } + } + + return true +} + +func sliceContains(slice []string, value string) bool { + return slices.Contains(slice, value) +} diff --git a/internal/pkg/rbac/families.go b/internal/pkg/rbac/families.go new file mode 100644 index 000000000..10d4120c4 --- /dev/null +++ b/internal/pkg/rbac/families.go @@ -0,0 +1,211 @@ +// Copyright (c) 2026 Sidero Labs, Inc. +// +// Use of this software is governed by the Business Source License +// included in the LICENSE file. + +package rbac + +import ( + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/resource/meta" + "github.com/cosi-project/runtime/pkg/state" + + authres "github.com/siderolabs/omni/client/pkg/omni/resources/auth" + "github.com/siderolabs/omni/client/pkg/omni/resources/omni" + "github.com/siderolabs/omni/client/pkg/omni/resources/siderolink" + "github.com/siderolabs/omni/client/pkg/omni/resources/system" + "github.com/siderolabs/omni/client/pkg/omni/resources/virtual" +) + +// Resource family constants. +const ( + FamilyClusters = "clusters" + FamilyMachines = "machines" + FamilyUsers = "users" + FamilyAuth = "auth" + FamilySystem = "system" +) + +// ResourceFamilyConfig defines the family and allowed COSI verbs for a resource type. +type ResourceFamilyConfig struct { + Family string + AllowedVerbs map[state.Verb]struct{} +} + +// Verb sets for common patterns. +var ( + readOnly = newVerbSet(state.Get, state.List, state.Watch) + fullCRUD = newVerbSet(state.Get, state.List, state.Watch, state.Create, state.Update, state.Destroy) + noCreate = newVerbSet(state.Get, state.List, state.Watch, state.Update, state.Destroy) +) + +func newVerbSet(verbs ...state.Verb) map[state.Verb]struct{} { + s := make(map[state.Verb]struct{}, len(verbs)) + for _, v := range verbs { + s[v] = struct{}{} + } + + return s +} + +// Registry maps each externally accessible resource type to its family and allowed verbs. +// +// Types not in the registry are denied to all external users. +// AuthConfigType is handled as a standalone exception (public, no auth required) and is not in any family. +// +//nolint:gochecknoglobals +var Registry = map[resource.Type]ResourceFamilyConfig{ + // ========================================================================= + // clusters family — all cluster-scoped resources + // ========================================================================= + + // full CRUD + omni.ClusterType: {Family: FamilyClusters, AllowedVerbs: fullCRUD}, + omni.MachineSetType: {Family: FamilyClusters, AllowedVerbs: fullCRUD}, + omni.MachineSetNodeType: {Family: FamilyClusters, AllowedVerbs: fullCRUD}, + omni.ConfigPatchType: {Family: FamilyClusters, AllowedVerbs: fullCRUD}, + omni.EtcdManualBackupType: {Family: FamilyClusters, AllowedVerbs: fullCRUD}, + omni.ImportedClusterSecretsType: {Family: FamilyClusters, AllowedVerbs: fullCRUD}, + omni.RotateTalosCAType: {Family: FamilyClusters, AllowedVerbs: fullCRUD}, + omni.RotateKubernetesCAType: {Family: FamilyClusters, AllowedVerbs: fullCRUD}, + omni.ExtensionsConfigurationType: {Family: FamilyClusters, AllowedVerbs: fullCRUD}, + omni.KernelArgsType: {Family: FamilyClusters, AllowedVerbs: fullCRUD}, + omni.NodeForceDestroyRequestType: {Family: FamilyClusters, AllowedVerbs: fullCRUD}, + omni.KubernetesManifestGroupType: {Family: FamilyClusters, AllowedVerbs: fullCRUD}, + omni.InfraMachineConfigType: {Family: FamilyClusters, AllowedVerbs: fullCRUD}, + + // read-only (controller-managed) + omni.ClusterBootstrapStatusType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.ClusterConfigVersionType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.ClusterDestroyStatusType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.ClusterEndpointType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.ClusterKubernetesNodesType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.ClusterWorkloadProxyStatusType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.ClusterTaintType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.ClusterUUIDType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.ClusterStatusType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.ClusterDiagnosticsType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.EtcdBackupStatusType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.ClusterSecretsRotationStatusType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.ClusterKubernetesManifestsStatusType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.ClusterMachineIdentityType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.ClusterMachineType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.ClusterMachineConfigPatchesType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.ClusterMachineConfigStatusType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.ClusterMachineTalosVersionType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.ClusterMachineStatusType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.ClusterMachineRequestStatusType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.ExposedServiceType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.ImagePullRequestType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.ImagePullStatusType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.MachineSetStatusType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.MachineSetDestroyStatusType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.MachineRequestSetStatusType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.EtcdBackupType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.SchematicConfigurationType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.MachineUpgradeStatusType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.KernelArgsStatusType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.MachineExtensionsStatusType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.MachineExtensionsType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.DiscoveryAffiliateDeleteTaskType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.RedactedClusterMachineConfigType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.MachineConfigDiffType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.MachinePendingUpdatesType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.ControlPlaneStatusType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.KubernetesNodeAuditResultType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.KubernetesStatusType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.KubernetesUpgradeManifestStatusType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.KubernetesUpgradeStatusType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.LoadBalancerConfigType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.LoadBalancerStatusType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.TalosUpgradeStatusType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + omni.UpgradeRolloutType: {Family: FamilyClusters, AllowedVerbs: readOnly}, + + // ========================================================================= + // machines family — unbound machines and machine metadata + // ========================================================================= + + // full CRUD + omni.MachineLabelsType: {Family: FamilyMachines, AllowedVerbs: fullCRUD}, + omni.MachineClassType: {Family: FamilyMachines, AllowedVerbs: fullCRUD}, + omni.MachineRequestSetType: {Family: FamilyMachines, AllowedVerbs: fullCRUD}, + omni.InstallationMediaConfigType: {Family: FamilyMachines, AllowedVerbs: fullCRUD}, + siderolink.GRPCTunnelConfigType: {Family: FamilyMachines, AllowedVerbs: fullCRUD}, + + // no create (update+destroy only) + siderolink.LinkType: {Family: FamilyMachines, AllowedVerbs: noCreate}, + siderolink.PendingMachineType: {Family: FamilyMachines, AllowedVerbs: noCreate}, + + // read-only (controller-managed) + omni.MachineType: {Family: FamilyMachines, AllowedVerbs: readOnly}, + omni.MachineStatusType: {Family: FamilyMachines, AllowedVerbs: readOnly}, + omni.MachineStatusSnapshotType: {Family: FamilyMachines, AllowedVerbs: readOnly}, + omni.MachineStatusLinkType: {Family: FamilyMachines, AllowedVerbs: readOnly}, + omni.MachineConfigGenOptionsType: {Family: FamilyMachines, AllowedVerbs: readOnly}, + omni.MaintenanceConfigStatusType: {Family: FamilyMachines, AllowedVerbs: readOnly}, + omni.InfraProviderCombinedStatusType: {Family: FamilyMachines, AllowedVerbs: readOnly}, + siderolink.LinkStatusType: {Family: FamilyMachines, AllowedVerbs: readOnly}, + siderolink.ConnectionParamsType: {Family: FamilyMachines, AllowedVerbs: readOnly}, + siderolink.APIConfigType: {Family: FamilyMachines, AllowedVerbs: readOnly}, + + // ========================================================================= + // users family — user and service account management + // ========================================================================= + + // full CRUD + authres.SAMLLabelRuleType: {Family: FamilyUsers, AllowedVerbs: fullCRUD}, + omni.EtcdBackupS3ConfType: {Family: FamilyUsers, AllowedVerbs: fullCRUD}, + + // read-only (mutations via ManagementService only) + authres.IdentityType: {Family: FamilyUsers, AllowedVerbs: readOnly}, + authres.IdentityLastActiveType: {Family: FamilyUsers, AllowedVerbs: readOnly}, + authres.IdentityStatusType: {Family: FamilyUsers, AllowedVerbs: readOnly}, + authres.UserType: {Family: FamilyUsers, AllowedVerbs: readOnly}, + authres.ServiceAccountStatusType: {Family: FamilyUsers, AllowedVerbs: readOnly}, + + // ========================================================================= + // auth family — RBAC and access policy management + // ========================================================================= + + // full CRUD + authres.AccessPolicyType: {Family: FamilyAuth, AllowedVerbs: fullCRUD}, + authres.RoleType: {Family: FamilyAuth, AllowedVerbs: fullCRUD}, + authres.RoleBindingType: {Family: FamilyAuth, AllowedVerbs: fullCRUD}, + siderolink.DefaultJoinTokenType: {Family: FamilyAuth, AllowedVerbs: fullCRUD}, + + // no create (must use management.CreateJoinToken API) + siderolink.JoinTokenType: {Family: FamilyAuth, AllowedVerbs: noCreate}, + + // ========================================================================= + // system family — always readable by any authenticated user + // ========================================================================= + + meta.NamespaceType: {Family: FamilySystem, AllowedVerbs: readOnly}, + meta.ResourceDefinitionType: {Family: FamilySystem, AllowedVerbs: readOnly}, + system.SysVersionType: {Family: FamilySystem, AllowedVerbs: readOnly}, + omni.FeaturesConfigType: {Family: FamilySystem, AllowedVerbs: readOnly}, + omni.OngoingTaskType: {Family: FamilySystem, AllowedVerbs: readOnly}, + omni.NotificationType: {Family: FamilySystem, AllowedVerbs: readOnly}, + omni.ClusterMetricsType: {Family: FamilySystem, AllowedVerbs: readOnly}, + omni.ClusterStatusMetricsType: {Family: FamilySystem, AllowedVerbs: readOnly}, + omni.MachineStatusMetricsType: {Family: FamilySystem, AllowedVerbs: readOnly}, + omni.TalosVersionType: {Family: FamilySystem, AllowedVerbs: readOnly}, + omni.KubernetesVersionType: {Family: FamilySystem, AllowedVerbs: readOnly}, + omni.TalosExtensionsType: {Family: FamilySystem, AllowedVerbs: readOnly}, + omni.InstallationMediaType: {Family: FamilySystem, AllowedVerbs: readOnly}, + omni.SchematicType: {Family: FamilySystem, AllowedVerbs: readOnly}, + omni.EtcdBackupOverallStatusType: {Family: FamilySystem, AllowedVerbs: readOnly}, + omni.EtcdBackupStoreStatusType: {Family: FamilySystem, AllowedVerbs: readOnly}, + siderolink.JoinTokenStatusType: {Family: FamilySystem, AllowedVerbs: readOnly}, + siderolink.NodeUniqueTokenStatusType: {Family: FamilySystem, AllowedVerbs: readOnly}, + system.ResourceLabelsType[*omni.MachineStatus](): {Family: FamilySystem, AllowedVerbs: readOnly}, + virtual.AdvertisedEndpointsType: {Family: FamilySystem, AllowedVerbs: readOnly}, + virtual.CurrentUserType: {Family: FamilySystem, AllowedVerbs: readOnly}, + virtual.PermissionsType: {Family: FamilySystem, AllowedVerbs: readOnly}, + virtual.ClusterPermissionsType: {Family: FamilySystem, AllowedVerbs: readOnly}, + virtual.SBCConfigType: {Family: FamilySystem, AllowedVerbs: readOnly}, + virtual.CloudPlatformConfigType: {Family: FamilySystem, AllowedVerbs: readOnly}, + virtual.MetalPlatformConfigType: {Family: FamilySystem, AllowedVerbs: readOnly}, + virtual.LabelsCompletionType: {Family: FamilySystem, AllowedVerbs: readOnly}, + virtual.KubernetesUsageType: {Family: FamilySystem, AllowedVerbs: readOnly}, +}