go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/auth_service/impl/model/snapshot.go (about)

     1  // Copyright 2021 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package model
    16  
    17  import (
    18  	"context"
    19  
    20  	"go.opentelemetry.io/otel"
    21  	"go.opentelemetry.io/otel/codes"
    22  	"golang.org/x/sync/errgroup"
    23  
    24  	"go.chromium.org/luci/common/errors"
    25  	"go.chromium.org/luci/gae/service/datastore"
    26  	"go.chromium.org/luci/server/auth/authdb"
    27  	"go.chromium.org/luci/server/auth/service/protocol"
    28  )
    29  
    30  var tracer = otel.Tracer("go.chromium.org/luci/auth_service")
    31  
    32  // Snapshot contains transactionally captured AuthDB entities.
    33  type Snapshot struct {
    34  	ReplicationState *AuthReplicationState
    35  	GlobalConfig     *AuthGlobalConfig
    36  	Groups           []*AuthGroup
    37  	IPAllowlists     []*AuthIPAllowlist
    38  	RealmsGlobals    *AuthRealmsGlobals
    39  	ProjectRealms    []*AuthProjectRealms
    40  
    41  	// TODO:
    42  	//   IPAllowlistAssignments
    43  }
    44  
    45  // TakeSnapshot takes a consistent snapshot of the replicated subset of AuthDB
    46  // entities.
    47  //
    48  // Runs a read-only transaction internally.
    49  func TakeSnapshot(ctx context.Context) (snap *Snapshot, err error) {
    50  	// This is a potentially slow operation. Capture it in the trace.
    51  	ctx, span := tracer.Start(ctx, "go.chromium.org/luci/auth_service/impl/model/TakeSnapshot")
    52  	defer func() {
    53  		if err != nil {
    54  			span.RecordError(err)
    55  			span.SetStatus(codes.Error, err.Error())
    56  		}
    57  		span.End()
    58  	}()
    59  
    60  	err = datastore.RunInTransaction(ctx, func(ctx context.Context) error {
    61  		snap = &Snapshot{
    62  			ReplicationState: &AuthReplicationState{
    63  				Kind:   "AuthReplicationState",
    64  				ID:     "self",
    65  				Parent: RootKey(ctx),
    66  			},
    67  			GlobalConfig: &AuthGlobalConfig{
    68  				Kind: "AuthGlobalConfig",
    69  				ID:   "root",
    70  			},
    71  		}
    72  
    73  		gr, ctx := errgroup.WithContext(ctx)
    74  		gr.Go(func() error {
    75  			return datastore.Get(ctx, snap.GlobalConfig)
    76  		})
    77  		gr.Go(func() error {
    78  			return datastore.Get(ctx, snap.ReplicationState)
    79  		})
    80  		gr.Go(func() (err error) {
    81  			snap.Groups, err = GetAllAuthGroups(ctx)
    82  			return
    83  		})
    84  		gr.Go(func() (err error) {
    85  			snap.IPAllowlists, err = GetAllAuthIPAllowlists(ctx)
    86  			return
    87  		})
    88  		gr.Go(func() (err error) {
    89  			snap.RealmsGlobals, err = GetAuthRealmsGlobals(ctx)
    90  			return
    91  		})
    92  		gr.Go(func() (err error) {
    93  			snap.ProjectRealms, err = GetAllAuthProjectRealms(ctx)
    94  			return
    95  		})
    96  
    97  		// TODO:
    98  		//  IPAllowlistAssignments
    99  
   100  		return gr.Wait()
   101  	}, &datastore.TransactionOptions{ReadOnly: true})
   102  
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  	return snap, nil
   107  }
   108  
   109  // ToAuthDBProto converts the snapshot to an AuthDB proto message.
   110  func (s *Snapshot) ToAuthDBProto(useV1Perms bool) (*protocol.AuthDB, error) {
   111  	groups := make([]*protocol.AuthGroup, len(s.Groups))
   112  	for i, v := range s.Groups {
   113  		groups[i] = &protocol.AuthGroup{
   114  			Name:        v.ID,
   115  			Members:     v.Members,
   116  			Globs:       v.Globs,
   117  			Nested:      v.Nested,
   118  			Description: setStringField(v.Description),
   119  			CreatedTs:   v.CreatedTS.UnixNano() / 1000,
   120  			CreatedBy:   v.CreatedBy,
   121  			ModifiedTs:  v.ModifiedTS.UnixNano() / 1000,
   122  			ModifiedBy:  v.ModifiedBy,
   123  			Owners:      v.Owners,
   124  		}
   125  	}
   126  
   127  	allowlists := make([]*protocol.AuthIPWhitelist, len(s.IPAllowlists))
   128  	for i, v := range s.IPAllowlists {
   129  		allowlists[i] = &protocol.AuthIPWhitelist{
   130  			Name:        v.ID,
   131  			Subnets:     v.Subnets,
   132  			Description: setStringField(v.Description),
   133  			CreatedTs:   v.CreatedTS.UnixNano() / 1000,
   134  			CreatedBy:   v.CreatedBy,
   135  			ModifiedTs:  v.ModifiedTS.UnixNano() / 1000,
   136  			ModifiedBy:  v.ModifiedBy,
   137  		}
   138  	}
   139  
   140  	realms, err := MergeRealms(s.RealmsGlobals, s.ProjectRealms, useV1Perms)
   141  	if err != nil {
   142  		return nil, errors.Annotate(err, "error merging realms").Err()
   143  	}
   144  
   145  	return &protocol.AuthDB{
   146  		OauthClientId:            setStringField(s.GlobalConfig.OAuthClientID),
   147  		OauthClientSecret:        setStringField(s.GlobalConfig.OAuthClientSecret),
   148  		OauthAdditionalClientIds: s.GlobalConfig.OAuthAdditionalClientIDs,
   149  		TokenServerUrl:           setStringField(s.GlobalConfig.TokenServerURL),
   150  		SecurityConfig:           s.GlobalConfig.SecurityConfig,
   151  		Groups:                   groups,
   152  		IpWhitelists:             allowlists,
   153  		Realms:                   realms,
   154  		IpWhitelistAssignments:   nil, // TODO
   155  	}, nil
   156  }
   157  
   158  // setStringField returns a non-empty string, given a string; it
   159  // defaults to the string value "empty".
   160  func setStringField(value string) string {
   161  	if value != "" {
   162  		return value
   163  	}
   164  	return "empty"
   165  }
   166  
   167  // ToAuthDB converts the snapshot to an authdb.SnapshotDB.
   168  //
   169  // It then can be used by the auth service itself to make ACL checks.
   170  func (s *Snapshot) ToAuthDB(useV1Perms bool) (*authdb.SnapshotDB, error) {
   171  	authDBProto, err := s.ToAuthDBProto(useV1Perms)
   172  	if err != nil {
   173  		return nil, errors.Annotate(err,
   174  			"failed converting AuthDB snapshot to proto").Err()
   175  	}
   176  	return authdb.NewSnapshotDB(authDBProto, "", s.ReplicationState.AuthDBRev, false)
   177  }