diff --git a/api/jsonschema/schema.json b/api/jsonschema/schema.json index d4092376e89c..9624b33c0a9e 100644 --- a/api/jsonschema/schema.json +++ b/api/jsonschema/schema.json @@ -760,6 +760,13 @@ }, "io.argoproj.workflow.v1alpha1.GetUserInfoResponse": { "properties": { + "email": { + "type": "string" + }, + "emailVerified": { + "format": "boolean", + "type": "boolean" + }, "groups": { "items": { "type": "string" diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json index f2f327f01061..f1585f8e660b 100644 --- a/api/openapi-spec/swagger.json +++ b/api/openapi-spec/swagger.json @@ -2813,6 +2813,13 @@ "io.argoproj.workflow.v1alpha1.GetUserInfoResponse": { "type": "object", "properties": { + "email": { + "type": "string" + }, + "emailVerified": { + "type": "boolean", + "format": "boolean" + }, "groups": { "type": "array", "items": { diff --git a/docs/workflow-controller-configmap.yaml b/docs/workflow-controller-configmap.yaml index 86f4933bcf5b..bcbd9809bc28 100644 --- a/docs/workflow-controller-configmap.yaml +++ b/docs/workflow-controller-configmap.yaml @@ -239,6 +239,7 @@ data: # Additional scopes to request. Typically needed for SSO RBAC. >= v2.12 scopes: - groups + - email # RBAC Config. >= v2.12 rbac: enabled: false diff --git a/docs/workflow-creator.md b/docs/workflow-creator.md index ce23c2b4d66b..bc5809d4ade7 100644 --- a/docs/workflow-creator.md +++ b/docs/workflow-creator.md @@ -13,6 +13,8 @@ metadata: name: my-wf labels: workflows.argoproj.io/creator: admin + # labels must be DNS formatted, so the "@" is replaces by '.at.' + workflows.argoproj.io/creator-email: admin.at.your.org ``` !!! NOTE diff --git a/manifests/quick-start/sso/overlays/argo-server-sa.yaml b/manifests/quick-start/sso/overlays/argo-server-sa.yaml index 4627408248b2..0cd3393f2a32 100644 --- a/manifests/quick-start/sso/overlays/argo-server-sa.yaml +++ b/manifests/quick-start/sso/overlays/argo-server-sa.yaml @@ -3,5 +3,5 @@ kind: ServiceAccount metadata: name: argo-server annotations: - workflows.argoproj.io/rbac-rule: "'authors' in groups" + workflows.argoproj.io/rbac-rule: "'authors' in groups && email == 'kilgore@kilgore.trout'" workflows.argoproj.io/rbac-rule-precedence: "1" diff --git a/manifests/quick-start/sso/overlays/workflow-controller-configmap.yaml b/manifests/quick-start/sso/overlays/workflow-controller-configmap.yaml index 88022429891c..b502c7f7c5e6 100644 --- a/manifests/quick-start/sso/overlays/workflow-controller-configmap.yaml +++ b/manifests/quick-start/sso/overlays/workflow-controller-configmap.yaml @@ -11,6 +11,7 @@ data: redirectUrl: http://localhost:2746/oauth2/callback scopes: - groups + - email rbac: enabled: true kind: ConfigMap diff --git a/pkg/apiclient/info/info.pb.go b/pkg/apiclient/info/info.pb.go index 928689ee5ff8..dd95cc02f58f 100644 --- a/pkg/apiclient/info/info.pb.go +++ b/pkg/apiclient/info/info.pb.go @@ -204,6 +204,8 @@ type GetUserInfoResponse struct { Issuer string `protobuf:"bytes,1,opt,name=issuer,proto3" json:"issuer,omitempty"` Subject string `protobuf:"bytes,2,opt,name=subject,proto3" json:"subject,omitempty"` Groups []string `protobuf:"bytes,3,rep,name=groups,proto3" json:"groups,omitempty"` + Email string `protobuf:"bytes,4,opt,name=email,proto3" json:"email,omitempty"` + EmailVerified bool `protobuf:"varint,5,opt,name=emailVerified,proto3" json:"emailVerified,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -263,6 +265,20 @@ func (m *GetUserInfoResponse) GetGroups() []string { return nil } +func (m *GetUserInfoResponse) GetEmail() string { + if m != nil { + return m.Email + } + return "" +} + +func (m *GetUserInfoResponse) GetEmailVerified() bool { + if m != nil { + return m.EmailVerified + } + return false +} + func init() { proto.RegisterType((*GetInfoRequest)(nil), "info.GetInfoRequest") proto.RegisterType((*InfoResponse)(nil), "info.InfoResponse") @@ -274,36 +290,38 @@ func init() { func init() { proto.RegisterFile("pkg/apiclient/info/info.proto", fileDescriptor_96940c93018255fa) } var fileDescriptor_96940c93018255fa = []byte{ - // 451 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x93, 0x41, 0x6b, 0x13, 0x41, - 0x14, 0xc7, 0xd9, 0x44, 0x5b, 0x9c, 0x94, 0x9a, 0x4e, 0x83, 0x5d, 0x17, 0x0d, 0x61, 0x4f, 0x45, - 0x61, 0x96, 0xd4, 0x53, 0xc1, 0x93, 0x1e, 0x42, 0x41, 0x14, 0x22, 0x7a, 0x10, 0x41, 0x26, 0xdb, - 0x97, 0xe9, 0x74, 0x37, 0xf3, 0xc6, 0x99, 0xd9, 0xf4, 0xe2, 0xc9, 0xab, 0x47, 0xbf, 0x8a, 0x1f, - 0xc2, 0xa3, 0xe0, 0x17, 0x90, 0xe0, 0x07, 0x91, 0x9d, 0x9d, 0x6d, 0x12, 0x15, 0x04, 0x2f, 0xcb, - 0x7b, 0x6f, 0xde, 0xfc, 0xdf, 0xef, 0xbd, 0x37, 0x4b, 0xee, 0xeb, 0x42, 0x64, 0x5c, 0xcb, 0xbc, - 0x94, 0xa0, 0x5c, 0x26, 0xd5, 0x1c, 0xfd, 0x87, 0x69, 0x83, 0x0e, 0xe9, 0x8d, 0xda, 0x4e, 0xee, - 0x09, 0x44, 0x51, 0x42, 0x9d, 0x97, 0x71, 0xa5, 0xd0, 0x71, 0x27, 0x51, 0xd9, 0x26, 0x27, 0x79, - 0x2a, 0xa4, 0xbb, 0xa8, 0x66, 0x2c, 0xc7, 0x45, 0xc6, 0x8d, 0x40, 0x6d, 0xf0, 0xd2, 0x1b, 0x59, - 0xd0, 0xb6, 0xd9, 0x15, 0x9a, 0x62, 0x5e, 0xe2, 0x55, 0xb6, 0x1c, 0xf3, 0x52, 0x5f, 0xf0, 0x71, - 0x26, 0x40, 0x81, 0xe1, 0x0e, 0xce, 0x1b, 0x91, 0xb4, 0x4f, 0xf6, 0x27, 0xe0, 0xce, 0xd4, 0x1c, - 0xa7, 0xf0, 0xbe, 0x02, 0xeb, 0xd2, 0x4f, 0x11, 0xd9, 0x6b, 0x7c, 0xab, 0x51, 0x59, 0xa0, 0x0f, - 0x48, 0x7f, 0xc1, 0x15, 0x17, 0x70, 0xfe, 0x9c, 0x2f, 0xc0, 0x6a, 0x9e, 0x43, 0x1c, 0x8d, 0xa2, - 0xe3, 0x5b, 0xd3, 0x3f, 0xe2, 0xf4, 0x05, 0xb9, 0x59, 0x4a, 0x55, 0xd8, 0xb8, 0x33, 0xea, 0x1e, - 0xf7, 0x4e, 0x4e, 0xd9, 0x9a, 0x91, 0xb5, 0x8c, 0xde, 0x60, 0xba, 0x10, 0xac, 0x66, 0x64, 0x2d, - 0x23, 0x6b, 0x19, 0xd9, 0x33, 0xa9, 0x8a, 0x69, 0xa3, 0x93, 0x1e, 0x92, 0x83, 0x09, 0xb8, 0xd7, - 0x60, 0xac, 0x44, 0xd5, 0x22, 0x0e, 0x08, 0x9d, 0x80, 0x7b, 0x65, 0xc1, 0x6c, 0x82, 0xbf, 0x23, - 0x87, 0x5b, 0xd1, 0x80, 0x7f, 0x87, 0xec, 0x48, 0x6b, 0x2b, 0x30, 0x01, 0x3a, 0x78, 0x34, 0x26, - 0xbb, 0xb6, 0x9a, 0x5d, 0x42, 0xee, 0xe2, 0x8e, 0x3f, 0x68, 0xdd, 0xfa, 0x86, 0x30, 0x58, 0x69, - 0x1b, 0x77, 0x47, 0xdd, 0xfa, 0x46, 0xe3, 0x9d, 0x7c, 0xe9, 0x90, 0x5e, 0x2d, 0xfd, 0x12, 0xcc, - 0x52, 0xe6, 0x40, 0xcf, 0xc8, 0x6e, 0x98, 0x1d, 0x1d, 0x30, 0xbf, 0xbc, 0xed, 0x51, 0x26, 0xb4, - 0x89, 0x6e, 0xe2, 0xa4, 0x83, 0x8f, 0xdf, 0x7f, 0x7e, 0xee, 0xec, 0xd3, 0x3d, 0xbf, 0xd5, 0xe5, - 0xd8, 0x6f, 0x9d, 0x7e, 0x20, 0x64, 0xdd, 0x26, 0x3d, 0xba, 0x56, 0xdb, 0x6e, 0x3c, 0x79, 0xfc, - 0x5f, 0xf3, 0x0c, 0x22, 0xe9, 0x91, 0x2f, 0x7d, 0x40, 0x6f, 0xb7, 0xa5, 0x97, 0xa1, 0xde, 0x5b, - 0xd2, 0xdb, 0x98, 0x1c, 0x8d, 0xaf, 0xcb, 0xff, 0x36, 0xe2, 0xe4, 0xee, 0x5f, 0x4e, 0x42, 0x5f, - 0xb1, 0x17, 0xa7, 0xb4, 0xdf, 0x8a, 0x57, 0x16, 0x4c, 0x9d, 0xfd, 0xe4, 0xf4, 0xeb, 0x6a, 0x18, - 0x7d, 0x5b, 0x0d, 0xa3, 0x1f, 0xab, 0x61, 0xf4, 0xe6, 0xe1, 0xbf, 0x5e, 0xed, 0xc6, 0x1f, 0x31, - 0xdb, 0xf1, 0x8f, 0xf4, 0xd1, 0xaf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x87, 0x05, 0x3d, 0x6e, 0x2e, - 0x03, 0x00, 0x00, + // 484 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x93, 0x41, 0x6f, 0xd3, 0x30, + 0x14, 0xc7, 0x95, 0x76, 0xdd, 0xc0, 0x1d, 0xa3, 0xf3, 0x2a, 0x66, 0x22, 0xa8, 0xaa, 0x88, 0x43, + 0x05, 0x92, 0xa3, 0x8e, 0xd3, 0x24, 0x4e, 0x70, 0xa8, 0x26, 0x21, 0x90, 0x8a, 0xd8, 0x01, 0x71, + 0x71, 0xd3, 0x57, 0xcf, 0x4b, 0x6a, 0x1b, 0xdb, 0xe9, 0x2e, 0x9c, 0xb8, 0x72, 0xe4, 0xc8, 0xd7, + 0xe0, 0x43, 0x70, 0x44, 0xe2, 0x0b, 0xa0, 0x8a, 0x0f, 0x82, 0xe2, 0x24, 0x6b, 0x03, 0x48, 0x48, + 0x5c, 0xa2, 0xf7, 0x9e, 0x9f, 0xff, 0xef, 0xe7, 0xf7, 0x5e, 0xd0, 0x7d, 0x9d, 0xf2, 0x98, 0x69, + 0x91, 0x64, 0x02, 0xa4, 0x8b, 0x85, 0x5c, 0x28, 0xff, 0xa1, 0xda, 0x28, 0xa7, 0xf0, 0x4e, 0x61, + 0x87, 0xf7, 0xb8, 0x52, 0x3c, 0x83, 0x22, 0x2f, 0x66, 0x52, 0x2a, 0xc7, 0x9c, 0x50, 0xd2, 0x96, + 0x39, 0xe1, 0x33, 0x2e, 0xdc, 0x45, 0x3e, 0xa3, 0x89, 0x5a, 0xc6, 0xcc, 0x70, 0xa5, 0x8d, 0xba, + 0xf4, 0x46, 0x5c, 0x69, 0xdb, 0xf8, 0x4a, 0x99, 0x74, 0x91, 0xa9, 0xab, 0x78, 0x35, 0x66, 0x99, + 0xbe, 0x60, 0xe3, 0x98, 0x83, 0x04, 0xc3, 0x1c, 0xcc, 0x4b, 0x91, 0xa8, 0x87, 0x0e, 0x26, 0xe0, + 0xce, 0xe4, 0x42, 0x4d, 0xe1, 0x5d, 0x0e, 0xd6, 0x45, 0x1f, 0x03, 0xb4, 0x5f, 0xfa, 0x56, 0x2b, + 0x69, 0x01, 0x3f, 0x44, 0xbd, 0x25, 0x93, 0x8c, 0xc3, 0xfc, 0x05, 0x5b, 0x82, 0xd5, 0x2c, 0x01, + 0x12, 0x0c, 0x83, 0xd1, 0xcd, 0xe9, 0x1f, 0x71, 0xfc, 0x12, 0x75, 0x32, 0x21, 0x53, 0x4b, 0x5a, + 0xc3, 0xf6, 0xa8, 0x7b, 0x72, 0x4a, 0x37, 0x8c, 0xb4, 0x66, 0xf4, 0x06, 0xd5, 0x29, 0xa7, 0x05, + 0x23, 0xad, 0x19, 0x69, 0xcd, 0x48, 0x9f, 0x0b, 0x99, 0x4e, 0x4b, 0x9d, 0xe8, 0x08, 0x1d, 0x4e, + 0xc0, 0x9d, 0x83, 0xb1, 0x42, 0xc9, 0x1a, 0xb1, 0x8f, 0xf0, 0x04, 0xdc, 0x6b, 0x0b, 0x66, 0x1b, + 0xfc, 0x73, 0x80, 0x8e, 0x1a, 0xe1, 0x8a, 0xff, 0x0e, 0xda, 0x15, 0xd6, 0xe6, 0x60, 0x2a, 0xea, + 0xca, 0xc3, 0x04, 0xed, 0xd9, 0x7c, 0x76, 0x09, 0x89, 0x23, 0x2d, 0x7f, 0x50, 0xbb, 0xc5, 0x0d, + 0x6e, 0x54, 0xae, 0x2d, 0x69, 0x0f, 0xdb, 0xc5, 0x8d, 0xd2, 0xc3, 0x7d, 0xd4, 0x81, 0x25, 0x13, + 0x19, 0xd9, 0xf1, 0xf9, 0xa5, 0x83, 0x1f, 0xa0, 0x5b, 0xde, 0x38, 0x07, 0x23, 0x16, 0x02, 0xe6, + 0xa4, 0x33, 0x0c, 0x46, 0x37, 0xa6, 0xcd, 0xe0, 0xc9, 0x97, 0x16, 0xea, 0x16, 0x58, 0xaf, 0xc0, + 0xac, 0x44, 0x02, 0xf8, 0x0c, 0xed, 0x55, 0x8d, 0xc7, 0x7d, 0xea, 0x27, 0xdf, 0x9c, 0x43, 0x88, + 0xcb, 0xe8, 0xf6, 0x53, 0xa2, 0xfe, 0x87, 0xef, 0x3f, 0x3f, 0xb5, 0x0e, 0xf0, 0xbe, 0x5f, 0x89, + 0xd5, 0xd8, 0xaf, 0x0c, 0x7e, 0x8f, 0xd0, 0xa6, 0x47, 0xf8, 0xf8, 0x5a, 0xad, 0xd9, 0xb5, 0xf0, + 0xc9, 0x7f, 0x0d, 0xa3, 0x12, 0x89, 0x8e, 0x7d, 0xe9, 0x43, 0x7c, 0xbb, 0x2e, 0xbd, 0xaa, 0xea, + 0xbd, 0x45, 0xdd, 0xad, 0xae, 0x63, 0x72, 0x5d, 0xfe, 0xb7, 0xf9, 0x84, 0x77, 0xff, 0x72, 0x52, + 0xbd, 0x8b, 0x78, 0x71, 0x8c, 0x7b, 0xb5, 0x78, 0x6e, 0xc1, 0x14, 0xd9, 0x4f, 0x4f, 0xbf, 0xae, + 0x07, 0xc1, 0xb7, 0xf5, 0x20, 0xf8, 0xb1, 0x1e, 0x04, 0x6f, 0x1e, 0xfd, 0x6b, 0xe5, 0xb7, 0x7e, + 0xa7, 0xd9, 0xae, 0xdf, 0xf0, 0xc7, 0xbf, 0x02, 0x00, 0x00, 0xff, 0xff, 0xff, 0x44, 0x46, 0x13, + 0x6b, 0x03, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -611,6 +629,23 @@ func (m *GetUserInfoResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } + if m.EmailVerified { + i-- + if m.EmailVerified { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x28 + } + if len(m.Email) > 0 { + i -= len(m.Email) + copy(dAtA[i:], m.Email) + i = encodeVarintInfo(dAtA, i, uint64(len(m.Email))) + i-- + dAtA[i] = 0x22 + } if len(m.Groups) > 0 { for iNdEx := len(m.Groups) - 1; iNdEx >= 0; iNdEx-- { i -= len(m.Groups[iNdEx]) @@ -726,6 +761,13 @@ func (m *GetUserInfoResponse) Size() (n int) { n += 1 + l + sovInfo(uint64(l)) } } + l = len(m.Email) + if l > 0 { + n += 1 + l + sovInfo(uint64(l)) + } + if m.EmailVerified { + n += 2 + } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } @@ -1145,6 +1187,58 @@ func (m *GetUserInfoResponse) Unmarshal(dAtA []byte) error { } m.Groups = append(m.Groups, string(dAtA[iNdEx:postIndex])) iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Email", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowInfo + } + 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 ErrInvalidLengthInfo + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthInfo + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Email = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 5: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field EmailVerified", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowInfo + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.EmailVerified = bool(v != 0) default: iNdEx = preIndex skippy, err := skipInfo(dAtA[iNdEx:]) diff --git a/pkg/apiclient/info/info.proto b/pkg/apiclient/info/info.proto index a0a2dc9fcadd..3d55b088a5fc 100644 --- a/pkg/apiclient/info/info.proto +++ b/pkg/apiclient/info/info.proto @@ -24,6 +24,8 @@ message GetUserInfoResponse { string issuer = 1; string subject = 2; repeated string groups = 3; + string email = 4; + bool emailVerified = 5; } service InfoService { diff --git a/server/auth/sso/sso.go b/server/auth/sso/sso.go index 70cd93e60a39..47c0b7ae8cb5 100644 --- a/server/auth/sso/sso.go +++ b/server/auth/sso/sso.go @@ -172,7 +172,7 @@ func newSso( if err != nil { return nil, fmt.Errorf("failed to create JWT encrpytor: %w", err) } - log.WithFields(log.Fields{"redirectUrl": config.RedirectURL, "issuer": c.Issuer, "clientId": c.ClientID}).Info("SSO configuration") + log.WithFields(log.Fields{"redirectUrl": config.RedirectURL, "issuer": c.Issuer, "clientId": c.ClientID, "scopes": config.Scopes}).Info("SSO configuration") return &sso{ config: config, idTokenVerifier: idTokenVerifier, @@ -233,7 +233,14 @@ func (s *sso) HandleCallback(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(fmt.Sprintf("failed to get claims: %v", err))) return } - argoClaims := &types.Claims{Claims: jwt.Claims{Issuer: issuer, Subject: c.Subject, Expiry: jwt.NewNumericDate(time.Now().Add(s.expiry))}, Groups: c.Groups} + argoClaims := &types.Claims{Claims: jwt.Claims{ + Issuer: issuer, + Subject: c.Subject, + Expiry: jwt.NewNumericDate(time.Now().Add(s.expiry))}, + Groups: c.Groups, + Email: c.Email, + EmailVerified: c.EmailVerified, + } raw, err := jwt.Encrypted(s.encrypter).Claims(argoClaims).CompactSerialize() if err != nil { panic(err) diff --git a/server/auth/types/claims.go b/server/auth/types/claims.go index 7b8c55e4161d..5bcda24e6cf7 100644 --- a/server/auth/types/claims.go +++ b/server/auth/types/claims.go @@ -4,5 +4,7 @@ import "gopkg.in/square/go-jose.v2/jwt" type Claims struct { jwt.Claims - Groups []string `json:"groups,omitempty"` + Groups []string `json:"groups,omitempty"` + Email string `json:"email,omitempty"` + EmailVerified bool `json:"email_verified,omitempty"` } diff --git a/server/info/info_server.go b/server/info/info_server.go index 57ba9a584a8a..759e9dac06f7 100644 --- a/server/info/info_server.go +++ b/server/info/info_server.go @@ -17,7 +17,13 @@ type infoServer struct { func (i *infoServer) GetUserInfo(ctx context.Context, _ *infopkg.GetUserInfoRequest) (*infopkg.GetUserInfoResponse, error) { claims := auth.GetClaims(ctx) if claims != nil { - return &infopkg.GetUserInfoResponse{Subject: claims.Subject, Issuer: claims.Issuer, Groups: claims.Groups}, nil + return &infopkg.GetUserInfoResponse{ + Subject: claims.Subject, + Issuer: claims.Issuer, + Groups: claims.Groups, + Email: claims.Email, + EmailVerified: claims.EmailVerified, + }, nil } return &infopkg.GetUserInfoResponse{}, nil } diff --git a/server/info/info_server_test.go b/server/info/info_server_test.go index 8e0d9cf0489b..fc9a77c29a87 100644 --- a/server/info/info_server_test.go +++ b/server/info/info_server_test.go @@ -13,11 +13,13 @@ import ( func Test_infoServer_GetUserInfo(t *testing.T) { i := &infoServer{} - ctx := context.WithValue(context.TODO(), auth.ClaimsKey, &types.Claims{Claims: jwt.Claims{Issuer: "my-iss", Subject: "my-sub"}, Groups: []string{"my-group"}}) + ctx := context.WithValue(context.TODO(), auth.ClaimsKey, &types.Claims{Claims: jwt.Claims{Issuer: "my-iss", Subject: "my-sub"}, Groups: []string{"my-group"}, Email: "my@email", EmailVerified: true}) info, err := i.GetUserInfo(ctx, nil) if assert.NoError(t, err) { assert.Equal(t, "my-iss", info.Issuer) assert.Equal(t, "my-sub", info.Subject) assert.Equal(t, []string{"my-group"}, info.Groups) + assert.Equal(t, "my@email", info.Email) + assert.True(t, info.EmailVerified) } } diff --git a/ui/src/app/userinfo/components/user-info.tsx b/ui/src/app/userinfo/components/user-info.tsx index 76854ae22487..626098dda544 100644 --- a/ui/src/app/userinfo/components/user-info.tsx +++ b/ui/src/app/userinfo/components/user-info.tsx @@ -40,6 +40,8 @@ export class UserInfo extends BasePage, State> {

Issuer: {this.state.userInfo.issuer || '-'}

Subject: {this.state.userInfo.subject || '-'}

Groups: {(this.state.userInfo.groups && this.state.userInfo.groups.length > 0 && this.state.userInfo.groups.join(', ')) || '-'}

+

Email: {this.state.userInfo.email || '-'}

+

Email Verified: {this.state.userInfo.emailVerified || '-'}

)} diff --git a/ui/src/models/info.ts b/ui/src/models/info.ts index 84927f32af5a..fe754c4c0b0b 100644 --- a/ui/src/models/info.ts +++ b/ui/src/models/info.ts @@ -17,4 +17,6 @@ export interface GetUserInfoResponse { subject?: string; issuer?: string; groups?: string[]; + email?: string; + emailVerified?: boolean; } diff --git a/workflow/common/common.go b/workflow/common/common.go index df3ff82fd55f..cf47cdc20d67 100644 --- a/workflow/common/common.go +++ b/workflow/common/common.go @@ -55,7 +55,8 @@ const ( // for the purposes of workflow segregation LabelKeyControllerInstanceID = workflow.WorkflowFullName + "/controller-instanceid" // Who created this workflow. - LabelKeyCreator = workflow.WorkflowFullName + "/creator" + LabelKeyCreator = workflow.WorkflowFullName + "/creator" + LabelKeyCreatorEmail = workflow.WorkflowFullName + "/creator-email" // LabelKeyCompleted is the metadata label applied on worfklows and workflow pods to indicates if resource is completed // Workflows and pods with a completed=true label will be ignored by the controller. // See also `LabelKeyWorkflowArchivingStatus`. diff --git a/workflow/creator/creator.go b/workflow/creator/creator.go index 5a8ffc4ed94d..e5d78cc0caf0 100644 --- a/workflow/creator/creator.go +++ b/workflow/creator/creator.go @@ -15,11 +15,18 @@ import ( func Label(ctx context.Context, obj metav1.Object) { claims := auth.GetClaims(ctx) if claims != nil { - value := regexp.MustCompile("[^-_.a-z0-9A-Z]").ReplaceAllString(claims.Subject, "-") - if len(value) > 63 { - value = value[len(value)-63:] + labels.Label(obj, common.LabelKeyCreator, dnsFriendly(claims.Subject)) + if claims.Email != "" { + labels.Label(obj, common.LabelKeyCreatorEmail, dnsFriendly(strings.Replace(claims.Email, "@", ".at.", 1))) } - value = strings.TrimLeft(value, "-") - labels.Label(obj, common.LabelKeyCreator, value) } } + +func dnsFriendly(s string) string { + value := regexp.MustCompile("[^-_.a-z0-9A-Z]").ReplaceAllString(s, "-") + if len(value) > 63 { + value = value[len(value)-63:] + } + value = strings.TrimLeft(value, "-") + return value +} diff --git a/workflow/creator/creator_test.go b/workflow/creator/creator_test.go index caf6db199245..1559a0b77eb8 100644 --- a/workflow/creator/creator_test.go +++ b/workflow/creator/creator_test.go @@ -23,16 +23,10 @@ func TestLabel(t *testing.T) { }) t.Run("NotEmpty", func(t *testing.T) { wf := &wfv1.Workflow{} - Label(context.WithValue(context.TODO(), auth.ClaimsKey, &types.Claims{Claims: jwt.Claims{Subject: "my-sub"}}), wf) + Label(context.WithValue(context.TODO(), auth.ClaimsKey, &types.Claims{Claims: jwt.Claims{Subject: strings.Repeat("x", 63) + "y"}, Email: "my@email"}), wf) if assert.NotEmpty(t, wf.Labels) { - assert.Contains(t, wf.Labels, common.LabelKeyCreator) - } - }) - t.Run("TooLong", func(t *testing.T) { - wf := &wfv1.Workflow{} - Label(context.WithValue(context.TODO(), auth.ClaimsKey, &types.Claims{Claims: jwt.Claims{Subject: strings.Repeat("x", 63) + "y"}}), wf) - if assert.NotEmpty(t, wf.Labels) { - assert.Equal(t, strings.Repeat("x", 62)+"y", wf.Labels[common.LabelKeyCreator]) + assert.Equal(t, strings.Repeat("x", 62)+"y", wf.Labels[common.LabelKeyCreator], "creator is truncated") + assert.Equal(t, "my.at.email", wf.Labels[common.LabelKeyCreatorEmail], "'@' is replaced by '.at.'") } }) t.Run("TooLongHyphen", func(t *testing.T) {