verify / verify (push) Successful in 4m17s
Signed-off-by: Cole Speelman <crspeelman@gmail.com>
415 lines
20 KiB
Go
415 lines
20 KiB
Go
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package authsqlite
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"slices"
|
|
"time"
|
|
|
|
"gamertan.com/web/access"
|
|
)
|
|
|
|
func (store *Store) SeedAccessPolicy(ctx context.Context, policy access.Policy) error {
|
|
if err := policy.Validate(); err != nil {
|
|
return err
|
|
}
|
|
tx, err := store.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
for name, description := range policy.Roles {
|
|
if _, err = tx.ExecContext(ctx, `INSERT INTO gwf_access_roles(name,description) VALUES(?,?) ON CONFLICT(name) DO UPDATE SET description=excluded.description`, name, description); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for name, description := range policy.Permissions {
|
|
if _, err = tx.ExecContext(ctx, `INSERT INTO gwf_access_permissions(name,description) VALUES(?,?) ON CONFLICT(name) DO UPDATE SET description=excluded.description`, name, description); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for role, permissions := range policy.Grants {
|
|
if _, err = tx.ExecContext(ctx, `DELETE FROM gwf_access_role_permissions WHERE role_name=?`, role); err != nil {
|
|
return err
|
|
}
|
|
for _, permission := range permissions {
|
|
if _, err = tx.ExecContext(ctx, `INSERT INTO gwf_access_role_permissions(role_name,permission_name) VALUES(?,?)`, role, permission); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
func (store *Store) Grant(ctx context.Context, binding access.Binding) error {
|
|
if !opaqueID(binding.ID) || (binding.SubjectKind != access.User && binding.SubjectKind != access.Team) || !opaqueID(binding.SubjectID) || !safeName(binding.Role) || binding.Scope.Validate() != nil || !opaqueID(binding.GrantedBy) || binding.GrantedAt.IsZero() {
|
|
return errors.New("authsqlite: invalid access binding")
|
|
}
|
|
tx, err := store.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
var exists int
|
|
query := `SELECT COUNT(*) FROM gwf_organization_memberships m JOIN gwf_organizations o ON o.id=m.organization_id AND o.status='active' WHERE m.organization_id=? AND m.user_id=? AND m.status='active'`
|
|
if binding.SubjectKind == access.Team {
|
|
query = `SELECT COUNT(*) FROM gwf_teams t JOIN gwf_organizations o ON o.id=t.organization_id AND o.status='active' WHERE t.organization_id=? AND t.id=? AND t.status='active'`
|
|
}
|
|
if err = tx.QueryRowContext(ctx, query, binding.Scope.OrganizationID, binding.SubjectID).Scan(&exists); err != nil {
|
|
return err
|
|
}
|
|
if exists != 1 {
|
|
return errors.New("authsqlite: access subject is not active in organization")
|
|
}
|
|
if err = tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM gwf_organization_memberships m JOIN gwf_organizations o ON o.id=m.organization_id AND o.status='active' WHERE m.organization_id=? AND m.user_id=? AND m.status='active'`, binding.Scope.OrganizationID, binding.GrantedBy).Scan(&exists); err != nil || exists != 1 {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return errors.New("authsqlite: grantor is not active in organization")
|
|
}
|
|
scopeQuery, arguments := `SELECT 1`, []any{}
|
|
switch {
|
|
case binding.Scope.ServiceID != "":
|
|
scopeQuery, arguments = `SELECT COUNT(*) FROM gwf_application_services WHERE id=? AND environment_id=? AND project_id=? AND organization_id=?`, []any{binding.Scope.ServiceID, binding.Scope.EnvironmentID, binding.Scope.ProjectID, binding.Scope.OrganizationID}
|
|
case binding.Scope.EnvironmentID != "":
|
|
scopeQuery, arguments = `SELECT COUNT(*) FROM gwf_environments WHERE id=? AND project_id=? AND organization_id=?`, []any{binding.Scope.EnvironmentID, binding.Scope.ProjectID, binding.Scope.OrganizationID}
|
|
case binding.Scope.ProjectID != "":
|
|
scopeQuery, arguments = `SELECT COUNT(*) FROM gwf_projects WHERE id=? AND organization_id=?`, []any{binding.Scope.ProjectID, binding.Scope.OrganizationID}
|
|
}
|
|
if err = tx.QueryRowContext(ctx, scopeQuery, arguments...).Scan(&exists); err != nil {
|
|
return err
|
|
}
|
|
if exists != 1 {
|
|
return errors.New("authsqlite: access scope does not exist")
|
|
}
|
|
if _, err = tx.ExecContext(ctx, `INSERT INTO gwf_access_bindings(id,organization_id,subject_kind,subject_id,role_name,project_id,environment_id,service_id,granted_by_user_id,granted_at) VALUES(?,?,?,?,?,NULLIF(?,''),NULLIF(?,''),NULLIF(?,''),?,?)`, binding.ID, binding.Scope.OrganizationID, binding.SubjectKind, binding.SubjectID, binding.Role, binding.Scope.ProjectID, binding.Scope.EnvironmentID, binding.Scope.ServiceID, binding.GrantedBy, binding.GrantedAt.Unix()); err != nil {
|
|
return err
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
func (store *Store) Revoke(ctx context.Context, bindingID, actorUserID string, when time.Time) error {
|
|
if !opaqueID(bindingID) || !opaqueID(actorUserID) || when.IsZero() {
|
|
return errors.New("authsqlite: invalid access revocation")
|
|
}
|
|
result, err := store.db.ExecContext(ctx, `UPDATE gwf_access_bindings SET revoked_by_user_id=?,revoked_at=? WHERE id=? AND revoked_at IS NULL`, actorUserID, when.Unix(), bindingID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if changed, _ := result.RowsAffected(); changed != 1 {
|
|
return errors.New("authsqlite: access binding not found")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (store *Store) EffectiveBindings(ctx context.Context, organizationID, userID string) ([]access.Binding, error) {
|
|
if !opaqueID(organizationID) || !opaqueID(userID) {
|
|
return nil, errors.New("authsqlite: invalid access query")
|
|
}
|
|
rows, err := store.db.QueryContext(ctx, `SELECT b.id,b.subject_kind,b.subject_id,b.role_name,b.project_id,b.environment_id,b.service_id,b.granted_by_user_id,b.granted_at
|
|
FROM gwf_access_bindings b
|
|
JOIN gwf_organizations o ON o.id=b.organization_id AND o.status='active'
|
|
WHERE b.organization_id=? AND b.revoked_at IS NULL
|
|
AND EXISTS (SELECT 1 FROM gwf_organization_memberships m WHERE m.organization_id=b.organization_id AND m.user_id=? AND m.status='active')
|
|
AND ((b.subject_kind='user' AND b.subject_id=?) OR (b.subject_kind='team' AND EXISTS (SELECT 1 FROM gwf_team_members tm JOIN gwf_teams t ON t.id=tm.team_id WHERE tm.team_id=b.subject_id AND tm.user_id=? AND t.organization_id=b.organization_id AND t.status='active')))
|
|
ORDER BY b.id`, organizationID, userID, userID, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var result []access.Binding
|
|
for rows.Next() {
|
|
var binding access.Binding
|
|
var project, environment, service sql.NullString
|
|
var granted int64
|
|
if err = rows.Scan(&binding.ID, &binding.SubjectKind, &binding.SubjectID, &binding.Role, &project, &environment, &service, &binding.GrantedBy, &granted); err != nil {
|
|
return nil, err
|
|
}
|
|
binding.Scope = access.Scope{OrganizationID: organizationID, ProjectID: project.String, EnvironmentID: environment.String, ServiceID: service.String}
|
|
binding.GrantedAt = time.Unix(granted, 0).UTC()
|
|
result = append(result, binding)
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
func (store *Store) OrganizationUserBindings(ctx context.Context, organizationID string, limit int) ([]access.Binding, error) {
|
|
if !opaqueID(organizationID) || limit < 1 || limit > 2000 {
|
|
return nil, errors.New("authsqlite: invalid organization binding query")
|
|
}
|
|
rows, err := store.db.QueryContext(ctx, `SELECT b.id,b.subject_id,b.role_name,b.granted_by_user_id,b.granted_at
|
|
FROM gwf_access_bindings b
|
|
JOIN gwf_organization_memberships m ON m.organization_id=b.organization_id AND m.user_id=b.subject_id
|
|
WHERE b.organization_id=? AND b.subject_kind='user'
|
|
AND b.project_id IS NULL AND b.environment_id IS NULL AND b.service_id IS NULL
|
|
AND b.revoked_at IS NULL
|
|
ORDER BY b.subject_id,b.role_name,b.id
|
|
LIMIT ?`, organizationID, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
result := make([]access.Binding, 0)
|
|
for rows.Next() {
|
|
var binding access.Binding
|
|
var granted int64
|
|
if err = rows.Scan(&binding.ID, &binding.SubjectID, &binding.Role, &binding.GrantedBy, &granted); err != nil {
|
|
return nil, err
|
|
}
|
|
binding.SubjectKind = access.User
|
|
binding.Scope = access.Scope{OrganizationID: organizationID}
|
|
binding.GrantedAt = time.Unix(granted, 0).UTC()
|
|
result = append(result, binding)
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
func (store *Store) ReplaceOrganizationUserRole(ctx context.Context, expected []string, replacement access.Binding, ownerRole string, audit access.AuditEvent) error {
|
|
return store.replaceOrganizationUserRoles(ctx, expected, []access.Binding{replacement}, ownerRole, audit, false)
|
|
}
|
|
|
|
func (store *Store) ReplaceOrganizationUserRoles(ctx context.Context, expected []string, replacements []access.Binding, ownerRole string, audit access.AuditEvent) error {
|
|
return store.replaceOrganizationUserRoles(ctx, expected, replacements, ownerRole, audit, true)
|
|
}
|
|
|
|
func (store *Store) replaceOrganizationUserRoles(ctx context.Context, expected []string, replacements []access.Binding, ownerRole string, audit access.AuditEvent, requireOwner bool) error {
|
|
if len(replacements) < 1 || len(replacements) > 16 {
|
|
return errors.New("authsqlite: invalid organization role set")
|
|
}
|
|
replacement := replacements[0]
|
|
roles := make([]string, 0, len(replacements))
|
|
ids := make(map[string]bool, len(replacements))
|
|
for _, value := range replacements {
|
|
if !validOrganizationRoleReplacement(expected, value, ownerRole, audit) || value.SubjectID != replacement.SubjectID || value.Scope != replacement.Scope || value.GrantedBy != replacement.GrantedBy || !value.GrantedAt.Equal(replacement.GrantedAt) || ids[value.ID] || slices.Contains(roles, value.Role) {
|
|
return errors.New("authsqlite: invalid organization role set")
|
|
}
|
|
roles = append(roles, value.Role)
|
|
ids[value.ID] = true
|
|
}
|
|
slices.Sort(roles)
|
|
tx, err := store.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
// Acquire the SQLite write lock before reading the optimistic binding set.
|
|
// This serializes competing role replacements so the loser observes the
|
|
// committed binding IDs and returns ErrRoleChangeConflict instead of an
|
|
// ambiguous busy-snapshot error.
|
|
result, err := tx.ExecContext(ctx, `UPDATE gwf_organization_memberships SET status=status
|
|
WHERE organization_id=? AND user_id=? AND status='active'
|
|
AND EXISTS (SELECT 1 FROM gwf_organizations o WHERE o.id=? AND o.status='active')
|
|
AND EXISTS (SELECT 1 FROM gwf_users u WHERE u.id=? AND u.status='active' AND u.registration_pending=0)`, replacement.Scope.OrganizationID, replacement.GrantedBy, replacement.Scope.OrganizationID, replacement.GrantedBy)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if changed, _ := result.RowsAffected(); changed != 1 {
|
|
return errors.New("authsqlite: role grantor is not active in organization")
|
|
}
|
|
|
|
var active int
|
|
if err = tx.QueryRowContext(ctx, `SELECT COUNT(*)
|
|
FROM gwf_organization_memberships m
|
|
JOIN gwf_organizations o ON o.id=m.organization_id AND o.status='active'
|
|
JOIN gwf_users u ON u.id=m.user_id AND u.status='active' AND u.registration_pending=0
|
|
WHERE m.organization_id=? AND m.user_id=? AND m.status='active'`, replacement.Scope.OrganizationID, replacement.SubjectID).Scan(&active); err != nil {
|
|
return err
|
|
}
|
|
if active != 1 {
|
|
return errors.New("authsqlite: access subject is not active in organization")
|
|
}
|
|
|
|
rows, err := tx.QueryContext(ctx, `SELECT id,role_name FROM gwf_access_bindings
|
|
WHERE organization_id=? AND subject_kind='user' AND subject_id=?
|
|
AND project_id IS NULL AND environment_id IS NULL AND service_id IS NULL
|
|
AND revoked_at IS NULL ORDER BY id`, replacement.Scope.OrganizationID, replacement.SubjectID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var currentIDs []string
|
|
var currentRoles []string
|
|
for rows.Next() {
|
|
var id, role string
|
|
if err = rows.Scan(&id, &role); err != nil {
|
|
rows.Close()
|
|
return err
|
|
}
|
|
currentIDs = append(currentIDs, id)
|
|
currentRoles = append(currentRoles, role)
|
|
}
|
|
if err = rows.Err(); err != nil {
|
|
rows.Close()
|
|
return err
|
|
}
|
|
if err = rows.Close(); err != nil {
|
|
return err
|
|
}
|
|
if !slices.Equal(currentIDs, expected) {
|
|
return access.ErrRoleChangeConflict
|
|
}
|
|
slices.Sort(currentRoles)
|
|
if slices.Equal(currentRoles, roles) {
|
|
return access.ErrRoleUnchanged
|
|
}
|
|
if requireOwner || slices.Contains(roles, ownerRole) || slices.Contains(currentRoles, ownerRole) {
|
|
actorIsOwner, ownerErr := hasDirectOwnerRole(ctx, tx, replacement.Scope.OrganizationID, replacement.GrantedBy, ownerRole)
|
|
if ownerErr != nil {
|
|
return ownerErr
|
|
}
|
|
if !actorIsOwner {
|
|
return access.ErrOwnerAuthority
|
|
}
|
|
}
|
|
if !slices.Contains(roles, ownerRole) && slices.Contains(currentRoles, ownerRole) {
|
|
var otherOwners int
|
|
if err = tx.QueryRowContext(ctx, `SELECT COUNT(DISTINCT b.subject_id)
|
|
FROM gwf_access_bindings b
|
|
JOIN gwf_organization_memberships m ON m.organization_id=b.organization_id AND m.user_id=b.subject_id AND m.status='active'
|
|
JOIN gwf_users u ON u.id=m.user_id AND u.status='active' AND u.registration_pending=0
|
|
WHERE b.organization_id=? AND b.subject_kind='user' AND b.subject_id<>? AND b.role_name=?
|
|
AND b.project_id IS NULL AND b.environment_id IS NULL AND b.service_id IS NULL
|
|
AND b.revoked_at IS NULL`, replacement.Scope.OrganizationID, replacement.SubjectID, ownerRole).Scan(&otherOwners); err != nil {
|
|
return err
|
|
}
|
|
if otherOwners == 0 {
|
|
return access.ErrLastOwner
|
|
}
|
|
}
|
|
|
|
if _, err = tx.ExecContext(ctx, `UPDATE gwf_access_bindings SET revoked_by_user_id=?,revoked_at=?
|
|
WHERE organization_id=? AND subject_kind='user' AND subject_id=?
|
|
AND project_id IS NULL AND environment_id IS NULL AND service_id IS NULL
|
|
AND revoked_at IS NULL`, replacement.GrantedBy, replacement.GrantedAt.Unix(), replacement.Scope.OrganizationID, replacement.SubjectID); err != nil {
|
|
return err
|
|
}
|
|
for _, value := range replacements {
|
|
result, err = tx.ExecContext(ctx, `INSERT INTO gwf_access_bindings(id,organization_id,subject_kind,subject_id,role_name,project_id,environment_id,service_id,granted_by_user_id,granted_at)
|
|
SELECT ?,?,'user',?,?,NULL,NULL,NULL,?,? FROM gwf_access_roles WHERE name=?`, value.ID, value.Scope.OrganizationID, value.SubjectID, value.Role, value.GrantedBy, value.GrantedAt.Unix(), value.Role)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if changed, _ := result.RowsAffected(); changed != 1 {
|
|
return errors.New("authsqlite: replacement role has not been seeded")
|
|
}
|
|
}
|
|
if err = appendAccessAudit(ctx, tx, audit); err != nil {
|
|
return err
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
func validOrganizationRoleReplacement(expected []string, replacement access.Binding, ownerRole string, audit access.AuditEvent) bool {
|
|
if !safeName(ownerRole) || !opaqueID(replacement.ID) || replacement.SubjectKind != access.User || !opaqueID(replacement.SubjectID) || !safeName(replacement.Role) || replacement.Scope.Validate() != nil || replacement.Scope.ProjectID != "" || replacement.Scope.EnvironmentID != "" || replacement.Scope.ServiceID != "" || !opaqueID(replacement.GrantedBy) || replacement.GrantedAt.IsZero() {
|
|
return false
|
|
}
|
|
if !validAccessAudit(audit) || audit.OrganizationID != replacement.Scope.OrganizationID || audit.ActorUserID != replacement.GrantedBy || audit.Action != "access.role.replace" || audit.ResourceType != "user" || audit.ResourceID != replacement.SubjectID || !audit.CreatedAt.Equal(replacement.GrantedAt) {
|
|
return false
|
|
}
|
|
if len(expected) > 16 || !slices.IsSorted(expected) {
|
|
return false
|
|
}
|
|
for index, id := range expected {
|
|
if !opaqueID(id) || index > 0 && expected[index-1] == id {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (store *Store) CreateBreakGlass(ctx context.Context, grant access.BreakGlass, audit access.AuditEvent) error {
|
|
if !validBreakGlass(grant) || !validAccessAudit(audit) || audit.OrganizationID != grant.OrganizationID || audit.ActorUserID != grant.UserID {
|
|
return errors.New("authsqlite: invalid break-glass event")
|
|
}
|
|
tx, err := store.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
var active int
|
|
if err = tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM gwf_organizations o JOIN gwf_organization_memberships m ON m.organization_id=o.id WHERE o.id=? AND o.status='active' AND m.user_id=? AND m.status='active'`, grant.OrganizationID, grant.UserID).Scan(&active); err != nil {
|
|
return err
|
|
}
|
|
if active != 1 {
|
|
return errors.New("authsqlite: break-glass principal is not active in organization")
|
|
}
|
|
if _, err = tx.ExecContext(ctx, `INSERT INTO gwf_break_glass(id,organization_id,user_id,permission_name,reason,created_at,expires_at) VALUES(?,?,?,?,?,?,?)`, grant.ID, grant.OrganizationID, grant.UserID, grant.Permission, grant.Reason, grant.CreatedAt.Unix(), grant.ExpiresAt.Unix()); err != nil {
|
|
return err
|
|
}
|
|
if err = appendAccessAudit(ctx, tx, audit); err != nil {
|
|
return err
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
func (store *Store) ActiveBreakGlass(ctx context.Context, organizationID, userID string, now time.Time) ([]access.BreakGlass, error) {
|
|
if !opaqueID(organizationID) || !opaqueID(userID) || now.IsZero() {
|
|
return nil, errors.New("authsqlite: invalid break-glass query")
|
|
}
|
|
rows, err := store.db.QueryContext(ctx, `SELECT b.id,b.permission_name,b.reason,b.created_at,b.expires_at FROM gwf_break_glass b JOIN gwf_organizations o ON o.id=b.organization_id AND o.status='active' JOIN gwf_organization_memberships m ON m.organization_id=b.organization_id AND m.user_id=b.user_id AND m.status='active' WHERE b.organization_id=? AND b.user_id=? AND b.expires_at>? ORDER BY b.expires_at`, organizationID, userID, now.Unix())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var result []access.BreakGlass
|
|
for rows.Next() {
|
|
var grant access.BreakGlass
|
|
var created, expires int64
|
|
if err = rows.Scan(&grant.ID, &grant.Permission, &grant.Reason, &created, &expires); err != nil {
|
|
return nil, err
|
|
}
|
|
grant.OrganizationID, grant.UserID = organizationID, userID
|
|
grant.CreatedAt, grant.ExpiresAt = time.Unix(created, 0).UTC(), time.Unix(expires, 0).UTC()
|
|
result = append(result, grant)
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
func (store *Store) AppendAccessAudit(ctx context.Context, audit access.AuditEvent) error {
|
|
if !validAccessAudit(audit) {
|
|
return errors.New("authsqlite: invalid access audit")
|
|
}
|
|
_, err := store.db.ExecContext(ctx, `INSERT INTO gwf_access_audit_events(id,organization_id,actor_user_id,action,resource_type,resource_id,request_id,summary,created_at) VALUES(?,?,?,?,?,?,NULLIF(?,''),?,?)`, audit.ID, audit.OrganizationID, audit.ActorUserID, audit.Action, audit.ResourceType, audit.ResourceID, audit.RequestID, audit.Summary, audit.CreatedAt.Unix())
|
|
return err
|
|
}
|
|
|
|
func (store *Store) AccessAudit(ctx context.Context, organizationID string, limit int) ([]access.AuditEvent, error) {
|
|
if !opaqueID(organizationID) || limit < 1 || limit > 1000 {
|
|
return nil, errors.New("authsqlite: invalid access audit query")
|
|
}
|
|
rows, err := store.db.QueryContext(ctx, `SELECT id,actor_user_id,action,resource_type,resource_id,request_id,summary,created_at FROM gwf_access_audit_events WHERE organization_id=? ORDER BY created_at DESC,id DESC LIMIT ?`, organizationID, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var result []access.AuditEvent
|
|
for rows.Next() {
|
|
var event access.AuditEvent
|
|
var requestID sql.NullString
|
|
var created int64
|
|
if err = rows.Scan(&event.ID, &event.ActorUserID, &event.Action, &event.ResourceType, &event.ResourceID, &requestID, &event.Summary, &created); err != nil {
|
|
return nil, err
|
|
}
|
|
event.OrganizationID = organizationID
|
|
event.RequestID = requestID.String
|
|
event.CreatedAt = time.Unix(created, 0).UTC()
|
|
result = append(result, event)
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
func appendAccessAudit(ctx context.Context, tx *sql.Tx, audit access.AuditEvent) error {
|
|
_, err := tx.ExecContext(ctx, `INSERT INTO gwf_access_audit_events(id,organization_id,actor_user_id,action,resource_type,resource_id,request_id,summary,created_at) VALUES(?,?,?,?,?,?,NULLIF(?,''),?,?)`, audit.ID, audit.OrganizationID, audit.ActorUserID, audit.Action, audit.ResourceType, audit.ResourceID, audit.RequestID, audit.Summary, audit.CreatedAt.Unix())
|
|
return err
|
|
}
|
|
|
|
func validBreakGlass(grant access.BreakGlass) bool {
|
|
return opaqueID(grant.ID) && opaqueID(grant.OrganizationID) && opaqueID(grant.UserID) && safeName(grant.Permission) && text(grant.Reason, 1024, false) && !grant.CreatedAt.IsZero() && grant.ExpiresAt.After(grant.CreatedAt) && grant.ExpiresAt.Sub(grant.CreatedAt) <= time.Hour
|
|
}
|
|
|
|
func validAccessAudit(audit access.AuditEvent) bool {
|
|
return opaqueID(audit.ID) && opaqueID(audit.OrganizationID) && opaqueID(audit.ActorUserID) && safeName(audit.Action) && safeName(audit.ResourceType) && text(audit.ResourceID, 256, false) && text(audit.RequestID, 128, true) && text(audit.Summary, 1024, true) && !audit.CreatedAt.IsZero()
|
|
}
|