From fcc133273f2fb5837c40fefb85ca2eecc73e0f3f Mon Sep 17 00:00:00 2001 From: Noah Stride Date: Tue, 23 Apr 2024 09:23:53 +0100 Subject: [PATCH 1/2] Emit an Audit log event when a Bot or Instance fails to join (#40329) * Emit audit event on instance/bot join failure * Add audit log on failure to azure and iam join methods * Tidy up a lil bit * Add missing icon to EventTypeCell.tsx * Regenerate snapshot * Correct status code * Add host_id and node_name to the join audit log * Remove "unknown" default values --- lib/auth/join.go | 140 +- lib/auth/join_azure.go | 21 +- lib/auth/join_iam.go | 41 +- lib/events/codes.go | 4 + .../src/Audit/EventList/EventTypeCell.tsx | 2 + .../__snapshots__/Audit.story.test.tsx.snap | 58 +- .../teleport/src/Audit/fixtures/index.ts | 35 + .../Clusters.story.test.tsx.snap | 1635 +---------------- .../teleport/src/services/audit/makeEvent.ts | 14 + .../teleport/src/services/audit/types.ts | 17 + 10 files changed, 280 insertions(+), 1687 deletions(-) diff --git a/lib/auth/join.go b/lib/auth/join.go index 8898eaf622700..537a67e7f0cdd 100644 --- a/lib/auth/join.go +++ b/lib/auth/join.go @@ -115,6 +115,87 @@ func setRemoteAddrFromContext(ctx context.Context, req *types.RegisterUsingToken return nil } +// handleJoinFailure logs and audits the failure of a join. It is intentionally +// designed to handle potential nullness of the input parameters. +func (a *Server) handleJoinFailure( + origErr error, + pt types.ProvisionToken, + attributeSource joinAttributeSourcer, + req *types.RegisterUsingTokenRequest, +) { + fields := logrus.Fields{} + if req != nil { + fields["role"] = req.Role + fields["host_id"] = req.HostID + fields["node_name"] = req.NodeName + } + + // Fetch and encode attributes if they are available. + var attributesProto *apievents.Struct + if attributeSource != nil { + var err error + attributes, err := attributeSource.JoinAuditAttributes() + if err != nil { + log.WithError(err).Warn("Unable to fetch join attributes from join method") + } + fields["attributes"] = attributes + attributesProto, err = apievents.EncodeMap(attributes) + if err != nil { + log.WithError(err).Warn("Unable to encode join attributes for audit event") + } + } + + // Add log fields from token if available. + if pt != nil { + fields["join_method"] = string(pt.GetJoinMethod()) + fields["token_name"] = pt.GetSafeName() + } + log.WithError(origErr).WithFields(fields).Warn("Failure to join cluster occurred") + + var evt apievents.AuditEvent + status := apievents.Status{ + Success: false, + Error: origErr.Error(), + } + if req != nil && req.Role == types.RoleBot { + botJoinEvent := &apievents.BotJoin{ + Metadata: apievents.Metadata{ + Type: events.BotJoinEvent, + Code: events.BotJoinFailureCode, + }, + Status: status, + Attributes: attributesProto, + } + if pt != nil { + botJoinEvent.Method = string(pt.GetJoinMethod()) + botJoinEvent.TokenName = pt.GetSafeName() + } + evt = botJoinEvent + } else { + instanceJoinEvent := &apievents.InstanceJoin{ + Metadata: apievents.Metadata{ + Type: events.InstanceJoinEvent, + Code: events.InstanceJoinFailureCode, + }, + Status: status, + Attributes: attributesProto, + } + if pt != nil { + instanceJoinEvent.Method = string(pt.GetJoinMethod()) + instanceJoinEvent.TokenName = pt.GetSafeName() + } + if req != nil { + instanceJoinEvent.Role = string(req.Role) + instanceJoinEvent.NodeName = req.NodeName + instanceJoinEvent.HostID = req.HostID + } + evt = instanceJoinEvent + } + if err := a.emitter.EmitAuditEvent(a.closeCtx, evt); err != nil { + log.WithError(err).Warn("Failed to emit failed join event") + } +} + // RegisterUsingToken returns credentials for a new node to join the Teleport // cluster using a previously issued token. // @@ -126,30 +207,21 @@ func setRemoteAddrFromContext(ctx context.Context, req *types.RegisterUsingToken // // If the token includes a specific join method, the rules for that join method // will be checked. -func (a *Server) RegisterUsingToken(ctx context.Context, req *types.RegisterUsingTokenRequest) (_ *proto.Certs, err error) { +func (a *Server) RegisterUsingToken(ctx context.Context, req *types.RegisterUsingTokenRequest) (certs *proto.Certs, err error) { + var joinAttributeSrc joinAttributeSourcer + var provisionToken types.ProvisionToken + defer func() { + // Emit a log message and audit event on join failure. + if err != nil { + a.handleJoinFailure(err, provisionToken, joinAttributeSrc, req) + } + }() + if err := req.CheckAndSetDefaults(); err != nil { return nil, trace.Wrap(err) } method := a.tokenJoinMethod(ctx, req.Token) - defer func() { - if err == nil { - return - } - level := logrus.WarnLevel - if trace.IsAccessDenied(err) { - level = logrus.DebugLevel - } - log.WithFields(logrus.Fields{ - "node_name": req.NodeName, - "host_id": req.HostID, - "role": req.Role, - "method": method, - logrus.ErrorKey: err, - }).Log(level, "Agent has failed to join the cluster.") - }() - - var joinAttributeSrc joinAttributeSourcer switch method { case types.JoinMethodEC2: if err := a.checkEC2JoinRequest(ctx, req); err != nil { @@ -162,40 +234,52 @@ func (a *Server) RegisterUsingToken(ctx context.Context, req *types.RegisterUsin "sure your node is configured to use the %s join method", method, method) case types.JoinMethodGitHub: claims, err := a.checkGitHubJoinRequest(ctx, req) + if claims != nil { + joinAttributeSrc = claims + } if err != nil { return nil, trace.Wrap(err) } - joinAttributeSrc = claims case types.JoinMethodGitLab: claims, err := a.checkGitLabJoinRequest(ctx, req) + if claims != nil { + joinAttributeSrc = claims + } if err != nil { return nil, trace.Wrap(err) } - joinAttributeSrc = claims case types.JoinMethodCircleCI: claims, err := a.checkCircleCIJoinRequest(ctx, req) + if claims != nil { + joinAttributeSrc = claims + } if err != nil { return nil, trace.Wrap(err) } - joinAttributeSrc = claims case types.JoinMethodKubernetes: claims, err := a.checkKubernetesJoinRequest(ctx, req) + if claims != nil { + joinAttributeSrc = claims + } if err != nil { return nil, trace.Wrap(err) } - joinAttributeSrc = claims case types.JoinMethodGCP: claims, err := a.checkGCPJoinRequest(ctx, req) + if claims != nil { + joinAttributeSrc = claims + } if err != nil { return nil, trace.Wrap(err) } - joinAttributeSrc = claims case types.JoinMethodSpacelift: claims, err := a.checkSpaceliftJoinRequest(ctx, req) + if claims != nil { + joinAttributeSrc = claims + } if err != nil { return nil, trace.Wrap(err) } - joinAttributeSrc = claims case types.JoinMethodToken: // carry on to common token checking logic default: @@ -206,7 +290,7 @@ func (a *Server) RegisterUsingToken(ctx context.Context, req *types.RegisterUsin } // perform common token checks - provisionToken, err := a.checkTokenJoinRequestCommon(ctx, req) + provisionToken, err = a.checkTokenJoinRequestCommon(ctx, req) if err != nil { return nil, trace.Wrap(err) } @@ -214,10 +298,10 @@ func (a *Server) RegisterUsingToken(ctx context.Context, req *types.RegisterUsin // With all elements of the token validated, we can now generate & return // certificates. if req.Role == types.RoleBot { - certs, err := a.generateCertsBot(ctx, provisionToken, req, joinAttributeSrc) + certs, err = a.generateCertsBot(ctx, provisionToken, req, joinAttributeSrc) return certs, trace.Wrap(err) } - certs, err := a.generateCerts(ctx, provisionToken, req, joinAttributeSrc) + certs, err = a.generateCerts(ctx, provisionToken, req, joinAttributeSrc) return certs, trace.Wrap(err) } diff --git a/lib/auth/join_azure.go b/lib/auth/join_azure.go index 282edb6634b9b..7447c56ea0944 100644 --- a/lib/auth/join_azure.go +++ b/lib/auth/join_azure.go @@ -340,7 +340,22 @@ func generateAzureChallenge() (string, error) { // The caller must provide a ChallengeResponseFunc which returns a // *proto.RegisterUsingAzureMethodRequest with a signed attested data document // including the challenge as a nonce. -func (a *Server) RegisterUsingAzureMethod(ctx context.Context, challengeResponse client.RegisterAzureChallengeResponseFunc, opts ...azureRegisterOption) (*proto.Certs, error) { +func (a *Server) RegisterUsingAzureMethod( + ctx context.Context, + challengeResponse client.RegisterAzureChallengeResponseFunc, + opts ...azureRegisterOption, +) (certs *proto.Certs, err error) { + var provisionToken types.ProvisionToken + var joinRequest *types.RegisterUsingTokenRequest + defer func() { + // Emit a log message and audit event on join failure. + if err != nil { + a.handleJoinFailure( + err, provisionToken, nil, joinRequest, + ) + } + }() + cfg := &azureRegisterConfig{} for _, opt := range opts { opt(cfg) @@ -362,7 +377,7 @@ func (a *Server) RegisterUsingAzureMethod(ctx context.Context, challengeResponse return nil, trace.Wrap(err) } - provisionToken, err := a.checkTokenJoinRequestCommon(ctx, req.RegisterUsingTokenRequest) + provisionToken, err = a.checkTokenJoinRequestCommon(ctx, req.RegisterUsingTokenRequest) if err != nil { return nil, trace.Wrap(err) } @@ -380,7 +395,7 @@ func (a *Server) RegisterUsingAzureMethod(ctx context.Context, challengeResponse ) return certs, trace.Wrap(err) } - certs, err := a.generateCerts( + certs, err = a.generateCerts( ctx, provisionToken, req.RegisterUsingTokenRequest, diff --git a/lib/auth/join_iam.go b/lib/auth/join_iam.go index 93a9837d40bbd..333282198be0a 100644 --- a/lib/auth/join_iam.go +++ b/lib/auth/join_iam.go @@ -35,7 +35,6 @@ import ( "github.com/aws/aws-sdk-go/service/sts" "github.com/coreos/go-semver/semver" "github.com/gravitational/trace" - "github.com/sirupsen/logrus" "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/client" @@ -340,7 +339,22 @@ func withFips(fips bool) iamRegisterOption { // The caller must provide a ChallengeResponseFunc which returns a // *types.RegisterUsingTokenRequest with a signed sts:GetCallerIdentity request // including the challenge as a signed header. -func (a *Server) RegisterUsingIAMMethod(ctx context.Context, challengeResponse client.RegisterIAMChallengeResponseFunc, opts ...iamRegisterOption) (_ *proto.Certs, err error) { +func (a *Server) RegisterUsingIAMMethod( + ctx context.Context, + challengeResponse client.RegisterIAMChallengeResponseFunc, + opts ...iamRegisterOption, +) (certs *proto.Certs, err error) { + var provisionToken types.ProvisionToken + var joinRequest *types.RegisterUsingTokenRequest + defer func() { + // Emit a log message and audit event on join failure. + if err != nil { + a.handleJoinFailure( + err, provisionToken, nil, joinRequest, + ) + } + }() + cfg := defaultIAMRegisterConfig(a.fips) for _, opt := range opts { opt(cfg) @@ -360,30 +374,11 @@ func (a *Server) RegisterUsingIAMMethod(ctx context.Context, challengeResponse c return nil, trace.Wrap(err) } - var method types.JoinMethod = "unknown" - defer func() { - if err == nil { - return - } - level := logrus.WarnLevel - if trace.IsAccessDenied(err) { - level = logrus.DebugLevel - } - log.WithFields(logrus.Fields{ - "node_name": req.RegisterUsingTokenRequest.NodeName, - "host_id": req.RegisterUsingTokenRequest.HostID, - "role": req.RegisterUsingTokenRequest.Role, - "method": method, - logrus.ErrorKey: err, - }).Log(level, "Agent has failed to join the cluster.") - }() - // perform common token checks - provisionToken, err := a.checkTokenJoinRequestCommon(ctx, req.RegisterUsingTokenRequest) + provisionToken, err = a.checkTokenJoinRequestCommon(ctx, req.RegisterUsingTokenRequest) if err != nil { return nil, trace.Wrap(err) } - method = provisionToken.GetJoinMethod() // check that the GetCallerIdentity request is valid and matches the token if err := a.checkIAMRequest(ctx, challenge, req, cfg); err != nil { @@ -394,7 +389,7 @@ func (a *Server) RegisterUsingIAMMethod(ctx context.Context, challengeResponse c certs, err := a.generateCertsBot(ctx, provisionToken, req.RegisterUsingTokenRequest, nil) return certs, trace.Wrap(err) } - certs, err := a.generateCerts(ctx, provisionToken, req.RegisterUsingTokenRequest, nil) + certs, err = a.generateCerts(ctx, provisionToken, req.RegisterUsingTokenRequest, nil) return certs, trace.Wrap(err) } diff --git a/lib/events/codes.go b/lib/events/codes.go index 53ca4d31d9a84..07bc53f98c203 100644 --- a/lib/events/codes.go +++ b/lib/events/codes.go @@ -420,8 +420,12 @@ const ( // BotJoinCode is the 'bot.join' event code. BotJoinCode = "TJ001I" + // BotJoinFailureCode is the 'bot.join' event code for failures. + BotJoinFailureCode = "TJ001E" // InstanceJoinCode is the 'node.join' event code. InstanceJoinCode = "TJ002I" + // InstanceJoinFailureCode is the 'node.join' event code for failures. + InstanceJoinFailureCode = "TJ002E" // BotCreateCode is the `bot.create` event code. BotCreateCode = "TB001I" diff --git a/web/packages/teleport/src/Audit/EventList/EventTypeCell.tsx b/web/packages/teleport/src/Audit/EventList/EventTypeCell.tsx index a10dbe4d235f8..61d270ed8f263 100644 --- a/web/packages/teleport/src/Audit/EventList/EventTypeCell.tsx +++ b/web/packages/teleport/src/Audit/EventList/EventTypeCell.tsx @@ -225,7 +225,9 @@ const EventIconMap: Record = { [eventCodes.SSMRUN_SUCCESS]: Icons.Info, [eventCodes.SSMRUN_FAIL]: Icons.Info, [eventCodes.BOT_JOIN]: Icons.Info, + [eventCodes.BOT_JOIN_FAILURE]: Icons.Warning, [eventCodes.INSTANCE_JOIN]: Icons.Info, + [eventCodes.INSTANCE_JOIN_FAILURE]: Icons.Warning, [eventCodes.LOGIN_RULE_CREATE]: Icons.Info, [eventCodes.LOGIN_RULE_DELETE]: Icons.Info, [eventCodes.SAML_IDP_AUTH_ATTEMPT]: Icons.Info, diff --git a/web/packages/teleport/src/Audit/__snapshots__/Audit.story.test.tsx.snap b/web/packages/teleport/src/Audit/__snapshots__/Audit.story.test.tsx.snap index 67e3cf112bb1c..e153e91347074 100644 --- a/web/packages/teleport/src/Audit/__snapshots__/Audit.story.test.tsx.snap +++ b/web/packages/teleport/src/Audit/__snapshots__/Audit.story.test.tsx.snap @@ -406,12 +406,12 @@ exports[`list of all events 1`] = ` - - 238 + 239 of - 238 + 239 + + thead > tr > th, -.c14 > tbody > tr > th, -.c14 > tfoot > tr > th, -.c14 > thead > tr > td, -.c14 > tbody > tr > td, -.c14 > tfoot > tr > td { - padding: 8px 8px; - vertical-align: middle; -} - -.c14 > thead > tr > th:first-child, -.c14 > tbody > tr > th:first-child, -.c14 > tfoot > tr > th:first-child, -.c14 > thead > tr > td:first-child, -.c14 > tbody > tr > td:first-child, -.c14 > tfoot > tr > td:first-child { - padding-left: 24px; -} - -.c14 > thead > tr > th:last-child, -.c14 > tbody > tr > th:last-child, -.c14 > tfoot > tr > th:last-child, -.c14 > thead > tr > td:last-child, -.c14 > tbody > tr > td:last-child, -.c14 > tfoot > tr > td:last-child { - padding-right: 24px; -} - -.c14 > tbody > tr > td { - vertical-align: middle; -} - -.c14 > thead > tr > th { - color: #FFFFFF; - font-weight: 600; - font-size: 14px; - line-height: 24px; - cursor: pointer; - padding-bottom: 0; - padding-top: 0; - text-align: left; - white-space: nowrap; -} - -.c14 > thead > tr > th svg { - height: 12px; -} - -.c14 > tbody > tr > td { - color: #FFFFFF; - font-weight: 300; - font-size: 14px; - line-height: 20px; - letter-spacing: 0.035px; -} - -.c14 tbody tr { - transition: all 150ms; - position: relative; - border-top: 2px solid rgba(255,255,255,0.07); - border-top-left-radius: 0px; - border-top-right-radius: 0px; - border-bottom-right-radius: 8px; - border-bottom-left-radius: 8px; -} - -.c14 tbody tr:hover { - border-top: 2px solid rgba(0,0,0,0); - background-color: #222C59; -} - -.c14 tbody tr:hover:after { - box-shadow: 0px 1px 10px 0px rgba(0,0,0,0.12),0px 4px 5px 0px rgba(0,0,0,0.14),0px 2px 4px -1px rgba(0,0,0,0.20); - content: ''; - position: absolute; - top: 0; - left: 0; - z-index: -1; - width: 100%; - height: 100%; -} - -.c14 tbody tr:hover + tr { - border-top: 2px solid rgba(0,0,0,0); -} - -.c8 { - display: flex; - flex-shrink: 0; - align-items: center; - justify-content: space-between; - padding: 16px 0; - max-height: 40px; - margin-bottom: 8px; -} - -.c13 { - position: relative; - height: 100%; - right: 0; - display: flex; - align-items: center; - justify-content: center; - border-radius: 0 200px 200px 0; -} - -.c12 { - position: absolute; - height: 100%; - right: 0; - display: flex; - align-items: center; - justify-content: center; - border-left: 1px solid rgba(255,255,255,0.07); - border-radius: 0 200px 200px 0; -} - -.c10 { - position: relative; - display: flex; - overflow: hidden; - border-radius: 200px; - height: 100%; - background: transparent; - max-width: 725px; -} - -.c9 { - background: #0C143D; - border-radius: 200px; - width: 100%; - height: 56px; - margin-bottom: 12px; -} - -.c11 { - border: none; - outline: none; - box-sizing: border-box; - font-size: 16px; - width: 100%; - transition: all 0.2s; - padding-left: 16px; - padding-right: 16px; - color: #FFFFFF; - background: rgba(255,255,255,0.07); - padding-right: 184px; - padding-left: 24px; -} - -.c15 td { - height: 22px; -} -
@@ -300,1408 +73,8 @@ exports[`render clusters 1`] = ` Manage Clusters
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - Name - - - - - - - -
-
- ROOT -
-
- localhost - - -
- - nidvojik - - -
- - lidtabih - - -
- - farovluv - - -
- - rozpaari - - -
- - wetjolune - - -
- - dashawic - - -
- - jesushenry58 - - -
- - jordansimpson35 - - -
- - leonamann249 - - -
- - bessiecohen207 - - -
- - philipjohnson10 - - -
- - teresastone14 - - -
- - connorsharp137 - - -
- - ricardosingleton242 - - -
- - rozpaari - - -
- - wetjolune - - -
- - dashawic - - -
- - williepayne223 - - -
- - samlewis176 - - -
- - nellwheeler72 - - -
- - albertowens200 - - -
- - beatricecarson171 - - -
- - besscarroll152 - - -
- - hannahsutton232 - - -
- - barrynelson110 - - -
- - rozpaari - - -
- - wetjolune - - -
- - dashawic - - -
- - henriettarios78 - - -
- - josephinewolfe55 - - -
- - jaysandoval137 - - -
- - isabellekim81 - - -
- - francismoran134 - - -
- - theodorefrazier78 - - -
- - hattiestanley34 - - -
- - tommybrooks146 - - -
+ /> `; diff --git a/web/packages/teleport/src/services/audit/makeEvent.ts b/web/packages/teleport/src/services/audit/makeEvent.ts index 1ac1984b3ffa9..e09e8918fc1d8 100644 --- a/web/packages/teleport/src/services/audit/makeEvent.ts +++ b/web/packages/teleport/src/services/audit/makeEvent.ts @@ -1461,6 +1461,13 @@ export const formatters: Formatters = { return `Bot [${bot_name}] joined the cluster using the [${method}] join method`; }, }, + [eventCodes.BOT_JOIN_FAILURE]: { + type: 'bot.join', + desc: 'Bot Join Failed', + format: ({ bot_name }) => { + return `Bot [${bot_name || 'unknown'}] failed to join the cluster`; + }, + }, [eventCodes.INSTANCE_JOIN]: { type: 'instance.join', desc: 'Instance Joined', @@ -1468,6 +1475,13 @@ export const formatters: Formatters = { return `Instance [${node_name}] joined the cluster with the [${role}] role using the [${method}] join method`; }, }, + [eventCodes.INSTANCE_JOIN_FAILURE]: { + type: 'instance.join', + desc: 'Instance Join Failed', + format: ({ node_name }) => { + return `Instance [${node_name || 'unknown'}] failed to join the cluster`; + }, + }, [eventCodes.BOT_CREATED]: { type: 'bot.create', desc: 'Bot Created', diff --git a/web/packages/teleport/src/services/audit/types.ts b/web/packages/teleport/src/services/audit/types.ts index d742e39c595c0..1e050631ee482 100644 --- a/web/packages/teleport/src/services/audit/types.ts +++ b/web/packages/teleport/src/services/audit/types.ts @@ -243,7 +243,9 @@ export const eventCodes = { CERTIFICATE_CREATED: 'TC000I', UPGRADE_WINDOW_UPDATED: 'TUW01I', BOT_JOIN: 'TJ001I', + BOT_JOIN_FAILURE: 'TJ001E', INSTANCE_JOIN: 'TJ002I', + INSTANCE_JOIN_FAILURE: 'TJ002E', BOT_CREATED: 'TB001I', BOT_UPDATED: 'TB002I', BOT_DELETED: 'TB003I', @@ -1307,6 +1309,13 @@ export type RawEvents = { method: string; } >; + [eventCodes.BOT_JOIN_FAILURE]: RawEvent< + typeof eventCodes.BOT_JOIN, + { + bot_name: string; + method: string; + } + >; [eventCodes.INSTANCE_JOIN]: RawEvent< typeof eventCodes.INSTANCE_JOIN, { @@ -1315,6 +1324,14 @@ export type RawEvents = { role: string; } >; + [eventCodes.INSTANCE_JOIN_FAILURE]: RawEvent< + typeof eventCodes.INSTANCE_JOIN, + { + node_name: string; + method: string; + role: string; + } + >; [eventCodes.BOT_CREATED]: RawEvent; [eventCodes.BOT_UPDATED]: RawEvent; [eventCodes.BOT_DELETED]: RawEvent; From 2b838718b1fc34680d758afae8a7a36b9eb636ac Mon Sep 17 00:00:00 2001 From: Noah Stride Date: Tue, 23 Apr 2024 12:12:26 +0100 Subject: [PATCH 2/2] Fix broken snapshots --- .../Clusters.story.test.tsx.snap | 1635 ++++++++++++++++- 1 file changed, 1631 insertions(+), 4 deletions(-) diff --git a/web/packages/teleport/src/Clusters/__snapshots__/Clusters.story.test.tsx.snap b/web/packages/teleport/src/Clusters/__snapshots__/Clusters.story.test.tsx.snap index e1d02cfab4602..920aacdc0fac1 100644 --- a/web/packages/teleport/src/Clusters/__snapshots__/Clusters.story.test.tsx.snap +++ b/web/packages/teleport/src/Clusters/__snapshots__/Clusters.story.test.tsx.snap @@ -1,6 +1,21 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`render clusters 1`] = ` +.c16 { + display: inline-flex; + align-items: center; + justify-content: center; +} + +.c19 { + display: inline-flex; + align-items: center; + justify-content: center; + color: rgba(255,255,255,0.72); + margin-left: 8px; + margin-right: -8px; +} + .c0 { box-sizing: border-box; padding-left: 40px; @@ -12,10 +27,47 @@ exports[`render clusters 1`] = ` margin-bottom: 24px; } -.c8 { +.c18 { + line-height: 1.5; + margin: 0; + display: inline-flex; + justify-content: center; + align-items: center; box-sizing: border-box; - margin: 72px; + border: none; + border-radius: 4px; + cursor: pointer; + font-family: inherit; + font-weight: 600; + outline: none; + position: relative; text-align: center; + text-decoration: none; + text-transform: uppercase; + transition: all 0.3s; + -webkit-font-smoothing: antialiased; + color: #FFFFFF; + background: rgba(255,255,255,0); + border: 1px solid rgba(255,255,255,0.36); + font-size: 10px; + min-height: 24px; + padding: 0px 16px; + height: 24px; +} + +.c18:hover, +.c18:focus { + background: rgba(255,255,255,0.07); +} + +.c18:active { + background: rgba(255,255,255,0.13); +} + +.c18:disabled { + background: rgba(255,255,255,0.12); + color: rgba(255,255,255,0.3); + cursor: auto; } .c6 { @@ -27,6 +79,19 @@ exports[`render clusters 1`] = ` margin: 0px; } +.c17 { + box-sizing: border-box; + border-radius: 10px; + display: inline-block; + font-size: 10px; + font-weight: 500; + padding: 0 8px; + margin: 1px 0; + vertical-align: middle; + background-color: #9F85FF; + color: #000000; +} + .c1 { display: flex; } @@ -61,6 +126,168 @@ exports[`render clusters 1`] = ` padding-bottom: 24px; } +.c14 { + border-collapse: collapse; + border-spacing: 0; + border-style: hidden; + font-size: 12px; + width: 100%; +} + +.c14 > thead > tr > th, +.c14 > tbody > tr > th, +.c14 > tfoot > tr > th, +.c14 > thead > tr > td, +.c14 > tbody > tr > td, +.c14 > tfoot > tr > td { + padding: 8px 8px; + vertical-align: middle; +} + +.c14 > thead > tr > th:first-child, +.c14 > tbody > tr > th:first-child, +.c14 > tfoot > tr > th:first-child, +.c14 > thead > tr > td:first-child, +.c14 > tbody > tr > td:first-child, +.c14 > tfoot > tr > td:first-child { + padding-left: 24px; +} + +.c14 > thead > tr > th:last-child, +.c14 > tbody > tr > th:last-child, +.c14 > tfoot > tr > th:last-child, +.c14 > thead > tr > td:last-child, +.c14 > tbody > tr > td:last-child, +.c14 > tfoot > tr > td:last-child { + padding-right: 24px; +} + +.c14 > tbody > tr > td { + vertical-align: middle; +} + +.c14 > thead > tr > th { + color: #FFFFFF; + font-weight: 600; + font-size: 14px; + line-height: 24px; + cursor: pointer; + padding-bottom: 0; + padding-top: 0; + text-align: left; + white-space: nowrap; +} + +.c14 > thead > tr > th svg { + height: 12px; +} + +.c14 > tbody > tr > td { + color: #FFFFFF; + font-weight: 300; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.035px; +} + +.c14 tbody tr { + transition: all 150ms; + position: relative; + border-top: 2px solid rgba(255,255,255,0.07); + border-top-left-radius: 0px; + border-top-right-radius: 0px; + border-bottom-right-radius: 8px; + border-bottom-left-radius: 8px; +} + +.c14 tbody tr:hover { + border-top: 2px solid rgba(0,0,0,0); + background-color: #222C59; +} + +.c14 tbody tr:hover:after { + box-shadow: 0px 1px 10px 0px rgba(0,0,0,0.12),0px 4px 5px 0px rgba(0,0,0,0.14),0px 2px 4px -1px rgba(0,0,0,0.20); + content: ''; + position: absolute; + top: 0; + left: 0; + z-index: -1; + width: 100%; + height: 100%; +} + +.c14 tbody tr:hover + tr { + border-top: 2px solid rgba(0,0,0,0); +} + +.c8 { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: space-between; + padding: 16px 0; + max-height: 40px; + margin-bottom: 8px; +} + +.c13 { + position: relative; + height: 100%; + right: 0; + display: flex; + align-items: center; + justify-content: center; + border-radius: 0 200px 200px 0; +} + +.c12 { + position: absolute; + height: 100%; + right: 0; + display: flex; + align-items: center; + justify-content: center; + border-left: 1px solid rgba(255,255,255,0.07); + border-radius: 0 200px 200px 0; +} + +.c10 { + position: relative; + display: flex; + overflow: hidden; + border-radius: 200px; + height: 100%; + background: transparent; + max-width: 725px; +} + +.c9 { + background: #0C143D; + border-radius: 200px; + width: 100%; + height: 56px; + margin-bottom: 12px; +} + +.c11 { + border: none; + outline: none; + box-sizing: border-box; + font-size: 16px; + width: 100%; + transition: all 0.2s; + padding-left: 16px; + padding-right: 16px; + color: #FFFFFF; + background: rgba(255,255,255,0.07); + padding-right: 184px; + padding-left: 24px; +} + +.c15 td { + height: 22px; +} +
@@ -73,8 +300,1408 @@ exports[`render clusters 1`] = ` Manage Clusters
-
+ > +
+
+ +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + Name + + + + + + + +
+
+ ROOT +
+
+ localhost + + +
+ + nidvojik + + +
+ + lidtabih + + +
+ + farovluv + + +
+ + rozpaari + + +
+ + wetjolune + + +
+ + dashawic + + +
+ + jesushenry58 + + +
+ + jordansimpson35 + + +
+ + leonamann249 + + +
+ + bessiecohen207 + + +
+ + philipjohnson10 + + +
+ + teresastone14 + + +
+ + connorsharp137 + + +
+ + ricardosingleton242 + + +
+ + rozpaari + + +
+ + wetjolune + + +
+ + dashawic + + +
+ + williepayne223 + + +
+ + samlewis176 + + +
+ + nellwheeler72 + + +
+ + albertowens200 + + +
+ + beatricecarson171 + + +
+ + besscarroll152 + + +
+ + hannahsutton232 + + +
+ + barrynelson110 + + +
+ + rozpaari + + +
+ + wetjolune + + +
+ + dashawic + + +
+ + henriettarios78 + + +
+ + josephinewolfe55 + + +
+ + jaysandoval137 + + +
+ + isabellekim81 + + +
+ + francismoran134 + + +
+ + theodorefrazier78 + + +
+ + hattiestanley34 + + +
+ + tommybrooks146 + + +
`;