| // Copyright 2016 The LUCI Authors. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package authdb |
| |
| import ( |
| "context" |
| "io" |
| "io/ioutil" |
| "net" |
| "strings" |
| |
| "github.com/golang/protobuf/proto" |
| |
| "go.chromium.org/luci/auth/identity" |
| "go.chromium.org/luci/common/errors" |
| "go.chromium.org/luci/common/logging" |
| "go.chromium.org/luci/common/trace" |
| "go.chromium.org/luci/server/auth/realms" |
| "go.chromium.org/luci/server/auth/service/protocol" |
| "go.chromium.org/luci/server/auth/signing" |
| |
| "go.chromium.org/luci/server/auth/authdb/internal/certs" |
| "go.chromium.org/luci/server/auth/authdb/internal/graph" |
| "go.chromium.org/luci/server/auth/authdb/internal/ipaddr" |
| "go.chromium.org/luci/server/auth/authdb/internal/oauthid" |
| "go.chromium.org/luci/server/auth/authdb/internal/realmset" |
| "go.chromium.org/luci/server/auth/authdb/internal/seccfg" |
| ) |
| |
| // SnapshotDB implements DB using AuthDB proto message. |
| // |
| // Use NewSnapshotDB to create new instances. Don't touch public fields |
| // of existing instances. |
| // |
| // Zero value represents an empty AuthDB. |
| type SnapshotDB struct { |
| AuthServiceURL string // where it was fetched from |
| Rev int64 // its revision number |
| |
| groups *graph.QueryableGraph // queryable representation of groups |
| realms *realmset.Realms // queryable representation of realms |
| clientIDs oauthid.Whitelist // set of allowed client IDs |
| whitelistedIPs ipaddr.Whitelist // set of named IP whitelists |
| securityCfg *seccfg.SecurityConfig // parsed SecurityConfig proto |
| |
| tokenServiceURL string // URL of the token server as provided by Auth service |
| tokenServiceCerts certs.Bundle // cached public keys of the token server |
| } |
| |
| var _ DB = &SnapshotDB{} |
| |
| // Revision returns a revision of an auth DB or 0 if it can't be determined. |
| // |
| // It's just a small helper that casts db to *SnapshotDB and extracts the |
| // revision from there. |
| func Revision(db DB) int64 { |
| if snap, _ := db.(*SnapshotDB); snap != nil { |
| return snap.Rev |
| } |
| return 0 |
| } |
| |
| // SnapshotDBFromTextProto constructs SnapshotDB by loading it from a text proto |
| // with AuthDB message. |
| func SnapshotDBFromTextProto(r io.Reader) (*SnapshotDB, error) { |
| blob, err := ioutil.ReadAll(r) |
| if err != nil { |
| return nil, errors.Annotate(err, "failed to read the file").Err() |
| } |
| msg := &protocol.AuthDB{} |
| if err := proto.UnmarshalText(string(blob), msg); err != nil { |
| return nil, errors.Annotate(err, "not a valid AuthDB text proto file").Err() |
| } |
| db, err := NewSnapshotDB(msg, "", 0, true) |
| if err != nil { |
| return nil, errors.Annotate(err, "failed to validate AuthDB").Err() |
| } |
| return db, nil |
| } |
| |
| // NewSnapshotDB creates new instance of SnapshotDB. |
| // |
| // It does some preprocessing to speed up subsequent checks. Returns errors if |
| // it encounters inconsistencies. |
| // |
| // If 'validate' is false, skips some expensive validation steps, assuming they |
| // were performed before, when AuthDB was initially received. |
| func NewSnapshotDB(authDB *protocol.AuthDB, authServiceURL string, rev int64, validate bool) (*SnapshotDB, error) { |
| if validate { |
| if err := validateAuthDB(authDB); err != nil { |
| return nil, err |
| } |
| } |
| |
| groups, err := graph.BuildQueryable(authDB.Groups) |
| if err != nil { |
| return nil, errors.Annotate(err, "failed to build groups graph").Err() |
| } |
| |
| var realms *realmset.Realms |
| if authDB.Realms != nil { |
| realms, err = realmset.Build(authDB.Realms, groups) |
| if err != nil { |
| return nil, errors.Annotate(err, "failed to prepare Realms DB").Err() |
| } |
| } |
| |
| ipWL, err := ipaddr.NewWhitelist(authDB.IpWhitelists, authDB.IpWhitelistAssignments) |
| if err != nil { |
| return nil, errors.Annotate(err, "bad IP whitelists in AuthDB").Err() |
| } |
| |
| securityCfg, err := seccfg.Parse(authDB.SecurityConfig) |
| if err != nil { |
| return nil, errors.Annotate(err, "bad SecurityConfig").Err() |
| } |
| |
| return &SnapshotDB{ |
| AuthServiceURL: authServiceURL, |
| Rev: rev, |
| groups: groups, |
| realms: realms, |
| clientIDs: oauthid.NewWhitelist(authDB.OauthClientId, authDB.OauthAdditionalClientIds), |
| whitelistedIPs: ipWL, |
| securityCfg: securityCfg, |
| tokenServiceURL: authDB.TokenServerUrl, |
| tokenServiceCerts: certs.Bundle{ServiceURL: authDB.TokenServerUrl}, |
| }, nil |
| } |
| |
| // IsAllowedOAuthClientID returns true if the given OAuth2 client ID can be used |
| // to authorize access from the given email. |
| func (db *SnapshotDB) IsAllowedOAuthClientID(_ context.Context, email, clientID string) (bool, error) { |
| return db.clientIDs.IsAllowedOAuthClientID(email, clientID), nil |
| } |
| |
| // IsInternalService returns true if the given hostname belongs to a service |
| // that is a part of the current LUCI deployment. |
| // |
| // What hosts are internal is controlled by 'internal_service_regexp' setting |
| // in security.cfg in the Auth Service configs. |
| func (db *SnapshotDB) IsInternalService(c context.Context, hostname string) (bool, error) { |
| if db.securityCfg != nil { |
| return db.securityCfg.IsInternalService(hostname), nil |
| } |
| return false, nil |
| } |
| |
| // IsMember returns true if the given identity belongs to any of the groups. |
| // |
| // Unknown groups are considered empty. May return errors if underlying |
| // datastore has issues. |
| func (db *SnapshotDB) IsMember(c context.Context, id identity.Identity, groups []string) (bool, error) { |
| if db.groups == nil { |
| return false, nil |
| } |
| |
| _, span := trace.StartSpan(c, "go.chromium.org/luci/server/auth/authdb.IsMember") |
| span.Attribute("cr.dev/groups", strings.Join(groups, ", ")) |
| defer span.End(nil) |
| |
| // TODO(vadimsh): Optimize multi-group case. |
| for _, gr := range groups { |
| if db.groups.IsMember(id, gr) { |
| return true, nil |
| } |
| } |
| return false, nil |
| } |
| |
| // CheckMembership returns groups from the given list the identity belongs to. |
| // |
| // Unlike IsMember, it doesn't stop on the first hit but continues evaluating |
| // all groups. |
| // |
| // Unknown groups are considered empty. The order of groups in the result may |
| // be different from the order in 'groups'. |
| // |
| // May return errors if underlying datastore has issues. |
| func (db *SnapshotDB) CheckMembership(c context.Context, id identity.Identity, groups []string) (out []string, err error) { |
| if db.groups == nil { |
| return |
| } |
| |
| _, span := trace.StartSpan(c, "go.chromium.org/luci/server/auth/authdb.CheckMembership") |
| span.Attribute("cr.dev/groups", strings.Join(groups, ", ")) |
| defer span.End(nil) |
| |
| // TODO(vadimsh): Optimize multi-group case. |
| for _, gr := range groups { |
| if db.groups.IsMember(id, gr) { |
| out = append(out, gr) |
| } |
| } |
| return |
| } |
| |
| // HasPermission returns true if the identity has the given permission in any |
| // of the realms. |
| func (db *SnapshotDB) HasPermission(c context.Context, id identity.Identity, perm realms.Permission, realm string) (ok bool, err error) { |
| _, span := trace.StartSpan(c, "go.chromium.org/luci/server/auth/authdb.HasPermission") |
| span.Attribute("cr.dev/permission", perm.Name()) |
| span.Attribute("cr.dev/realm", realm) |
| defer func() { span.End(err) }() |
| |
| // This may happen if the AuthDB proto has no Realms yet. |
| if db.realms == nil { |
| return false, errors.Reason("Realms API is not available").Err() |
| } |
| |
| permIdx, ok := db.realms.PermissionIndex(perm) |
| if !ok { |
| logging.Warningf(c, "Checking permission %q not present in the AuthDB", perm) |
| return false, nil |
| } |
| |
| // Verify such realm is defined in the DB or fallback to its @root. |
| if !db.realms.HasRealm(realm) { |
| if err := realms.ValidateRealmName(realm, realms.GlobalScope); err != nil { |
| return false, errors.Annotate(err, "when checking %q", perm).Err() |
| } |
| project, name := realms.Split(realm) |
| root := realms.Join(project, realms.RootRealm) |
| if realm == root { |
| logging.Warningf(c, "Checking %q in a non-existing root realm %q: denying", perm, realm) |
| return false, nil |
| } |
| if !db.realms.HasRealm(root) { |
| logging.Warningf(c, "Checking %q in a non-existing realm %q that doesn't have a root realm (no such project?): denying", perm, realm) |
| return false, nil |
| } |
| // Don't log @legacy => @root fallbacks, they are semi-expected. |
| if name != realms.LegacyRealm { |
| logging.Warningf(c, "Checking %q in a non-existing realm %q: falling back to the root realm %q", perm, realm, root) |
| } |
| realm = root |
| } |
| |
| // For the given <realm, permission> pair, get indexes of groups with |
| // principals that have the permission (if any) and a set of identities |
| // mentioned in the realm ACL explicitly (not via a group). |
| switch groups, idents := db.realms.QueryAuthorized(realm, permIdx); { |
| case idents.Has(string(id)): |
| return true, nil // `id` was granted the permission explicitly in the ACL |
| case db.groups.IsMemberOfAny(id, groups): |
| return true, nil // `id` has the permission through a group |
| default: |
| return false, nil |
| } |
| } |
| |
| // GetCertificates returns a bundle with certificates of a trusted signer. |
| // |
| // Currently only the Token Server is a trusted signer. |
| func (db *SnapshotDB) GetCertificates(c context.Context, signerID identity.Identity) (*signing.PublicCertificates, error) { |
| if db.tokenServiceURL == "" { |
| logging.Warningf( |
| c, "Delegation is not supported, the token server URL is not set by %s", |
| db.AuthServiceURL) |
| return nil, nil |
| } |
| switch tokenServerID, certs, err := db.tokenServiceCerts.GetCerts(c); { |
| case err != nil: |
| return nil, err |
| case signerID != tokenServerID: |
| return nil, nil // signerID is not trusted since it's not a token server |
| default: |
| return certs, nil |
| } |
| } |
| |
| // GetWhitelistForIdentity returns name of the IP whitelist to use to check |
| // IP of requests from given `ident`. |
| // |
| // It's used to restrict access for certain account to certain IP subnets. |
| // |
| // Returns ("", nil) if `ident` is not IP restricted. |
| func (db *SnapshotDB) GetWhitelistForIdentity(c context.Context, ident identity.Identity) (string, error) { |
| return db.whitelistedIPs.GetWhitelistForIdentity(ident), nil |
| } |
| |
| // IsInWhitelist returns true if IP address belongs to given named IP whitelist. |
| // |
| // IP whitelist is a set of IP subnets. Unknown IP whitelists are considered |
| // empty. May return errors if underlying datastore has issues. |
| func (db *SnapshotDB) IsInWhitelist(c context.Context, ip net.IP, whitelist string) (bool, error) { |
| return db.whitelistedIPs.IsInWhitelist(ip, whitelist), nil |
| } |
| |
| // GetAuthServiceURL returns root URL ("https://<host>") of the auth service |
| // the snapshot was fetched from. |
| // |
| // This is needed to implement authdb.DB interface. |
| func (db *SnapshotDB) GetAuthServiceURL(c context.Context) (string, error) { |
| if db.AuthServiceURL == "" { |
| return "", errors.Reason("not using Auth Service").Err() |
| } |
| return db.AuthServiceURL, nil |
| } |
| |
| // GetTokenServiceURL returns root URL ("https://<host>") of the token server. |
| // |
| // This is needed to implement authdb.DB interface. |
| func (db *SnapshotDB) GetTokenServiceURL(c context.Context) (string, error) { |
| return db.tokenServiceURL, nil |
| } |
| |
| // GetRealmData returns data attached to a realm. |
| func (db *SnapshotDB) GetRealmData(ctx context.Context, realm string) (*protocol.RealmData, error) { |
| // This may happen if the AuthDB proto has no Realms yet. |
| if db.realms == nil { |
| return nil, errors.Reason("Realms API is not available").Err() |
| } |
| |
| // Verify such realm is defined in the DB or fallback to its @root. |
| if !db.realms.HasRealm(realm) { |
| if err := realms.ValidateRealmName(realm, realms.GlobalScope); err != nil { |
| return nil, err |
| } |
| project, _ := realms.Split(realm) |
| root := realms.Join(project, realms.RootRealm) |
| if realm == root || !db.realms.HasRealm(root) { |
| return nil, nil // no such project or it doesn't have realms.cfg |
| } |
| realm = root |
| } |
| |
| data := db.realms.Data(realm) |
| if data == nil { |
| data = &protocol.RealmData{} |
| } |
| return data, nil |
| } |