// Copyright © Microsoft <wastore@microsoft.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package sddl

import (
	"errors"
	"fmt"
	"regexp"
	"sort"
	"strings"
)

// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/f4296d69-1c0f-491f-9587-a960b292d070
// https://docs.microsoft.com/en-us/windows/win32/secauthz/security-descriptor-definition-language

// Owner and group SIDs need replacement
type SDDLString struct {
	OwnerSID, GroupSID string
	DACL, SACL         ACLList
}

type ACLList struct {
	Flags      string
	ACLEntries []ACLEntry
}

// field 5 and field 6 will contain SIDs.
// field 5 is a lone SID, but field 6 will contain SIDs under SID(.*)
type ACLEntry struct {
	Sections []string
}

func (s *SDDLString) PortableString() string {
	output := ""

	if s.OwnerSID != "" {
		tx, err := translateSID(s.OwnerSID)

		if err != nil {
			output += "O:" + s.OwnerSID
		} else {
			output += "O:" + tx
		}
	}

	if s.GroupSID != "" {
		tx, err := translateSID(s.GroupSID)

		if err != nil {
			output += "G:" + s.GroupSID
		} else {
			output += "G:" + tx
		}
	}

	if s.DACL.Flags != "" || len(s.DACL.ACLEntries) != 0 {
		output += "D:" + s.DACL.PortableString()
	}

	if s.SACL.Flags != "" || len(s.SACL.ACLEntries) != 0 {
		output += "S:" + s.SACL.PortableString()
	}

	return output
}

var LiteralSIDRegex = regexp.MustCompile(`SID\(.*?\)`)
var StringRegex = regexp.MustCompile(`("")|(".*?[^\\]")`)

// PortableString returns a SDDL that's been ported from non-descript, well known SID strings (such as DU, DA, etc.)
// to domain-specific strings. This allows us to not mix up the admins from one domain to another.
// Azure Files requires that we do this.
func (a *ACLList) PortableString() string {
	output := a.Flags

	for _, v := range a.ACLEntries {
		output += "("

		for k, s := range v.Sections {
			// Append a ; after the last section
			if k > 0 {
				output += ";"
			}

			switch k {
			case 5:
				// This section is a lone SID, so we can make a call to windows and translate it.
				tx, err := translateSID(strings.TrimSpace(s))

				if err != nil {
					output += s
				} else {
					output += tx
				}
			case 6:
				// This section will potentially have SIDs unless it's not a conditional ACE.
				// They're identifiable as they're inside a literal SID container. ex "SID(S-1-1-0)"

				workingString := ""
				lastAddPoint := 0
				if v.Sections[0] == "XA" || v.Sections[0] == "XD" || v.Sections[0] == "XU" || v.Sections[0] == "ZA" {
					// We shouldn't do any replacing if we're inside of a string.
					// In order to handle this, we'll handle it as a list of events that occur.

					stringEntries := StringRegex.FindAllStringIndex(s, -1)
					sidEntries := LiteralSIDRegex.FindAllStringIndex(s, -1)
					eventMap := map[int]int{} // 1 = string start, 2 = string end, 3 = SID start, 4 = SID end.
					eventList := make([]int, 0)
					inString := false
					SIDStart := -1
					processSID := false

					// Register string beginnings and ends
					for _, v := range stringEntries {
						eventMap[v[0]] = 1
						eventMap[v[1]] = 2
						eventList = append(eventList, v...)
					}

					// Register SID beginnings and ends
					for _, v := range sidEntries {
						eventMap[v[0]] = 3
						eventMap[v[1]] = 4
						eventList = append(eventList, v...)
					}

					// sort the list
					sort.Ints(eventList)

					// Traverse it.
					// Handle any SIDs outside of strings.
					for _, v := range eventList {
						event := eventMap[v]

						switch event {
						case 1: // String start
							inString = true
							// Add everything prior to this
							workingString += s[lastAddPoint:v]
							lastAddPoint = v
						case 2:
							inString = false
							// Add everything prior to this
							workingString += s[lastAddPoint:v]
							lastAddPoint = v
						case 3:
							processSID = !inString
							SIDStart = v
							// If we're going to process this SID, add everything prior to this.
							if processSID {
								workingString += s[lastAddPoint:v]
								lastAddPoint = v
							}
						case 4:
							if processSID {
								// We have to process the sid string now.
								sidString := strings.TrimSuffix(strings.TrimPrefix(s[SIDStart:v], "SID("), ")")

								tx, err := translateSID(strings.TrimSpace(sidString))

								// It seems like we should probably still add the string if we error out.
								// However, this just gets handled exactly like we're not processing the SID.
								// When the next event happens, we just add everything to the string, including the original SID.
								if err == nil {
									workingString += "SID(" + tx + ")"
									lastAddPoint = v
								}
							}
						}
					}
				}

				if workingString != "" {
					if lastAddPoint != len(s) {
						workingString += s[lastAddPoint:]
					}

					s = workingString
				}

				output += s
			default:
				output += s
			}
		}

		output += ")"
	}

	return strings.TrimSpace(output)
}

func (a *ACLList) String() string {
	output := a.Flags

	for _, v := range a.ACLEntries {
		output += "("

		for k, s := range v.Sections {
			if k > 0 {
				output += ";"
			}

			output += s
		}

		output += ")"
	}

	return strings.TrimSpace(output)
}

