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 }