func (s *SDDLString) String() string {
	output := ""

	if s.OwnerSID != "" {
		output += "O:" + s.OwnerSID
	}

	if s.GroupSID != "" {
		output += "G:" + s.GroupSID
	}

	if s.DACL.Flags != "" || len(s.DACL.ACLEntries) != 0 {
		output += "D:" + s.DACL.String()
	}

	if s.SACL.Flags != "" || len(s.SACL.ACLEntries) != 0 {
		output += "S:" + s.SACL.String()
	}

	return output
}

// place an element onto the current ACL
func (s *SDDLString) putACLElement(element string, aclType rune) error {
	var aclEntries *[]ACLEntry
	switch aclType {
	case 'D':
		aclEntries = &s.DACL.ACLEntries
	case 'S':
		aclEntries = &s.SACL.ACLEntries
	default:
		return fmt.Errorf("%s ACL type invalid", string(aclType))
	}

	aclEntriesLength := len(*aclEntries)
	if aclEntriesLength == 0 {
		return errors.New("ACL Entries too short")
	}

	entry := (*aclEntries)[aclEntriesLength-1]
	entry.Sections = append(entry.Sections, element)
	(*aclEntries)[aclEntriesLength-1] = entry
	return nil
}

// create a new ACL
func (s *SDDLString) startACL(aclType rune) error {
	var aclEntries *[]ACLEntry
	switch aclType {
	case 'D':
		aclEntries = &s.DACL.ACLEntries
	case 'S':
		aclEntries = &s.SACL.ACLEntries
	default:
		return fmt.Errorf("%s ACL type invalid", string(aclType))
	}

	*aclEntries = append(*aclEntries, ACLEntry{Sections: make([]string, 0)})

	return nil
}

func (s *SDDLString) setACLFlags(flags string, aclType rune) error {
	var aclFlags *string
	switch aclType {
	case 'D':
		aclFlags = &s.DACL.Flags
	case 'S':
		aclFlags = &s.SACL.Flags
	default:
		return fmt.Errorf("%s ACL type invalid", string(aclType))
	}

	*aclFlags = strings.TrimSpace(flags)

	return nil
}

func (s SDDLString) Compare(other SDDLString) bool {
	matching := true

	s, _ = ParseSDDL(s.PortableString())
	o, _ := ParseSDDL(other.PortableString())

	matching = matching && (s.OwnerSID == o.OwnerSID) // Compare owners
	matching = matching && (s.GroupSID == o.GroupSID)

	// compare flags
	matching = matching && compareFlags(strings.TrimSuffix(s.DACL.Flags, "NO_ACCESS_CONTROL"), strings.TrimSuffix(o.DACL.Flags, "NO_ACCESS_CONTROL"))
	matching = matching && compareFlags(strings.TrimSuffix(s.SACL.Flags, "NO_ACCESS_CONTROL"), strings.TrimSuffix(o.SACL.Flags, "NO_ACCESS_CONTROL"))

	// compare ACEs
	matching = matching && compareACEs(s.DACL.ACLEntries, o.DACL.ACLEntries)
	matching = matching && compareACEs(s.SACL.ACLEntries, o.SACL.ACLEntries)

	return matching
}

func compareFlags(a, b string) bool {
	a = strings.ToUpper(a)
	b = strings.ToUpper(b)

	if len(a) != len(b) {
		// obvious indicator
		return false
	}

	aEntries := make(map[string]bool)

	if len(a)%2 != 0 {
		// this only happens with P (protected). It could also happen with A or D, but we don't use this function for ACE type.
		aidx := strings.IndexByte(a, 'P')
		bidx := strings.IndexByte(b, 'P')

		a = a[:aidx] + a[aidx+1:]
		b = b[:bidx] + b[bidx+1:]
	}

	for i := 0; i < len(a); i += 2 { // flags, outside of NO_ACCESS_CONTROL (which should be trimmed before hitting this) are pairs of two upper-case letters.
		aEntries[a[i:i+2]] = true
	}

	for i := 0; i < len(b); i += 2 {
		str := b[i : i+2]

		if ok := aEntries[str]; ok {
			delete(aEntries, str)
		} else {
			return false
		}
	}

	if len(aEntries) > 0 {
		return false
	}

	return true
}

func compareACEs(a, b []ACLEntry) bool {
	if len(a) != len(b) {
		// obvious indicator
		return false
	}

	aMismatches := make([]ACLEntry, len(a))
	copy(aMismatches, a)

	for _, bACE := range b {
		foundMatch := false

		for k, aACE := range aMismatches {
			aceMatch := true

			if len(aACE.Sections) != len(bACE.Sections) {
				continue // not a match, for sure
			}

			aceMatch = aceMatch && (aACE.Sections[0] == bACE.Sections[0])           // match ace type
			aceMatch = aceMatch && compareFlags(aACE.Sections[1], bACE.Sections[1]) // compare ace flags
			aceMatch = aceMatch && compareFlags(aACE.Sections[2], bACE.Sections[2]) // compare rights
			aceMatch = aceMatch && aACE.Sections[3] == bACE.Sections[3]             // compare object guid
			aceMatch = aceMatch && aACE.Sections[4] == bACE.Sections[4]             // compare inherit object guid
			aceMatch = aceMatch && aACE.Sections[5] == bACE.Sections[5]             // compare SID
			if len(aACE.Sections) == 7 {
				aceMatch = aceMatch && aACE.Sections[6] == bACE.Sections[6] // compare resource attribute (in a naive way, since we don't use them in tests.)
			}

			if aceMatch {
				// delete matches.
				aMismatches = append(aMismatches[:k], aMismatches[k+1:]...)
				foundMatch = true
				break
			}
		}

		if !foundMatch {
			return false
		}
	}

	return true
}
